# Helix v1 Cold-Start CLI Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Ship the locked v1 helix-cli (`helix query`, `helix status`, `helix diag corpus`, `helix config show`, `helix ingest`) as a cold-start CLI on top of `helix_context.api.HelixSession`, so the walk-bench harness has its required surface and end users get `pip install helix-context` → `helix query …`.

**Architecture:** New `helix_context/cli/` package with stdlib `argparse` dispatcher + per-subcommand modules; each subcommand opens (or reuses) a `HelixSession` via `helix_context.api.open_session()` and renders text or JSON to stdout. SQLite is opened read-only for query/diag/config/status; ingest takes the single-writer path. No daemon — every invocation is a fresh process (cold-start). The daemon-mode auto-detect from `HELIX_DAEMON_DESIGN.md` is **out of scope** for this plan.

**Tech Stack:** Python 3.11+, stdlib `argparse`, `helix_context.api` (already wired with `adaptive_caps=False`), `helix_context.config.load_config`, `pytest` for tests. No new runtime deps.

---

## Source Spec

The locked CLI surface (cite verbatim — do not redesign):

```bash
helix query "..." [--k N] [--json] [--tier broad|focused] [--learn]
helix status                  # daemon up? corpus indexed? config valid?
helix diag corpus             # corpus shape, tier distribution, staleness
helix config show             # effective config (helix.toml + env overrides)
helix ingest <path>           # only if reliable; defer if not
```

— `docs/benchmarks/SESSION_AWARE_BENCH_DESIGN.md` §12.1 (RESOLVED 2026-05-11).

Deferred (do NOT implement in this plan):
- `helix serve` (and `--detach`, `--stop`, `--status`, `--reload-config`, `--no-fastapi`) — daemon, see `docs/architecture/HELIX_DAEMON_DESIGN.md`. Implement a friendly stub that exits 4 with a "deferred" message so users don't think it crashed.
- `helix label set`, `helix drift baseline/compare`, `helix rerank`, `helix dims` — v2.

## Open question (call out, then proceed)

The spec uses `--tier broad|focused`. The internal `decoder_mode` taxonomy in `helix_context/mcp_server.py:324` is `{condensed, broad, dense}`. Mapping for v1:
- `--tier broad` → `decoder_mode="broad"`
- `--tier focused` → `decoder_mode="condensed"`
- omitted → no override (classifier picks)

Document the drift in the cli `--help` text and in `docs/clients/cli.md` (this plan creates it). Resolving the terminology is a v1.1 follow-up; not blocking.

## File Structure

```
helix_context/
  cli/
    __init__.py        # exports main()
    dispatcher.py      # top-level argparse, routes to cmd_*
    output.py          # tty vs --json rendering helpers, exit codes
    cmd_query.py       # helix query
    cmd_status.py      # helix status
    cmd_diag.py        # helix diag corpus
    cmd_config.py      # helix config show
    cmd_ingest.py      # helix ingest
    cmd_serve.py       # helix serve — stub that exits 4 "deferred"
tests/
  test_cli_dispatcher.py
  test_cli_config.py
  test_cli_status.py
  test_cli_query.py
  test_cli_ingest.py
  test_cli_diag.py
  test_cli_serve_stub.py
  test_cli_integration.py   # one end-to-end test with HELIX_GENOME_PATH=:memory:
docs/
  clients/
    cli.md              # operator docs — usage, exit codes, env vars
  superpowers/
    plans/
      2026-05-11-helix-cli-v1.md   # this plan
```

`pyproject.toml` change:
```toml
[project.scripts]
helix = "helix_context.cli:main"              # NEW (replaces server entry)
helix-server = "helix_context.server:main"    # NEW back-compat for legacy `helix` → server
helix-launcher = "helix_context.launcher.app:main"
helix-status = "helix_status:main"            # unchanged — separate tool
helix-vault = "helix_context.vault.cli:main"  # unchanged
```

Rationale: the legacy `helix` entry currently launches the FastAPI server (`helix_context.server:main`). Replacing it would break anyone with muscle memory; renaming the legacy to `helix-server` keeps that workflow alive while the new `helix` becomes the subcommand dispatcher.

## Exit-code contract (used by every subcommand)

| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Operation failed (genome unreadable, ingest write error, etc.) |
| 2 | Bad CLI arguments (delegated to argparse default) |
| 3 | Status check failed (genome missing, config invalid) — `helix status` only |
| 4 | Subcommand deferred / not implemented (e.g. `helix serve`) |

Defined as constants in `helix_context/cli/output.py` so tests can import them.

---

### Task 1: Scaffold the cli package + dispatcher entry point

**Files:**
- Create: `helix_context/cli/__init__.py`
- Create: `helix_context/cli/dispatcher.py`
- Create: `helix_context/cli/output.py`
- Modify: `pyproject.toml` lines 112-116 (`[project.scripts]` block)
- Test: `tests/test_cli_dispatcher.py`

- [ ] **Step 1: Write the failing test**

Create `tests/test_cli_dispatcher.py`:

```python
"""Tests for the top-level `helix` CLI dispatcher (no subcommand work yet)."""
from __future__ import annotations

import io
import contextlib

import pytest

from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


def test_no_args_prints_help_and_returns_nonzero():
    rc, out, err = _run([])
    assert rc != 0, "running `helix` with no subcommand should be an error"
    combined = (out + err).lower()
    assert "usage:" in combined
    assert "helix" in combined


def test_help_flag_exits_zero():
    with pytest.raises(SystemExit) as exc:
        main(["--help"])
    # argparse exits 0 on --help
    assert exc.value.code == 0


def test_unknown_subcommand_returns_two():
    with pytest.raises(SystemExit) as exc:
        main(["does-not-exist"])
    # argparse exits 2 on unknown choice
    assert exc.value.code == 2
```

- [ ] **Step 2: Run test to verify it fails**

```bash
python -m pytest tests/test_cli_dispatcher.py -v
```
Expected: FAIL with `ModuleNotFoundError: No module named 'helix_context.cli'`.

- [ ] **Step 3: Create the dispatcher and output helpers**

Create `helix_context/cli/output.py`:

```python
"""Shared output helpers + exit-code constants for the helix CLI.

Every subcommand returns one of these integers from main(). Keeps the
contract testable without parsing stderr.
"""
from __future__ import annotations

import json
import sys
from typing import Any

# Exit codes (see docs/superpowers/plans/2026-05-11-helix-cli-v1.md).
EXIT_OK = 0
EXIT_ERROR = 1
EXIT_BAD_ARGS = 2          # argparse hands this out itself
EXIT_STATUS_FAIL = 3       # `helix status` only
EXIT_DEFERRED = 4          # `helix serve` and any other "not yet" subcommand


def print_json(obj: Any) -> None:
    """Stable, machine-readable JSON output to stdout.

    sort_keys=True so the bench walker can hash deterministic output.
    """
    sys.stdout.write(json.dumps(obj, indent=2, sort_keys=True, default=str))
    sys.stdout.write("\n")


def print_lines(lines: list[str]) -> None:
    """TTY-friendly newline-joined output."""
    sys.stdout.write("\n".join(lines))
    sys.stdout.write("\n")


def eprint(msg: str) -> None:
    """Single-line error to stderr (no trailing newline-on-newline)."""
    sys.stderr.write(msg.rstrip() + "\n")
```

Create `helix_context/cli/dispatcher.py`:

```python
"""Top-level argparse dispatcher for `helix <subcommand>`.

Subcommands are registered via add_parser; each one points at a
``cmd_*.run(args)`` callable that returns an integer exit code.

This module deliberately does NOT import the heavy retrieval stack at
module-load time — subcommand modules import their dependencies lazily
inside ``run()`` so `helix --help` stays sub-100ms cold-start.
"""
from __future__ import annotations

import argparse
from typing import Callable, Optional

from . import output


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="helix",
        description=(
            "Helix Context CLI — cold-start retrieval over a local genome. "
            "See `helix <subcommand> --help` for details."
        ),
    )
    sub = parser.add_subparsers(dest="subcommand", metavar="<subcommand>")

    # Each cmd module owns its own argparse population; we only declare
    # the subcommand stub here so --help lists it.
    sub.add_parser(
        "query",
        help="Run one retrieval pipeline pass over the genome and print the result.",
    )
    sub.add_parser(
        "ingest",
        help="Add a file (or directory of files) to the genome.",
    )
    sub.add_parser(
        "status",
        help="Check whether the genome + config + (optional) server are healthy.",
    )
    sub.add_parser(
        "diag",
        help="Diagnostic introspection (e.g. `helix diag corpus`).",
    )
    sub.add_parser(
        "config",
        help="Inspect effective configuration (`helix config show`).",
    )
    sub.add_parser(
        "serve",
        help="(DEFERRED in v1; see HELIX_DAEMON_DESIGN.md)",
    )

    return parser


def _resolve(name: str) -> Optional[Callable[[list[str]], int]]:
    """Lazy-load the cmd module so unused subcommands don't pay import cost."""
    if name == "query":
        from . import cmd_query
        return cmd_query.run
    if name == "ingest":
        from . import cmd_ingest
        return cmd_ingest.run
    if name == "status":
        from . import cmd_status
        return cmd_status.run
    if name == "diag":
        from . import cmd_diag
        return cmd_diag.run
    if name == "config":
        from . import cmd_config
        return cmd_config.run
    if name == "serve":
        from . import cmd_serve
        return cmd_serve.run
    return None


def main(argv: Optional[list[str]] = None) -> int:
    parser = _build_parser()
    # We don't use parser.parse_args() for dispatch because each
    # subcommand defines its own flags — argparse subparsers would
    # require declaring all subcommand args here, defeating the lazy-
    # import design. Instead: peel off the first positional and pass
    # the rest to the subcommand.
    args = argv if argv is not None else None
    # `helix` with no args → print help to stderr, return 1.
    if not args:
        import sys
        parser.print_help(file=sys.stderr)
        return output.EXIT_ERROR
    # `helix --help` / `helix -h` → argparse default (exit 0).
    if args[0] in ("--help", "-h"):
        parser.parse_args(args)  # this SystemExit(0)s
        return output.EXIT_OK    # unreachable, kept for type clarity

    sub_name, sub_argv = args[0], args[1:]
    runner = _resolve(sub_name)
    if runner is None:
        # Force argparse to do the "invalid choice" error formatting for us
        # so the message matches what users expect.
        parser.parse_args(args)
        return output.EXIT_BAD_ARGS  # unreachable

    return runner(sub_argv)
```

Create `helix_context/cli/__init__.py`:

```python
"""Helix Context CLI — cold-start retrieval surface.

Entry point: ``helix`` (configured in pyproject.toml).

See ``docs/superpowers/plans/2026-05-11-helix-cli-v1.md`` for design.
"""
from __future__ import annotations

from .dispatcher import main

__all__ = ["main"]
```

Modify `pyproject.toml` lines 112-116 (replace the `[project.scripts]` block):

```toml
[project.scripts]
helix = "helix_context.cli:main"
helix-server = "helix_context.server:main"
helix-launcher = "helix_context.launcher.app:main"
helix-status = "helix_status:main"
helix-vault = "helix_context.vault.cli:main"
```

- [ ] **Step 4: Run test to verify it passes**

```bash
python -m pytest tests/test_cli_dispatcher.py -v
```
Expected: PASS (3 tests).

- [ ] **Step 5: Smoke the entry point in editable install**

```bash
pip install -e . --quiet
helix --help
```
Expected: prints subcommand list, exits 0. (Run `helix` alone and confirm it exits 1 with usage to stderr.)

- [ ] **Step 6: Commit**

```bash
git add helix_context/cli/__init__.py helix_context/cli/dispatcher.py helix_context/cli/output.py tests/test_cli_dispatcher.py pyproject.toml
git commit -m "feat(cli): scaffold helix CLI dispatcher + entry point

Adds helix_context/cli package with stdlib-argparse dispatcher,
exit-code constants, and the new \`helix\` entry point that replaces
the previous FastAPI launcher (now reachable as \`helix-server\`).
No subcommands wired yet — that's tasks 2-7.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 2: `helix config show`

**Files:**
- Create: `helix_context/cli/cmd_config.py`
- Test: `tests/test_cli_config.py`

`helix config show` prints the effective config — `helix.toml` + env overrides — in JSON (default) or text. No genome access; pure config introspection. This is the safest subcommand to implement first because it has zero retrieval-stack imports.

- [ ] **Step 1: Write the failing tests**

Create `tests/test_cli_config.py`:

```python
"""Tests for `helix config show`."""
from __future__ import annotations

import io
import json
import contextlib

from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


def test_config_show_emits_json_by_default(monkeypatch, tmp_path):
    # Use a tmp helix.toml so the test doesn't depend on cwd helix.toml.
    cfg = tmp_path / "helix.toml"
    cfg.write_text(
        "[budget]\nribosome_tokens = 3500\n[server]\nport = 11437\n",
        encoding="utf-8",
    )
    monkeypatch.setenv("HELIX_CONFIG", str(cfg))

    rc, out, err = _run(["config", "show"])
    assert rc == 0, err
    payload = json.loads(out)
    assert payload["budget"]["ribosome_tokens"] == 3500
    assert payload["server"]["port"] == 11437


def test_config_show_text_mode(monkeypatch, tmp_path):
    cfg = tmp_path / "helix.toml"
    cfg.write_text("[budget]\nribosome_tokens = 4000\n", encoding="utf-8")
    monkeypatch.setenv("HELIX_CONFIG", str(cfg))

    rc, out, err = _run(["config", "show", "--text"])
    assert rc == 0, err
    # Text mode is human-readable; just check the key+value appear.
    assert "ribosome_tokens" in out
    assert "4000" in out


def test_config_show_falls_back_to_defaults_when_no_toml(monkeypatch, tmp_path):
    # Point HELIX_CONFIG at a non-existent file so load_config returns defaults.
    monkeypatch.setenv("HELIX_CONFIG", str(tmp_path / "missing.toml"))

    rc, out, err = _run(["config", "show"])
    assert rc == 0, err
    payload = json.loads(out)
    # Defaults populate every section as a dict.
    assert isinstance(payload.get("budget"), dict)
    assert isinstance(payload.get("server"), dict)
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
python -m pytest tests/test_cli_config.py -v
```
Expected: FAIL (`cmd_config` module not found, dispatcher resolver returns None).

- [ ] **Step 3: Implement `cmd_config.py`**

Create `helix_context/cli/cmd_config.py`:

```python
"""`helix config show` — print effective helix.toml + env overrides."""
from __future__ import annotations

import argparse
import dataclasses
from typing import Any, Dict

from . import output


def _config_to_dict(cfg: Any) -> Dict[str, Any]:
    """Convert a HelixConfig dataclass to a JSON-serializable dict.

    HelixConfig has nested dataclasses; dataclasses.asdict walks them
    recursively. Non-dataclass fields (synonym_map: dict[str, list[str]])
    pass through unchanged.
    """
    return dataclasses.asdict(cfg)


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="helix config",
        description="Inspect the effective Helix configuration.",
    )
    sub = parser.add_subparsers(dest="action", metavar="<action>")
    show = sub.add_parser("show", help="Print effective config (helix.toml + env).")
    show.add_argument(
        "--text",
        action="store_true",
        help="Print as flat key=value lines instead of JSON.",
    )
    show.add_argument(
        "--config",
        default=None,
        help="Path to helix.toml (default: $HELIX_CONFIG or ./helix.toml).",
    )
    return parser


def run(argv: list[str]) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)
    if args.action != "show":
        parser.print_help()
        return output.EXIT_BAD_ARGS

    # Late import — avoid loading config module when other subcommands run.
    from helix_context.config import load_config

    cfg = load_config(args.config)
    payload = _config_to_dict(cfg)

    if args.text:
        lines = _flatten_for_text(payload)
        output.print_lines(lines)
    else:
        output.print_json(payload)
    return output.EXIT_OK


def _flatten_for_text(obj: Any, prefix: str = "") -> list[str]:
    """Render nested dict/list as `dotted.key = value` lines.

    Lists serialize as JSON-style brackets so the reader can tell them
    apart from scalars.
    """
    import json
    lines: list[str] = []
    if isinstance(obj, dict):
        for k, v in obj.items():
            key = f"{prefix}.{k}" if prefix else k
            if isinstance(v, dict):
                lines.extend(_flatten_for_text(v, prefix=key))
            elif isinstance(v, list):
                lines.append(f"{key} = {json.dumps(v)}")
            else:
                lines.append(f"{key} = {v}")
    else:
        lines.append(f"{prefix} = {obj}")
    return lines
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
python -m pytest tests/test_cli_config.py -v
```
Expected: PASS (3 tests).

- [ ] **Step 5: Smoke**

```bash
helix config show --text 2>&1 | head -20
```
Expected: human-readable `key.subkey = value` lines.

- [ ] **Step 6: Commit**

```bash
git add helix_context/cli/cmd_config.py tests/test_cli_config.py
git commit -m "feat(cli): \`helix config show\` — print effective config

JSON by default, --text for flat key=value lines. Honors --config
flag and HELIX_CONFIG env. No genome dependency.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 3: `helix status`

**Files:**
- Create: `helix_context/cli/cmd_status.py`
- Test: `tests/test_cli_status.py`

`helix status` answers three questions: "is the genome on disk and readable?", "is the config valid?", "is the HTTP server up (optional)?". Reuses `helix_status.collect_status` for the HTTP/launcher/MCP probes — that file already has the right shape. Adds cold-start-specific checks for genome and config.

- [ ] **Step 1: Write the failing tests**

Create `tests/test_cli_status.py`:

```python
"""Tests for `helix status`."""
from __future__ import annotations

import io
import json
import contextlib
import sqlite3

from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


def _make_genome_file(path):
    """Create a minimal SQLite file so cold-start probe can open it RO."""
    conn = sqlite3.connect(str(path))
    conn.execute("CREATE TABLE IF NOT EXISTS genes (id TEXT PRIMARY KEY, content TEXT)")
    conn.commit()
    conn.close()


def test_status_exit_3_when_genome_missing(monkeypatch, tmp_path):
    cfg = tmp_path / "helix.toml"
    cfg.write_text(
        f"[genome]\npath = \"{tmp_path / 'does-not-exist.db'}\"\n",
        encoding="utf-8",
    )
    monkeypatch.setenv("HELIX_CONFIG", str(cfg))

    rc, out, err = _run(["status", "--json", "--no-network"])
    assert rc == 3, err
    payload = json.loads(out)
    assert payload["genome"]["reachable"] is False
    assert "next_action" in payload


def test_status_exit_0_when_genome_present(monkeypatch, tmp_path):
    genome = tmp_path / "genome.db"
    _make_genome_file(genome)
    cfg = tmp_path / "helix.toml"
    cfg.write_text(
        f"[genome]\npath = \"{genome}\"\n",
        encoding="utf-8",
    )
    monkeypatch.setenv("HELIX_CONFIG", str(cfg))

    rc, out, err = _run(["status", "--json", "--no-network"])
    assert rc == 0, err
    payload = json.loads(out)
    assert payload["genome"]["reachable"] is True
    assert payload["config"]["valid"] is True


def test_status_text_mode_human_readable(monkeypatch, tmp_path):
    genome = tmp_path / "genome.db"
    _make_genome_file(genome)
    cfg = tmp_path / "helix.toml"
    cfg.write_text(f"[genome]\npath = \"{genome}\"\n", encoding="utf-8")
    monkeypatch.setenv("HELIX_CONFIG", str(cfg))

    rc, out, err = _run(["status", "--no-network"])
    assert rc == 0, err
    assert "Genome:" in out
    assert "Config:" in out
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
python -m pytest tests/test_cli_status.py -v
```
Expected: FAIL (`cmd_status` not found).

- [ ] **Step 3: Implement `cmd_status.py`**

Create `helix_context/cli/cmd_status.py`:

```python
"""`helix status` — cold-start health check."""
from __future__ import annotations

import argparse
import sqlite3
from pathlib import Path
from typing import Any, Dict

from . import output


def _probe_genome(path: str) -> Dict[str, Any]:
    """Open the genome read-only and count rows. Cheap; no full pipeline."""
    p = Path(path)
    if not p.exists():
        return {
            "reachable": False,
            "path": str(p),
            "error": "file_not_found",
            "next_action": f"Create the genome by ingesting content: `helix ingest <path>`. Expected at {p}.",
        }
    try:
        # Open URI-style read-only so we don't accidentally lock the file.
        uri = f"file:{p.as_posix()}?mode=ro"
        conn = sqlite3.connect(uri, uri=True, timeout=2.0)
        try:
            row = conn.execute("SELECT COUNT(*) FROM genes").fetchone()
            gene_count = int(row[0]) if row else 0
        finally:
            conn.close()
        return {
            "reachable": True,
            "path": str(p),
            "gene_count": gene_count,
        }
    except sqlite3.DatabaseError as exc:
        return {
            "reachable": False,
            "path": str(p),
            "error": "not_a_helix_genome",
            "detail": str(exc),
            "next_action": "Delete the file and re-ingest, or point [genome] path at the correct DB.",
        }


def _probe_config(config_path: str | None) -> Dict[str, Any]:
    """Validate helix.toml loads without error."""
    from helix_context.config import load_config
    try:
        cfg = load_config(config_path)
        return {
            "valid": True,
            "path": config_path or "(defaults)",
            "genome_path": cfg.genome.path,
            "server_port": cfg.server.port,
        }
    except Exception as exc:
        return {
            "valid": False,
            "path": config_path or "(defaults)",
            "error": type(exc).__name__,
            "detail": str(exc),
            "next_action": "Fix the [genome]/[server]/... sections in helix.toml.",
        }


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="helix status",
        description="Check genome / config / (optional) HTTP server health.",
    )
    parser.add_argument("--json", action="store_true", help="Machine-readable output.")
    parser.add_argument(
        "--no-network",
        action="store_true",
        help="Skip HTTP server / launcher probes (offline check only).",
    )
    parser.add_argument(
        "--config",
        default=None,
        help="Path to helix.toml (default: $HELIX_CONFIG or ./helix.toml).",
    )
    return parser


def _render_text(report: Dict[str, Any]) -> list[str]:
    lines = []
    g = report["genome"]
    lines.append(f"Genome: {'up' if g['reachable'] else 'down'} ({g.get('path', '?')})")
    if g["reachable"]:
        lines.append(f"  gene_count: {g.get('gene_count', '?')}")
    elif g.get("next_action"):
        lines.append(f"  fix: {g['next_action']}")

    c = report["config"]
    lines.append(f"Config: {'valid' if c['valid'] else 'invalid'} ({c.get('path', '?')})")
    if not c["valid"]:
        lines.append(f"  fix: {c.get('next_action', '')}")

    s = report.get("server")
    if s is not None:
        lines.append(
            f"Server: {'up' if s.get('reachable') else 'down'} ({s.get('url', '?')})"
        )

    lines.append("")
    lines.append(f"Next action: {report['next_action']}")
    return lines


def run(argv: list[str]) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    from helix_context.config import load_config
    try:
        cfg = load_config(args.config)
        genome_path = cfg.genome.path
    except Exception:
        genome_path = "genome.db"   # best-effort default

    config_report = _probe_config(args.config)
    genome_report = _probe_genome(genome_path)

    report: Dict[str, Any] = {
        "genome": genome_report,
        "config": config_report,
    }

    # Optional network probes — reuse the existing helix-status logic.
    if not args.no_network:
        try:
            from helix_status import collect_status
            net = collect_status()
            report["server"] = {
                "reachable": net["server"]["reachable"],
                "url": net["server"]["url"],
            }
            report["launcher"] = {
                "reachable": net["launcher"]["reachable"],
                "url": net["launcher"]["url"],
            }
        except Exception as exc:
            report["server"] = {"reachable": False, "error": str(exc)}

    # Compute aggregate next_action + return code.
    if not genome_report["reachable"]:
        report["next_action"] = genome_report.get("next_action", "Bring the genome online.")
        rc = output.EXIT_STATUS_FAIL
    elif not config_report["valid"]:
        report["next_action"] = config_report.get("next_action", "Fix helix.toml.")
        rc = output.EXIT_STATUS_FAIL
    else:
        report["next_action"] = "Healthy — try `helix query \"...\"`."
        rc = output.EXIT_OK

    if args.json:
        output.print_json(report)
    else:
        output.print_lines(_render_text(report))
    return rc
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
python -m pytest tests/test_cli_status.py -v
```
Expected: PASS (3 tests).

- [ ] **Step 5: Commit**

```bash
git add helix_context/cli/cmd_status.py tests/test_cli_status.py
git commit -m "feat(cli): \`helix status\` — cold-start health check

Validates genome reachability (read-only SQLite probe + gene_count),
config validity, and optional HTTP server / launcher reachability.
Returns exit code 3 if genome or config check fails; 0 on healthy.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 4: `helix query`

**Files:**
- Create: `helix_context/cli/cmd_query.py`
- Test: `tests/test_cli_query.py`

The load-bearing subcommand. Drives the retrieval pipeline via `helix_context.api.open_session().query(...)`. Supports `--k`, `--json`, `--tier broad|focused` (mapped to internal `decoder_mode`), `--learn`.

Because the full retrieval stack is heavy, the test mocks `helix_context.api.open_session` to return a stub session whose `query()` returns a deterministic `QueryResult`. This keeps cli tests fast (<1s) and independent of corpus state.

- [ ] **Step 1: Write the failing tests**

Create `tests/test_cli_query.py`:

```python
"""Tests for `helix query`. Mocks helix_context.api.open_session so the
CLI surface is tested in isolation from the retrieval stack."""
from __future__ import annotations

import io
import json
import contextlib
from unittest.mock import MagicMock, patch

import pytest

from helix_context.api import QueryResult
from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


@pytest.fixture
def fake_session():
    sess = MagicMock()
    sess.session_id = "sess-test-0000000000000000"
    sess.query.return_value = QueryResult(
        expressed_context="<helix>example bytes</helix>",
        document_ids=["gene-001", "gene-002"],
        know=None,
        miss=None,
        estimated_tokens=42,
        decision_reason="test fixture verdict",
        next_action="answer_from_evidence",
    )
    return sess


def test_query_text_mode_prints_expressed_context(fake_session):
    with patch("helix_context.cli.cmd_query.open_session", return_value=fake_session):
        rc, out, err = _run(["query", "hello world"])
    assert rc == 0, err
    assert "<helix>example bytes</helix>" in out


def test_query_json_emits_agent_payload(fake_session):
    with patch("helix_context.cli.cmd_query.open_session", return_value=fake_session):
        rc, out, err = _run(["query", "hello world", "--json"])
    assert rc == 0, err
    payload = json.loads(out)
    assert payload["expressed_context"] == "<helix>example bytes</helix>"
    assert payload["evidence"] == ["gene-001", "gene-002"]
    assert payload["estimated_tokens"] == 42
    assert payload["decision_reason"] == "test fixture verdict"
    assert payload["next_action"] == "answer_from_evidence"


def test_query_passes_k_through(fake_session):
    with patch("helix_context.cli.cmd_query.open_session", return_value=fake_session):
        rc, out, err = _run(["query", "test", "--k", "8"])
    assert rc == 0
    # The session.query should have been called with k=8.
    _, kwargs = fake_session.query.call_args
    assert kwargs["k"] == 8


def test_query_tier_broad_maps_to_broad_decoder(fake_session):
    with patch("helix_context.cli.cmd_query.open_session", return_value=fake_session):
        rc, _, _ = _run(["query", "test", "--tier", "broad"])
    assert rc == 0
    _, kwargs = fake_session.query.call_args
    assert kwargs["decoder_mode"] == "broad"


def test_query_tier_focused_maps_to_condensed_decoder(fake_session):
    with patch("helix_context.cli.cmd_query.open_session", return_value=fake_session):
        rc, _, _ = _run(["query", "test", "--tier", "focused"])
    assert rc == 0
    _, kwargs = fake_session.query.call_args
    assert kwargs["decoder_mode"] == "condensed"


def test_query_learn_flag_passes_through(fake_session):
    with patch("helix_context.cli.cmd_query.open_session", return_value=fake_session):
        rc, _, _ = _run(["query", "test", "--learn"])
    assert rc == 0
    _, kwargs = fake_session.query.call_args
    assert kwargs["learn"] is True


def test_query_returns_one_when_session_raises():
    sess = MagicMock()
    sess.query.side_effect = RuntimeError("genome unreachable")
    with patch("helix_context.cli.cmd_query.open_session", return_value=sess):
        rc, out, err = _run(["query", "test", "--json"])
    assert rc == 1
    payload = json.loads(out)
    assert payload["ok"] is False
    assert "genome unreachable" in payload["error"]
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
python -m pytest tests/test_cli_query.py -v
```
Expected: FAIL (`cmd_query` not found).

- [ ] **Step 3: Implement `cmd_query.py`**

Create `helix_context/cli/cmd_query.py`:

```python
"""`helix query` — run the retrieval pipeline once and print the result.

The CLI is a thin wrapper around ``helix_context.api.HelixSession.query``.
Mapping notes:
  * --tier broad   → decoder_mode="broad"     (more genes, longer)
  * --tier focused → decoder_mode="condensed" (fewer genes, tighter)
  * --tier (omit)  → no override; classifier picks

The drift between the bench spec's {broad, focused} vocabulary and the
internal {condensed, broad, dense} vocabulary is intentional in v1;
reconciliation is a v1.1 follow-up.
"""
from __future__ import annotations

import argparse
from typing import Any, Dict

from . import output
from helix_context.api import open_session


# Spec-vocab → internal-vocab mapping (see module docstring).
_TIER_TO_DECODER = {
    "broad": "broad",
    "focused": "condensed",
}


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="helix query",
        description="Run the helix retrieval pipeline for one query.",
    )
    parser.add_argument("text", help="The query string.")
    parser.add_argument(
        "--k", type=int, default=None,
        help="Cap on returned documents (default: honor static config).",
    )
    parser.add_argument(
        "--json", action="store_true",
        help="Machine-readable JSON output (use for agent / bench consumption).",
    )
    parser.add_argument(
        "--tier", choices=("broad", "focused"), default=None,
        help="Walk-tier hint. broad=scout, focused=narrow. See cli.md for vocabulary.",
    )
    parser.add_argument(
        "--learn", action="store_true",
        help="Replicate the query (and eventually the response) back into the genome.",
    )
    return parser


def _render_text(payload: Dict[str, Any]) -> list[str]:
    lines = [
        f"verdict: {payload.get('verdict', 'unknown')}",
        f"evidence: {', '.join(payload.get('evidence', [])) or '(none)'}",
        f"estimated_tokens: {payload.get('estimated_tokens', 0)}",
    ]
    if payload.get("decision_reason"):
        lines.append(f"decision_reason: {payload['decision_reason']}")
    if payload.get("next_action"):
        lines.append(f"next_action: {payload['next_action']}")
    lines.append("")
    lines.append("--- expressed_context ---")
    lines.append(payload.get("expressed_context", ""))
    return lines


def run(argv: list[str]) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    decoder_mode = _TIER_TO_DECODER.get(args.tier) if args.tier else None

    try:
        sess = open_session()
        result = sess.query(
            args.text,
            k=args.k,
            decoder_mode=decoder_mode,
            learn=args.learn,
        )
    except Exception as exc:
        err = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
        if args.json:
            output.print_json(err)
        else:
            output.eprint(err["error"])
        return output.EXIT_ERROR

    payload = result.to_agent_json()
    if args.json:
        output.print_json(payload)
    else:
        output.print_lines(_render_text(payload))
    return output.EXIT_OK
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
python -m pytest tests/test_cli_query.py -v
```
Expected: PASS (7 tests).

- [ ] **Step 5: Commit**

```bash
git add helix_context/cli/cmd_query.py tests/test_cli_query.py
git commit -m "feat(cli): \`helix query\` — cold-start retrieval invocation

Wraps HelixSession.query with --k, --json, --tier {broad|focused},
--learn flags per docs/benchmarks/SESSION_AWARE_BENCH_DESIGN.md §12.1.

--tier maps to decoder_mode (broad→broad, focused→condensed) —
vocabulary alignment is a v1.1 follow-up.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 5: `helix ingest`

**Files:**
- Create: `helix_context/cli/cmd_ingest.py`
- Test: `tests/test_cli_ingest.py`

`helix ingest <path>` reads a file (or every file in a directory, non-recursive for v1) and writes the contents through `HelixSession.ingest`. Single-writer SQLite path. Reports gene_ids on stdout.

- [ ] **Step 1: Write the failing tests**

Create `tests/test_cli_ingest.py`:

```python
"""Tests for `helix ingest`."""
from __future__ import annotations

import io
import json
import contextlib
from unittest.mock import MagicMock, patch

import pytest

from helix_context.api import IngestResult
from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


@pytest.fixture
def fake_session():
    sess = MagicMock()
    sess.ingest.return_value = IngestResult(
        gene_ids=["gene-aaaa", "gene-bbbb"], chunks=2, bytes_written=128
    )
    return sess


def test_ingest_single_file(fake_session, tmp_path):
    src = tmp_path / "doc.txt"
    src.write_text("hello helix\n", encoding="utf-8")

    with patch("helix_context.cli.cmd_ingest.open_session", return_value=fake_session):
        rc, out, err = _run(["ingest", str(src), "--json"])
    assert rc == 0, err
    payload = json.loads(out)
    assert payload["files_processed"] == 1
    assert payload["gene_ids"] == ["gene-aaaa", "gene-bbbb"]
    assert payload["bytes_written"] == 128

    # The session.ingest was called once with the file contents.
    assert fake_session.ingest.call_count == 1
    args, kwargs = fake_session.ingest.call_args
    assert args == ("hello helix\n",) or kwargs.get("content") == "hello helix\n"


def test_ingest_directory_walks_top_level_files(fake_session, tmp_path):
    (tmp_path / "a.txt").write_text("a", encoding="utf-8")
    (tmp_path / "b.md").write_text("b", encoding="utf-8")
    (tmp_path / "sub").mkdir()
    (tmp_path / "sub" / "c.txt").write_text("c", encoding="utf-8")   # recursed-into only if --recursive

    with patch("helix_context.cli.cmd_ingest.open_session", return_value=fake_session):
        rc, out, err = _run(["ingest", str(tmp_path), "--json"])
    assert rc == 0, err
    payload = json.loads(out)
    assert payload["files_processed"] == 2   # a.txt + b.md, not sub/c.txt


def test_ingest_returns_one_on_missing_path(tmp_path):
    rc, out, err = _run(["ingest", str(tmp_path / "missing.txt"), "--json"])
    assert rc == 1
    payload = json.loads(out)
    assert payload["ok"] is False
    assert "not found" in payload["error"].lower() or "no such" in payload["error"].lower()


def test_ingest_recursive_flag_walks_subdirs(fake_session, tmp_path):
    (tmp_path / "a.txt").write_text("a", encoding="utf-8")
    (tmp_path / "sub").mkdir()
    (tmp_path / "sub" / "c.txt").write_text("c", encoding="utf-8")

    with patch("helix_context.cli.cmd_ingest.open_session", return_value=fake_session):
        rc, out, err = _run(["ingest", str(tmp_path), "--recursive", "--json"])
    assert rc == 0, err
    payload = json.loads(out)
    assert payload["files_processed"] == 2  # a.txt + sub/c.txt
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
python -m pytest tests/test_cli_ingest.py -v
```
Expected: FAIL (`cmd_ingest` not found).

- [ ] **Step 3: Implement `cmd_ingest.py`**

Create `helix_context/cli/cmd_ingest.py`:

```python
"""`helix ingest <path>` — read a file or directory into the genome."""
from __future__ import annotations

import argparse
from pathlib import Path
from typing import Iterable, List

from . import output
from helix_context.api import open_session


_DEFAULT_EXTENSIONS = (".txt", ".md", ".rst", ".py", ".ts", ".js", ".json", ".toml", ".yml", ".yaml")


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="helix ingest",
        description="Add a file (or directory of files) to the genome.",
    )
    parser.add_argument("path", help="Path to a file or directory.")
    parser.add_argument(
        "--recursive", "-r", action="store_true",
        help="Walk subdirectories when path is a directory.",
    )
    parser.add_argument(
        "--json", action="store_true",
        help="Machine-readable output.",
    )
    parser.add_argument(
        "--ext",
        action="append",
        default=None,
        help="File extension to include (e.g. --ext .txt). Repeatable. "
             "Default: " + ", ".join(_DEFAULT_EXTENSIONS),
    )
    return parser


def _collect_files(root: Path, recursive: bool, exts: Iterable[str]) -> List[Path]:
    ext_set = {e.lower() if e.startswith(".") else "." + e.lower() for e in exts}
    if root.is_file():
        return [root]
    if not root.is_dir():
        return []
    iterator = root.rglob("*") if recursive else root.iterdir()
    return sorted(p for p in iterator if p.is_file() and p.suffix.lower() in ext_set)


def run(argv: list[str]) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    root = Path(args.path)
    if not root.exists():
        err = {"ok": False, "error": f"path not found: {root}"}
        if args.json:
            output.print_json(err)
        else:
            output.eprint(err["error"])
        return output.EXIT_ERROR

    exts = args.ext or _DEFAULT_EXTENSIONS
    files = _collect_files(root, args.recursive, exts)
    if not files:
        msg = {
            "ok": False,
            "error": f"no matching files under {root} (extensions: {list(exts)})",
        }
        if args.json:
            output.print_json(msg)
        else:
            output.eprint(msg["error"])
        return output.EXIT_ERROR

    try:
        sess = open_session()
        all_gene_ids: list[str] = []
        total_bytes = 0
        for f in files:
            content = f.read_text(encoding="utf-8", errors="replace")
            result = sess.ingest(content)
            all_gene_ids.extend(result.gene_ids)
            total_bytes += result.bytes_written
    except Exception as exc:
        err = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
        if args.json:
            output.print_json(err)
        else:
            output.eprint(err["error"])
        return output.EXIT_ERROR

    payload = {
        "ok": True,
        "files_processed": len(files),
        "gene_ids": all_gene_ids,
        "bytes_written": total_bytes,
    }
    if args.json:
        output.print_json(payload)
    else:
        output.print_lines([
            f"ingested {len(files)} file(s)",
            f"  gene_ids: {len(all_gene_ids)}",
            f"  bytes:    {total_bytes}",
        ])
    return output.EXIT_OK
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
python -m pytest tests/test_cli_ingest.py -v
```
Expected: PASS (4 tests).

- [ ] **Step 5: Commit**

```bash
git add helix_context/cli/cmd_ingest.py tests/test_cli_ingest.py
git commit -m "feat(cli): \`helix ingest <path>\` — file or directory write

Top-level by default; --recursive walks subdirs. Filters by extension
(.txt, .md, .py, ... — extendable via --ext). Reports gene_ids and
bytes_written via JSON or human-readable text.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 6: `helix diag corpus`

**Files:**
- Create: `helix_context/cli/cmd_diag.py`
- Test: `tests/test_cli_diag.py`

`helix diag corpus` reports corpus shape: total genes, codon count, chromatin tier distribution (open / euchromatin / heterochromatin), compression ratio, and "staleness" (count of genes whose source fingerprint is newer than the gene body — i.e. consolidation candidates). Pulls from `HelixSession.stats()`.

The "staleness" number is best-effort for v1: `StatsResult.metadata` already carries health-summary fields from `genome.health_summary()`. If a `stale_genes` count is present we surface it; otherwise we report `null`.

- [ ] **Step 1: Write the failing tests**

Create `tests/test_cli_diag.py`:

```python
"""Tests for `helix diag corpus`."""
from __future__ import annotations

import io
import json
import contextlib
from unittest.mock import MagicMock, patch

import pytest

from helix_context.api import StatsResult
from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


@pytest.fixture
def fake_session():
    sess = MagicMock()
    sess.stats.return_value = StatsResult(
        total_genes=125,
        total_codons=500,
        chromatin_open=80,
        chromatin_eu=35,
        chromatin_hetero=10,
        compression_ratio=4.2,
        metadata={"health": {"stale_genes": 3}},
    )
    return sess


def test_diag_corpus_json(fake_session):
    with patch("helix_context.cli.cmd_diag.open_session", return_value=fake_session):
        rc, out, err = _run(["diag", "corpus", "--json"])
    assert rc == 0, err
    payload = json.loads(out)
    assert payload["total_genes"] == 125
    assert payload["tier_distribution"]["open"] == 80
    assert payload["tier_distribution"]["euchromatin"] == 35
    assert payload["tier_distribution"]["heterochromatin"] == 10
    assert payload["compression_ratio"] == 4.2
    assert payload["staleness"]["stale_genes"] == 3


def test_diag_corpus_text(fake_session):
    with patch("helix_context.cli.cmd_diag.open_session", return_value=fake_session):
        rc, out, err = _run(["diag", "corpus"])
    assert rc == 0, err
    assert "total_genes: 125" in out
    assert "open: 80" in out
    assert "heterochromatin: 10" in out


def test_diag_unknown_target_returns_two(fake_session):
    with pytest.raises(SystemExit) as exc:
        main(["diag", "nope"])
    assert exc.value.code == 2
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
python -m pytest tests/test_cli_diag.py -v
```
Expected: FAIL (`cmd_diag` not found).

- [ ] **Step 3: Implement `cmd_diag.py`**

Create `helix_context/cli/cmd_diag.py`:

```python
"""`helix diag <target>` — diagnostic introspection.

v1 only ships `corpus`. Future targets (`genome`, `index`, ...) plug
into the same argparse subparser.
"""
from __future__ import annotations

import argparse
from typing import Any, Dict

from . import output
from helix_context.api import open_session


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="helix diag",
        description="Diagnostic introspection.",
    )
    sub = parser.add_subparsers(dest="target", metavar="<target>", required=True)
    corpus = sub.add_parser("corpus", help="Corpus shape: gene count, tier mix, staleness.")
    corpus.add_argument("--json", action="store_true", help="Machine-readable output.")
    return parser


def _corpus_payload(stats) -> Dict[str, Any]:
    """Project a StatsResult into the operator-facing corpus shape."""
    health = stats.metadata.get("health") if isinstance(stats.metadata, dict) else None
    return {
        "total_genes": stats.total_genes,
        "total_codons": stats.total_codons,
        "tier_distribution": {
            "open": stats.chromatin_open,
            "euchromatin": stats.chromatin_eu,
            "heterochromatin": stats.chromatin_hetero,
        },
        "compression_ratio": stats.compression_ratio,
        "staleness": health if isinstance(health, dict) else None,
    }


def _render_text(payload: Dict[str, Any]) -> list[str]:
    td = payload["tier_distribution"]
    lines = [
        f"total_genes: {payload['total_genes']}",
        f"total_codons: {payload['total_codons']}",
        f"compression_ratio: {payload['compression_ratio']}",
        "tier_distribution:",
        f"  open: {td['open']}",
        f"  euchromatin: {td['euchromatin']}",
        f"  heterochromatin: {td['heterochromatin']}",
    ]
    stale = payload.get("staleness")
    if isinstance(stale, dict):
        lines.append("staleness:")
        for k, v in stale.items():
            lines.append(f"  {k}: {v}")
    else:
        lines.append("staleness: (unavailable)")
    return lines


def run(argv: list[str]) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    if args.target != "corpus":
        parser.print_help()
        return output.EXIT_BAD_ARGS

    try:
        sess = open_session()
        stats = sess.stats()
    except Exception as exc:
        err = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
        if args.json:
            output.print_json(err)
        else:
            output.eprint(err["error"])
        return output.EXIT_ERROR

    payload = _corpus_payload(stats)
    if args.json:
        output.print_json(payload)
    else:
        output.print_lines(_render_text(payload))
    return output.EXIT_OK
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
python -m pytest tests/test_cli_diag.py -v
```
Expected: PASS (3 tests).

- [ ] **Step 5: Commit**

```bash
git add helix_context/cli/cmd_diag.py tests/test_cli_diag.py
git commit -m "feat(cli): \`helix diag corpus\` — corpus shape report

Reports gene + codon totals, tier distribution (open/eu/hetero),
compression ratio, and best-effort staleness from genome health
summary. JSON or text output.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 7: `helix serve` stub (deferred, friendly exit)

**Files:**
- Create: `helix_context/cli/cmd_serve.py`
- Test: `tests/test_cli_serve_stub.py`

The daemon (`helix serve`) is deferred per `HELIX_DAEMON_DESIGN.md`. But users seeing it in `helix --help` will try it. Print a clear message + pointer to the FastAPI runbook and exit 4.

- [ ] **Step 1: Write the failing test**

Create `tests/test_cli_serve_stub.py`:

```python
"""`helix serve` is deferred in v1; the stub prints a clear message."""
from __future__ import annotations

import io
import contextlib

from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


def test_serve_exits_four_with_deferred_message():
    rc, out, err = _run(["serve"])
    assert rc == 4
    combined = (out + err).lower()
    assert "deferred" in combined
    assert "helix-server" in combined or "uvicorn" in combined
```

- [ ] **Step 2: Run test to verify it fails**

```bash
python -m pytest tests/test_cli_serve_stub.py -v
```
Expected: FAIL (`cmd_serve` not found).

- [ ] **Step 3: Implement `cmd_serve.py`**

Create `helix_context/cli/cmd_serve.py`:

```python
"""`helix serve` — DEFERRED in v1.

Per the 2026-05-11 council pass, v1 ships as cold-start CLI; the
daemon design lives in docs/architecture/HELIX_DAEMON_DESIGN.md but
is parked until benchmarking proves the thesis. This stub keeps the
subcommand visible in `helix --help` and tells the user what to do
in the meantime.
"""
from __future__ import annotations

from . import output


_MESSAGE = """\
`helix serve` is deferred in v1.

For the FastAPI proxy + retrieval HTTP surface, use the legacy entry
point or uvicorn directly:

  helix-server
  # or
  python -m uvicorn helix_context._asgi:app --host 127.0.0.1 --port 11437

The JSON-RPC daemon design lives in
docs/architecture/HELIX_DAEMON_DESIGN.md and will land in v1.x after
walk-bench numbers come in.
"""


def run(argv: list[str]) -> int:
    output.eprint(_MESSAGE)
    return output.EXIT_DEFERRED
```

- [ ] **Step 4: Run test to verify it passes**

```bash
python -m pytest tests/test_cli_serve_stub.py -v
```
Expected: PASS (1 test).

- [ ] **Step 5: Commit**

```bash
git add helix_context/cli/cmd_serve.py tests/test_cli_serve_stub.py
git commit -m "feat(cli): \`helix serve\` stub (deferred, exits 4)

Daemon is deferred per HELIX_DAEMON_DESIGN.md; until then \`helix
serve\` prints a pointer at \`helix-server\` / uvicorn and exits 4.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 8: End-to-end integration smoke test

**Files:**
- Create: `tests/test_cli_integration.py`

Each command has unit tests with mocked sessions. We need one end-to-end test that exercises the real `HelixSession` against an in-memory genome (`HELIX_GENOME_PATH=:memory:`). This catches wiring bugs the mocks can hide.

- [ ] **Step 1: Write the integration test**

Create `tests/test_cli_integration.py`:

```python
"""End-to-end smoke: real HelixSession against an :memory: genome.

These tests are slow-ish (cold-start the manager once) but catch
wiring bugs the per-subcommand unit tests can't.
"""
from __future__ import annotations

import io
import json
import contextlib
import os

import pytest

from helix_context.cli import main


def _run(argv):
    out, err = io.StringIO(), io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
        rc = main(argv)
    return rc, out.getvalue(), err.getvalue()


@pytest.fixture(autouse=True)
def _isolate_genome(monkeypatch, tmp_path):
    """Force the integration test to use an in-memory genome.

    Without this, the cold-start CLI would try to open genome.db from
    cwd and either fail or pollute the developer's working genome.
    """
    monkeypatch.setenv("HELIX_GENOME_PATH", ":memory:")
    monkeypatch.setenv("HELIX_CONFIG", str(tmp_path / "no-such-file.toml"))
    # Reset the cached module-level manager so each test gets fresh state.
    from helix_context import api
    api.close_manager()
    yield
    api.close_manager()


@pytest.mark.live
def test_end_to_end_ingest_then_query():
    """Ingest a small fact, query for it, verify the bytes round-trip."""
    import tempfile
    with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
        f.write("The flux capacitor requires 1.21 gigawatts of electricity.\n")
        fname = f.name

    try:
        rc, out, err = _run(["ingest", fname, "--json"])
        assert rc == 0, err
        ingest_payload = json.loads(out)
        assert ingest_payload["ok"] is True
        assert ingest_payload["files_processed"] == 1

        rc, out, err = _run(["query", "flux capacitor", "--json"])
        assert rc == 0, err
        query_payload = json.loads(out)
        # Don't over-assert on retrieval quality — the pipeline can route
        # to "miss" with a small genome. Just verify the wire format.
        assert "verdict" in query_payload
        assert "expressed_context" in query_payload
        assert isinstance(query_payload["evidence"], list)
    finally:
        os.unlink(fname)


@pytest.mark.live
def test_end_to_end_diag_corpus_reports_genes():
    """After ingest, diag corpus shows non-zero gene count."""
    rc, out, err = _run(["diag", "corpus", "--json"])
    assert rc == 0, err
    payload = json.loads(out)
    assert isinstance(payload["total_genes"], int)
    # Allow zero (fresh :memory: genome) — we just check the field exists.
    assert payload["total_genes"] >= 0
```

- [ ] **Step 2: Run the integration test**

```bash
python -m pytest tests/test_cli_integration.py -v -m live
```
Expected: PASS (2 tests). Skip if `-m "not live"` is passed (this respects the existing live marker in `pyproject.toml`).

- [ ] **Step 3: Commit**

```bash
git add tests/test_cli_integration.py
git commit -m "test(cli): end-to-end smoke against :memory: genome

Marked \`live\` so the default \`pytest -m \"not live\"\` skips it,
but \`pytest -m live\` exercises the real HelixSession wiring.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 9: Operator documentation

**Files:**
- Create: `docs/clients/cli.md`
- Modify: `CLAUDE.md` (replace Quick Start with the new CLI flow)

- [ ] **Step 1: Write the docs file**

Create `docs/clients/cli.md`:

```markdown
# Helix CLI — operator reference

`helix` is the cold-start command-line surface for the Helix Context
genome. Every invocation is a fresh Python process: it opens the
SQLite genome (read-only by default), runs the requested operation,
and exits. No daemon, no long-lived state. The daemon design (see
`docs/architecture/HELIX_DAEMON_DESIGN.md`) is parked until walk-bench
numbers come in.

## Install

\`\`\`bash
pip install helix-context
helix --help
\`\`\`

The legacy FastAPI launcher is still available as `helix-server` (or
`python -m uvicorn helix_context._asgi:app`).

## Subcommands

### `helix query "<text>" [--k N] [--json] [--tier broad|focused] [--learn]`

Run the retrieval pipeline once and print the result.

- `--k N` — cap on returned documents. Default: honor the static
  config (`[budget] max_genes_per_turn` in helix.toml).
- `--json` — emit `to_agent_json()` shape (verdict / evidence /
  expressed_context / estimated_tokens / decision_reason / next_action).
  This is the format the walk-bench harness expects.
- `--tier broad|focused` — walk-tier hint. `broad` = scatter-gather
  scout. `focused` = narrow converge. Maps to internal `decoder_mode`:
  `broad → broad`, `focused → condensed`. The vocabulary drift between
  spec (broad/focused/tight) and code (condensed/broad/dense) is a
  v1.1 follow-up.
- `--learn` — replicate the query back into the genome (default off,
  so repeated CLI calls never silently mutate state).

Exit codes: `0` success, `1` pipeline error.

### `helix ingest <path> [--recursive] [--ext .EXT] [--json]`

Add a file or directory to the genome. Top-level only by default; pass
`--recursive` to walk subdirectories. The default extension filter is:
`.txt .md .rst .py .ts .js .json .toml .yml .yaml`. Repeat `--ext`
to add more.

Exit codes: `0` success, `1` file error or write failure.

### `helix status [--json] [--no-network] [--config PATH]`

Three checks: (1) genome reachable and gene_count > 0, (2) config
valid, (3) optional HTTP server / launcher probe (skipped if
`--no-network`).

Exit codes: `0` healthy, `3` genome or config check failed.

### `helix diag corpus [--json]`

Reports corpus shape: total_genes, total_codons, tier_distribution
(open / euchromatin / heterochromatin), compression_ratio, and
best-effort staleness from the genome health summary.

Exit codes: `0` success, `1` stats call failed.

### `helix config show [--text] [--config PATH]`

Print the effective configuration (helix.toml merged with env
overrides). JSON by default; `--text` for flat `dotted.key = value`
lines.

Exit codes: `0` success.

### `helix serve` (DEFERRED)

Prints a pointer at `helix-server` and exits 4. Daemon design lives
in `docs/architecture/HELIX_DAEMON_DESIGN.md`.

## Environment variables

- `HELIX_CONFIG` — path to `helix.toml`. Default: `./helix.toml`.
- `HELIX_GENOME_PATH` — overrides `[genome] path` (use `:memory:` for
  tests and ephemeral runs).

## Exit code table

| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Operation failed |
| 2 | Bad CLI arguments (argparse default) |
| 3 | Status check failed (`helix status` only) |
| 4 | Subcommand deferred / not implemented |
```

- [ ] **Step 2: Update `CLAUDE.md` to mention the new CLI**

Modify `CLAUDE.md`, replacing the existing Quick Start block (the section starting `## Quick Start` and ending before `## How It Works`). New content:

\`\`\`markdown
## Quick Start

```bash
# Install
pip install helix-context

# Ingest some content
helix ingest path/to/your/docs/ --recursive

# Query the genome
helix query "what does the splice step do?"

# Inspect corpus state
helix diag corpus
helix status

# Run the legacy FastAPI proxy (still useful for IDE integrations)
helix-server
# or
python -m uvicorn helix_context._asgi:app --host 127.0.0.1 --port 11437
```

Full CLI reference: `docs/clients/cli.md`.
\`\`\`

- [ ] **Step 3: Verify docs render**

```bash
python -m pytest tests/ -k cli -v
```
Expected: every cli test still passes (no regressions from doc work).

```bash
helix --help
helix query --help
helix ingest --help
helix status --help
helix diag --help
helix config --help
```
Expected: every help output mentions the documented flags.

- [ ] **Step 4: Commit**

```bash
git add docs/clients/cli.md CLAUDE.md
git commit -m "docs(cli): operator reference + Quick Start refresh

Adds docs/clients/cli.md with the v1 CLI surface (query / ingest /
status / diag / config / serve-stub) and rewrites the README Quick
Start to lead with \`helix ingest\` + \`helix query\` instead of the
manual uvicorn invocation.

Refs: docs/superpowers/plans/2026-05-11-helix-cli-v1.md"
```

---

### Task 10: Final sweep — full test suite + self-check

**Files:**
- None (verification only)

- [ ] **Step 1: Run the full CLI test set**

```bash
python -m pytest tests/test_cli_*.py -v
```
Expected: every cli test passes.

- [ ] **Step 2: Run the full non-live suite to catch regressions**

```bash
python -m pytest tests/ -m "not live" -v --tb=short 2>&1 | tail -30
```
Expected: no test failures introduced by the cli changes. The
modified `helix_context/server.py` may have prior unrelated changes
in your working tree — note any failures that predate this plan
and triage separately.

- [ ] **Step 3: Manually exercise the CLI on a small genome**

```bash
mkdir -p /tmp/helix-smoke && cd /tmp/helix-smoke
echo "The cache TTL defaults to 600 seconds." > note.txt
HELIX_GENOME_PATH=$(pwd)/genome.db helix ingest note.txt
HELIX_GENOME_PATH=$(pwd)/genome.db helix diag corpus
HELIX_GENOME_PATH=$(pwd)/genome.db helix query "cache TTL" --json
HELIX_GENOME_PATH=$(pwd)/genome.db helix status
```
Expected: ingest reports 1 file, diag reports non-zero genes, query
returns a payload with `verdict` / `expressed_context` / `evidence`,
status exits 0.

- [ ] **Step 4: Commit a CHANGELOG entry**

Append to top of `CHANGELOG.md` (if it exists; otherwise create one):

```markdown
# Changelog

## Unreleased

- **feat(cli): v1 cold-start CLI shipped.** `helix query`, `helix ingest`,
  `helix status`, `helix diag corpus`, `helix config show`. The legacy
  FastAPI launcher is now reachable as `helix-server`. Daemon (`helix
  serve`) remains deferred per `docs/architecture/HELIX_DAEMON_DESIGN.md`.
  See `docs/clients/cli.md`.
```

```bash
git add CHANGELOG.md
git commit -m "docs: changelog entry for v1 cold-start CLI"
```

---

## Self-review notes

Run through the spec one more time after Task 10:

1. **Spec coverage check.** Every command from `SESSION_AWARE_BENCH_DESIGN.md` §12.1 is implemented? `query`, `status`, `diag corpus`, `config show`, `ingest` — yes (Tasks 4, 3, 6, 2, 5). The deferred set (`label set`, `drift baseline/compare`, `rerank`, `dims`) is not implemented — correct per spec.
2. **Cold-start contract.** Every command opens a fresh `HelixSession` per invocation (no daemon assumed)? Yes — every cmd module calls `open_session()` itself. The module-level `_DEFAULT_MANAGER` cache in `api.py` keeps within-process calls cheap but doesn't survive process exit.
3. **JSON output stable for bench consumption?** Every JSON path goes through `output.print_json(...)` which uses `sort_keys=True`. Determinism: yes, for the same input.
4. **Exit codes match the documented table?** Walk through: dispatcher returns 1 on no args, 2 via argparse on bad subcommand, subcommands return 0/1/3/4 per their docstrings. Yes.
5. **Windows safety.** No `subprocess.Popen` introduced in this plan. The CLI is pure-Python stdlib + helix_context internals.
6. **Backwards compatibility.** `helix-launcher`, `helix-status`, `helix-vault` entry points are untouched. The legacy `helix` (FastAPI launcher) moves to `helix-server` — note this in CHANGELOG so existing users update muscle memory.
