---
phase: 01-core-hardening
plan: 05
type: execute
wave: 2
depends_on: [02]
files_modified:
  - src/ztlctl/infrastructure/vault.py
  - src/ztlctl/commands/_context.py
  - src/ztlctl/services/check.py
  - src/ztlctl/infrastructure/database/migrations/versions/003_schema_version_marker.py
  - tests/services/test_upgrade.py
  - README.md
autonomous: true
requirements: [HARD-08, HARD-03, HARD-04]

must_haves:
  truths:
    - "Stale schema is detected on vault initialization and emits a warning to stderr"
    - "Stale schema detection does NOT block CLI operation — it is a non-fatal warning"
    - "ztlctl upgrade applies pending Alembic migrations and stamps the version"
    - "Pre-Alembic vaults (no alembic_version row but tables exist) are treated as current, not stale"
    - "ztlctl check treats stale schema as an error category"
    - "README commands are accurate and match actual CLI help output"
  artifacts:
    - path: "src/ztlctl/infrastructure/vault.py"
      provides: "Schema stale detection on vault init"
      contains: "_check_schema_current"
    - path: "src/ztlctl/commands/_context.py"
      provides: "Stale schema warning on AppContext vault access"
      contains: "schema"
    - path: "tests/services/test_upgrade.py"
      provides: "Stale schema detection tests"
      contains: "test_stale_schema"
  key_links:
    - from: "src/ztlctl/infrastructure/vault.py"
      to: "alembic"
      via: "MigrationContext.get_current_revision() compared to ScriptDirectory head"
      pattern: "get_current_revision"
    - from: "src/ztlctl/commands/_context.py"
      to: "src/ztlctl/infrastructure/vault.py"
      via: "calls _check_schema_current on lazy vault init"
      pattern: "_check_schema_current"
---

<objective>
Add vault schema versioning with stale detection (HARD-08), fix CLI/UX rough edges (HARD-03), and audit documentation accuracy (HARD-04).

Purpose: Users and agents should get clear warnings about stale schemas, and docs/help text must match actual behavior.
Output: Schema stale detection wired into vault init, stale check in `ztlctl check`, optional v2 migration file, corrected README and help text.
</objective>

<execution_context>
@/Users/shparki/.claude/get-shit-done/workflows/execute-plan.md
@/Users/shparki/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-core-hardening/01-RESEARCH.md
@.planning/codebase/ARCHITECTURE.md

<interfaces>
<!-- Key types and contracts the executor needs. -->

From src/ztlctl/infrastructure/vault.py:
```python
class Vault:
    def __init__(self, settings: ZtlSettings): ...
    # Lazy DB init, engine creation
    @property
    def engine(self) -> Engine: ...
```

From src/ztlctl/commands/_context.py:
```python
class AppContext:
    @property
    def vault(self) -> Vault:
        # Lazy init — first access creates Vault
        ...
```

From src/ztlctl/services/upgrade.py:
```python
class UpgradeService(BaseService):
    def check_pending(self) -> ServiceResult: ...  # returns pending migrations
    def apply(self) -> ServiceResult: ...           # runs Alembic upgrade head
    def stamp_current(self) -> ServiceResult: ...   # stamps without migrating
    def _tables_exist(self) -> bool: ...            # checks if DB has tables
```

From src/ztlctl/infrastructure/database/migrations/:
- versions/001_baseline.py
- versions/002_node_timestamps.py
- env.py, build_config() function

PITFALL (from RESEARCH.md): Pre-Alembic vaults have tables but no
alembic_version row. MigrationContext.get_current_revision() returns None.
The stale check must handle this: if current is None and tables exist,
it is NOT stale (UpgradeService.apply() handles stamping).
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add schema stale detection and stale check integration</name>
  <files>src/ztlctl/infrastructure/vault.py, src/ztlctl/commands/_context.py, src/ztlctl/services/check.py, tests/services/test_upgrade.py</files>
  <read_first>
    - src/ztlctl/infrastructure/vault.py (full file — understand lazy init, engine property, _init_db method)
    - src/ztlctl/commands/_context.py (full file — understand AppContext.vault lazy property)
    - src/ztlctl/services/upgrade.py (full file — understand check_pending, apply, _tables_exist, build_config)
    - src/ztlctl/infrastructure/database/migrations/env.py (build_config function)
    - src/ztlctl/services/check.py (check method — understand scan categories, where to add schema check)
    - tests/services/test_upgrade.py (existing test patterns)
  </read_first>
  <action>
    **1. Add _check_schema_current() to Vault (src/ztlctl/infrastructure/vault.py):**

    Add a method to the Vault class:
    ```python
    def _check_schema_current(self) -> bool:
        """Return True if database schema is at the latest Alembic revision.

        Returns True for pre-Alembic vaults (tables exist but no alembic_version
        row) — UpgradeService.apply() handles stamping those.
        """
        from alembic.runtime.migration import MigrationContext
        from alembic.script import ScriptDirectory
        from ztlctl.infrastructure.database.migrations import build_config

        cfg = build_config(f"sqlite:///{self._db_path}")
        script = ScriptDirectory.from_config(cfg)
        head = script.get_current_head()
        with self.engine.connect() as conn:
            ctx = MigrationContext.configure(conn)
            current = ctx.get_current_revision()
        # Pre-Alembic vaults: tables exist but no version stamp → treat as current
        if current is None:
            return True
        return current == head
    ```

    Use lazy local imports (following the 6 established precedents). `self._db_path` should be derived from the existing vault path — check how the Vault stores its DB path.

    **2. Wire stale warning into AppContext (src/ztlctl/commands/_context.py):**

    In the `vault` property (lazy init), after the Vault is created and initialized, check schema:
    ```python
    @property
    def vault(self) -> Vault:
        if self._vault is None:
            self._vault = Vault(self._settings)
            # ... existing init code ...
            if not self._vault._check_schema_current():
                click.echo(
                    "WARNING: Vault schema is out of date. "
                    "Run 'ztlctl upgrade' to apply pending migrations.",
                    err=True,
                )
        return self._vault
    ```

    The warning goes to stderr (err=True) and does NOT block operation. Users can still run read-only commands.

    **3. Add schema stale check to CheckService.check() (src/ztlctl/services/check.py):**

    In the `check()` method, add a new check category (alongside the existing 4 categories: db_file_consistency, schema_integrity, graph_health, structural_validation):

    Add a method:
    ```python
    def _check_schema_version(self) -> list[dict[str, Any]]:
        """Check if vault schema is at the latest Alembic revision."""
        issues: list[dict[str, Any]] = []
        if not self._vault._check_schema_current():
            issues.append({
                "category": "schema_version",
                "severity": "error",
                "message": "Vault schema is out of date. Run 'ztlctl upgrade' to apply pending migrations.",
                "fix": "Run: ztlctl upgrade",
            })
        return issues
    ```

    Call this method in `check()` alongside the other checks.

    **4. Add tests in tests/services/test_upgrade.py:**

    - `test_check_schema_current_returns_true_when_at_head`: Create a vault, run upgrade, verify `_check_schema_current()` returns True.
    - `test_check_schema_current_handles_pre_alembic_vault`: Create a vault with tables but no alembic_version row. Verify `_check_schema_current()` returns True (not stale).
    - `test_stale_schema_warning_on_vault_access`: Mock `_check_schema_current` to return False. Access `app.vault`. Verify stderr contains the warning message.
  </action>
  <verify>
    <automated>uv run pytest tests/services/test_upgrade.py -x -v && uv run ruff check src/ztlctl/infrastructure/vault.py src/ztlctl/commands/_context.py src/ztlctl/services/check.py && uv run mypy src/ztlctl/infrastructure/vault.py src/ztlctl/commands/_context.py --strict</automated>
  </verify>
  <acceptance_criteria>
    - src/ztlctl/infrastructure/vault.py contains `def _check_schema_current(self) -> bool:`
    - src/ztlctl/infrastructure/vault.py contains `get_current_revision`
    - src/ztlctl/infrastructure/vault.py contains `if current is None:` with `return True`
    - src/ztlctl/commands/_context.py contains `_check_schema_current`
    - src/ztlctl/commands/_context.py contains `WARNING: Vault schema is out of date`
    - src/ztlctl/services/check.py contains `_check_schema_version` method
    - tests/services/test_upgrade.py contains `def test_check_schema_current`
    - `uv run pytest tests/services/test_upgrade.py -x -v` exits 0
  </acceptance_criteria>
  <done>Schema stale detection added to Vault. Non-fatal warning emitted on AppContext.vault access. CheckService escalates stale schema to error category. Pre-Alembic vaults handled correctly. Tests pass.</done>
</task>

<task type="auto">
  <name>Task 2: UX polish and documentation audit</name>
  <files>README.md, src/ztlctl/commands/serve.py</files>
  <read_first>
    - README.md (full file — check for outdated commands, missing commands, incorrect examples)
    - src/ztlctl/commands/ (ls to see all command files)
    - .planning/codebase/CONCERNS.md (fragile areas section — Copier recopy fallback UX issue)
  </read_first>
  <action>
    **1. README audit (HARD-04):**

    Run `uv run ztlctl --help` and capture the output. Compare against README.md. Fix any discrepancies:

    - Verify all commands mentioned in README exist in `ztlctl --help` output
    - Verify all `ztlctl --help` commands are documented in README
    - Check that command examples use correct flags and syntax
    - Verify the "Installation" section is accurate for current pyproject.toml
    - Verify the "Quick Start" section steps actually work

    Commands to verify are documented (from CLAUDE.md memory):
    - vector, workflow, graph unlink, --log-json (added in v1.1.1 README update)
    - upgrade (added with Alembic infrastructure)
    - serve (MCP server)
    - check (integrity scanner)
    - archive, extract, supersede (lifecycle operations)

    **2. CLI help text audit (HARD-03 + HARD-04):**

    Run `uv run ztlctl <command> --help` for each command group. Verify:
    - Help text is accurate (matches actual behavior)
    - No misleading descriptions
    - Progressive disclosure is consistent (brief help, --help for details)

    Fix any CLI help strings in the Click command decorators that are inaccurate.

    **3. Copier recopy fallback UX (HARD-03):**

    In the serve command's HTTP transport, the warning was already added in Plan 02.

    For the Copier recopy fallback (identified in CONCERNS.md fragile areas): verify that `src/ztlctl/services/workflow.py` includes the fallback warning in ServiceResult. If the warning is not prominently surfaced at the CLI level, add a click.echo to the workflow update command:
    ```python
    # In the CLI command handler for workflow update, after calling the service:
    if result.warnings:
        for w in result.warnings:
            click.echo(f"WARNING: {w}", err=True)
    ```
    (Only add if this pattern isn't already in place.)
  </action>
  <verify>
    <automated>uv run pytest tests/ -x -q && uv run ruff check . && uv run ruff format --check .</automated>
  </verify>
  <acceptance_criteria>
    - README.md mentions `ztlctl upgrade` command
    - README.md mentions `ztlctl serve` command
    - README.md mentions `ztlctl check` command
    - README.md mentions `--log-json` flag
    - `uv run pytest tests/ -x -q` exits 0
    - `uv run ruff check .` exits 0
    - `uv run ruff format --check .` exits 0
  </acceptance_criteria>
  <done>README accurately documents all commands and flags. CLI help text is consistent. Copier recopy fallback warning is surfaced. No docs/code regressions.</done>
</task>

</tasks>

<verification>
- `uv run pytest tests/ -x -q` — full suite passes
- `uv run ruff check . && uv run ruff format --check .` — clean
- `uv run mypy src/ --strict` — no type errors
- `uv run ztlctl --help` output matches README command list
</verification>

<success_criteria>
- Schema stale detection works on vault init with non-fatal warning
- Pre-Alembic vaults not falsely flagged as stale
- ztlctl check includes schema version check category
- README accurately lists all commands and flags
- CLI help text is accurate and consistent
- All tests pass, mypy strict, ruff clean
</success_criteria>

<output>
After completion, create `.planning/phases/01-core-hardening/01-05-SUMMARY.md`
</output>
