TT-Studio CLI (python run.py / tt_setup/)
python run.py is the one entrypoint for everything (setup, start, stop, purge,
logs, status, bug reports). run.py itself is a thin shim: it bootstraps deps
into a managed venv and delegates to the tt_setup/ package. startup.sh is
deprecated.
Read this before touching the launcher. For the terminal-output design system
specifically, dev-docs/launcher-terminal-design.md is the canonical reference —
this skill covers the broader "how the launcher is built and how we like CLIs."
Architecture
run.py → tt_setup.bootstrap.ensure_environment() (venv) → tt_setup.cli.main().
The package is split into focused subpackages, each with an __init__.py that
re-exports its prior public surface:
| Area |
Package / module |
| CLI surface + orchestration |
cli/ (_args.py = Typer flags, _run.py = phased flow + early dispatch) |
| Terminal design system |
console/ (_theme, _stepper, _panels, _prompts, _steps) |
| Env & secrets config |
env_config/ (_values, _dotenv, _preferences, _hf_access, _version, _configure) |
| Host services lifecycle |
services/ (_ports, _fastapi, _docker_control, _frontend, _health) |
| Inference-server artifact |
inference_server/ (_catalog, _config, _env, _git, _metadata, _privileges, _orchestrator) |
| Stop / purge teardown |
cleanup/ (_runtime, _orchestrate, _resource_ops) |
| Docker build/failure diag |
docker_diag/ (_build_progress, _diagnostics) |
| Standalone modules |
bootstrap.py, shell.py, startup_checks.py, docker.py, constants.py, logging.py, monitor.py/monitor_app.py (--status TUI), bug_report.py (--report-bug), shortcut.py (--install-shortcut), spdx.py, settings.py, venv_utils.py |
The re-export rule. A package's __init__.py imports and re-exports the names
its submodules define, so from tt_setup.console import step and
import tt_setup.console as C keep working. When you add/move a submodule, wire
its public names through the __init__.
Linter blind spot — read this before moving code between submodules. Most
submodules do from tt_setup.constants import *. That makes ruff downgrade
undefined-name detection (F821) to a silent F403, so a name you use but
forgot to import in a submodule will NOT be flagged and will crash at runtime.
When you move a function into a submodule, add every import it needs locally
(names are module-scoped now, not inherited from the old monolith). tests/ test_no_undefined_names.py is the backstop; run it.
Adding / changing a CLI flag
- Declare it in
tt_setup/cli/_args.py as a typer.Option, with a
rich_help_panel= so it lands in the right --help group: Setup &
Configuration · Model Deployment · Lifecycle · Reset (--purge-all) · Advanced ·
Developer Tools · Troubleshooting & Info. Deprecated flags use hidden=True
and warn + normalize onto the current flag.
- Thread it into the
args = SimpleNamespace(...) passed to _run(args).
- Dispatch it. Utility/lifecycle flags (
--stop, --status, --logs,
--info, --report-bug, --install-shortcut, --check-headers) run in the
early-dispatch block at the top of _run() — do the work, then return /
sys.exit() before the 5-phase startup. Only flags that modify a normal
startup fall through to the phases.
- Name it for the user: lowercase, action-oriented, and match the wording of
its output ("Stopping/Stopped" for
--stop, not "Cleaning up").
- Add a
tests/test_cli.py case (see Testing) and document it in CLAUDE.md
(Common commands) + dev-docs/run-py-guide.md (options table).
Terminal output — the short version
Full rules: dev-docs/launcher-terminal-design.md. The essentials:
- Everything goes through
tt_setup/console/ — never raw print() + ANSI.
- Calm, minimal by default;
-v reveals everything. Gate routine "done" lines
on if show_detail():. The pinned phase stepper + one collapsing line per
step() is the normal signal; endpoint URLs live in the ready panel.
- Always show failures, prompts, and actionable warnings — never fold those.
- Never wrap an interactive prompt in a capturing
step() or an active phase
spinner — suspend around prompts/sudo/anything with its own Live.
- Degrade cleanly on non-TTY (piped) — no escape-code garbage.
bootstrap.py runs before Rich exists → stdlib only, never import
tt_setup.console. Mimic the calm style by hand (it has a stdlib spinner).
Reusable renderers. When a piece of output should be re-viewable (e.g. the
ready panel via --info), extract a pure renderer that probes live state
(show_ready_panel in cli/_run.py) and call it from both places — don't
duplicate the assembly.
CLI design principles — "what we like"
These emerged across the launcher work; apply them to new launcher behavior.
- Fail fast, with a fix and an escape hatch. Stop early (at the Checks
phase) when something is genuinely wrong, but the message must say how to fix it
and how to opt out — e.g. "fix your TT tooling and re-run, or set
IS_QB2=false". Never a bare traceback; use an error notice_panel.
- Lenient defaults; strict behavior is opt-in. Never block a dev laptop or
cloud/no-hardware box out of the box. Strict checks are opt-in (
IS_QB2) or
gated on real signals (tt-smi installed / /dev/tenstorrent present).
- Assume-and-verify, not assume-and-trust. A config claim (e.g. "this is a
QB2") is checked against reality (tt-smi); a mismatch is surfaced, not hidden.
- Plain language in user-facing text — say "couldn't read the QB2's chips,"
not "tt-smi returned nonzero." Explain internal tool/var names in passing.
- Capture subprocess noise; surface it only on failure. A child process must
not scroll under a live spinner. Capture stdout/stderr (e.g. compose runs with
--ansi never + captured output; run_docker_command(interactive=False) for
captured non-interactive sudo) and reveal the buffer only when the step fails —
the way console.step() does.
- Reuse existing helpers (
build_docker_compose_command, console primitives)
instead of hand-rolling equivalents.
- Make state re-viewable and hints discoverable — the ready panel footer
advertises
--stop / --logs / --info.
Hardware & QB2
- Detect:
detect_tt_hardware() (docker.py, checks /dev/tenstorrent);
check_tt_smi() (shell.py, runs tt-smi -s, returns
(status, detail, board_type)). Classification lives in shell.py:
_classify_boards() mirrors board_control/services.py:get_board_type
(substring match + device count → P300x2/T3K/…); describe_board() maps to
a friendly name; resolve_hardware_label() builds the ready-panel label +
optional QB2 warning (pure/testable).
- QB2 ==
P300x2 (Blackhole QuietBox). IS_QB2 is an opt-in flag,
default false — a dev laptop/cloud is never held to the strict check. The
tt_qb2_launch release branch ships IS_QB2=true. It is independent of
TT_INFERENCE_ARTIFACT_BRANCH, which only selects the inference-server build
(that decoupling is deliberate — don't re-derive QB2 from the artifact branch).
- Behavior when
IS_QB2=true: confirm via tt-smi → label QuietBox (QB2);
a different board → non-fatal warning; can't read the chips while real TT
tooling is present → hard-stop at Checks; no TT tooling at all → skip
(escape hatch). With IS_QB2 false (default) the whole strict path is skipped.
Testing & verification
- Tests live in
tests/ (stdlib unittest), one test_<area>.py per module,
each with the pre-refactor shim try: from tt_setup import X ... except ImportError: import run as X. Run: .tt_studio_run_venv/bin/python -m pytest tests/ -q (keep green).
- Extract pure helpers for anything with branching logic (e.g.
resolve_hardware_label, _classify_boards) so it's unit-testable without
hardware, a TTY, or Docker. Test the matrix of inputs directly.
- Verify terminal output under a real PTY (
pty.spawn([...])) to confirm
spinner animation + \r\033[2K self-heal + collapse to ✓, and pipe non-TTY
output through | cat to confirm no stray escape codes. The live spinner/rule
sweep and real prompts only render on a true terminal — check those on hardware.
tests/test_no_undefined_names.py guards the import * blind spot; keep it
passing when you move code between submodules.
- Try
COLUMNS=80 and COLUMNS=120 for panel/rule widths, and -v to confirm
folded detail returns.
Conventions
- SPDX headers are mandatory on every new
.py (not on .md):
# SPDX-License-Identifier: Apache-2.0 / # SPDX-FileCopyrightText: © 2026 Tenstorrent AI ULC. Run python run.py --check-headers.
- Docker errors link, they don't fix. Point to official docs; never advertise
--fix-docker or Linux-only sudo service docker start. Compose v2 only.
- Git / PRs: follow the
feature-branch-pr skill (branch off dev, minimal
diff, verify, no AI attribution in commits/PRs).
- Keep
CLAUDE.md (Common commands) and dev-docs/run-py-guide.md in sync when
you add a flag; the file map in dev-docs/launcher-terminal-design.md predates
the package split and lists the old monolith paths — treat this skill's table as
the current layout.
1---2name: tt-studio-cli3description: The TT-Studio CLI skill — how the `python run.py` command-line interface (the `tt_setup/` package) is built and how to change it the TT-Studio way: Typer CLI conventions (flag grouping, early-dispatch utility flags, `--help`/`--help-env` docs), package layout and the re-export pattern, the calm minimal-by-default terminal output, hardware/QB2 detection (IS_QB2 opt-in, verify-don't-trust), and the testing/verification patterns (pure helpers, PTY render checks). Use for any CLI work: BEFORE adding or changing a `python run.py` flag or its help text, editing anything under `tt_setup/` or its terminal output, touching startup/teardown flow, or changing hardware detection.4---56# TT-Studio CLI (`python run.py` / `tt_setup/`)78`python run.py` is the one entrypoint for everything (setup, start, stop, purge,9logs, status, bug reports). `run.py` itself is a thin shim: it bootstraps deps10into a managed venv and delegates to the **`tt_setup/`** package. `startup.sh` is11deprecated.1213> Read this before touching the launcher. For the terminal-output design system14> specifically, `dev-docs/launcher-terminal-design.md` is the canonical reference —15> this skill covers the broader "how the launcher is built and how we like CLIs."1617## Architecture1819`run.py` → `tt_setup.bootstrap.ensure_environment()` (venv) → `tt_setup.cli.main()`.2021The package is split into focused **subpackages**, each with an `__init__.py` that22**re-exports its prior public surface**:2324| Area | Package / module |25|---|---|26| CLI surface + orchestration | `cli/` (`_args.py` = Typer flags, `_run.py` = phased flow + early dispatch) |27| Terminal design system | `console/` (`_theme`, `_stepper`, `_panels`, `_prompts`, `_steps`) |28| Env & secrets config | `env_config/` (`_values`, `_dotenv`, `_preferences`, `_hf_access`, `_version`, `_configure`) |29| Host services lifecycle | `services/` (`_ports`, `_fastapi`, `_docker_control`, `_frontend`, `_health`) |30| Inference-server artifact | `inference_server/` (`_catalog`, `_config`, `_env`, `_git`, `_metadata`, `_privileges`, `_orchestrator`) |31| Stop / purge teardown | `cleanup/` (`_runtime`, `_orchestrate`, `_resource_ops`) |32| Docker build/failure diag | `docker_diag/` (`_build_progress`, `_diagnostics`) |33| Standalone modules | `bootstrap.py`, `shell.py`, `startup_checks.py`, `docker.py`, `constants.py`, `logging.py`, `monitor.py`/`monitor_app.py` (`--status` TUI), `bug_report.py` (`--report-bug`), `shortcut.py` (`--install-shortcut`), `spdx.py`, `settings.py`, `venv_utils.py` |3435**The re-export rule.** A package's `__init__.py` imports and re-exports the names36its submodules define, so `from tt_setup.console import step` and37`import tt_setup.console as C` keep working. When you add/move a submodule, wire38its public names through the `__init__`.3940**Linter blind spot — read this before moving code between submodules.** Most41submodules do `from tt_setup.constants import *`. That makes ruff downgrade42undefined-name detection (`F821`) to a silent `F403`, so a name you *use* but43forgot to *import* in a submodule will NOT be flagged and will crash at runtime.44When you move a function into a submodule, add every import it needs locally45(names are module-scoped now, not inherited from the old monolith). `tests/46test_no_undefined_names.py` is the backstop; run it.4748## Adding / changing a CLI flag49501. **Declare it** in `tt_setup/cli/_args.py` as a `typer.Option`, with a51 `rich_help_panel=` so it lands in the right `--help` group: *Setup &52 Configuration · Model Deployment · Lifecycle · Reset (--purge-all) · Advanced ·53 Developer Tools · Troubleshooting & Info*. Deprecated flags use `hidden=True`54 and warn + normalize onto the current flag.552. **Thread it** into the `args = SimpleNamespace(...)` passed to `_run(args)`.563. **Dispatch it.** *Utility/lifecycle* flags (`--stop`, `--status`, `--logs`,57 `--info`, `--report-bug`, `--install-shortcut`, `--check-headers`) run in the58 **early-dispatch block at the top of `_run()`** — do the work, then `return` /59 `sys.exit()` **before** the 5-phase startup. Only flags that modify a normal60 startup fall through to the phases.614. **Name it** for the user: lowercase, action-oriented, and match the wording of62 its output ("Stopping/Stopped" for `--stop`, not "Cleaning up").635. Add a `tests/test_cli.py` case (see Testing) and document it in `CLAUDE.md`64 (Common commands) + `dev-docs/run-py-guide.md` (options table).6566## Terminal output — the short version6768Full rules: `dev-docs/launcher-terminal-design.md`. The essentials:6970- **Everything goes through `tt_setup/console/`** — never raw `print()` + ANSI.71- **Calm, minimal by default; `-v` reveals everything.** Gate routine "done" lines72 on `if show_detail():`. The pinned phase stepper + one collapsing line per73 `step()` is the normal signal; endpoint URLs live in the ready panel.74- **Always show failures, prompts, and actionable warnings** — never fold those.75- **Never wrap an interactive prompt in a capturing `step()` or an active phase76 spinner** — suspend around prompts/sudo/anything with its own `Live`.77- **Degrade cleanly on non-TTY** (piped) — no escape-code garbage.78- **`bootstrap.py` runs before Rich exists → stdlib only, never import79 `tt_setup.console`.** Mimic the calm style by hand (it has a stdlib spinner).8081**Reusable renderers.** When a piece of output should be re-viewable (e.g. the82ready panel via `--info`), extract a pure renderer that probes live state83(`show_ready_panel` in `cli/_run.py`) and call it from both places — don't84duplicate the assembly.8586## CLI design principles — "what we like"8788These emerged across the launcher work; apply them to new launcher behavior.8990- **Fail fast, with a fix *and* an escape hatch.** Stop early (at the Checks91 phase) when something is genuinely wrong, but the message must say how to fix it92 *and* how to opt out — e.g. "fix your TT tooling and re-run, **or** set93 `IS_QB2=false`". Never a bare traceback; use an `error` `notice_panel`.94- **Lenient defaults; strict behavior is opt-in.** Never block a dev laptop or95 cloud/no-hardware box out of the box. Strict checks are opt-in (`IS_QB2`) or96 gated on real signals (tt-smi installed / `/dev/tenstorrent` present).97- **Assume-and-verify, not assume-and-trust.** A config claim (e.g. "this is a98 QB2") is checked against reality (tt-smi); a mismatch is surfaced, not hidden.99- **Plain language in user-facing text** — say "couldn't read the QB2's chips,"100 not "tt-smi returned nonzero." Explain internal tool/var names in passing.101- **Capture subprocess noise; surface it only on failure.** A child process must102 not scroll under a live spinner. Capture stdout/stderr (e.g. compose runs with103 `--ansi never` + captured output; `run_docker_command(interactive=False)` for104 captured non-interactive sudo) and reveal the buffer only when the step fails —105 the way `console.step()` does.106- **Reuse existing helpers** (`build_docker_compose_command`, console primitives)107 instead of hand-rolling equivalents.108- **Make state re-viewable and hints discoverable** — the ready panel footer109 advertises `--stop` / `--logs` / `--info`.110111## Hardware & QB2112113- **Detect:** `detect_tt_hardware()` (`docker.py`, checks `/dev/tenstorrent`);114 `check_tt_smi()` (`shell.py`, runs `tt-smi -s`, returns115 `(status, detail, board_type)`). Classification lives in `shell.py`:116 `_classify_boards()` mirrors `board_control/services.py:get_board_type`117 (substring match + device count → `P300x2`/`T3K`/…); `describe_board()` maps to118 a friendly name; `resolve_hardware_label()` builds the ready-panel label +119 optional QB2 warning (pure/testable).120- **QB2 == `P300x2`** (Blackhole QuietBox). `IS_QB2` is an **opt-in** flag,121 **default false** — a dev laptop/cloud is never held to the strict check. The122 `tt_qb2_launch` release branch ships `IS_QB2=true`. It is **independent of123 `TT_INFERENCE_ARTIFACT_BRANCH`**, which only selects the inference-server build124 (that decoupling is deliberate — don't re-derive QB2 from the artifact branch).125- **Behavior when `IS_QB2=true`:** confirm via tt-smi → label `QuietBox (QB2)`;126 a *different* board → non-fatal warning; can't read the chips *while real TT127 tooling is present* → **hard-stop** at Checks; no TT tooling at all → skip128 (escape hatch). With `IS_QB2` false (default) the whole strict path is skipped.129130## Testing & verification131132- **Tests** live in `tests/` (stdlib `unittest`), one `test_<area>.py` per module,133 each with the pre-refactor shim `try: from tt_setup import X ... except134 ImportError: import run as X`. Run: `.tt_studio_run_venv/bin/python -m pytest135 tests/ -q` (keep green).136- **Extract pure helpers** for anything with branching logic (e.g.137 `resolve_hardware_label`, `_classify_boards`) so it's unit-testable without138 hardware, a TTY, or Docker. Test the matrix of inputs directly.139- **Verify terminal output under a real PTY** (`pty.spawn([...])`) to confirm140 spinner animation + `\r\033[2K` self-heal + collapse to `✓`, and pipe non-TTY141 output through `| cat` to confirm no stray escape codes. The live spinner/rule142 sweep and real prompts only render on a true terminal — check those on hardware.143- **`tests/test_no_undefined_names.py`** guards the `import *` blind spot; keep it144 passing when you move code between submodules.145- Try `COLUMNS=80` and `COLUMNS=120` for panel/rule widths, and `-v` to confirm146 folded detail returns.147148## Conventions149150- **SPDX headers** are mandatory on every new `.py` (not on `.md`):151 `# SPDX-License-Identifier: Apache-2.0` / `# SPDX-FileCopyrightText: © 2026152 Tenstorrent AI ULC`. Run `python run.py --check-headers`.153- **Docker errors link, they don't fix.** Point to official docs; never advertise154 `--fix-docker` or Linux-only `sudo service docker start`. Compose **v2** only.155- **Git / PRs:** follow the `feature-branch-pr` skill (branch off `dev`, minimal156 diff, verify, no AI attribution in commits/PRs).157- Keep `CLAUDE.md` (Common commands) and `dev-docs/run-py-guide.md` in sync when158 you add a flag; the file map in `dev-docs/launcher-terminal-design.md` predates159 the package split and lists the old monolith paths — treat this skill's table as160 the current layout.