# REQ-008: Agent Version Check — Outdated Agent Detection on TUI Startup

## Metadata
- **Status:** review
- **Priority:** 3
- **Dependencies:** REQ-006, REQ-007
- **Created:** 2026-03-03
- **Implemented:** 2026-03-03

## Context

### The problem

When a user upgrades ievo via `pip install ievo --upgrade`, the bundled agent `.md` files inside the package are updated to newer versions. However, agents already installed into a project at `.claude/agents/<name>.md` are NOT updated — they remain at the version recorded in the project manifest. The user has no way to know their installed agents are stale.

### Existing data model

**Project manifest** — `.ievo/manifest.yaml` — tracks each installed agent and the version at install time:

```yaml
project: my-project
version: 0.1.0
agents:
  spec-writer: 0.2.0
  architect: 0.2.0
  coder: 0.1.0   # <-- old version
```

Loaded by `ProjectManifest.load(root: Path) -> ProjectManifest`.
The `agents` field is `dict[str, str]` — keys are agent names, values are version strings (SemVer, e.g. `"0.2.0"`).

**Bundled registry** — `src/ievo/marketplace/registry.yaml` — ships inside the pip package with the latest version for each agent:

```yaml
agents:
  - name: coder
    version: 0.2.0   # <-- newer
    ...
```

Loaded by `Registry.load_cached() -> Registry`. Returns a `Registry` instance with `_entries: dict[str, RegistryEntry]`. Each `RegistryEntry` has `.version: str`.

**Registry lookup**: `registry.get(name: str) -> RegistryEntry | None`.

### Current TUI architecture (relevant parts)

- `IevoApp(App)` in `src/ievo/tui/app.py`.
- `watch_project_root()` is called on mount and on refresh — it loads `ProjectManifest` and `Registry`, then calls `_load_agents()`.
- `_load_agents(root, manifest)` populates the `SelectionList[str](id="agents-list")` with one `Selection` per registry entry. Each entry currently shows: agent name, model, `[mandatory]` badge (if applicable), category, description.
- `SelectionList` uses `initial_state=manifest.has_agent(entry.name)` to mark installed agents.
- The `_manifest: ProjectManifest` and `_agent_registry: Registry` are stored as instance attributes.

### Version comparison rule

An installed agent is **outdated** when:
```
installed_version = manifest.agents[name]   # e.g. "0.1.0"
latest_version    = registry.get(name).version  # e.g. "0.2.0"
installed_version != latest_version
```

Use string inequality (not SemVer parsing) — versions in this project are always bumped forward, never rolled back. If the strings differ, the bundled version is newer.

### Design decision (from IDEA-003)

- Symlinks were rejected — they break across environments and prevent per-project agent customization.
- MD5 content hashing was rejected — overkill, versions are already in the manifest and registry.
- Version comparison is the canonical mechanism.
- The user stays in control — the TUI shows an indicator and offers to update, but does NOT auto-update.

## Acceptance Criteria

### Version comparison logic

- [x] `compare_agent_versions(manifest: ProjectManifest, registry: Registry) -> dict[str, tuple[str, str]]` returns a dict of `{agent_name: (installed_version, latest_version)}` for every agent in the manifest whose installed version differs from the registry version. Agents not in the registry are omitted.
- [x] Given manifest `{coder: "0.1.0"}` and registry `{coder: version="0.2.0"}` → `compare_agent_versions` returns `{"coder": ("0.1.0", "0.2.0")}`.
- [x] Given manifest `{coder: "0.2.0"}` and registry `{coder: version="0.2.0"}` → `compare_agent_versions` returns `{}` (no outdated agents).
- [x] Given manifest `{coder: "0.1.0"}` and registry that does NOT contain `coder` → `compare_agent_versions` returns `{}` (unknown agents are skipped, not an error).
- [x] Given an empty manifest (`agents: {}`) → `compare_agent_versions` returns `{}`.

### TUI — Agents tab visual indicator

- [x] On TUI startup (`watch_project_root`), after loading agents, `compare_agent_versions()` is called. If ≥1 agent is outdated, each outdated agent's row in `SelectionList` shows an `[outdated]` badge in yellow (e.g., `"[yellow][outdated 0.1.0 → 0.2.0][/yellow]"`). Up-to-date agents show no badge.
- [x] If 0 agents are outdated, no badge is shown on any row — the list is identical to the current behavior.
- [x] The `[outdated]` badge includes both the installed version and the latest version (e.g., `0.1.0 → 0.2.0`), not just a generic "outdated" label.
- [x] The outdated badge appears on the same `SelectionList` row as the agent name, not as a separate row or widget.

### TUI — Update prompt on toggle

- [x] When a user toggles ON an agent that is already installed (checked) AND outdated — i.e., the user unchecks and then rechecks it, or the `_install_agent` path is triggered for an installed-but-outdated agent — the agent's `.md` file is overwritten with the bundled version and the manifest version is updated to the latest version.
- [x] After updating an agent, the `[outdated]` badge disappears from that agent's row.
- [x] When a user presses `u` key, ALL outdated agents are updated in batch — each `.md` file is overwritten with the bundled version, and the manifest is saved once after all updates. A notification shows: `"Updated N agent(s): name1, name2"`.
- [x] If there are no outdated agents when `u` is pressed, a notification shows: `"All agents are up to date"` and no files are changed.
- [x] The `u` binding appears in the `Footer` with label `"Update outdated"`.

### Notification on startup

- [x] On TUI startup, if ≥1 outdated agent is detected, a toast notification is shown: `"N agent(s) outdated. Press u to update all."` with severity `"warning"` and timeout of 8 seconds.
- [x] If 0 agents are outdated, no startup notification is shown.

### Edge cases

- [x] If `_agent_registry` is `None` (no registry loaded), `compare_agent_versions` is not called and no outdated badge is shown — no crash.
- [x] If `_manifest` is `None`, no comparison is attempted — no crash.
- [x] An agent present in the manifest but with no `.md` file in `.claude/agents/` is still compared and may show as outdated (the file absence is not relevant to version comparison).

## Negative Acceptance Criteria

- This does NOT auto-update agents on startup — user must explicitly press `u` or reinstall via toggle.
- This does NOT use SemVer parsing (no `packaging` or `semver` library) — string inequality is sufficient.
- This does NOT fetch the registry from the network — only the locally loaded registry (cached or bundled) is used for comparison.
- This does NOT add a dedicated "Updates" tab — all indicators are in the existing Agents tab.
- This does NOT show a diff of what changed between versions — only the version numbers.
- This does NOT block the user from using an outdated agent — they remain fully functional.

## Technical Notes

- Implement `compare_agent_versions` as a standalone function in `src/ievo/core/project.py` or a new `src/ievo/core/versions.py` — not as a method on `IevoApp`. This makes unit testing trivial without starting the TUI.
- The `u` keybinding must be added to `IevoApp.BINDINGS` as `Binding("u", "update_outdated", "Update outdated")`.
- The `action_update_outdated()` method calls `_install_agent()` for each outdated agent (reusing the existing install path), then calls `_load_agents()` to refresh the list.
- Use `Text.assemble()` (already used in `_load_agents`) to build the updated row label with the badge — do not change the `Selection` value (still the agent name string).
- Outdated badge must use Rich markup compatible with Textual's `markup=True` rendering.

## Examples

### Example 1: One outdated agent — startup

```
Manifest agents: {spec-writer: "0.2.0", coder: "0.1.0"}
Registry:        {spec-writer: "0.2.0", coder: "0.2.0"}

compare_agent_versions() → {"coder": ("0.1.0", "0.2.0")}

Agents tab SelectionList:
  [x] spec-writer  sonnet  [mandatory]  core
        Features → atomic, testable requirements
  [x] coder  sonnet  [mandatory]  [yellow][outdated 0.1.0 → 0.2.0][/yellow]  core
        Plans → TDD code following spec and architecture

Toast on startup: "1 agent(s) outdated. Press u to update all."
```

### Example 2: Press `u` — all outdated agents updated

```
Before: coder is outdated (0.1.0 → 0.2.0), architect is outdated (0.1.0 → 0.2.0)

User presses u →
  _install_agent(root, "coder", registry_entry)  # copies bundled coder.md
  _install_agent(root, "architect", registry_entry)  # copies bundled architect.md
  manifest saved once with {coder: "0.2.0", architect: "0.2.0"}
  _load_agents() called → both rows now show no [outdated] badge

Toast: "Updated 2 agent(s): coder, architect"
```

### Example 3: No outdated agents

```
Manifest: {spec-writer: "0.2.0"}
Registry: {spec-writer: "0.2.0"}

compare_agent_versions() → {}
No badge shown. No startup toast. Pressing u shows: "All agents are up to date"
```
