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

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

## Context

### The problem

When `ievo` initialises a project it copies 7 bundled template files (IEVO.md,
REQ_TEMPLATE.md, QUESTION_TEMPLATE.md, PLAN_TEMPLATE.md,
CHANGE_REQUEST_TEMPLATE.md, ACCEPTANCE_REPORT_TEMPLATE.md,
PROPOSAL_TEMPLATE.md) into the project's `.ievo/` and `spec/templates/`
directories. When the user later upgrades the ievo CLI via pip/uv, the bundled
templates are updated inside the package, but the project copies are never
refreshed. There is no mechanism for the user to know their templates are stale.

### Chosen approach (IDEA-004, decided by PO)

Record the CLI version that last deployed templates into the project manifest
(`cli_version` field). On TUI startup compare that field against the running CLI
version. If they differ, notify the user and offer a one-key refresh that copies
newer bundled templates over the project copies and updates `cli_version`.

Per-template tracking was explicitly rejected — it adds complexity without
benefit because templates only change when the CLI package version changes.

### Existing system state

**Project manifest** — `.ievo/manifest.yaml` — is the Python dataclass
`ProjectManifest` in `src/ievo/core/project.py`:

```python
@dataclass
class ProjectManifest:
    project: str
    version: str = "0.1.0"
    created: str = ""
    agents: dict[str, str] = field(default_factory=dict)
    overrides: dict[str, dict] = field(default_factory=dict)
    # NEW field to add:
    # cli_version: str = ""
```

`ProjectManifest.load(root)` reads the YAML and constructs the dataclass.
`ProjectManifest.save(root)` serialises and writes back.

**IEVO_MANIFEST** constant: `".ievo/manifest.yaml"` (from `src/ievo/core/config.py`).

**CLI version** — `src/ievo/version.py`:
```python
__version__ = "0.1.0"
```
Import as: `from ievo.version import __version__`.

**Bundled templates** — `src/ievo/marketplace/templates/`:
- `IEVO.md`
- `REQ_TEMPLATE.md`
- `QUESTION_TEMPLATE.md`
- `PLAN_TEMPLATE.md`
- `CHANGE_REQUEST_TEMPLATE.md`
- `ACCEPTANCE_REPORT_TEMPLATE.md`
- `PROPOSAL_TEMPLATE.md`

Access via `src/ievo/marketplace/__init__.py`:
- `bundled_template_path(filename: str) -> Path | None` — returns path to
  a single bundled template, or `None` if it does not exist.
- `list_bundled_templates() -> list[str]` — returns sorted list of bundled
  template filenames.

**Project template destinations** — deployed by `ievo init` in
`src/ievo/commands/init.py`:
- `.ievo/IEVO.md` ← `IEVO.md`
- `spec/templates/REQ_TEMPLATE.md` ← `REQ_TEMPLATE.md`
- `spec/templates/QUESTION_TEMPLATE.md` ← `QUESTION_TEMPLATE.md`
- `spec/templates/PLAN_TEMPLATE.md` ← `PLAN_TEMPLATE.md`
- `spec/templates/CR_TEMPLATE.md` ← `CHANGE_REQUEST_TEMPLATE.md`
- `spec/templates/ACCEPTANCE_REPORT_TEMPLATE.md` ← `ACCEPTANCE_REPORT_TEMPLATE.md`
- `spec/templates/PROPOSAL_TEMPLATE.md` ← `PROPOSAL_TEMPLATE.md`

Constants: `SPEC_DIR = "spec"` from `src/ievo/core/config.py`.

**TUI** — `src/ievo/tui/app.py`, class `IevoApp(App)`. The analogous agent
version check (`compare_agent_versions`) runs in `watch_project_root()` and
fires a toast when stale agents are found. The same pattern applies here.

**Agent version check analogy** (already implemented as REQ-008):
```python
# In watch_project_root():
self._outdated = compare_agent_versions(manifest, self._agent_registry)
if self._outdated:
    self.notify("N agent(s) outdated. Press u to update all.",
                severity="warning", timeout=8)
```
Template version check follows the identical pattern, firing a separate toast.

**Key binding for update** — `BINDINGS` already has `("u", "update_outdated",
"Update outdated")` for agents. The template refresh needs its own binding:
`("t", "refresh_templates", "Refresh templates")`.

### Scope of "user modification" handling

Overwriting is unconditional — the user explicitly pressed `t` to trigger the
refresh. No content-hash comparison or per-file diff is performed. A single
warning notification is shown before overwrite stating that project template
files will be overwritten and any manual edits will be lost; the refresh
proceeds immediately (no second confirmation prompt in the TUI).

## Acceptance Criteria

### 1 — ProjectManifest gains cli_version field

- [x] `ProjectManifest` has a new `cli_version: str = ""` field.
- [x] `ProjectManifest.load(root)` reads `cli_version` from the YAML if present; defaults to `""` when the key is absent (backwards compat with existing manifests).
- [x] `ProjectManifest.save(root)` always writes `cli_version` to the YAML, even when it is `""`.

### 2 — ievo init writes cli_version

- [x] After `ievo init` completes, `.ievo/manifest.yaml` contains `cli_version: <current __version__>` (e.g. `cli_version: 0.1.0`).
- [x] Re-running `ievo init` on an already-initialised project (the manifest already exists) does NOT overwrite `cli_version` — the existing manifest is unchanged.

### 3 — Version comparison utility

- [x] A pure function `compare_template_versions(manifest: ProjectManifest | None, cli_version: str) -> bool` in `src/ievo/core/project.py` returns `True` when `manifest.cli_version != cli_version` AND `manifest.cli_version != ""`, returns `False` otherwise.
- [x] Given `manifest.cli_version = "0.1.0"` and `cli_version = "0.2.0"` → returns `True`.
- [x] Given `manifest.cli_version = "0.2.0"` and `cli_version = "0.2.0"` → returns `False`.
- [x] Given `manifest.cli_version = ""` (old project, field absent) and `cli_version = "0.1.0"` → returns `False` (silent — no nagging on projects that predate this feature).
- [x] Given `manifest = None` → returns `False`.

### 4 — TUI startup notification

- [x] On TUI startup in `watch_project_root()`, after loading the manifest, `compare_template_versions(manifest, __version__)` is called. If it returns `True`, a toast is shown: `"Templates may be outdated (project: X, cli: Y). Press t to refresh."` with severity `"warning"` and timeout 8 seconds, where `X` is `manifest.cli_version` and `Y` is the running `__version__`.
- [x] If `compare_template_versions` returns `False`, no template toast is shown.
- [x] The template toast is independent of the agent outdated toast — both may appear simultaneously if both conditions are true.
- [x] If `project_root` is `None` (no project), no template toast is shown.

### 5 — Template refresh action

- [x] `IevoApp` has a new action `action_refresh_templates()` bound to key `"t"` with footer label `"Refresh templates"`.
- [x] When `t` is pressed and `compare_template_versions` returns `False` (templates already current), a notification shows `"Templates are up to date"` and no files are changed.
- [x] When `t` is pressed and templates are outdated: all 7 bundled template files are copied over their project counterparts (overwriting any existing content), `manifest.cli_version` is updated to the running `__version__`, `manifest.save(root)` is called, and a success notification shows `"Templates refreshed (cli: X)"` where X is the new version.
- [x] After a successful refresh, a subsequent `compare_template_versions(manifest, __version__)` call returns `False`.

### 6 — Refresh is idempotent

- [x] Pressing `t` multiple times when templates are already current shows `"Templates are up to date"` each time — no files are written.

### 7 — Edge cases

- [x] If a bundled template file does not exist for a given destination (i.e. `bundled_template_path(filename)` returns `None`), that file is skipped silently — the refresh continues for the remaining templates.
- [x] If a destination directory does not exist (e.g. `spec/templates/` was deleted), it is created before writing the template files.
- [x] If `project_root` is `None` when `t` is pressed, the action is a no-op (no notification, no file writes).

## Negative Acceptance Criteria

- Does NOT implement per-template version tracking — there is a single `cli_version` field covering all templates.
- Does NOT compute or compare file content hashes — the CLI version string is the sole staleness signal.
- Does NOT show a confirmation prompt before overwriting — the `t` keystroke is the confirmation. Only a single warning notification is shown.
- Does NOT auto-refresh templates on startup — user must explicitly press `t`.
- Does NOT affect agent version tracking (REQ-008) — those remain separate.
- Does NOT modify templates that were written with the same CLI version — `cli_version == __version__` is not a refresh trigger.
- Does NOT nag users of projects initialised before this feature was added (manifests with `cli_version = ""`).
- Does NOT add a new tab or a dedicated UI panel — all interaction is through existing notification/toast system + keybinding.

## Technical Notes

- `compare_template_versions` must be a standalone pure function in `src/ievo/core/project.py` (parallel to `compare_agent_versions`) — not a method on `IevoApp`. Pure functions are trivial to unit test without mounting the TUI.
- Import `__version__` in `app.py` via `from ievo.version import __version__`. Do not hardcode the version string.
- The `"t"` binding must be added to `IevoApp.BINDINGS` alongside the existing `"u"` binding.
- Template copy logic: `shutil.copy2(bundled_path, dest_path)` — same pattern used in `_install_agent`. Use `_TEMPLATE_MAP` (a constant list of `(bundled_filename, project_dest_path_relative_to_root)` tuples) to avoid duplication between `init` and `action_refresh_templates`. Define it in a shared location (e.g. `src/ievo/commands/init.py` or a new `src/ievo/core/templates.py`).
- Tests for `compare_template_versions`: pure unit tests, no TUI mounting needed.
- Tests for `action_refresh_templates`: Textual Pilot tests in `tests/test_tui_integration.py` — use `app.run_test()`, `await pilot.press("t")`, verify `manifest.cli_version == __version__` after action, verify template files contain bundled content.

## Examples

### Example 1: Project with stale templates on startup

```
manifest.cli_version = "0.1.0"
running __version__  = "0.2.0"

compare_template_versions(manifest, "0.2.0") → True

TUI startup toast:
  "Templates may be outdated (project: 0.1.0, cli: 0.2.0). Press t to refresh."
  severity: "warning", timeout: 8s

User presses t →
  All 7 template files overwritten with bundled content
  manifest.cli_version = "0.2.0"
  manifest.save(root) called
  Toast: "Templates refreshed (cli: 0.2.0)"
```

### Example 2: Up-to-date project

```
manifest.cli_version = "0.2.0"
running __version__  = "0.2.0"

compare_template_versions(manifest, "0.2.0") → False
No toast on startup.

User presses t →
  Toast: "Templates are up to date"
  No files written.
```

### Example 3: Old project (cli_version missing from manifest)

```
manifest.cli_version = ""   # key absent in YAML, defaulted to ""
running __version__  = "0.2.0"

compare_template_versions(manifest, "0.2.0") → False
No toast. User is not prompted to refresh.
```

### Example 4: Fresh init writes cli_version

```
ievo init my-project

.ievo/manifest.yaml contains:
  project: my-project
  version: 0.1.0
  created: 2026-03-03
  cli_version: 0.1.0
  agents: {}
```
