# Implementation Plan: REQ-008 -- Agent Version Check

## Domain Research

- **Problem type**: Data comparison + TUI rendering. Not CPU-bound or I/O-bound. The core logic is a simple dict-to-dict comparison. The TUI part is event-driven widget updates.
- **Approaches evaluated**:
  1. SemVer parsing (packaging.version, semver library) -- overkill. REQ explicitly rejects this. Versions only move forward.
  2. String inequality comparison -- chosen. Simple `!=` check. REQ mandates this approach.
  3. Content hashing (MD5 of .md files) -- rejected in IDEA-003.
- **Recommended approach**: String inequality on `manifest.agents[name]` vs `registry.get(name).version`. Pure function, no library needed.
- **Libraries/tools**: None new. Existing `ProjectManifest`, `Registry`, `Text.assemble()`, Textual `Binding`, `notify()`.
- **Known pitfalls**:
  - `_load_agents()` currently builds `Text.assemble(*parts)` -- the outdated badge must be injected into this same assembly pipeline, not rendered separately.
  - `SelectionList` `Selection` values must remain plain agent name strings (not change due to badge).
  - `_install_agent()` already handles the full install path (copy bundled .md, update manifest, save). The batch update action just calls it N times + refreshes.
  - Textual `Binding` must use `priority=False` (default) so it does not steal `u` from text input widgets.
  - Toast `timeout=8` for startup notification per REQ spec.

## Architecture Assessment

- **Fits existing architecture**: yes -- pure function in `core/`, TUI integration in `tui/app.py`, follows established patterns.
- **New patterns introduced**: none. Reuses `Text.assemble()`, `Binding`, `notify()`, `_install_agent()`.
- **Modules affected**: `src/ievo/core/project.py` (new function), `src/ievo/tui/app.py` (new binding, badge in _load_agents, new action method).
- **Risk level**: low -- additive change, no existing behavior modified for up-to-date agents.
- **Estimated complexity**: M (small M -- approximately 2-3 hours of agent work)

## Pre-conditions

- [x] REQ-006 implemented (bundled marketplace -- registry + bundled_agent_path)
- [x] REQ-007 implemented (TUI integration tests -- Pilot patterns established)
- [x] Git workflow confirmed in DECISIONS.md (D-012: trunk-based, `feat/<ticket>-<description>`)

## Sub-tasks

### TASK-008-1: Core comparison function + unit tests

**Scope**: Implement `compare_agent_versions()` as a standalone pure function. Unit-test all AC scenarios.
**Tests**: ~8
**Files**: `src/ievo/core/project.py`, `tests/test_project.py`
**Time estimate**: ~10 minutes

### TASK-008-2: TUI outdated badge + startup toast + unit tests

**Scope**: Wire `compare_agent_versions()` into `_load_agents()` to show `[outdated]` badge. Add startup toast in `watch_project_root()`. Unit tests with mocks for badge rendering and toast.
**Tests**: ~8
**Files**: `src/ievo/tui/app.py`, `tests/test_tui.py`
**Time estimate**: ~12 minutes

### TASK-008-3: Batch update action (`u` key) + unit tests

**Scope**: Add `Binding("u", ...)`, implement `action_update_outdated()` that calls `_install_agent()` per outdated agent, refreshes list, shows toast. Unit tests.
**Tests**: ~6
**Files**: `src/ievo/tui/app.py`, `tests/test_tui.py`
**Time estimate**: ~10 minutes

### TASK-008-4: Pilot integration tests for outdated badge, toast, and `u` key

**Scope**: Full Textual Pilot tests exercising the end-to-end flow: app starts with outdated agents visible, badge rendered, toast shown, `u` pressed, badge disappears.
**Tests**: ~8
**Files**: `tests/test_tui_integration.py`
**Time estimate**: ~12 minutes

## Micro-Steps

### Step 1: `compare_agent_versions()` -- Red

Write failing tests for the pure function `compare_agent_versions` in `tests/test_project.py`.

**Tests to write:**
1. `test_compare_agent_versions_one_outdated` -- manifest `{coder: "0.1.0"}`, registry `{coder: "0.2.0"}` -> returns `{"coder": ("0.1.0", "0.2.0")}`.
2. `test_compare_agent_versions_all_up_to_date` -- manifest `{coder: "0.2.0"}`, registry `{coder: "0.2.0"}` -> returns `{}`.
3. `test_compare_agent_versions_agent_not_in_registry` -- manifest `{coder: "0.1.0"}`, registry has no `coder` -> returns `{}`.
4. `test_compare_agent_versions_empty_manifest` -- manifest `{agents: {}}` -> returns `{}`.
5. `test_compare_agent_versions_multiple_outdated` -- manifest `{coder: "0.1.0", architect: "0.1.0"}`, registry `{coder: "0.2.0", architect: "0.2.0"}` -> returns both.
6. `test_compare_agent_versions_mixed` -- one outdated, one up-to-date -> returns only the outdated one.
7. `test_compare_agent_versions_none_registry` -- `registry=None` -> returns `{}`.
8. `test_compare_agent_versions_none_manifest` -- `manifest=None` -> returns `{}`.

**Criteria covered**: AC-1 through AC-5, edge cases AC-16, AC-17.
**Files**: `tests/test_project.py`
**Commit**: `REQ-008: step 1 -- red tests for compare_agent_versions`

### Step 2: `compare_agent_versions()` -- Green + Refactor

Implement the function in `src/ievo/core/project.py`. Signature:

```python
def compare_agent_versions(
    manifest: ProjectManifest | None,
    registry: Registry | None,
) -> dict[str, tuple[str, str]]:
```

Logic:
- If `manifest is None` or `registry is None`, return `{}`.
- For each `(name, installed_version)` in `manifest.agents.items()`:
  - `entry = registry.get(name)` -- if `None`, skip.
  - If `entry.version != installed_version`, add to result.

All 8 tests from Step 1 must pass.

**Criteria covered**: AC-1 through AC-5, AC-16, AC-17.
**Files**: `src/ievo/core/project.py`
**Commit**: `REQ-008: step 2 -- implement compare_agent_versions`

### Step 3: TUI outdated badge in `_load_agents` + startup toast -- Red

Write failing tests in `tests/test_tui.py`:

1. `test_load_agents_shows_outdated_badge` -- Set up manifest with `{coder: "0.1.0"}`, registry with `{coder: "0.2.0"}`. Call `_load_agents()`. Verify the `Selection` prompt for `coder` contains `"outdated"` text.
2. `test_load_agents_no_badge_when_up_to_date` -- manifest `{coder: "0.2.0"}`, registry `{coder: "0.2.0"}`. Verify `coder` prompt does NOT contain `"outdated"`.
3. `test_load_agents_badge_includes_version_numbers` -- Verify badge includes both `"0.1.0"` and `"0.2.0"`.
4. `test_load_agents_no_badge_for_uninstalled_agent` -- agent not in manifest -> no badge regardless of registry version.
5. `test_watch_project_root_shows_outdated_toast` -- Set up project with outdated agents. Call `watch_project_root(root)`. Verify `notify()` called with severity `"warning"` and message containing `"outdated"`.
6. `test_watch_project_root_no_toast_when_all_up_to_date` -- All versions match. Verify `notify()` NOT called with severity `"warning"`.
7. `test_watch_project_root_no_toast_when_no_registry` -- `_agent_registry = None`. Verify no crash and no outdated toast.
8. `test_watch_project_root_toast_message_format` -- Verify toast message matches `"N agent(s) outdated. Press u to update all."`.

**Criteria covered**: AC-6, AC-7, AC-8, AC-9, AC-13, AC-14, AC-16.
**Files**: `tests/test_tui.py`
**Commit**: `REQ-008: step 3 -- red tests for outdated badge and startup toast`

### Step 4: TUI outdated badge + startup toast -- Green + Refactor

Modify `_load_agents()` in `src/ievo/tui/app.py`:
- After building the current `parts` list for each entry, check if the entry is installed AND outdated.
- Call `compare_agent_versions(manifest, self._agent_registry)` once and store the result dict (e.g., `self._outdated: dict[str, tuple[str, str]]`).
- Store `self._outdated` as instance attribute so `action_update_outdated` can read it later.
- If agent name is in `_outdated`, append badge to `parts`: `("  [outdated {old} -> {new}]", "yellow")`.

Modify `watch_project_root()`:
- After `self._load_agents(root, manifest)`, check `self._outdated`.
- If `len(self._outdated) > 0`: `self.notify(f"{n} agent(s) outdated. Press u to update all.", severity="warning", timeout=8)`.

Import `compare_agent_versions` from `ievo.core.project`.

All 8 tests from Step 3 must pass.

**Criteria covered**: AC-6 through AC-9, AC-13, AC-14, AC-16.
**Files**: `src/ievo/tui/app.py`
**Commit**: `REQ-008: step 4 -- implement outdated badge and startup toast`

### Step 5: Batch update action (`u` key) -- Red

Write failing tests in `tests/test_tui.py`:

1. `test_binding_u_exists` -- Verify `"u"` is in `IevoApp.BINDINGS`.
2. `test_action_update_outdated_updates_agents` -- Set up app with `_outdated = {"coder": ("0.1.0", "0.2.0")}`, mock `_install_agent` and `_load_agents`. Call `action_update_outdated()`. Verify `_install_agent` called for `coder`. Verify `_load_agents` called after.
3. `test_action_update_outdated_saves_manifest_once` -- Two outdated agents. Verify `manifest.save()` is called (via `_install_agent`), and `_load_agents()` called once after all installs.
4. `test_action_update_outdated_shows_toast` -- Verify `notify` called with `"Updated 2 agent(s): coder, architect"`.
5. `test_action_update_outdated_no_outdated` -- `_outdated = {}`. Verify `notify` called with `"All agents are up to date"`. Verify `_install_agent` NOT called.
6. `test_action_update_outdated_no_project` -- `project_root = None`. Verify no crash.

**Criteria covered**: AC-10, AC-11, AC-12, AC-13, AC-14.
**Files**: `tests/test_tui.py`
**Commit**: `REQ-008: step 5 -- red tests for batch update action`

### Step 6: Batch update action -- Green + Refactor

Add to `IevoApp.BINDINGS`:
```python
Binding("u", "update_outdated", "Update outdated"),
```

Implement `action_update_outdated()` in `IevoApp`:
```python
def action_update_outdated(self) -> None:
    root = self.project_root
    if root is None or self._manifest is None or self._agent_registry is None:
        return
    if not self._outdated:
        self.notify("All agents are up to date", severity="information", timeout=3)
        return
    updated_names = []
    for name in self._outdated:
        entry = self._agent_registry.get(name)
        self._install_agent(root, name, entry)
        updated_names.append(name)
    self._load_agents(root, self._manifest)
    self._load_pipeline(self._manifest)
    names_str = ", ".join(updated_names)
    self.notify(
        f"Updated {len(updated_names)} agent(s): {names_str}",
        severity="information",
        timeout=5,
    )
```

All 6 tests from Step 5 must pass.

**Criteria covered**: AC-10, AC-11, AC-12, AC-13, AC-14.
**Files**: `src/ievo/tui/app.py`
**Commit**: `REQ-008: step 6 -- implement batch update action`

### Step 7: Pilot integration tests -- end-to-end

Write Pilot-based async integration tests in `tests/test_tui_integration.py`:

1. `test_outdated_badge_visible_on_startup` -- Create project with manifest `{spec-writer: "0.1.0"}`, registry has `spec-writer: "0.2.0"`. Run TUI. Query `SelectionList`, find spec-writer row. Verify rendered content includes `"outdated"`.
2. `test_no_outdated_badge_when_up_to_date` -- Create project with matching versions. Verify rendered content does NOT include `"outdated"`.
3. `test_outdated_toast_on_startup` -- Capture notifications. Verify a warning toast with `"outdated"` text appears.
4. `test_no_outdated_toast_when_up_to_date` -- Verify no warning toast.
5. `test_u_key_updates_all_outdated` -- Start with outdated agent. Press `u`. Verify:
   - Agent .md file is overwritten (check file exists and content matches bundled).
   - Manifest version updated.
   - Badge disappears from row.
   - Notification shows `"Updated"`.
6. `test_u_key_when_all_up_to_date` -- No outdated agents. Press `u`. Verify notification `"All agents are up to date"`.
7. `test_outdated_badge_disappears_after_single_agent_reinstall` -- Outdated agent. Navigate to it, toggle OFF then toggle ON (reinstall). Verify badge gone.
8. `test_no_crash_when_no_project_press_u` -- No project loaded. Press `u`. No crash.

**Implementation note**: To test with a specific outdated version, the test helper `_make_project()` already accepts `agents={"spec-writer": "0.1.0"}`. The bundled registry has `spec-writer: "0.2.0"`, so versions will naturally differ. To test "up to date", pass `agents={"spec-writer": "0.2.0"}` to match registry.

**Important Pilot pattern**: Use `notifications=True` in `run_test()` for toast assertions. Use `await pilot.pause()` after every action.

**Criteria covered**: AC-6 through AC-14, AC-16, AC-17, AC-18.
**Files**: `tests/test_tui_integration.py`
**Commit**: `REQ-008: step 7 -- Pilot integration tests for version check`

### Step 8: Final coverage check and cleanup

Run `uv run pytest --cov --cov-report=term-missing` and verify 100% coverage on:
- `src/ievo/core/project.py` -- `compare_agent_versions` function
- `src/ievo/tui/app.py` -- `_load_agents` badge path, `watch_project_root` toast path, `action_update_outdated`

Fix any uncovered branches. Run `uv run pre-commit run --files src/ievo/core/project.py src/ievo/tui/app.py tests/test_project.py tests/test_tui.py tests/test_tui_integration.py`.

**Files**: potentially any of the above if coverage gaps found.
**Commit**: `REQ-008: step 8 -- coverage cleanup`

## Traceability

| Acceptance Criterion | Covered in Step |
|---------------------|-----------------|
| AC-1: `compare_agent_versions` returns dict of outdated agents | Steps 1-2 |
| AC-2: One outdated agent returns correct tuple | Steps 1-2 |
| AC-3: Up-to-date agent returns empty dict | Steps 1-2 |
| AC-4: Agent not in registry is skipped | Steps 1-2 |
| AC-5: Empty manifest returns empty dict | Steps 1-2 |
| AC-6: `[outdated]` badge shown in SelectionList | Steps 3-4, 7 |
| AC-7: No badge when all up-to-date | Steps 3-4, 7 |
| AC-8: Badge includes both version numbers | Steps 3-4, 7 |
| AC-9: Badge on same row as agent name | Steps 3-4, 7 |
| AC-10: Reinstall updates .md and manifest version | Steps 5-6, 7 |
| AC-11: Badge disappears after update | Steps 5-6, 7 |
| AC-12: `u` key batch-updates all outdated | Steps 5-6, 7 |
| AC-13: No outdated + `u` shows "All up to date" | Steps 5-6, 7 |
| AC-14: `u` binding in Footer with label | Steps 5-6 |
| AC-15: Startup toast when outdated agents detected | Steps 3-4, 7 |
| AC-16: No startup toast when all up-to-date | Steps 3-4, 7 |
| AC-17: No crash when registry is None | Steps 1-2, 3-4, 7 |
| AC-18: No crash when manifest is None | Steps 1-2, 7 |
| AC-19: Agent in manifest but no .md file still compared | Steps 1-2 (function is version-only, does not check file existence) |

## Scope Guard

- **Considered but excluded**: SemVer parsing library -- REQ explicitly rejects this.
- **Considered but excluded**: Network fetch for latest versions -- REQ says use locally loaded registry only.
- **Considered but excluded**: Diff view of what changed between versions -- REQ explicitly excludes this.
- **Considered but excluded**: Dedicated "Updates" tab -- REQ says stay in Agents tab.
- **Considered but excluded**: Auto-update on startup -- REQ says user must explicitly act.

## Architecture Notes for Coder

1. **Place `compare_agent_versions` in `src/ievo/core/project.py`** as a module-level function (not a method on `ProjectManifest`). This keeps it testable without TUI instantiation. Import `Registry` type from `ievo.core.registry`.

2. **Store `_outdated` as instance attribute on `IevoApp`**. Initialize to `{}` in class body: `_outdated: dict[str, tuple[str, str]] = {}`. Populate in `_load_agents()` right before the loop. This avoids calling `compare_agent_versions` twice (once for badges, once for toast).

3. **Badge rendering** -- Use Rich markup tuple in `Text.assemble()`, not Rich markup string. The existing pattern is `("text", "style")`. For the outdated badge:
   ```python
   (f"  [outdated {old} \u2192 {new}]", "yellow")
   ```
   Use the Unicode right arrow `\u2192` for `->` to keep it clean. Or use ` -> ` if markup conflicts arise.

4. **`_load_agents()` must compute outdated dict before the loop** so it is available for each entry. Call `compare_agent_versions(manifest, self._agent_registry)` at the top of `_load_agents` and store as `self._outdated`.

5. **`_install_agent()` already copies the bundled .md and calls `manifest.save()`**. The batch update just calls it in a loop. After the loop, call `_load_agents()` once to rebuild the list (this will also recompute `_outdated`, clearing the badge for updated agents).

6. **Do NOT refactor `_install_agent` to avoid saving the manifest per agent**. It saves on each call today and changing that would affect the existing toggle handler. Keep it simple. The overhead of N saves (where N is at most 13) is negligible.

7. **Import note**: `from ievo.core.project import compare_agent_versions` in `app.py`. The existing import `from ievo.core.project import ProjectManifest` is already there -- add to the same line.

8. **Test helper reuse**: In `tests/test_tui_integration.py`, `_make_project(tmp_path, agents={"spec-writer": "0.1.0"})` will naturally produce an outdated state because the bundled registry has `spec-writer: "0.2.0"`. For up-to-date state, use `agents={"spec-writer": "0.2.0"}`.

9. **Notification capture in Pilot tests**: Use the same `capture_notify` pattern from `test_toggle_mandatory_agent_off_shows_warning` (already in `test_tui_integration.py`). Alternatively use `notifications=True` and inspect `app._notifications` (Textual internal, less reliable). The capture pattern is safer.

10. **Existing `watch_project_root` has no `self.notify()` calls currently**. The notification must be added AFTER `_load_agents()` (which populates `_outdated`). The `notify()` call in `watch_project_root` requires the app to be mounted. Since `watch_project_root` is called from `on_mount` reactively, the app IS mounted -- this is safe.

11. **13 agents in registry**: Tests referencing `option_count == 13` will remain correct -- we are not adding/removing agents, just adding a badge to some rows.
