# Implementation Plan: REQ-009 -- Template Version Tracking via cli_version in Manifest

## Domain Research

- **Problem type**: Data model extension + file copy + TUI notification. Not CPU-bound or I/O-bound. The core logic is a string comparison between two version strings. The file refresh is `shutil.copy2` on 7 small files.
- **Approaches evaluated**:
  1. Per-template version map in manifest -- rejected by D-020. Adds complexity without benefit since templates only change when the CLI package version changes.
  2. Content hashing (MD5/SHA of each template file) -- rejected by D-020 and REQ-009 negative AC. Overkill.
  3. Single `cli_version` string comparison -- chosen. `manifest.cli_version != __version__` AND `cli_version != ""`. The simplest possible staleness signal.
- **Recommended approach**: Single `cli_version: str` field on `ProjectManifest`. Pure function `compare_template_versions()` in `project.py`. TUI toast on startup + `t` keybinding for refresh. Template deploy mapping extracted as shared constant `TEMPLATE_MAP`.
- **Libraries/tools**: None new. `shutil.copy2` (stdlib), existing `bundled_template_path()`, existing TUI `Binding`/`notify()` patterns.
- **Known pitfalls**:
  - `ProjectManifest.save()` currently builds a `data` dict manually -- must include `cli_version` in it. Omitting it breaks the roundtrip.
  - `ProjectManifest.load()` must handle existing manifests that lack `cli_version` key (backwards compat) -- default to `""`.
  - The template deploy mapping (`(bundled_name, dest_relative_path)`) is currently hardcoded inline in `init.py` lines 150-162. It includes IEVO.md (deployed to `.ievo/IEVO.md`) and 6 spec templates (deployed to `spec/templates/`). The `action_refresh_templates` needs the same mapping. Must extract to a shared location.
  - `init.py` uses `_write_if_missing()` -- templates are only written on first init. On re-init (existing manifest) the command exits early. So `cli_version` is only set during fresh init.
  - The `t` binding MUST NOT conflict with any existing binding. Current bindings: `a`, `p`, `e`, `f`, `r`, `u`, `q`. The `t` is free.
  - When `project_root is None`, pressing `t` must be a safe no-op (same pattern as `action_update_outdated` guard).
  - Destination directories (`spec/templates/`) might not exist if user deleted them. Must `mkdir(parents=True, exist_ok=True)` before copy.

## Architecture Assessment

- **Fits existing architecture**: yes -- follows identical pattern to REQ-008 (pure comparison function in `core/project.py`, TUI toast + keybinding in `tui/app.py`).
- **New patterns introduced**: Shared template mapping constant (`TEMPLATE_MAP`). This is a new pattern but a necessary extraction to avoid duplication between `init.py` and `tui/app.py`.
- **Modules affected**:
  - `src/ievo/core/project.py` -- new `cli_version` field + `compare_template_versions()` function
  - `src/ievo/commands/init.py` -- set `cli_version` on new projects, use shared `TEMPLATE_MAP`
  - `src/ievo/tui/app.py` -- template toast in `watch_project_root()`, new `action_refresh_templates()` + `t` binding
  - `src/ievo/core/templates.py` -- NEW file, shared `TEMPLATE_MAP` constant + `refresh_templates()` helper
- **Risk level**: low -- additive change, no existing behavior modified for current projects (empty `cli_version` = no toast).
- **Estimated complexity**: M (approximately 2-3 hours of agent work)

## Pre-conditions

- [x] REQ-000 implemented (project setup)
- [x] REQ-006 implemented (bundled marketplace -- `bundled_template_path()`, `list_bundled_templates()`)
- [x] Git workflow confirmed in DECISIONS.md (D-012: trunk-based, `feat/<ticket>-<description>`)
- [x] D-020 confirmed: single `cli_version` field, not per-template tracking

## Sub-tasks

### TASK-009-1: `cli_version` field on ProjectManifest + unit tests

**Scope**: Add `cli_version: str = ""` field to `ProjectManifest`. Update `load()` and `save()`. Add `compare_template_versions()` pure function. Unit-test all scenarios.
**Tests**: ~12
**Files**: `src/ievo/core/project.py`, `tests/test_project.py`
**Time estimate**: ~10 minutes

### TASK-009-2: Shared template map + `refresh_templates()` core logic + unit tests

**Scope**: Create `src/ievo/core/templates.py` with `TEMPLATE_MAP` constant and `refresh_templates(root, cli_version)` function. Refactor `init.py` to use `TEMPLATE_MAP`. Unit-test refresh logic.
**Tests**: ~8
**Files**: `src/ievo/core/templates.py`, `src/ievo/commands/init.py`, `tests/test_templates.py`, `tests/test_init.py`
**Time estimate**: ~12 minutes

### TASK-009-3: `ievo init` writes `cli_version` + tests

**Scope**: Set `manifest.cli_version = __version__` in `init()` before `manifest.save()`. Verify existing re-init guard still works (no overwrite). Test the value is persisted.
**Tests**: ~3
**Files**: `src/ievo/commands/init.py`, `tests/test_init.py`
**Time estimate**: ~8 minutes

### TASK-009-4: TUI template toast + `t` keybinding + Pilot integration tests

**Scope**: Wire `compare_template_versions()` into `watch_project_root()`. Add `t` binding and `action_refresh_templates()`. Pilot integration tests.
**Tests**: ~10
**Files**: `src/ievo/tui/app.py`, `tests/test_tui.py`, `tests/test_tui_integration.py`
**Time estimate**: ~15 minutes

## Micro-Steps

### Step 1: `cli_version` on ProjectManifest + `compare_template_versions()` -- Red

Write failing tests in `tests/test_project.py`.

**Tests to write:**

1. `test_manifest_cli_version_default` -- `ProjectManifest(project="test")` -> `cli_version == ""`.
2. `test_manifest_load_with_cli_version` -- Write YAML with `cli_version: "0.1.0"`. Load. Assert `m.cli_version == "0.1.0"`.
3. `test_manifest_load_without_cli_version` -- Write YAML without `cli_version` key. Load. Assert `m.cli_version == ""` (backwards compat).
4. `test_manifest_save_includes_cli_version` -- Save manifest with `cli_version="0.2.0"`. Read raw YAML. Assert `cli_version` key is present with value `"0.2.0"`.
5. `test_manifest_save_includes_empty_cli_version` -- Save manifest with `cli_version=""`. Read raw YAML. Assert `cli_version` key is present with value `""`.
6. `test_compare_template_versions_outdated` -- `manifest.cli_version = "0.1.0"`, `cli_version = "0.2.0"` -> returns `True`.
7. `test_compare_template_versions_up_to_date` -- `manifest.cli_version = "0.2.0"`, `cli_version = "0.2.0"` -> returns `False`.
8. `test_compare_template_versions_empty_cli_version` -- `manifest.cli_version = ""`, `cli_version = "0.1.0"` -> returns `False` (no nagging on old projects).
9. `test_compare_template_versions_none_manifest` -- `manifest=None`, `cli_version = "0.1.0"` -> returns `False`.
10. `test_save_roundtrip_preserves_cli_version` -- Save with `cli_version="0.3.0"`, load, assert `cli_version == "0.3.0"`.

**Criteria covered**: AC-1, AC-3 (all sub-bullets)
**Files**: `tests/test_project.py`
**Commit**: `REQ-009: step 1 -- red tests for cli_version field and compare_template_versions`

### Step 2: `cli_version` on ProjectManifest + `compare_template_versions()` -- Green

Implement in `src/ievo/core/project.py`:

1. Add `cli_version: str = ""` field to `ProjectManifest` dataclass.
2. In `load()`, add `cli_version=data.get("cli_version", "")`.
3. In `save()`, add `"cli_version": self.cli_version` to the `data` dict (always, even when empty -- per AC-1).
4. Add standalone function:

```python
def compare_template_versions(
    manifest: ProjectManifest | None,
    cli_version: str,
) -> bool:
    """Return True when project templates may be outdated.

    Compares manifest.cli_version against the running CLI version.
    Returns False for None manifest or empty cli_version (old projects
    that predate this feature -- no nagging).
    """
    if manifest is None:
        return False
    if manifest.cli_version == "":
        return False
    return manifest.cli_version != cli_version
```

All 10 tests from Step 1 must pass.

**Criteria covered**: AC-1, AC-3
**Files**: `src/ievo/core/project.py`
**Commit**: `REQ-009: step 2 -- implement cli_version field and compare_template_versions`

### Step 3: Shared template map + `refresh_templates()` -- Red

Write failing tests in `tests/test_templates.py` (NEW file).

**Tests to write:**

1. `test_template_map_has_seven_entries` -- `TEMPLATE_MAP` has exactly 7 entries (IEVO.md + 6 spec templates).
2. `test_template_map_ievo_md_destination` -- `("IEVO.md", ".ievo/IEVO.md")` is in the map.
3. `test_template_map_req_template_destination` -- `("REQ_TEMPLATE.md", "spec/templates/REQ_TEMPLATE.md")` is in the map.
4. `test_refresh_templates_copies_all_files` -- Set up a `tmp_path` project with existing templates (stale content). Call `refresh_templates(root, "0.2.0")`. Assert all 7 destination files exist and contain bundled content (not stale).
5. `test_refresh_templates_updates_cli_version` -- After `refresh_templates()`, load manifest. Assert `cli_version == "0.2.0"`.
6. `test_refresh_templates_creates_missing_dirs` -- Delete `spec/templates/` before refresh. Assert it is recreated.
7. `test_refresh_templates_skips_missing_bundled` -- Patch `bundled_template_path` to return `None` for one template. Assert other templates still copied. No crash.
8. `test_refresh_templates_returns_count` -- Returns the number of templates actually copied (for the notification message).

**Criteria covered**: AC-5 (all sub-bullets), AC-6, AC-7 (partially)
**Files**: `tests/test_templates.py`
**Commit**: `REQ-009: step 3 -- red tests for TEMPLATE_MAP and refresh_templates`

### Step 4: Shared template map + `refresh_templates()` -- Green + Refactor init.py

Create `src/ievo/core/templates.py`:

```python
"""Template deploy mapping and refresh logic."""

from __future__ import annotations

import shutil
from pathlib import Path

from ievo.core.config import IEVO_MANIFEST
from ievo.marketplace import bundled_template_path

# (bundled_filename, project_relative_destination)
TEMPLATE_MAP: list[tuple[str, str]] = [
    ("IEVO.md", ".ievo/IEVO.md"),
    ("REQ_TEMPLATE.md", "spec/templates/REQ_TEMPLATE.md"),
    ("CHANGE_REQUEST_TEMPLATE.md", "spec/templates/CR_TEMPLATE.md"),
    ("QUESTION_TEMPLATE.md", "spec/templates/QUESTION_TEMPLATE.md"),
    ("PLAN_TEMPLATE.md", "spec/templates/PLAN_TEMPLATE.md"),
    ("ACCEPTANCE_REPORT_TEMPLATE.md", "spec/templates/ACCEPTANCE_REPORT_TEMPLATE.md"),
    ("PROPOSAL_TEMPLATE.md", "spec/templates/PROPOSAL_TEMPLATE.md"),
]


def refresh_templates(root: Path, cli_version: str) -> int:
    """Copy all bundled templates to the project and update cli_version.

    Returns the number of template files actually copied.
    """
    from ievo.core.project import ProjectManifest

    count = 0
    for bundled_name, dest_rel in TEMPLATE_MAP:
        src = bundled_template_path(bundled_name)
        if src is None:
            continue
        dest = root / dest_rel
        dest.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src, dest)
        count += 1

    # Update manifest
    manifest = ProjectManifest.load(root)
    manifest.cli_version = cli_version
    manifest.save(root)

    return count
```

Refactor `src/ievo/commands/init.py`:
- Import `TEMPLATE_MAP` from `ievo.core.templates`.
- Replace the hardcoded IEVO.md deploy (lines 139-141) and spec template loop (lines 150-162) with a single loop over `TEMPLATE_MAP`, still using `_write_if_missing()`:

```python
from ievo.core.templates import TEMPLATE_MAP

# Deploy all bundled templates (write-if-missing for init)
for bundled_name, dest_rel in TEMPLATE_MAP:
    tpl_path = bundled_template_path(bundled_name)
    if tpl_path and _write_if_missing(root / dest_rel, tpl_path.read_text()):
        step(dest_rel)
```

**Important**: The refactored init.py MUST produce identical file output. Verify existing `test_init.py` tests still pass without changes.

All 8 tests from Step 3 must pass. All existing `test_init.py` tests must still pass.

**Criteria covered**: AC-5, AC-6, AC-7
**Files**: `src/ievo/core/templates.py`, `src/ievo/commands/init.py`, `tests/test_templates.py`
**Commit**: `REQ-009: step 4 -- implement TEMPLATE_MAP, refresh_templates, refactor init.py`

### Step 5: `ievo init` writes `cli_version` -- Red + Green

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

**Tests to write:**

1. `test_init_writes_cli_version` -- After `init(name="cv-test")`, load manifest, assert `cli_version == __version__` (import from `ievo.version`).
2. `test_init_does_not_overwrite_existing_manifest` -- Pre-create manifest with `cli_version: "0.0.1"`. Run `init(name=".")`. Assert `typer.Exit` raised (existing guard) and manifest unchanged.

Implement in `src/ievo/commands/init.py`:
- After constructing the `ProjectManifest(...)`, add:

```python
from ievo.version import __version__

manifest = ProjectManifest(
    project=project_name,
    version="0.1.0",
    created=str(date.today()),
    cli_version=__version__,   # <-- ADD THIS
)
```

All new tests + existing tests must pass.

**Criteria covered**: AC-2
**Files**: `src/ievo/commands/init.py`, `tests/test_init.py`
**Commit**: `REQ-009: step 5 -- ievo init writes cli_version to manifest`

### Step 6: TUI template toast + `t` keybinding -- Red

Write failing tests. Unit tests in `tests/test_tui.py` and Pilot integration tests in `tests/test_tui_integration.py`.

**Unit tests (test_tui.py):**

1. `test_binding_t_exists` -- Verify `"t"` is in `IevoApp.BINDINGS`.
2. `test_action_refresh_templates_no_project_noop` -- `project_root = None`. Call `action_refresh_templates()`. Verify no crash, no notification.

**Pilot integration tests (test_tui_integration.py):**

3. `test_template_outdated_toast_on_startup` -- Create project with `cli_version="0.0.1"`. Patch `ievo.version.__version__` to `"0.2.0"`. Run TUI. Capture notifications. Assert warning toast with `"Templates may be outdated"` shown.
4. `test_no_template_toast_when_up_to_date` -- Create project with `cli_version` matching `__version__`. Assert no template-related warning toast.
5. `test_no_template_toast_when_cli_version_empty` -- Create project without `cli_version` field (old project). Assert no template-related warning toast.
6. `test_t_key_refreshes_templates` -- Create project with `cli_version="0.0.1"`. Press `t`. Verify all 7 template files contain bundled content. Verify `manifest.cli_version == __version__`. Verify success notification `"Templates refreshed"`.
7. `test_t_key_when_up_to_date` -- `cli_version` matches `__version__`. Press `t`. Verify notification `"Templates are up to date"`. Verify no files changed.
8. `test_t_key_idempotent` -- Press `t` twice when up to date. Both times shows `"Templates are up to date"`.
9. `test_t_key_no_project_noop` -- No project loaded. Press `t`. No crash, no notification.
10. `test_template_and_agent_toasts_coexist` -- Create project with both `cli_version="0.0.1"` AND outdated agent. Verify both toast messages appear.

**Criteria covered**: AC-4, AC-5, AC-6, AC-7
**Files**: `tests/test_tui.py`, `tests/test_tui_integration.py`
**Commit**: `REQ-009: step 6 -- red tests for TUI template toast and t keybinding`

### Step 7: TUI template toast + `t` keybinding -- Green

Implement in `src/ievo/tui/app.py`:

1. Add import: `from ievo.core.project import ProjectManifest, compare_agent_versions, compare_template_versions`
2. Add import: `from ievo.version import __version__`
3. Add import: `from ievo.core.templates import refresh_templates`
4. Add binding to `BINDINGS`:
```python
Binding("t", "refresh_templates", "Refresh templates"),
```
5. In `watch_project_root()`, after the `_outdated` toast block, add:
```python
if compare_template_versions(self._manifest, __version__):
    self.notify(
        f"Templates may be outdated (project: {self._manifest.cli_version}, "
        f"cli: {__version__}). Press t to refresh.",
        severity="warning",
        timeout=8,
    )
```
6. Add `action_refresh_templates()`:
```python
def action_refresh_templates(self) -> None:
    """Refresh project templates from bundled marketplace content."""
    root = self.project_root
    if root is None or self._manifest is None:
        return
    if not compare_template_versions(self._manifest, __version__):
        self.notify("Templates are up to date", severity="information", timeout=3)
        return
    count = refresh_templates(root, __version__)
    # Reload manifest to pick up updated cli_version
    self._manifest = ProjectManifest.load(root)
    self.notify(
        f"Templates refreshed (cli: {__version__})",
        severity="information",
        timeout=5,
    )
```

All tests from Step 6 must pass.

**Criteria covered**: AC-4, AC-5, AC-6, AC-7
**Files**: `src/ievo/tui/app.py`
**Commit**: `REQ-009: step 7 -- implement TUI template toast and t keybinding`

### 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` -- `cli_version` field, `compare_template_versions()` function
- `src/ievo/core/templates.py` -- `TEMPLATE_MAP`, `refresh_templates()`
- `src/ievo/commands/init.py` -- `cli_version` in manifest construction, refactored template loop
- `src/ievo/tui/app.py` -- template toast in `watch_project_root`, `action_refresh_templates`

Fix any uncovered branches. Run `uv run pre-commit run --files <all-changed-files>`.

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

## Traceability

| Acceptance Criterion | Covered in Step |
|---------------------|-----------------|
| AC-1: ProjectManifest gains cli_version field | Steps 1-2 |
| AC-1a: load() reads cli_version with default "" | Steps 1-2 |
| AC-1b: save() always writes cli_version | Steps 1-2 |
| AC-2: ievo init writes cli_version | Step 5 |
| AC-2a: re-init does not overwrite | Step 5 |
| AC-3: compare_template_versions returns True when outdated | Steps 1-2 |
| AC-3a: "0.1.0" vs "0.2.0" -> True | Steps 1-2 |
| AC-3b: "0.2.0" vs "0.2.0" -> False | Steps 1-2 |
| AC-3c: "" vs "0.1.0" -> False (no nag) | Steps 1-2 |
| AC-3d: None manifest -> False | Steps 1-2 |
| AC-4: TUI startup notification | Steps 6-7 |
| AC-4a: toast message format | Steps 6-7 |
| AC-4b: no toast when False | Steps 6-7 |
| AC-4c: independent of agent toast | Steps 6-7 |
| AC-4d: no toast when no project | Steps 6-7 |
| AC-5: Template refresh action | Steps 3-4, 6-7 |
| AC-5a: t binding + footer label | Steps 6-7 |
| AC-5b: t when up-to-date shows message | Steps 6-7 |
| AC-5c: t when outdated copies files + updates manifest | Steps 3-4, 6-7 |
| AC-5d: subsequent compare returns False | Steps 6-7 |
| AC-6: Idempotent refresh | Steps 6-7 |
| AC-7a: missing bundled template skipped | Steps 3-4 |
| AC-7b: missing dest directory created | Steps 3-4 |
| AC-7c: no project -> no-op | Steps 6-7 |

## Scope Guard

- **Considered but excluded**: Per-template version tracking -- rejected by D-020 and REQ-009 negative AC.
- **Considered but excluded**: Content hash comparison -- rejected by D-020 and REQ-009 negative AC.
- **Considered but excluded**: Confirmation prompt before overwrite -- REQ says `t` keystroke IS the confirmation. Only a warning notification before copy.
- **Considered but excluded**: Auto-refresh on startup -- REQ says user must explicitly press `t`.
- **Considered but excluded**: Dedicated UI panel for templates -- REQ says toast + keybinding only.
- **Considered but excluded**: Diff view of template changes -- out of scope.

## Architecture Notes for Coder

1. **Place `compare_template_versions` in `src/ievo/core/project.py`** as a module-level function (not a method on `ProjectManifest`). This follows the same pattern as `compare_agent_versions`. Keeps it trivially unit-testable.

2. **Create `src/ievo/core/templates.py` as a NEW file** for `TEMPLATE_MAP` and `refresh_templates()`. This is the shared constant that both `init.py` and `tui/app.py` need. Do NOT put it in `init.py` (would create a circular-ish import from TUI to commands) or in `marketplace/__init__.py` (that module is about bundled file resolution, not deploy logic).

3. **`TEMPLATE_MAP` format**: List of `(bundled_filename, project_relative_dest_path)` tuples. The dest path is relative to project root. Example: `("IEVO.md", ".ievo/IEVO.md")`. This captures both the IEVO.md special case (deployed to `.ievo/`) and the spec templates (deployed to `spec/templates/`). Note the rename: `CHANGE_REQUEST_TEMPLATE.md` -> `CR_TEMPLATE.md` in the dest path.

4. **Refactoring init.py**: Replace the IEVO.md deploy block (lines 138-141) and the spec template loop (lines 149-162) with a single loop over `TEMPLATE_MAP`. Keep using `_write_if_missing()` -- init only writes templates that do not exist. The log messages should show the dest_rel path (e.g., `step("spec/templates/REQ_TEMPLATE.md")`). The section headers ("Spec files") can remain as-is or be adjusted -- the key constraint is that all existing `test_init.py` tests pass without modification.

5. **`refresh_templates()` loads and saves manifest**: The function takes `(root, cli_version)`, loads the manifest, copies files, updates `cli_version`, saves. This makes it a self-contained operation. The caller in `action_refresh_templates` should reload `self._manifest` after calling it to pick up the updated `cli_version`.

6. **`watch_project_root()` ordering**: The template toast should fire AFTER the agent outdated toast. Both may appear simultaneously (per AC-4c). The ordering is: `_load_agents()` (computes `_outdated`) -> agent toast -> template toast -> `_load_pipeline()` -> etc.

7. **Notification capture in Pilot tests**: Reuse the `capture_notify` pattern already established in `test_tui_integration.py`. To distinguish template toasts from agent toasts, check the message content -- template toast contains `"Templates"`, agent toast contains `"agent(s) outdated"`.

8. **`__version__` import in app.py**: Import as `from ievo.version import __version__`. The existing `from ievo.core.project import ProjectManifest` import line already exists -- add `compare_template_versions` to it.

9. **Edge case: `cli_version` in conftest fixture**: The `tmp_project` fixture in `tests/conftest.py` does not include `cli_version` in its manifest YAML. This is correct -- it simulates an old project. Tests that need a specific `cli_version` should write it into their own manifest. Do NOT change `conftest.py` fixtures.

10. **Init log grouping**: The current `init.py` has section headers like `info("[bold]Spec files[/bold]")` before the template loop. After refactoring to use `TEMPLATE_MAP`, the single loop will emit template paths under whatever section header is active. Maintain the existing log structure by keeping the `info("[bold]Spec files[/bold]")` and `info("[bold]IEVO.md from bundled marketplace[/bold]")` headers. One approach: split the `TEMPLATE_MAP` loop into two groups (IEVO.md first, then spec templates), each under its own header. Or simply iterate all under one header. The key constraint is existing test assertions pass.

11. **`test_tui_integration.py` `_make_project` helper**: To test template scenarios, create projects with `cli_version` in the manifest. Extend the helper or write manifest YAML directly in each test. Since `_make_project` currently only sets `project`, `version`, and `agents`, the simplest approach is to add an optional `cli_version` parameter defaulting to `None` (omitted from YAML when None, simulating old projects).
