# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What is Kitaru?

Kitaru is ZenML's **durable execution layer for AI agents**. It provides primitives (`flow`, `checkpoint`, `save`, `load`, `wait`, `log`) that make agent workflows persistent, replayable, and observable — without requiring users to learn a graph DSL or change their Python control flow.

**Core philosophy:** Primitives first, frameworks second. Sync-first. Every checkpoint output persisted invisibly for replay. Zero config locally, one-line connect for production.

**ZenML mapping:** `@flow` → `@pipeline(dynamic=True)`, `@checkpoint` → `@step`, `kitaru.log()` → `log_metadata()`, `kitaru.wait()` → new ZenML core work. `kitaru init` creates `.kitaru/` (not `.zen/`) as the local project marker via `ZENML_REPOSITORY_DIRECTORY_NAME`.

**Unified config directory:** Kitaru and ZenML share a single config directory. The `kitaru_init_hook` sets `ZENML_CONFIG_PATH` to Kitaru's app dir (e.g. `~/.config/kitaru/` on Linux, `~/Library/Application Support/kitaru/` on macOS) so the database, credentials, local stores, and Kitaru's own `kitaru.yaml` all live together. `KITARU_CONFIG_PATH` overrides this for both. Server subprocesses that set `ZENML_CONFIG_PATH` directly are respected.

## Project layout

```
src/kitaru/           # Python SDK package (src layout)
  cli.py              # CLI facade / console entrypoint (cyclopts)
  _cli/               # Internal command modules + shared CLI helpers
  adapters/           # Framework adapters (includes PydanticAI and OpenAI Agents)
  mcp/                # MCP server tools (optional `kitaru[mcp]` extra)
tests/                # pytest tests
tests/mcp/            # MCP-specific unit tests (runs in `[mcp]` CI path)
examples/             # Runnable SDK examples
docs/                 # FumaDocs Next.js app — documentation content + static export
  content/docs/       # Documentation content (MDX files)
  scripts/            # Node-side doc generation (convert-sdk-docs.mjs)
  app/                # Next.js app routes, layout, metadata, search, sitemap
scripts/              # Doc generation, smoke test, and UI bundle scripts
  download-ui.sh             # Bundles stable/prerelease Kitaru UI releases into the package tree
  generate_cli_docs.py       # Generates CLI reference MDX from cyclopts introspection
  generate_changelog_docs.py # Generates changelog MDX from CHANGELOG.md
  generate_sdk_docs.py       # Extracts Python SDK API to JSON (griffe → docs/.generated/sdk-api.json)
  smoke-test.sh              # Pre-release end-to-end sanity check (CLI, flows, MCP, LLM)
FRONTEND-TESTING.md   # Read first for Kitaru UI bundle/frontend testing,
                       # stable/prerelease release validation, and token boundaries
docker/               # Dockerfiles — see docker/CLAUDE.md for full architecture details
  Dockerfile          # Production server (FROM zenmldocker/zenml-server + Kitaru + Kitaru UI)
  Dockerfile.server-dev  # Dev server for local UI testing (local source + local UI dist)
  Dockerfile.dev      # Flow-execution image for remote stacks (K8s, etc.)
design/               # Design docs, meeting notes (gitignored, never commit)
```

### Docs and web ownership

The docs app stays in this repo because it is generated from the Python SDK and CLI source:

1. Python scripts generate docs content (CLI reference, changelog, SDK reference JSON)
2. `docs/` builds a static export into `docs/out/` (Next.js with `basePath: '/docs'`)
3. Root `wrangler.toml` deploys a docs-only Cloudflare Worker named `kitaru-site` so `kitaru.ai/docs` and PR previews still come from this repo

The public marketing/runtime site for Kitaru now lives in the sibling `zenml-io-v2` repository. If a task involves Astro pages, public site assets, marketing Cloudflare Pages deployment, or runtime web APIs such as waitlist/get-started/newsletter endpoints, switch to `zenml-io-v2` and follow that repo's instructions instead of adding that code back here.

## Website and marketing assets

The Kitaru marketing site and its asset pipeline now live in `zenml-io-v2`. Do not add Astro pages, public site assets, R2 blog tooling, or runtime website changes to this repository. If a task is about the public website rather than the Python SDK/docs source, work in `zenml-io-v2` instead.

## Docs guidance

- Treat `KITARU_*` environment variables as the public configuration surface in docs and examples. Mention `ZENML_*` only as a compatibility note when needed.
- `kitaru model register` still writes aliases to local config, but submitted/replayed runs automatically receive a transported registry snapshot via `KITARU_MODEL_REGISTRY`. Describe `kitaru model list` as listing aliases available in the current environment, not just aliases stored locally.
- Static hand-written MDX pages under `docs/content/docs/` are tracked and can be edited directly when behavior changes.
- In docs MDX and generated docs, link to other docs pages with docs-app-root paths like `/cli/executions/` or `/concepts/checkpoints/`, not `/docs/...`; the Next `basePath` adds `/docs` for public HTML. From public surfaces such as the marketing site in `zenml-io-v2`, use public `/docs/...` links. Public `.md` copies are rewritten during docs export.
- Do not commit temporary agent planning/review files such as `docs/plans/*`, `docs/reviews/*`, or prompt exports unless the user explicitly asks for a durable tracked document. Treat them as coordination scratchpads, not product docs.
- Generated reference output should still come from the existing generation scripts rather than manual edits.
- Agent-facing CLI docs should describe the shared `--output json` / `-o json` contract: single-item commands emit `{command, item}`, list commands emit `{command, items, count}`, and `kitaru executions logs --follow --output json` emits JSONL event objects.
- Login docs/guidance should treat bare `kitaru login` as local server startup and `kitaru login <server>` as remote login. Local server support requires the `kitaru[local]` extra.
- Only `kitaru.llm()` auto-resolves alias-linked secrets today. If you need to document non-LLM secret access, present it as the current low-level pattern rather than implying a public Kitaru helper already exists.
- If generated CLI reference syntax is wrong, fix `scripts/generate_cli_docs.py` and/or the relevant `src/kitaru/_cli/_*.py` module (use `src/kitaru/cli.py` only for facade/bootstrap issues), not the generated `docs/content/docs/cli/*` output.
- Current shipped stack-create types on the CLI/MCP surface are `local`, `kubernetes`, `vertex`, `sagemaker`, and `azureml`. Advanced CLI/MCP stack creation also supports `--extra` / structured `extra` plus the remote-only `--async` / `async_mode` convenience flag. The public Python SDK `kitaru.create_stack(...)` still provisions local stacks only, so docs should keep that distinction explicit.
- `examples/**/README.md` files are **public-facing, educational content** for users learning Kitaru — not internal documentation. Their audience is a developer encountering the example for the first time and trying to understand what Kitaru does, what the example demonstrates, and how to run it themselves. Keep them focused on concepts, primitives in use, and the flow of the example. Do **not** add sections like "Testing" (how maintainers run the example's test suite), internal CI setup notes, contributor-only credential instructions for stubbed/mocked test runs, or any content that only makes sense to someone working on Kitaru itself. Those details belong in `tests/` docstrings, internal contributor docs, or PR descriptions. A good rule of thumb: if the section wouldn't help a brand-new user understand Kitaru, it doesn't belong in an example README.

## Branching strategy

- **`develop`** is the default branch and the target for all PRs.
- **`main`** contains only released versions. Updated by force-pushing during releases. Never push directly to `main`.
- **`release/X.Y.Z`** branches are archival snapshots created during the release process.
- **Tags** follow `vX.Y.Z` (e.g. `v0.1.0`).

### Releasing a new version

1. Ensure `develop` has all changes for the release.
2. Ensure `CHANGELOG.md` `[Unreleased]` section is complete — cross-check against `git log v<prev>..develop` for any missing user-facing changes.
3. Run the smoke test: `./scripts/smoke-test.sh` (or `./scripts/smoke-test.sh -s` to skip reinstall). This exercises CLI, SDK flows, MCP tools, and LLM integration against a local server. Set `OPENAI_API_KEY` to include LLM tests. Use `-k` to keep the server running and inspect the dashboard afterward.
4. Go to Actions > Release > Run workflow (or push a `vX.Y.Z` tag).
5. Enter the version (e.g. `0.2.0`); optionally enable dry-run.
6. The workflow bundles the highest stable/full `kitaru-ui-v*` release from `zenml-io/zenml-frontend-monorepo` into the Python package, bumps version, runs CI, publishes to PyPI, builds and pushes the Docker image (`zenmldocker/kitaru:<version>` + `latest`), builds and pushes the Helm chart to Amazon ECR Public as an OCI chart, creates `release/X.Y.Z`, updates `main`, tags, and creates a GitHub Release with auto-generated notes. Docker copies the UI from the installed Kitaru package; it does not download UI assets itself.
7. After the workflow completes, edit the GitHub Release notes (`gh release edit vX.Y.Z --notes ...`) to replace the auto-generated PR list with a structured changelog: a **Highlights** section for the most notable changes, then **Added/Changed/Fixed/Infrastructure** categories mirroring the changelog.

## Development commands

This project uses [just](https://github.com/casey/just) as a command stack. Run `just --list` to see all recipes.

### Core workflow (the three commands you'll use most)

| Command | What it does | When to run |
|---|---|---|
| **`just check`** | Runs *all* checks: format, lint, typecheck, typos, yaml, actions lint, links | After every chunk of work and before committing/pushing |
| **`just fix`** | Auto-fixes formatting, lint issues, and yaml | When `just check` reports fixable issues — handles most linting problems automatically |
| **`just test`** | Runs the full pytest suite | After code changes and before committing/pushing |

**Typical loop:** write code → `just fix` (auto-fix what it can) → `just check` (verify everything passes) → `just test` (make sure nothing is broken) → commit.

**Worktree setup gotcha:** In a fresh `git worktree`, the `test_phase*_example::*_runs_end_to_end` tests (~14 of them) fail with `RuntimeError: Unable to resolve dynamic pipeline source. Make sure your pipeline is defined at the top level of your module.` This is because ZenML's dynamic pipeline resolver uses `get_source_root()` to locate the project root before re-importing the pipeline module by dotted path, and that root comes from the `.kitaru/` marker. Worktrees don't inherit `.kitaru/` from the main checkout, so the resolver can't find `examples.features.basic_flow....` on `sys.path`. **Fix:** run `uv run kitaru init` once in the new worktree after `uv sync`. The unit test suite passes without this — only the end-to-end example tests need it.

```bash
# Setup
uv sync                              # Install dependencies
uv sync --extra local                # Include local ZenML runtime components
uv run kitaru init                   # Required in a fresh git worktree — see note below

# Common Python workflows
just check                            # Run all checks (format, lint, typecheck, typos, yaml, actions lint, links)
just test                             # Run all tests
just test tests/test_foo.py           # Run a single test file
just test tests/test_foo.py::test_bar # Run a single test
just test -x                          # Stop on first failure
just fix                              # Auto-fix formatting, lint, and yaml

# Agent tip: the full suite takes ~4 minutes. When running it through a
# pager/truncated stream that may drop the failure list, pipe through
# grep so the failure names survive:
#   just test 2>&1 | grep -E "FAILED|ERROR|passed|failed" | tail -20
# That keeps the PASS/FAIL summary and every FAILED line without
# forcing a rerun just to recover the list.

# Individual checks
just lint                             # Lint only
just typecheck                        # Type check only
just typos                            # Typo check only
just format-check                     # Check formatting without modifying
just yaml-check                       # Check YAML formatting
just actions-lint                     # Lint GitHub Actions workflows (requires actionlint)
just zizmor                           # Audit GitHub Actions workflow security
just audit                            # Audit Python dependencies with pip-audit
just links                            # Check markdown links offline (requires lychee)
just build                            # Build wheel + sdist locally

# Docs workflows (require Node 22+ and pnpm)
just generate-docs                    # Generate CLI reference + changelog + SDK reference docs
just docs                             # Preview docs dev server (localhost:3000)
just docs-build                       # Build docs static export
just docs-validate                    # Validate the static export as served under /docs

# Kitaru UI bundle testing
# Read FRONTEND-TESTING.md before changing UI bundle, frontend smoke, Docker dashboard, or release UI workflows.
just ui-bundle                                # Download latest stable/full kitaru-ui-v* bundle
just UI_TAG=kitaru-ui-v0.2.0 ui-bundle        # Download a specific stable UI bundle
just UI_TAG=kitaru-ui-v0.3.0-rc.1 ui-bundle-prerelease  # Explicit prerelease opt-in
just ui-login                                 # Start local Kitaru with prepared bundle
just ui-smoke                                 # Smoke test prepared bundle and keep server running

# Docker
just server-image                              # Build production server image (bundles latest stable UI first)
just DOCKER_TAG=v0.2.0 server-image            # Build with specific image tag
just UI_TAG=kitaru-ui-v0.2.0 server-image      # Build with specific stable Kitaru UI release
just server-image-push                         # Build + push to Docker Hub
just server-dev-image                          # Build dev server image (requires docker/kitaru-ui-dist/)

# Public website deploys are owned by the sibling zenml-io-v2 repository.
```

### CI/CD workflows

| Workflow | Trigger | Purpose |
|---|---|---|
| `ci.yml` | Push/PR to `develop` | Python checks: lint, format, yaml, typos, typecheck, dependency audit, links, Docker server smoke, wheel packaging, and tests across base installs (3.11 + 3.12 + 3.13) plus additional `kitaru[mcp]` test lanes |
| `docs.yml` | Manual dispatch; push to `main`; selected docs/script/source PR paths | Generate, build, validate, and deploy `kitaru.ai/docs`; create/clean same-repo PR preview Workers |
| `release.yml` | Workflow dispatch or `v*` tag | Stable Kitaru UI bundling, version/changelog/lock handling for dispatch releases, PyPI publish, Docker image publish, Helm OCI chart publish, release branch/main update, GitHub Release |
| `ui-prerelease-smoke.yml` | Manual dispatch | Tests an explicit prerelease Kitaru UI bundle against a Kitaru ref without publishing PyPI, Docker, Helm, tags, or releases |
| `spellcheck.yml` | Manual/reusable runs, push to `develop`, non-draft PRs | Separate typo/spell checking |
| `image-optimiser.yml` | PRs changing JPG/JPEG/PNG/WebP files | Image compression for same-repo non-draft PRs |
| `zizmor.yml` | Workflow/dependabot changes, weekly schedule, manual runs | GitHub Actions security analysis |

When working with Python, invoke the relevant /astral:<skill> for uv, ty, and ruff to ensure best practices are followed.

## Architecture

> **Note:** Most SDK primitives and CLI commands are implemented (see table below). Replay is now implemented across SDK, flow objects, CLI, and MCP surfaces.

### Current MVP primitives

| Primitive | Status |
|---|---|
| `@flow` | Implemented |
| `@checkpoint` | Implemented |
| `kitaru.wait()` | Implemented |
| `kitaru.llm()` | Implemented |
| `kitaru.log()` | Implemented |
| `kitaru.save()` | Implemented |
| `kitaru.load()` | Implemented |
| Stack lifecycle (`list_stacks` / `current_stack` / `use_stack` / `create_stack` / `delete_stack`) | Implemented |
| `kitaru.configure()` + config precedence | Implemented |
| `KitaruClient` (`get/list/latest/logs/input/retry/resume/cancel/replay` + artifact browsing) | Implemented |
| Execution CLI (`kitaru executions get/list/logs/input/replay/retry/resume/cancel`) | Implemented |
| Secrets CLI (`kitaru secrets set/show/list/delete`) | Implemented |
| `KitaruClient.executions.replay()` | Implemented |
| `kitaru init` (project initialization, creates `.kitaru/`) | Implemented |
| `kitaru clean` (`project` / `global` / `all` — reset Kitaru state with dry-run, backup, and model registry protection) | Implemented |
| Enhanced `kitaru info` (`--all`, `--all-packages`, `--packages`, `--file` — config provenance, connection sources, system info) | Implemented |

### Key design patterns

- **Flows are top-level orchestration boundaries** — direct flow calls are blocked; start executions with `.run()`
- **Nested checkpoint calls are blocked in the current MVP implementation**
- **Concurrency** uses `.submit()` + `.result()` (ZenML futures), not a dedicated primitive
- **Isolated runtime** via `@checkpoint(runtime="isolated")` runs a checkpoint in its own container on remote orchestrators; locally it falls back to inline
- **Replay** works by re-running the flow from the top: checkpoints before the replay point return cached outputs; checkpoints at/after the replay point re-execute
- **Artifact overrides** let you swap a checkpoint's cached output during replay

### Framework adapters

Implemented framework adapters include `kitaru.adapters.pydantic_ai.KitaruAgent(agent, ...)` and the OpenAI Agents adapter under `kitaru.adapters.openai_agents`. The OpenAI Agents adapter is behind the `openai-agents` optional extra (`uv sync --extra openai-agents` or `kitaru[openai-agents]`) and exposes `KitaruRunner` for durable OpenAI Agents runs.

The PydanticAI adapter keeps the enclosing checkpoint as the replay boundary while tracking PydanticAI model requests and tool calls as child events/artifacts under that checkpoint. At flow scope, `run()` / `run_sync()` automatically open a synthetic checkpoint per turn so tracking still works without an explicit outer checkpoint; outside any flow they auto-open a local flow (remote stacks require an explicit `@kitaru.flow`). Capture is controlled via a `CapturePolicy` (`tool_capture="full"|"metadata"|None` plus per-tool overrides). HITL is auto-bridged: PydanticAI's native `requires_approval=True`, `ApprovalRequired`, and `CallDeferred` all route through `kitaru.wait(...)` with no decorator. For explicit HITL markers, use `kitaru.adapters.pydantic_ai.hitl_tool(...)`. Per-turn checkpoint behavior (runtime, retries, type) is configurable via `KitaruAgent(..., turn_checkpoint_config={"runtime": "inline"})`; adapter-managed checkpoints do not yet support `runtime="isolated"`.

### Observability (current MVP + planned)

Current MVP observability includes:

- `kitaru.log()` for structured metadata on executions/checkpoints
- Runtime log retrieval via `KitaruClient.executions.logs(...)`, `kitaru executions logs`, and MCP `get_execution_logs`
- Global runtime log-store configuration via `kitaru log-store set/show/reset`
  (defaults to `artifact-store`, supports global external backend override, and now warns when preference differs from the active stack log store)

Future work will add richer OpenTelemetry-native tracing and exporter integration.

## Code style

- **US English spelling** everywhere (code, comments, docs): "initialize", "color", "serialize"
- **Comments explain *why*, not *what*.** No change-tracking comments ("Updated from X", "Refactored this"). No narrating obvious code (`x = x + 1  # increment x`). Add comments only for intent, trade-offs, constraints, edge cases, or non-obvious decisions. Prefer expressive names and small functions over inline commentary.
- **Prefer typing over dynamic attribute checks.** Use Protocols/ABCs or `isinstance` narrowing instead of `getattr`/`hasattr`. If dynamic access is unavoidable, isolate it in a small typed helper.
- **No postponed annotations in flow/checkpoint modules.** Do not add `from __future__ import annotations` to files that define Kitaru `@flow`/`@checkpoint` functions or ZenML `@pipeline`/`@step` functions. ZenML inspects step output annotations at runtime and currently rejects string annotations such as `"dict[str, Any]"`; use real runtime annotations instead. Python 3.11+ already supports `list[str]` / `str | None` without the future import.
- **Util function placement:** Put a helper on the class if it's tied to the class's behavior or heavily used by subclasses (saves imports, subclasses just call `self.method()`). Use standalone util files only for truly generic functions used across unrelated modules.
- **`_underscore` means private.** `_method()` on a class → only call from within that class. `_function()` in a module → only call from within that module. Do not call private methods/functions from outside their owning class or module.

## Analytics instrumentation

Kitaru collects anonymous usage analytics for users who have opted in (via ZenML's global analytics setting). When adding new features, discuss analytics coverage with the core team during planning to decide what (if anything) should be tracked.

- **Event registry:** all event names live in the `AnalyticsEvent` enum in `src/kitaru/analytics.py`. Add new events there — never use raw strings.
- **Privacy by design:** track only non-sensitive metadata (event names, boolean flags, enum values, counts). Never include user content, file paths, prompts, model outputs, secret values, or positional CLI arguments. The CLI command tracker uses an allowlist of known multi-word commands (`_MULTI_TOKEN_COMMANDS`) to avoid leaking positional args.
- **Three instrumentation surfaces:**
  - **CLI** (`src/kitaru/cli.py` + `src/kitaru/_cli/`): entry-point tracking in `cli()`, per-command feature events in subcommand handlers.
  - **MCP** (`src/kitaru/mcp/server.py`): `@tracked_mcp_tool` decorator wraps each tool with automatic success/failure tracking.
  - **Core SDK** (`src/kitaru/`): `track(AnalyticsEvent.X, {...})` calls at key lifecycle points (flow submit/terminal, wait, LLM calls, artifact save/load, replay, etc.).
- **Graceful degradation:** all `track()` calls silently fail if analytics is unavailable. Never let a tracking failure break user-facing functionality.
- **Source tagging:** each entry point calls `set_source()` (cli, mcp, python) so events can be segmented by surface without leaking specifics.

## Versioning and changelog

- **Single source of truth:** the `version` field in `pyproject.toml`. The release workflow bumps it automatically — never change it by hand.
- **Never hardcode the version** in tests or application code. Use `importlib.metadata.version("kitaru")` to read it at runtime.
- **Update `CHANGELOG.md`** when making user-facing changes. Add entries under the `[Unreleased]` heading. The release workflow moves `[Unreleased]` to a versioned heading (e.g. `[0.2.0] - 2026-04-01`) at release time.

## Commits and PRs

- **Run CI checks locally before committing/pushing.** Always run `just check` and `just test` before pushing to `develop`. All checks must pass locally — do not rely on CI to catch failures. This includes format, lint, typecheck, typos, yaml, actions lint, links, and tests.
- **Fix pre-existing failures too.** If `just check` or `just test` surfaces failures that predate your changes, fix them rather than ignoring them. Other people may be working in the same repo, so not every failure is yours — but don't default to "not my problem." Ask the user if unsure whether a failure should be addressed in this commit.
- **Commits:** Imperative mood, concise summary (50 chars or less): "Add feature" not "Added feature". Explain *why* in the body (blank line after summary), reference issues when applicable (`Fixes #1234`).
- **Bug fixes:** Always add a regression test that would have caught the bug. Understand root cause before implementing the fix.
- **PRs:** Human-readable titles (no "feat:"/"doc:" prefixes). Write comprehensive descriptions: what the changes do, why they're needed, key implementation decisions, and areas needing reviewer attention.
- **PR reviewer guidance:** Every PR description should include a "Reviewer Notes" H2 or H3 section, but it should read like a guided walkthrough rather than a file inventory. Explain the story of the change, where the risky behavior lives, what would break if the implementation is wrong, and why the named files or functions matter.
- **PR reproduction:** Include a concrete "Reproduction" subsection inside Reviewer Notes or immediately after it. Prefer a runnable example, CLI flow, or UI path that proves the behavior end to end. Tell the reviewer exactly what to run and what to look for afterward, such as a named `examples/...` script, a UI artifact/checkpoint name, or a `kitaru executions list` / `kitaru executions logs` result.
- **PR local checks:** Do not create a standalone "Verification" section that only lists `just check`, `just test`, or `/simplify`. Those are still required local hygiene, but they are not useful reviewer guidance by themselves. If useful, include them as a short "Local checks run" note after the reproduction instructions.
- **Before opening a PR or making a large commit**, always run `/simplify` to review changed code for reuse opportunities, quality issues, and efficiency improvements. Fix any issues it finds before committing.
- **Update the smoke test** (`scripts/smoke-test.sh`) when adding new CLI commands, MCP tools, or SDK features that can be exercised non-interactively. New commands should have at least a `--dry-run` or `--help` invocation in the smoke script so pre-release validation catches regressions. Use `--dry-run` where available to keep the smoke test non-destructive.
- **Review analytics coverage** when expanding the CLI, MCP, or SDK surface. Check whether the new feature needs a tracking event in `AnalyticsEvent` and whether the event is wired into the appropriate surface (CLI handler, `@tracked_mcp_tool`, or SDK lifecycle point). See the [Analytics instrumentation](#analytics-instrumentation) section for patterns. If multi-word CLI commands are added, update `_MULTI_TOKEN_COMMANDS` in `cli.py` to avoid leaking positional arguments into analytics.
- Never include a "[Codex] " or "feat: " prefix to PR titles.

## CLI

The CLI uses [cyclopts](https://cyclopts.readthedocs.io/). `src/kitaru/cli.py` is the thin facade / console entrypoint, and the actual command implementations live under `src/kitaru/_cli/`. The `kitaru` console script is registered in `pyproject.toml` under `[project.scripts]`.

- Add new subcommands in the appropriate `src/kitaru/_cli/_*.py` module and register them on the shared app there
- Version is read automatically from package metadata via `importlib.metadata.version()`
- When testing CLI commands, always pass an explicit arg list: `app(["--help"])`, never bare `app()` (which reads `sys.argv`)
- CLI commands raise `SystemExit(0)` on success — wrap in `pytest.raises(SystemExit)` in tests

### CLI output styling

CLI output uses [Rich](https://rich.readthedocs.io/) for styled terminal output with a **dual-mode pattern**: Rich panels/colors for interactive terminals, plain text for non-TTY output (pipes, CI, tests). The `_is_interactive()` helper controls mode selection.

- Use `_emit_snapshot()` for key/value views (status, info), `_print_success()` for success messages, `_exit_with_error()` for errors
- Use `rich.text.Text` objects for user-supplied values — never interpolate them into Rich markup strings (avoids `[`/`]` misinterpretation)
- Create `Console()` lazily inside helpers, not at module level (pytest replaces streams after import)
- Tests use `capsys` and assert on plain-text substrings — the non-TTY path keeps this stable

## Conventions

- Python 3.11+
- Type hint all function parameters and return values
- Use modern type annotations: `list[str]` not `List[str]`, `str | None` not `Optional[str]`, `dict[str, int]` not `Dict[str, int]` — no `from typing import` for these
- src layout (`src/kitaru/`)
- Use `uv` for all package management (never raw pip)
- Use `ruff` for linting/formatting, `ty` for type checking
- Use `pytest` for testing
- Prefer Pydantic models for data structures
- Return values from checkpoints must be serializable (prefer Pydantic models or JSON-compatible types)
- Design docs live in `design/` — this folder is gitignored and must never be committed
- Never commit RepoPrompt/orchestration scratch Markdown such as plans, reviews, investigations, handoffs, or prompt exports. Keep `docs/plans/*.md`, `docs/reviews/*.md`, `docs/investigations/*.md`, `prompt-exports/*.md`, and ad-hoc handoff files out of repo history unless the user explicitly asks for that artifact to be committed.
- Follow Google Python style for docstrings
