# REQ-005: Startup Version Check

## Metadata
- **Status:** ready
- **Priority:** medium
- **Dependencies:** REQ-004
- **Blocks:** —
- **Questions:** 0
- **Created:** 2026-03-03

## Context

Every time the user runs `ievo` (with or without arguments), the CLI entry point in `src/ievo/cli.py` calls `ensure_startup(home)` from `src/ievo/core/startup.py`. That function currently handles Claude auth and telemetry opt-in — it is the correct hook for adding a passive version check.

**What REQ-004 provides (must be implemented first):**
- `src/ievo/core/updater.py` — version check and installer detection module.
- `fetch_latest_version() -> str | None` — fetches latest version string from `https://pypi.org/pypi/ievo/json` using `httpx`. Returns `None` on any network failure.
- Current installed version is read via `importlib.metadata.version("ievo")`.

**The check must be non-blocking.** If PyPI is slow or unreachable, startup must proceed normally — the user must never wait on a version check to use ievo.

**Throttle (cache-based).** The version check must not hit PyPI on every launch. Cache the result for 24 hours in `~/.ievo/config.json` under these keys:
- `"version_check_last_run"` — ISO 8601 timestamp of the last check
- `"version_check_result"` — latest version string found during that check (or `null`)

On each launch: if `version_check_last_run` exists and is less than 24 hours ago, skip the network call entirely and use `version_check_result` from cache. If 24+ hours have passed (or no cache), run the check and update both keys.

**Notification message.** When a newer version is available, print a single informational line using the `info()` console helper before any other output:

```
A new version of ievo is available: v0.1.2. Run 'ievo update' to upgrade.
```

The version displayed is the latest PyPI version. The currently installed version is NOT shown in this message (it is shown in the full `ievo update` output).

**No message when up to date.** If installed version equals latest, print nothing. Silent is correct.

**No message on cache miss + network error.** If the check is attempted and PyPI is unreachable, print nothing — fail silently. Do not show an error to the user at startup.

**Where in startup.** The version notification must run AFTER telemetry and auth setup, at the end of `ensure_startup()`. Reason: auth and telemetry setup is higher priority than an optional version hint.

**Key files:**
- `src/ievo/core/startup.py` — add version notification logic at end of `ensure_startup()`
- `src/ievo/core/updater.py` — `fetch_latest_version()` is reused from REQ-004 (do not duplicate)
- `src/ievo/core/global_config.py` — `get_config(home, key)` / `set_config(home, key, value)` for reading and writing the cache keys
- `tests/test_startup.py` — existing test file; extend with new version-check tests

**Console helpers** (from `ievo.utils.console`):
- `info(msg)` — dim informational text; use this for the notification

**Global config format** (`~/.ievo/config.json` managed by `src/ievo/core/global_config.py`):
```json
{
  "claude_auth": "...",
  "telemetry": true,
  "version_check_last_run": "2026-03-03T10:00:00",
  "version_check_result": "0.1.2"
}
```

## User-facing behavior

**Scenario A — new version available, cache expired:**
```
$ ievo
A new version of ievo is available: v0.1.2. Run 'ievo update' to upgrade.
[TUI dashboard opens normally]
```

**Scenario B — already up to date (no output):**
```
$ ievo
[TUI dashboard opens normally, no version message]
```

**Scenario C — check throttled (cache fresh, < 24h):**
No network call. If cached result showed a newer version → still show notification. If cached result showed up to date → no output.

**Scenario D — network unreachable:**
```
$ ievo
[TUI dashboard opens normally, no error, no version message]
```

## Acceptance Criteria

- `ensure_startup()` when `version_check_last_run` is absent or older than 24 hours → calls `fetch_latest_version()`, stores the result in `version_check_result` and updates `version_check_last_run` in `~/.ievo/config.json`
- `ensure_startup()` when `version_check_last_run` is less than 24 hours ago → does NOT call `fetch_latest_version()` (zero network calls); uses cached `version_check_result` for the notification decision
- `ensure_startup()` when fetched latest version is greater than installed version → prints exactly one `info()` line containing the new version and the text `'ievo update'`
- `ensure_startup()` when installed version equals latest (or cached result equals installed) → prints no version-related output
- `ensure_startup()` when `fetch_latest_version()` returns `None` (network failure) → prints no error, proceeds normally, does NOT update `version_check_result` or `version_check_last_run`
- `ensure_startup()` version notification runs AFTER auth and telemetry steps (i.e., it is the last step inside `ensure_startup()`)

## Negative Acceptance Criteria

- Does NOT block startup — if the network call takes longer than the configured timeout (inherited from `fetch_latest_version()`'s 10s timeout), startup is NOT delayed beyond that; a non-blocking approach (thread or cached-only path) is preferred but implementation detail is for Architect to decide
- Does NOT auto-update — prints notification only; never invokes `uv tool upgrade` or any update subprocess
- Does NOT show an error message to the user when PyPI is unreachable
- Does NOT display the currently installed version in the startup notification (only the new version)
- Does NOT check for updates more than once per 24 hours per machine (per `~/.ievo/config.json`)
- Does NOT require a project directory (`.ievo/manifest.yaml` not needed — version check is global, not project-scoped)

## Technical Notes

- Reuse `fetch_latest_version()` from `src/ievo/core/updater.py` (REQ-004). Do NOT duplicate the PyPI fetch logic.
- Cache keys in `~/.ievo/config.json`: `version_check_last_run` (ISO 8601 string), `version_check_result` (version string or `null`).
- Version comparison: use `packaging.version.Version` for correct semver comparison (already available via the `packaging` package, which is a transitive dependency of pip). Alternatively, `importlib.metadata` / tuple comparison on split strings is acceptable — Architect decides.
- The 24-hour threshold is hardcoded (not user-configurable).
- `set_config(home, key, value)` from `global_config.py` must support writing any JSON-serializable value to `config.json`. If this function only supports specific keys today, it may need to be extended — Architect assesses.
