The tan CLI
tan is the Alp SDK's sole user-facing command-line front door (ADR-0020, end-state B). One verb set scaffolds a project, builds it (multi-core aware), runs it on the simulator or in Renode, checks its footprint against the SoM memory budget, flashes it, bundles it for OTA, cleans it, inspects the generated configuration, validates board.yaml, compiles AI models, sanity-checks the host environment, opens a serial console, and scaffolds the metadata for porting a new SoM.
There is no alp binary. ADR-0020 Phase 4 retired the SDK-side build executor; the SDK's pyproject.toml registers only alp-mcp, and alp_cli survives as an SDK-side library — never a user-installed command, and no longer something tan shells out to. alp_orchestrate's own --help says it plainly: "Planner/emit CLI for board.yaml (the executor was retired -- ADR-0020 Phase 4)."
tan is the headless companion to the VS Code extension: both front doors drive the same SDK, so a validate in CI matches what the editor sees.
Install
tan is a standalone, independently-versioned, public Python program (alplabai/tan-cli), shipped as a self-contained PyInstaller freeze. You install it separately from the SDK bootstrap, and it needs no Python interpreter and no toolchain on the host:
curl -fsSL https://raw.githubusercontent.com/alplabai/tan-cli/main/install.sh | sh
install.ps1 is the Windows equivalent. The released version is 0.5.1.
Each release publishes exactly four archives — PyInstaller --onedir freezes:
| Platform | Asset |
|---|---|
| Windows x86_64 | tan-x86_64-pc-windows-msvc.zip |
| macOS x86_64 | tan-x86_64-apple-darwin.tar.gz |
| macOS arm64 | tan-aarch64-apple-darwin.tar.gz |
| Linux x86_64 (glibc) | tan-x86_64-unknown-linux-gnu.tar.gz |
From a checkout, pip install ./python installs the same program from source.
First install through to first build, in order:
curl -fsSL https://raw.githubusercontent.com/alplabai/tan-cli/main/install.sh | sh
git clone https://github.com/alplabai/alp-sdk
tan bootstrap --sdk-root ./alp-sdk
tan init --name my-app
cd my-app
tan validate
tan build
tan size
tan run --flash
The bootstrap scripts — scripts/bootstrap.sh on Linux / macOS / WSL2, scripts/bootstrap.ps1 on native Windows (see Installation) — install alp_cli as an editable package into the workspace venv. That is the backend, not a user command: a git pull in the alp-sdk checkout updates it in place, but it never puts an alp executable on your PATH.
For backend debugging, the Python side also runs straight from a checkout:
# From the alp-sdk repo root (or with <sdk>/scripts on PYTHONPATH):
PYTHONPATH=scripts python3 -m alp_cli --help
What tan runs itself, and what it delegates
tan both plans and executes. Every verb is a native Python implementation inside tan — model, monitor, new-som and faultdecode included; those were ports, not forwards. tan does not forward any verb to python -m alp_cli.
tan's own build executor —build,flash,size,image,clean,renode,run. alp-sdk is plans-only here:tanconsumesalp_orchestrate --emit build-planand--emit system-manifest, then runswest/bitbake/cmakeper slice itself, owns skip-vs-fail policy, and programs the hardware.- In-process generation —
generaterenders fromtan.plannerinside the process. Setting the environment variableTAN_GENERATE_EXECUTOR=subprocessis an opt-in escape hatch that spawns the SDK'sscripts/alp_project.pyinstead. - The remaining spawns —
validatespawns the SDK'sscripts/validate_board_yaml.py;migrate/lock/qualityforward to the survivingwest alp-*extension commands.
Where tan does delegate, it is a thin wrapper — the actual logic lives in the same scripts and packages the west alp-* commands drive (scripts/alp_orchestrate/, scripts/alp_project.py, scripts/validate_board_yaml.py). tan adds discoverability and sane defaults; it never forks the behaviour.
tan resolves the SDK checkout itself — from --sdk-root, a pin set with tan sdk switch <version|path>, or a checkout near the project. When it can't, it says so rather than guessing.
Global flags
These apply to every verb (they replace several per-verb flags the retired Python CLI carried):
| Flag | Meaning |
|---|---|
--project PATH | Project root (default: the current directory) |
--board-yaml PATH | Explicit board.yaml path; overrides project resolution |
--sdk-root PATH | alp-sdk checkout root |
--target EMIT | Generation target for generate / explain |
--all | Run against all relevant targets |
--format text|json | Output format; json emits the machine-readable envelope |
--verbose / --quiet | More diagnostic detail / suppress non-essential output |
--no-color | Disable ANSI colour |
--non-interactive | Never prompt; fail instead of asking |
--ci | CI mode: implies --non-interactive and disables colour |
tan vs west alp-* — which do I use?
Four west alp-* extension commands survive ADR-0020 Phase 4 (scripts/west-commands.yml): alp-lock, alp-migrate, alp-quality, alp-emit. The build/flash/image/size/renode/clean extensions were retired along with the executor.
| You are… | Use |
|---|---|
| Building, flashing, sizing, bundling, cleaning, or Renode-booting | tan build / tan flash / tan size / tan image / tan clean / tan renode |
| Scaffolding, validating, compiling a model, checking the host, opening a console, decoding a fault | tan init / tan validate / tan model / tan doctor / tan monitor / tan faultdecode |
| Writing the board-derived config files without a full build | tan generate (or west alp-emit <mode> from a west workspace) |
Locking deps, migrating board.yaml, running the quality gates | west alp-lock / west alp-migrate / west alp-quality — or their pass-through twins tan lock / tan migrate / tan quality |
Rules of thumb:
- The multi-slice build surface is
tan's, not the SDK's. alp-sdk emits plans;tanexecutes them. Nothing in alp-sdk builds or flashes any more. tan migrate/tan lock/tan qualityforward their tail verbatim towest alp-migrate/west alp-lock/west alp-quality, so the flag references below apply to either spelling.- The generated-artefact catalog has several front doors:
tan generaterenders fromtan.planner, whilewest alp-emitandscripts/alp_project.py --emitare the SDK-side front doors onto the SDK's own emitters.
west alp-* only resolves when alp-sdk is the workspace's manifest repo — west discovers the commands through the manifest's self.west-commands (or a consuming manifest's projects.alp-sdk.west-commands), not through zephyr/module.yml. A workspace initialised from the plain upstream Zephyr manifest reports unknown command "alp-migrate". scripts/bootstrap.sh gets this right (#769) and hard-fails at bootstrap time if west alp-migrate doesn't resolve in the new workspace; see Installation. tan itself does not depend on this — it locates the SDK checkout directly.
Verb reference
tan 0.5.1 ships 32 verbs, all native Python:
bootstrap, build, clean, completion, debug-config, diff, doctor, examples, explain, faultdecode, flash, generate, image, init, inspect, kconfig, lock, migrate, model, monitor, new-som, pinmux, presets, quality, renode, run, scaffold, sdk, size, support-bundle, trace, validate
tan init — scaffold a new project
tan init --template minimal-app --name my-app --som E1M-AEN801
tan init --from-example audio/i2s-tone --name tone-demo
tan init --template minimal-app --name my-app --preview # show planned files, write nothing
Expands a project template — or, with --from-example, copies one of the SDK's ready-made example projects verbatim (tan examples lists them; the value is the example's category/name source dir under the SDK's examples/ directory). tan explain --template <id> describes a template before you commit to it.
| Option | Meaning |
|---|---|
--template | Project template id |
--from-example | SDK example category/name to copy verbatim (conflicts with --template) |
--name | Project name; creates a sub-directory when provided |
--destination | Destination directory (default: the current directory or --project) |
--som | Target SoM SKU written into the generated board.yaml, e.g. E1M-AEN701 |
--cores | Comma-separated cores for a heterogeneous project, id[:os] (e.g. m33_sm:zephyr,a55_cluster:yocto); the OS is inferred from the id when omitted |
--preview | Show planned files without writing anything |
--force | Allow overwriting existing files |
tan scaffold --template <id> --name <name> is the in-project sibling: it adds a module to an existing project, with the same --destination / --preview / --force contract.
tan new-som — scaffold metadata for a new SoM port
tan new-som --sku E1M-NX9555 --soc-ref nxp:imx9:imx95 --family nxp-imx9
tan new-som # interactive: prompts for every field
The vendor-N+1 porting kit, implemented natively in tan. Generates the two metadata skeletons a new SoM port needs — metadata/e1m_modules/<SKU>.yaml (the SoM preset) and, when the target SoC has no spec yet, metadata/socs/<vendor>/<family>/<part>.json — with every schema-required hardware-fact field present as an explicit TBD placeholder. Values are never invented; the JSON carries its TODOs in the schema-sanctioned _pending_reason + notes fields. Both skeletons are schema-valid on arrival, and the command finishes by printing a numbered porting checklist (fill the TBDs, extend the schema sku pattern for a brand-new family, register the silicon ref, validate, regenerate, run the conformance suite).
The scaffold is committable as-is: every input is validated before anything is written (a rejected invocation never leaves half-written files). Use --dry-run to see the planned files — and run the full validation — without writing anything. In a pipe or CI, missing required flags fail fast with an explicit list instead of dropping into the prompts. The full walkthrough lives in the SDK's docs/porting-new-som.md.
| Option | Meaning |
|---|---|
--sku | New SoM SKU, e.g. E1M-NX9555 |
--soc-ref | Silicon triple-colon ref, e.g. nxp:imx9:imx95 |
--family | Human-readable family slug, e.g. nxp-imx9 |
--vendor | Vendor display name for the SoC JSON (default: soc-ref vendor segment) |
--display-name | Preset display name (default derived from the SKU) |
--inference-backend | ethos_u / drpai / deepx_dxm1 / tbd (default tbd) |
--ethos-u-variant | u55 / u65 / u85; required with --inference-backend ethos_u |
--cores | Comma-separated canonical core ids (default: a tbd_core0 placeholder) |
--default-board | Stock carrier board (default E1M-EVK) |
--default-hw-rev | Default hardware revision (default r1) |
--output-root | Root to generate metadata/ under (default: the SDK checkout) |
--dry-run | Validate and print the planned files; write nothing |
--force | Overwrite an existing preset for this SKU |
tan build — build the project (multi-core aware)
tan build # materialise the plan + run every slice's build command
tan --project my-app build # same, for a project outside the cwd
tan build --plan # show the build plan only, no build
tan build --manifest # show the system manifest projection
tan build asks the SDK for --emit build-plan — the machine-readable, write-free build recipe, one entry per non-off core, carrying the resolved app source dir, the exact tool command, and the environment — materialises the plan's generated files under the build root, then runs each slice's west / bitbake / cmake command. It seeds its own system-manifest.yaml from --emit system-manifest: the single source of truth that tan flash, tan size, tan image, and OTA consume. It never runs the produced binary itself — that's tan run.
| Option | Meaning |
|---|---|
--plan | Show the build plan and exit without building |
--plan-from FILE | Read the plan from a JSON file instead of invoking the SDK (implies --plan) |
--materialise | Write the plan's generated files to disk instead of just showing the plan |
--native | Build natively (consume plan, materialise, run each slice's command). This is the default; the flag is an explicit opt-in |
--manifest | Show the system manifest — the post-build IDE/tool contract |
--manifest-from FILE | Read the manifest from a YAML file instead of invoking the SDK (implies --manifest) |
alp-sdk's own contribution to this pipeline is fully inspectable without tan:
PYTHONPATH=scripts python3 -m alp_orchestrate --input board.yaml --emit build-plan
PYTHONPATH=scripts python3 -m alp_orchestrate --input board.yaml --emit system-manifest
tan run — build, then run it
tan run # build, then execute the produced native_sim binary
tan run --flash # hardware target: build, then program the board
tan run --flash --core m33_sm # …flashing only one slice
A thin orchestrator over the native build + flash engines — it reuses them, never re-derives them. The host-vs-hardware target is read from what the build produces (a native_sim binary ⇒ host), so run needs no board selector: board.yaml already names the target. --flash is a required opt-in on hardware; without it, run on a hardware project builds and reports but never programs the board. It is ignored for a native_sim/host target, which always runs the produced binary and never flashes.
tan flash — program the built project
tan build && tan flash # the whole system, one-liner
tan flash --core m33_sm # one slice only
tan flash --helper gd32_bridge # one helper MCU only
tan flash --dry-run # print commands, touch nothing
Walks build/system-manifest.yaml (produced by tan build) and programs every slice + helper MCU via the registered flash backends, in boot_order:.
| Option | Meaning |
|---|---|
APP_PATH (argument, optional) | App directory (default .); a non-. value overrides --project |
--core | Flash only the slice with this core_id (skips every other slice and all helpers) |
--helper | Flash only the helper MCU with this name (skips all slices and every other helper) |
--dry-run | Print the command each backend would run; spawn nothing. Also bypasses the required-tool PATH gate |
--build-root | Override the build root holding system-manifest.yaml (default <app_path>/build) |
--skip-missing-tools | Warn + skip an entry whose backend tools are all absent from PATH. No effect under --dry-run |
tan size — footprint vs the SoM memory budget
tan build && tan size # does each image fit the silicon?
tan size --fail-over-budget # CI gate: non-zero exit when over
tan size --format json # machine-readable (VS Code extension)
Walks build/system-manifest.yaml, measures every Zephyr slice's zephyr.elf, and compares FLASH and RAM usage against the budget resolved from the SoM's SoC metadata — before you flash. A slice whose budget can't be resolved is skipped and reported, never guessed.
| Option | Meaning |
|---|---|
APP_PATH (argument, optional) | App directory (default .) |
--build-root | Override the build root (default <app_path>/build) |
--board SKU | Override the SoM SKU used to resolve the memory budget (default: hw_info.sku from the manifest). Distinct from the global --board-yaml |
--fail-over-budget | Exit non-zero if any slice exceeds its resolved budget |
tan image — assemble a flashable bundle
tan build && tan image # produces build/image-bundle/
Reads build/system-manifest.yaml and assembles a single flashable bundle under build/image-bundle/: per-slice tar.gz archives, helper-MCU firmware, and a bundle-manifest.json with per-artefact SHA-256s. Takes an optional APP_PATH argument and --build-root.
tan clean — remove build outputs
tan clean # remove <app>/build + orchestrator state cache
tan clean --dry-run # list what would go, delete nothing
Removes the per-project build directory and the orchestrator's .alp-build-state.json. Takes an optional APP_PATH argument (default .) and --build-root.
tan renode — boot the build in Renode (no hardware)
tan build && tan renode # headless smoke boot
tan renode --expect "[hello] done" # exit 0 when seen, 1 if not
tan renode --log out.log --timeout 60
Boots the built system manifest's Zephyr slice in the Renode emulator, headless, with a wall-clock timeout — a no-hardware smoke test. The SoM family maps to a platform descriptor under metadata/renode/. Requires the renode binary on PATH; when it's absent the command exits non-zero with install guidance, never a silent pass.
| Option | Meaning |
|---|---|
APP_PATH (argument, optional) | App directory (default .) |
--board SKU | Override the SoM SKU used to pick the Renode platform descriptor (default: hw_info.sku from the manifest) |
--build-root | Override the build root (default <app_path>/build) |
--log FILE | Tee the console output to this file (default <build_root>/renode.log) |
--timeout SECS | Wall-clock cap in seconds (default 120) |
--expect STR | Stop early (exit 0) when this substring appears in any console line; exit 1 if the run ends without it |
--image-bundle DIR | Directory of pre-built per-slice artefacts; accepted for parity with the dual-OS flow |
--sim-mode | Studio hardware-simulator mode — see below |
The advisory CI gates install a pinned Renode v1.16.1 portable build, so that's the version the SDK's Renode lanes are exercised against. The command itself doesn't pin or version-check Renode — it just requires the binary on PATH.
--sim-mode — the studio simulator contract
tan renode --sim-mode --board E1M-AEN801 --image-bundle build/image-bundle/
tan--sim-mode is accepted for surface stability but currently errors with "not yet ported" (studio sim gateway, issue #674) — it does not boot a simulator today. What follows is the contract it is being ported to; the board.yaml field it depends on is already live.
The mode boots the bundle's firmware in headless Renode and exposes it to Alp Studio's sim gateway via three things: a sim-descriptor.json written into the bundle directory (naming the sockets, framebuffers and peripherals), a UART socket streaming the firmware console raw, and a line-oriented control socket. The control socket is deliberately not Renode's raw telnet monitor — the bridge translates each studio line into the right Renode monitor call and normalises the reply to exactly one line, because the studio wire vocabulary doesn't map 1:1 onto Renode's monitor API.
Two SKUs are wired, and they take opposite console paths — which is the interesting part:
| SKU | Console kind | How the console reaches the socket |
|---|---|---|
E1M-AEN801 | uart (sysbus.uart5) | The M55 has a real hardware UART console; Renode connects it to a socket terminal |
E1M-V2N101 | ram_console | The M33-SM is headless (no hardware UART) — the console is polled out of the Zephyr ram_console_buf RAM ring in SRAM and pushed to the socket |
The ram_console path is why diagnostics.sim_console: exists: a headless core's console auto-resolves to none, so without that opt-in the simulator's UART socket would simply stay silent.
tan generate — write the board-derived config files
tan generate # every generate target for this board
tan generate --target zephyr-conf # just the per-core Zephyr fragment
tan generate --target dts-overlay --force # overwrite an existing file
Renders the board-derived config in-process from tan.planner and writes it into the project. Without --target it produces the full default set; --force allows overwriting existing files. Setting TAN_GENERATE_EXECUTOR=subprocess switches to the opt-in escape hatch that spawns the SDK's scripts/alp_project.py instead.
--target accepts 12 targets. Nine are in the default / --all set: zephyr-conf, dts-overlay, native-sim-overlay, cmake-args, yocto-conf, carrier-netlist, west-libraries, hw-info-h, and os-topology. Three more are explicit-only and never run as part of the default set: zephyr-board (which additionally requires --core), composed-route-table, and ipc-contract-h.
carrier-netlist is a board-level export (the deterministic carrier nets + BOM handoff Alp Studio consumes) rather than a per-core build config, and native-sim-overlay is the Zephyr overlay (alp,pin-array on zephyr,gpio-emul) that makes a GPIO app resolve under native_sim.
tan explain --target <mode> describes a generation target; tan trace shows the generation decisions a build would make, and tan diff shows how board.yaml normalisation changes the effective config.
The full emit catalog
The generate targets are a subset of the SDK's one catalog of generated artefacts. Every mode is emitted by the single implementation that owns it — a front door only routes:
| Mode | Artefact | Emitted by |
|---|---|---|
zephyr-conf | Per-core Zephyr alp.conf Kconfig fragment | alp_project.py |
cmake-args | Per-core -D CMake argument list | alp_project.py |
yocto-conf | Per-core local.conf fragment | alp_project.py |
dts-overlay | Board DTS overlay (bus aliases + pin array) | alp_project.py |
native-sim-overlay | native_sim overlay: alp,pin-array on zephyr,gpio-emul | alp_project.py |
hw-info-h | Build-time hw_info.h macro header | alp_project.py |
west-libraries | west.yml fragment for libraries: deps | alp_project.py |
zephyr-board | Per-core Zephyr board tree (directory — see below) | alp_project.py |
composed-route-table | JSON route-table dump (demonstrator) | alp_project.py |
carrier-netlist | Studio-facing carrier nets + BOM JSON handoff | alp_project.py |
os-topology | Per-core natural-vs-effective OS facts | alp_project.py |
scaffold | New-project {path, contents}[] envelope for a template | alp_project.py |
system-manifest | Full-system manifest (slices, boot order) | orchestrator |
dts-reservations | DTS reserved-memory overlay (cross-core carve-outs) | orchestrator |
ipc-contract-h | Cross-core IPC contract header | orchestrator |
dts-partitions | DTS fixed-partitions overlay (storage: entries) | orchestrator |
storage-mounts-c | Static C storage mount table | orchestrator |
tfm-sysbuild-conf | TF-M sysbuild child-image overlay (security.psa:) | orchestrator |
build-plan | Per-slice build plan, JSON (IDE / CI / tan consumers) | orchestrator |
kconfig | Board-scoped, user-settable Kconfig symbol menu for the prj.conf LSP, JSON | orchestrator |
west alp-emit <mode> exposes the orchestrator subset for west-centric scripting: system-manifest, ipc-contract-h, dts-reservations, dts-partitions, storage-mounts-c, tfm-sysbuild-conf, build-plan. Read-only — it prints to stdout and builds nothing. The rest are reachable from scripts/alp_project.py --emit <mode> (which delegates the project-level ones to the same orchestrator emitters), so no mode can produce different bytes depending on which front door you used.
scaffold is the odd one out on inputs: it materialises a new project, so there is no board.yaml to find yet — it takes --template <catalog-id> and --sku <SKU> instead (metadata/templates/catalog-v1.json holds nine ids: minimal, peripheral, sensor, multicore-rpmsg, gateway, edge-ai, diagnostics, iot — the only one still preview — and multicore-mailbox).
Every template record carries a cores: [{id, dir, os}, ...] array transcribing its canonical example's own board.yaml cores: map, drift-checked against it. multicore-mailbox is the first template with two explicit os: zephyr cores — { id: m55_hp, dir: ./src, os: zephyr } and { id: m55_he, dir: ./peer, os: zephyr } — so scaffolding applies each core's own --core substitution to that core's own CMakeLists.txt (src/ gets --core m55_hp, peer/ gets --core m55_he) instead of stamping one scalar core onto every core directory.
kconfig is the odd one out on dependencies: it projects a board-scoped, user-settable Kconfig symbol menu ({schemaVersion, board, core, symbols[]}) for the VS Code prj.conf LSP, and to do so it configures a throwaway stub app via west build --cmake-only — so unlike every other mode it is workspace-dependent (--core is required and it exits loudly when ZEPHYR_BASE / kconfiglib aren't available, rather than emitting a partial menu). It is therefore outside the hermetic emit-snapshot gate; a cross-repo golden-fixture conformance check keeps its envelope in lockstep with the tan and VS Code consumers instead.
metadata/emit-registry-v1.json is the single source of truth for the emit-mode set — which modes exist, which CLI parses each, and the module + function that implements it. Frontends (the VS Code extension, Alp Studio) read that file instead of hand-mirroring these lists, and a drift gate (scripts/check_emit_registry.py) asserts it matches the real --emit choices parsed out of the CLIs.
zephyr-board — generate a Zephyr board tree
PYTHONPATH=scripts python3 scripts/alp_project.py --input board.yaml \
--emit zephyr-board --core m55_hp --output build/boards/alp_e1m_aen801_m55_hp/
west build --board-root build/boards -b alp_e1m_aen801_m55_hp/ae822fa0e5597ls0/rtss_hp <app>
Generates a per-core Zephyr board tree from the SoM preset + SoC JSON (#523) — the Zephyr board artefacts are derived from metadata rather than hand-maintained. This mode is the odd one out among the emitters: it writes a directory of files, not a stream, so both --core and --output are required, and --output names the board directory itself (not its parent).
Coverage is honestly partial, and differs by family:
| Family | Generated | Still hand-authored |
|---|---|---|
Alif Ensemble (E1M-AEN*) | board.yml, Kconfig.alp_<board>, twister .yaml, -pinctrl.dtsi, _defconfig, Kconfig.defconfig, .dts | board.cmake |
Renesas RZ/V2N (E1M-V2N* / E1M-V2M*) | board.yml, Kconfig.alp_<board>, twister .yaml | .dts, -pinctrl.dtsi, _defconfig |
NXP i.MX93 (E1M-NX9*) | board.yml, Kconfig.alp_<board>, twister .yaml | .dts, -pinctrl.dtsi, _defconfig |
Only the Alif Ensemble family is fully generated. The RZ/V2N and i.MX93 families generate just the three family-agnostic files: their Renesas/NXP-side SoM-internal pin assignments aren't captured in metadata yet, and the generator does not guess values it has no source for. The command silently emits the shorter set for those families — it reports only the files it actually wrote and doesn't announce the absent ones, so don't read a 3-file run as a failure. board.cmake is never generated for any family.
See carrier-netlist in the board.yaml reference for what the Studio handoff contains — and, just as importantly, what it deliberately excludes.
tan validate — check a board.yaml
tan validate # the resolved project's board.yaml
tan --board-yaml path/to/board.yaml validate
tan validate --format json # machine envelope for an IDE / LSP / CI
tan validate --offline # structural validator only, no Python spawn
Spawns the SDK's scripts/validate_board_yaml.py against the resolved board.yaml — the rich diagnostic validator (JSON-Schema pass, SoM/preset cross-references, peripheral-vs-SoC capability check) that renders every finding as a Rust-style diagnostic block with an ALP-Bxxx code. It is the same shared implementation the orchestrator's loader and the build preflight use, so every front door reports identical violations. --offline runs only tan's own structural validator, which spawns nothing and needs no SDK checkout.
Exit 0 means no hard errors; schema / preset / hardware-revision violations exit non-zero, and each maps to a stable exit class.
| Option | Meaning |
|---|---|
--offline | Run the offline structural validator only (no SDK spawn) |
--format json (global) | Emit the machine-readable envelope (outcome, issue count, resolved board.yaml path) |
The versioned machine diagnostics the SDK validator produces are governed by metadata/schemas/diagnostic-v1.schema.json — the IDE/LSP/CI-facing contract that replaces parsing terminal prose. Its ranges are zero-based (LSP Position/Range convention), while a SARIF 2.1.0 log's regions are one-based per the SARIF spec; the human renderer's Rust-style 1-based line:col is unchanged by either. Both structured formats print only the structured document to stdout, so both are safe to pipe. The SARIF exporter lives in the backend validator (python -m alp_cli validate --format sarif).
tan model — compile + package AI models
tan model build # compile board.yaml `models:` entries
tan model build --board path/to/board.yaml --out build/models
A native tan command. Compiles every models: entry declared in board.yaml into a .alpmodel package via the SoM-appropriate backend (Vela for Ethos-U, DRP-AI for RZ/V2N, …). See Inference for the end-to-end flow.
tan doctor — readiness preflight
tan doctor # debug readiness for the resolved target
tan doctor --target-kind zephyr-mcu --server jlink
tan doctor --build # build-readiness preflight instead
tan doctor --build --fix # …and auto-repair a fixable blocker
tan doctor --format json # machine-readable report
Hardware-free. By default it diagnoses debug readiness for a target/server combination — probing the binaries on PATH and the resolved project context — and exits non-zero when any check fails. --build switches it to the build-readiness preflight (the Zephyr workspace, the SDK checkout, the host build tools); with --fix it will run tan bootstrap when no Zephyr workspace is resolved, then re-check. Run it first whenever a build machine misbehaves.
| Option | Meaning |
|---|---|
--target-kind | zephyr-mcu / baremetal-mcu / yocto-userspace / native-host |
--server | jlink / openocd / pyocd / gdbserver / none |
--build | Run the build-readiness preflight instead of the debug-readiness checks |
--fix | With --build: run tan bootstrap when no Zephyr workspace is resolved, then re-check |
tan support-bundle packages inspect + trace + doctor into one diagnostic export when you need to hand the state to someone else.
tan monitor — serial console
tan monitor --port COM7 # Windows
tan monitor --port /dev/ttyUSB0 # Linux
tan monitor # lists available ports if none given
A native tan command, with stdio kept attached so the terminal stays interactive. Opens pyserial's miniterm (Ctrl+] to quit). Baud defaults to 115200 (--baud to override). When no port is given, or the requested one doesn't exist, it lists every serial port on the host and exits non-zero instead of hanging on a wrong device.
tan explain / tan faultdecode — inspection and diagnostics
tan explain # overview of everything explainable
tan explain --template minimal-app # what a project/module template produces
tan explain --target zephyr-conf # what a generation target emits
tan faultdecode fault.txt # decode an ARMv8-M (M33/M55) fault dump
tan faultdecode --cfsr 0x8200 --format json
tan explain describes a project or module template, or a generation target; passing both selectors is an error, and an unknown id fails with the catalog of valid ids. tan faultdecode is a native tan command that decodes a Cortex-M fault-register dump into a human-readable cause chain (--format json emits the machine-readable report).
To decode a validator diagnostic code, use the backend's explain verb:
PYTHONPATH=scripts python3 -m alp_cli explain ALP-B001 # cause + fix + doc link
tan inspect (resolved project/debug context), tan presets (SKUs and catalogue defaults), tan pinmux (the E1M pad → silicon function capability table), and tan examples round out the read-only inspection verbs.
West-only commands
Three west alp-* commands operate on the workspace or the repo rather than on one app. tan migrate / tan lock / tan quality forward to them verbatim, so the flags below are the flags either spelling takes.
west alp-lock — dependency lock file
west alp-lock # write ./alp.lock for the workspace
west alp-lock --check # recompute + diff; non-zero on drift
west alp-lock --board path/to/board.yaml
Writes or verifies alp.lock, the project's dependency lock — the resolved identity of everything the build consumes. --check recomputes the lock and diffs it against the committed file, printing each drifted path as locked <x> != actual <y> and exiting non-zero: the CI shape of the command.
| Option | Meaning |
|---|---|
--check | Verify against the workspace instead of writing; non-zero on drift |
--workspace | Workspace root (default: the west topdir) |
--board | board.yaml to record resolution.board from |
alp.lock is also the input to the deterministic SBOM: scripts/gen_sbom.py renders a CycloneDX 1.5 document from it with a stable, lock-derived serial number and no wall-clock field, so the same lock always produces the same SBOM. Releases attach that SBOM alongside a byte-reproducible source tarball.
west alp-migrate — board.yaml migration engine
west alp-migrate --check # report versions; non-zero on drift
west alp-migrate --preview # unified diff + diagnostic-v1 JSON, no writes
west alp-migrate --apply # rewrite in place, then regen derived files
west alp-migrate --apply --all # every board.yaml under the repo
Versions and migrates a board.yaml byte-faithfully — comments, flow style and indentation survive a migration, so an applied migration produces a reviewable diff rather than a reformatted file. --preview prints a unified diff plus a diagnostic-v1 JSON report and writes nothing. Exactly one of --check / --preview / --apply is required.
| Option | Meaning |
|---|---|
--check / --preview / --apply | Mode (exactly one, required) |
--all | Every board.yaml under the repo |
--board | A single board.yaml path (default: ./board.yaml) |
--no-verify | Skip the post-apply regeneration of derived files |
No migration exists yet. The engine is in place so that the first real schema change can ship with a migration rather than a breakage, but today --check passes on everything and --apply is a no-op. This is coupled to board.yaml's lazy versioning: an absent schemaVersion: is version 1, so hand-written and external projects are never "out of date" and never need the key. See schemaVersion.
west alp-quality — run the quality gates
west alp-quality --profile pr # exactly the gates CI runs
west alp-quality --profile full
west alp-quality --profile pr --json r.json --junit r.xml --sarif r.sarif
Runs the quality-task registry (metadata/quality-tasks-v1.json) for a named profile and emits a human summary plus optional JSON, JUnit XML, and SARIF reports. The registry is the single source of truth for which check_*.py gates exist, whether each is a hard gate or informational, and which profiles run it — scripts/test-all.sh derives its required-gate list from the same registry rather than a hand-maintained array, which is what keeps local and CI from drifting apart.
| Profile | Runs |
|---|---|
pr | All hard gates — exactly the gates CI's PR lane runs. The one to reach for before pushing |
full | The pr set plus the informational (non-gating) checks |
release | All hard gates |
quick | Accepted by the parser, but no task currently opts into it — it runs nothing today |
| Option | Meaning |
|---|---|
--profile | quick / pr / full / release (required) |
--json | Write a JSON report to this path |
--junit | Write a JUnit XML report to this path |
--sarif | Write a SARIF report to this path |
Only tasks marked as gates affect the exit code; informational tasks report but never fail the run.
The fourth surviving extension command, west alp-emit, is the read-only inspector for the orchestrator's emit subset — see the emit catalog above.
Environment
| Variable | Effect |
|---|---|
ALP_SDK_ROOT | Explicit path to the alp-sdk checkout; otherwise tan resolves it from --sdk-root, a tan sdk switch pin, or a checkout near the project |
ZEPHYR_BASE | The Zephyr tree used by the build and checked by tan doctor |
The verbs that shell out to the SDK backend export ALP_SDK_ROOT and put <sdk>/scripts on PYTHONPATH for their sub-processes — the same wiring the west alp-* wrappers use, so a tan-invoked orchestrator run behaves identically to a west one. For the slices it builds, tan reads the plan's own env / envAppendPath entries (sourced from the SDK's --emit build-plan) to set up EXTRA_ZEPHYR_MODULES and PYTHONPATH.
See also
- Installation — bootstrap the workspace, then install the CLI with
install.sh/install.ps1 - VS Code extension — the same SDK behind a GUI
board.yamlreference — the schematan validateenforces- Heterogeneous Builds — the orchestrated multi-core pipeline
tan buildfronts - Architecture — where the orchestrator fits into the build
alplabai/tan-cli— the CLI's own repository