zc2 0.0.23

P2P compute broker with credit-based billing, WAL, and broker mesh support
# `zc` Terminal Refactor — Handoff Spec

Self-contained brief for an agent taking over the interactive-terminal refactor.
Read this together with the full design in
[`docs/TERMINAL_REFACTOR.md`](./TERMINAL_REFACTOR.md). This file says what is
**done**, what is **in progress**, and exactly **what to do next**.

---

## 1. Goal & scope

Bring the `zc` interactive terminal up to a Claude Code–style interface. Scope is
the **interface only**, on three pillars:

1. **Rich REPL UX** — editable prompt, history, multiline, streaming scrollback.
2. **Slash commands & modes**`/`-triggered command palette, autocomplete,
   mode-aware event loop, status line.
3. **Theming & rendering**`Theme` type + config, markdown/code rendering,
   spinners, reusable panels.

**Out of scope (confirmed):** agentic tool-use / diff-apply loop.

## 2. Adopted decisions (from §7 of the design doc)

- **Launch:** ~~bare `zc` keeps printing help; the REPL launches via an explicit
  `zc shell`.~~ **SUPERSEDED (2026-06, user decision):** bare `zc` now launches
  the REPL by default (falling back to `help` when stdout is not a TTY).
  `zc shell` / `zc repl` remain explicit entry points; `zc -h/--help/--full`
  still print help.
- **Dependencies:** MSRV-safe baseline — prefer `pulldown-cmark` (markdown) +
  `fuzzy-matcher` (palette). Treat `nucleo`, `syntect`, `tui-markdown` as
  optional upgrades behind a Cargo feature, each validated individually.
- **Agentic loop:** excluded.

## 3. Hard constraints (do not violate)

- **MSRV is Rust 1.75.** Pinned by `ratatui = "0.26"` / `crossterm = "0.27"`.
  Every new crate must build on 1.75 and be compatible with ratatui 0.26.
- **`cargo-deny` runs in CI** (`.github/workflows/security.yml`: `check
  advisories bans licenses sources`). Any new dependency may require updating
  the deny config. **Phase 0 added zero dependencies on purpose.**
- **No global lint gate.** There is no `#![deny(warnings)]` and CI does not run
  `clippy -D warnings`. CI (`build.yml`) runs `cargo build --release` then
  `cargo test --release --bin zc`. Warnings won't fail CI, but keep it clean.
- **Crate facts:** binary `zc`, package `zc2`, version `0.0.16`, edition 2021.
  TUI stack is `ratatui` + `crossterm`. CLI uses **no** arg-parser crate — it's a
  hand-rolled `match args.len()` ladder in `src/main.rs`.
- **`ratatui` has no `serde` feature enabled**, so `ratatui::style::Color` is
  **not** `Serialize`/`Deserialize`. Persist colors as strings (already handled
  in `tui/theme.rs`).

## 4. ⚠️ Verification environment

The agent that produced Phase 0 had **no Rust toolchain** (sandboxed Linux, no
root, rustup network-blocked) and **could not compile**. All Rust code so far is
written from careful static review only. **Before trusting any phase, compile on
a real machine:**

```sh
cargo build
cargo test --bin zc -- theme command   # Phase 0 unit tests
cargo build --release                   # what CI runs
```

If you have a toolchain, compile-verify after every phase. If a phase doesn't
compile, the safest recovery is to delete the newly-added files for that phase
(the existing working code was left intact — see below).

---

## 5. Current status

### Phase 0 — DONE & COMPILE-VERIFIED (additive, no behavior change)

Compile-verified on a real toolchain (`cargo build`, `cargo test --bin zc --
theme command` → 10/10 pass). One fix was required: `builtin_specs()` returned a
reference to a temporary array; it now returns a `static SPECS: &[CommandSpec]`.

Files **added**:

- `src/tui/theme.rs``Theme` struct (ratatui `Color` palette). `Theme::dark()`
  reproduces the legacy `broker::tui::colors` constants **exactly**; `light()`
  variant; `named()` lookup; TOML load/save via a string-based `ThemeConfig`
  (no ratatui serde dependency). Has unit tests.
- `src/command/spec.rs``CommandSpec`, `ArgSpec`, `Category` (declarative
  command model; `Copy`-able; built from `&'static` data via const fns).
- `src/command/mod.rs``CommandRegistry` (alias-aware `resolve`, `by_category`,
  substring `search`), `CommandResult`, `RenderBlock`, and `builtin_specs()` — the
  full catalog mirroring today's `main.rs` commands. Has unit tests.
- `src/tui/mod.rs` — module root; declares `pub mod theme;`, documents target
  layout.

Files **changed**:

- `src/main.rs` — added `mod command;` and `mod tui;` (only change). New modules
  carry `#![allow(dead_code)]` so unused-warnings stay quiet until wired.

### Phase 1 — DONE & COMPILE-VERIFIED (cutover complete)

Goal achieved: a normalized `Snapshot` + `DataSource` trait now render the
dashboard from **one** code path. `broker/tui.rs` dropped from **1465 → 296
lines** (the entire duplicated `TuiApp`/`RemoteTuiApp` render + input bodies were
deleted).

Files **added**:

- `src/tui/views/snapshot.rs``Snapshot`, `SourceMode`, `Connection`,
  `WorkerRow`, `WorkerKind` (normalized model; reuses `BrokerMetrics`,
  `TransactionRecord`, `TaskOfferRecord` verbatim).
- `src/tui/views/monitor.rs` — the single `render(frame, &Snapshot, &Theme,
  &MonitorUi)` renderer + `Panel`/`MonitorUi`. Local/remote-only differences
  (task-pile columns, the stream panel, header, footer hint, help overlay keys)
  are keyed off `Snapshot::mode` so visuals match the legacy UI exactly.
- `src/tui/views/mod.rs`, `src/tui/data/mod.rs` — module roots + re-exports.
- `src/tui/data/local.rs``LocalSource` (mirrors old `TuiApp::get_metrics`).
- `src/tui/data/remote.rs``RemoteSource` + `RemoteShared` (replaces the old
  private `RemoteTuiShared`). `snapshot()` maps the latest `StatsResponse`;
  stats-present takes priority over a stale error, matching legacy behavior.

Files **changed**:

- `src/tui/mod.rs` — declares `pub mod data;` / `pub mod views;`.
- `src/broker/tui.rs`**cutover**. `run_tui` / `run_remote_tui` /
  `cleanup_terminal` keep identical public signatures; they now build a
  `DataSource`, run the event loop, and call `monitor::render`. The remote
  background fetch/diff thread + `zk_<user>_…` key parsing were preserved
  verbatim. `mod colors` and both `*TuiApp` structs are gone.

Verification: `cargo build`, `cargo build --release`, and `cargo test --release
--bin zc` (155 pass; the only failures are pre-existing/environmental — a
`bind 127.0.0.2:0` test that can't run on this host, and a broker-startup timeout
under heavy parallel test load). New TUI modules compile warning-free.

> ⚠️ Not yet done: interactive visual confirmation of `zc -t broker` and
> `zc attach …` on a live broker (needs a real broker + TTY). The port is a
> faithful 1:1 of the legacy render code, but a human should eyeball both
> dashboards before considering Phase 1 fully signed off.

---

## 6. Phase 1 — detailed spec

### 6.1 New files

```
src/tui/views/mod.rs
src/tui/views/snapshot.rs   # normalized data model
src/tui/views/monitor.rs    # single dashboard renderer: fn render(frame, &Snapshot, &Theme)
src/tui/data/mod.rs         # DataSource trait
src/tui/data/local.rs       # LocalSource: SharedBrokerState + StatsCollector -> Snapshot
src/tui/data/remote.rs      # RemoteSource: wraps shared HTTP-fetch state -> Snapshot
```

Add to `src/tui/mod.rs`: `pub mod views;` and `pub mod data;`.

### 6.2 `Snapshot` model (`views/snapshot.rs`)

Reuse `broker::stats::{BrokerMetrics, TransactionRecord, TaskOfferRecord}`
directly (don't re-derive). Normalize only the worker shape, since local yields
`broker::worker::Worker` and remote yields `broker::stats::WorkerStats`.

```rust
pub struct Snapshot {
    pub title: String,                 // "Zakuro Compute Broker"
    pub mode: SourceMode,              // Local | P2P | Remote
    pub host_port: String,             // "0.0.0.0:9000"
    pub wireguard_ip: Option<String>,
    pub ledger_connected: bool,
    pub connection: Connection,        // Connected | Connecting | Error(String)
    pub metrics: BrokerMetrics,        // reused
    pub workers: Vec<WorkerRow>,       // normalized
    pub transactions: Vec<TransactionRecord>,
    pub task_offers: Vec<TaskOfferRecord>,
    pub rps_history: Vec<f64>,
    pub events: Vec<String>,           // stream log (remote only today)
}

pub enum SourceMode { Local, P2p, Remote }
pub enum Connection { Connected, Connecting, Error(String) }

pub struct WorkerRow {
    pub status: WorkerKind,            // normalized status
    pub name: String,
    pub cpus_available: f64,
    pub memory_gib: f64,
    pub avg_latency_ms: f64,
}
pub enum WorkerKind { Healthy, Busy, Unhealthy, Draining, Unknown }
```

### 6.3 `DataSource` trait (`data/mod.rs`)

```rust
pub trait DataSource: Send {
    fn snapshot(&self) -> Snapshot;     // non-blocking; for render
    fn request_refresh(&self) {}        // e.g. 'r' key (remote)
}
```

### 6.4 `monitor::render` (`views/monitor.rs`)

Port the existing render functions from `broker/tui.rs` **once**, taking
`&Snapshot` + `&Theme` instead of `&self` and `colors::*`:

- header (title, mode badge, ledger dot, host:port, wireguard, uptime)
- task pile, transactions (zebra rows via `theme.row_alt`), stream (remote)
- workers, metrics + RPS sparkline, footer, help overlay

Replace every `colors::X` with `theme.x`. Keep ratatui 0.26 APIs identical to the
current file (`Table::new(rows, widths)`, `Sparkline`, `symbols::bar::NINE_LEVELS`,
`frame.size()`, etc.).

### 6.5 Adapters

`LocalSource { state: SharedBrokerState, stats: Arc<StatsCollector> }` — mirror
`TuiApp::get_metrics`: `let workers = state.workers.list(); let active =
workers.iter().filter(|w| w.status == WorkerStatus::Healthy).count();
stats.metrics(active, workers.len(), state.is_local_mode(), false,
state.own_wireguard_ip.clone())`. Map each `Worker``WorkerRow` (memory:
`w.resources.memory_available as f64 / (1024.0^3)`).

`RemoteSource` — wrap the existing background-fetch shared state
(`Arc<Mutex<{stats, last_error, event_log}>>` + `refresh_requested: Arc<AtomicBool>`).
`snapshot()` reads the latest `StatsResponse` and maps `WorkerStats` → `WorkerRow`
(`memory_gib = w.memory_available_gib`, status from `w.status` string). Keep the
fetch thread + offer/transaction diffing from `run_remote_tui` as-is for the
cutover.

### 6.6 Cutover (final Phase 1 step, after the above compiles)

Re-implement `broker::tui::run_tui(state, stats, running)` and
`run_remote_tui(broker_url, api_key)` as thin wrappers: build the right
`DataSource`, then run a single shared event loop that calls
`tui::views::monitor::render`. Delete the duplicated `TuiApp` / `RemoteTuiApp`
render methods. Keep `cleanup_terminal()` and the public signatures unchanged
(callers: `broker::server` for `run_tui`; `src/main.rs` for `run_remote_tui` and
`cleanup_terminal`, re-exported in `broker/mod.rs`).

---

## 7. Domain-type cheat-sheet (verified against source)

`broker::stats::BrokerMetrics`: `total_requests u64, successful_requests u64,
failed_requests u64, total_credits_spent f64, requests_per_sec f64,
avg_latency_ms f64, p95_latency_ms f64, active_workers usize, total_workers usize,
uptime_secs u64, local_mode bool, ledger_connected bool, wireguard_ip
Option<String>, wireguard_connected bool`.

`broker::stats::StatsResponse`: `host String, port u16, transactions
Vec<TransactionRecord>, task_offers Vec<TaskOfferRecord>, workers
Vec<WorkerStats>, metrics BrokerMetrics, rps_history Vec<f64>, wireguard_ip
Option<String>, wireguard_connected bool`.

`broker::stats::WorkerStats`: `id, name, uri: String; status: String;
cpus_available f64, memory_available_gib f64, gpus_available u32, price_per_hour
f64, active_requests u32, avg_latency_ms f64`.

`StatsCollector` methods: `recent_task_offers(limit) -> Vec<TaskOfferRecord>`,
`recent_transactions(limit) -> Vec<TransactionRecord>`, `rps_history() ->
Vec<f64>`, `tick_rps()`, `metrics(active_workers usize, total_workers usize,
local_mode bool, ledger_connected bool, wireguard_ip Option<String>) ->
BrokerMetrics`.

`broker::worker`: `Worker { name: String, status: WorkerStatus, resources:
WorkerResources, avg_latency_ms: f64, .. }`; `WorkerResources { cpus_available
f64, memory_available u64 /* bytes */, .. }`; `enum WorkerStatus { Healthy, Busy,
Unhealthy, Draining }`; `WorkerRegistry::list() -> Vec<Worker>`.

`broker::SharedBrokerState = Arc<BrokerState>`; `BrokerState { workers:
WorkerRegistry, config: BrokerConfig, stats: Arc<StatsCollector>, own_wireguard_ip:
Option<String>, .. }`; `BrokerState::is_local_mode() -> bool`. `BrokerConfig` has
`host: String, port: u16`.

`broker::stats::{format_uptime(secs: u64) -> String, format_credits(amount: f64)
-> String}`.

Legacy palette in `broker/tui.rs` `mod colors` (now mirrored by `Theme::dark()`):
`BG=Reset, FG=White, ACCENT=Cyan, SUCCESS=Green, WARNING=Yellow, ERROR=Red,
MUTED=DarkGray, BORDER=DarkGray, HEADER_BG=Rgb(30,30,40), HIGHLIGHT=Rgb(60,60,80)`;
plus inline `Rgb(25,25,30)` (zebra rows → `theme.row_alt`) and `Rgb(20,20,30)`
(overlay bg → `theme.overlay_bg`).

---

## 8. Later phases (summary; full detail in `TERMINAL_REFACTOR.md` §3, §6)

- **Phase 2 — Command registry dispatch.** Give each `CommandSpec` an executable
  handler (`Command` trait + `Ctx` output sink); replace the `match args.len()`
  ladder in `main.rs` with `cli::run(argv)`; generate `help()` / `-h` from the
  registry (delete the hand-written `println!` help wall). Builtins live in
  `src/command/builtins/*.rs`, reused by CLI and REPL.
- **Phase 3 — REPL shell. DONE (compile-verified, headless TestBackend tests).**
  `tui/app.rs` (Mode state machine + event loop), `input.rs` (prompt via
  `tui-textarea`, history, multiline via Alt+Enter), `scrollback.rs` (blocks +
  raw banner lines + scroll), `statusline.rs`. **Launch decision changed by user
  (2026-06): bare `zc` now launches the shell by default** (falls back to `help`
  when stdout isn't a TTY); `zc shell`/`zc repl` also work. Deps added:
  `tui-textarea 0.4`, `fuzzy-matcher 0.3`, `pulldown-cmark 0.10` (see §4 note on
  MSRV). **Not yet done:** long commands on a worker thread streaming
  `RenderBlock`s — execution is currently shell-native commands only (`help`,
  `theme`, `clear`, `quit`, `mode`).
- **Phase 2 — command execution. DONE (REPL path).** Non-shell-native commands
  now run as a `zc <args>` subprocess on a worker thread, streaming stdout into
  the scrollback live (`Scrollback::begin_stream`/`stream_line`), with a
  braille spinner in the footer and **Ctrl-C to interrupt** (kills the child).
  stderr is surfaced on completion; startup `DEBUG:` lines are filtered.
  Interactive commands (`attach`) are blocked with guidance. **Not yet done**
  (the originally-specified Phase 2): replacing the `main.rs` `match args.len()`
  ladder with registry-driven dispatch + generated help, and a first-class
  `Command` trait / `builtins/*.rs`. The REPL currently shells out to the same
  binary rather than calling handlers in-process — a pragmatic, low-risk first
  cut that the registry-dispatch refactor can later supersede.
  - **Network mode + auth (user decision, 2026-06):** the shell starts in
    **local** mode with **no forced login**. A `/mode` command shows/switches
    `local``p2p`; switching to **p2p** requires a `ZAKURO_API_KEY` — if absent,
    a masked `Mode::Login` prompt opens and **keeps asking** until a
    `zk_<user>_…`-shaped key is entered (invalid/empty keys are rejected, never
    partially signed in, and the secret is never stored in command history).
    On success the key is applied via `std::env::set_var` for the session and
    the mode flips to p2p; Esc cancels back to local. The footer shows the
    active mode (`● local` / `● p2p`).
  - **Launch banner:** the Zakuro pomegranate logo (ASCII density map in
    `LOGO`, downsampled 2:1 at runtime by `logo_lines`) + version/cwd + tip.
- **Phase 4 — Slash commands & palette. DONE (basics).** `tui/palette.rs`:
  `/`-triggered fuzzy palette (`fuzzy-matcher`), Up/Down select, Tab complete,
  Enter run, Esc cancel, arg hints from `CommandSpec::usage`. Mode badge in the
  footer. Remaining: richer inline arg hints as you type past the command name.
- **Phase 5 — Rich rendering & theming. PARTIAL.** Done: `render/markdown.rs`
  (pulldown-cmark 0.10 → ratatui `Text`; headings/bold/italic/code/lists/quote/
  rule), `render/spinner.rs`, live `/theme` hot-swap. Remaining: `syntect`
  highlight (feature-gated), `render/panels.rs`, loading a theme from
  `~/.config/zc/theme.toml`.
- **Phase 6 — Verification & docs.** Snapshot tests via
  `ratatui::backend::TestBackend` (+ `insta`); registry/palette/theme unit tests;
  update `README.md` and `docs/USAGE.md`.

## 9. Immediate next actions for the takeover agent

1. ~~Compile-verify Phase 0 (§4).~~ **DONE** — 10/10 tests pass; fixed the
   `builtin_specs()` temporary-array return (now a `static`).
2. ~~Implement Phase 1 §6.1–6.5 additively.~~ **DONE** — builds + tests green.
3. ~~Do the Phase 1 cutover (§6.6); verify duplicated lines are gone.~~ **DONE**
   `broker/tui.rs` 1465 → 296 lines. **Still TODO:** eyeball `zc -t broker`
   and `zc attach …` on a live broker + TTY (couldn't be done headlessly).
4. **NEXT: Phase 2 — command registry dispatch.** Heads-up for the next agent:
   `builtin_specs()` currently mirrors only the "documented" commands. The real
   `main.rs` ladder (≈673 lines) also dispatches `dist`, `vars`, `push`,
   `context`, `build`, and the `-t`/`-d`/`-h` flag prefixes — the registry +
   `Command` trait must cover (or deliberately exclude) these before the
   `match args.len()` ladder can be replaced. Treat the help-wall deletion as the
   last step, after dispatch parity is proven.