diff --git a/docs/cli-navigation.md b/docs/cli-navigation.md
new file mode 100644
index 00000000..d668f9d5
--- /dev/null
+++ b/docs/cli-navigation.md
@@ -0,0 +1,191 @@
+# tracebloc CLI — navigation map
+
+**The single source of truth for how a user moves through the CLI.** Every command, decision point, and place a user can end up. Diagrams are [Mermaid](https://mermaid.js.org) — they render natively on GitHub and in the docs, and this file is version-controlled, so **edit this file + open a PR to change the map** (that PR is where we discuss flow changes).
+
+> **Basis:** `develop` @ `27c5392`. Two things are marked distinctly:
+> - **`resources show`** has shipped to `develop` (#237) → drawn solid; **`resources set`** (#241) is still on a feature branch → drawn **dashed / "not shipped"**.
+> - The **status-aware home screen** (greeting + sign-in + environment) is a *proposed* redesign (see `home-screen-spec.md`); today's home screen is **stateless** (same for everyone). Proposed bits are dashed.
+
+**How to read it:** `{diamond}` = decision · `[box]` = step · `([rounded])` = where you end up · green = exit 0 · red = non-zero exit · grey dashed = hidden (installer/back-compat) · orange dashed = proposed / not-yet-shipped.
+
+---
+
+## 1. Top-level map — what's reachable from `tracebloc` / `tb`
+
+```mermaid
+flowchart TD
+ H0["tracebloc / tb — home screen"]
+
+ H0 --> ACCT["Account (needs sign-in)"]
+ H0 --> ENVC["Your secure environment & data (needs a reachable client)"]
+ H0 --> OFF(["delete — offboard this machine"])
+
+ ACCT --> login["login"]
+ ACCT --> logout["logout"]
+ ACCT --> authst["auth status"]
+ ACCT --> clis["client status"]
+ ACCT -.-> clcreate["client create"]:::hidden
+ ACCT -.-> cllist["client list"]:::hidden
+
+ ENVC --> di["data ingest"]
+ ENVC --> dl["data list"]
+ ENVC --> dd["data delete"]
+ ENVC --> dv["data validate (local only, no cluster)"]
+ ENVC --> ci["cluster info"]
+ ENVC --> doc["doctor (a.k.a. cluster doctor today)"]
+ ENVC --> res["resources show"]
+ ENVC -.-> resset["resources set"]:::proposed
+
+ classDef hidden fill:#eee,stroke:#999,stroke-dasharray:3 3,color:#666;
+ classDef proposed fill:#fff5e6,stroke:#e0a24a,stroke-dasharray:5 5;
+```
+
+`tb` is a convenience alias for `tracebloc` (installer-placed symlink; identical behavior). Aliases kept one deprecation cycle: `data`↔`dataset`, `data ingest`↔`push`, `data delete`↔`rm`. Hidden nodes are fully functional but off the everyday surface (installer / back-compat).
+
+---
+
+## 2. The two gate chains — the key structural fact
+
+Commands split into **two independent families** that authenticate differently. Account commands need you signed in (a user token). Data / environment commands are **auth-free w.r.t. your login** — they reach the cluster via kubeconfig RBAC and mint an in-cluster token. Only `client create` and `delete` cross both.
+
+```mermaid
+flowchart TD
+ subgraph A["Chain A — Account (your user token)"]
+ a0["login · logout · auth status · client * · delete"] --> a1{"signed in?"}
+ a1 -->|no| aL(["exit 1 → run: login"]):::fail
+ a1 -->|yes| a2{"token valid? (WhoAmI)"}
+ a2 -->|"401 / 403"| aL
+ a2 -->|"426"| aU(["exit 1 → upgrade the CLI"]):::fail
+ a2 -->|ok| aOK(["proceed"]):::ok
+ end
+
+ subgraph B["Chain B — Data & environment (kubeconfig + in-cluster token; NO sign-in)"]
+ b0["data ingest / list / delete · cluster info · resources"] --> b1{"kubeconfig reachable?"}
+ b1 -->|no| bK(["exit 3 → fix kubeconfig · doctor"]):::fail
+ b1 -->|yes| b2{"client / release found?"}
+ b2 -->|no| bR(["exit 4 → run the installer, or --namespace"]):::fail
+ b2 -->|yes| b3{"shared storage bound? (ingest / delete)"}
+ b3 -->|no| bR
+ b3 -->|yes| b4{"in-cluster token mintable?"}
+ b4 -->|no| bT(["exit 5 → grant RBAC"]):::fail
+ b4 -->|yes| bOK(["proceed"]):::ok
+ end
+
+ classDef fail fill:#fdecec,stroke:#d9534f;
+ classDef ok fill:#eafaea,stroke:#5cb85c;
+```
+
+---
+
+## 3. `data ingest` — the core flow
+
+The wizard fills only what flags left empty; off a TTY (or `--no-input` / `--output-json`) every gap becomes a hard error instead of a prompt.
+
+```mermaid
+flowchart TD
+ di["data ingest <path>"] --> wiz{"interactive TTY
and not --no-input?"}
+ wiz -->|yes| W["guided wizard — fills only what's missing:
train/test → name → path → task → label → extras → review → confirm"]
+ wiz -->|no| FLAGS["flag-only path"]
+ W -->|"cancel / Ctrl-C"| cwiz(["exit 0 — nothing ingested"]):::ok
+ W --> LOCAL
+ FLAGS --> LOCAL
+
+ LOCAL{"local checks — path exists · name valid · task supported ·
flags relevant · layout walk · schema · content preflight"}
+ LOCAL -->|"bad input"| e2(["exit 2"]):::fail
+ LOCAL -->|"unreadable / parse"| e3(["exit 3"]):::fail
+ LOCAL -->|ok| CONN["Connecting to your workspace… (Chain B: kubeconfig→3, no client/storage→4)"]
+
+ CONN --> DRY{"--dry-run?"}
+ DRY -->|yes| e0dry(["exit 0 — checks out, nothing created"]):::ok
+ DRY -->|no| DEST{"destination already exists?"}
+ DEST -->|"new"| STAGE
+ DEST -->|"exists · interactive"| REPL{"replace it?"}
+ REPL -->|no| e0a(["exit 0 — left as-is"]):::ok
+ REPL -->|yes| TD
+ DEST -->|"exists · non-interactive · no --overwrite"| e6(["exit 6 — already exists"]):::fail
+ DEST -->|"--overwrite"| TD["teardown existing (fail → exit 7)"]
+ TD --> STAGE
+
+ STAGE["Step 2/3 — copy into your workspace (stage pod; fail → exit 7)"] --> SUB["Step 3/3 — submit → watch → summary"]
+ SUB --> OUT{"outcome"}
+ OUT -->|"token 401/403"| e5(["exit 5"]):::fail
+ OUT -->|"submit rejected 4xx/5xx"| e8(["exit 8"]):::fail
+ OUT -->|"Failed / Unknown / watch lost / completed-with-failures"| e9(["exit 9"]):::fail
+ OUT -->|"--detach / Ctrl-C mid-watch"| e0d(["exit 0 — detached (kubectl logs -f)"]):::ok
+ OUT -->|"Succeeded, clean"| e0s(["exit 0 — done"]):::ok
+
+ classDef fail fill:#fdecec,stroke:#d9534f;
+ classDef ok fill:#eafaea,stroke:#5cb85c;
+```
+
+> Note: exit codes are **not** monotonic in execution order — staging (exit 7) runs *before* the token mint (exit 5). The diagram shows the true order.
+
+---
+
+## 4. `resources` — show & set *(`show` shipped #237, on `develop`; `set` on feature branch #241, not yet on `develop`)*
+
+```mermaid
+flowchart TD
+ R0["resources (bare = show)"] --> Rg{"Chain B gates
kubeconfig→3 · no client→4"}
+ Rg -->|ok| RS(["show machine capacity + per-run ceiling — exit 0"]):::ok
+
+ RSET["resources set [max]"] --> RVsh{"request valid?
(not max+flags; not empty)"}
+ RVsh -->|no| re2a(["exit 2"]):::fail
+ RVsh -->|ok| Rmode{"max · flags · or wizard?"}
+ Rmode -->|"wizard (TTY)"| RW["current vs machine →
'Use as much as possible' (default) / choose / leave"]
+ Rmode -->|flags| RF["override only the passed dimensions"]
+ Rmode -->|max| RM["whole machine − overhead"]
+ RW --> FIT
+ RF --> FIT
+ RM --> FIT
+
+ FIT{"fits the machine? (+ floors)"}
+ FIT -->|no| re2b(["exit 2 — too big / too small
(macOS: raise Docker Desktop)"]):::fail
+ FIT -->|"no change"| re0n(["exit 0 — nothing to change"]):::ok
+ FIT -->|ok| CONF{"confirm? (--yes skips)"}
+ CONF -->|"declined / non-TTY, no --yes"| re0c(["exit 0 declined / exit 1 non-TTY"]):::fail
+ CONF -->|yes| PIN{"chart version pinned?"}
+ PIN -->|no| re1(["exit 1 — refuse unpinned upgrade"]):::fail
+ PIN -->|yes| APPLY(["helm upgrade — applies to your next run — exit 0"]):::ok
+
+ classDef fail fill:#fdecec,stroke:#d9534f;
+ classDef ok fill:#eafaea,stroke:#5cb85c;
+```
+
+---
+
+## Exit codes
+
+| code | meaning |
+|---|---|
+| 0 | success (incl. dry-run, detached, "nothing to change", declined-safely) |
+| 1 | generic / account-auth (not signed in, token rejected, upgrade required) |
+| 2 | bad input / schema violation / doesn't fit |
+| 3 | kubeconfig unreachable, or a local file/parse error |
+| 4 | cluster reached but no tracebloc client / storage |
+| 5 | in-cluster token could not be minted (RBAC) |
+| 6 | destination dataset already exists |
+| 7 | staging / teardown failed |
+| 8 | jobs-manager rejected the submit |
+| 9 | ingestion Job failed / partial-failure / watch error |
+| 130 | Ctrl-C |
+
+## Cross-links — where a dead-end points
+
+- **not signed in / token 401·403** → `login`
+- **426 upgrade-required** → upgrade the CLI
+- **kubeconfig (exit 3)** → fix `--kubeconfig`/`--context`, then `doctor`
+- **no client / environment (exit 4)** → run the installer (or `--namespace`); triage with `doctor`
+- **no token (exit 5)** → grant RBAC; diagnose with `cluster info` / `doctor`
+- **destination exists (exit 6)** → `--overwrite`, a different `--name`, or `data delete` first
+- **staging partial (exit 7)** → `data delete` then re-ingest
+- **ingest failed (exit 9)** → the panel / `kubectl get job` / `kubectl logs -f`
+- **no active client** (client status / delete) → `client create` / re-run installer
+
+## Known gaps / decisions (raise in review)
+
+1. **Home screen is stateless today.** The status-aware redesign (greeting + sign-in + environment + compute) in `home-screen-spec.md` is *proposed* — it's the target, not current behavior.
+2. **`delete` (offboard) exits 0 even on a *partial/degraded* teardown** — it warns but never returns non-zero, so a script can't detect an incomplete offboard. A dedicated non-zero "partial offboard" code would close this.
+3. **`resources set` is unshipped on `develop`.** `show` shipped via #237 (now on `develop`); `set` lands via #241 (still on a feature branch).
+4. **`cluster doctor` → `tb doctor`** rename is decided (home-screen spec); the map already shows `doctor`. `cluster info`'s home (stay under `cluster`, or also promote) is open.
+5. Terminology in the live copy (client / cluster / `