Skip to main content

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:

PlatformAsset
Windows x86_64tan-x86_64-pc-windows-msvc.zip
macOS x86_64tan-x86_64-apple-darwin.tar.gz
macOS arm64tan-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 tanmodel, 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 executorbuild, flash, size, image, clean, renode, run. alp-sdk is plans-only here: tan consumes alp_orchestrate --emit build-plan and --emit system-manifest, then runs west / bitbake / cmake per slice itself, owns skip-vs-fail policy, and programs the hardware.
  • In-process generationgenerate renders from tan.planner inside the process. Setting the environment variable TAN_GENERATE_EXECUTOR=subprocess is an opt-in escape hatch that spawns the SDK's scripts/alp_project.py instead.
  • The remaining spawnsvalidate spawns the SDK's scripts/validate_board_yaml.py; migrate / lock / quality forward to the surviving west 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):

FlagMeaning
--project PATHProject root (default: the current directory)
--board-yaml PATHExplicit board.yaml path; overrides project resolution
--sdk-root PATHalp-sdk checkout root
--target EMITGeneration target for generate / explain
--allRun against all relevant targets
--format text|jsonOutput format; json emits the machine-readable envelope
--verbose / --quietMore diagnostic detail / suppress non-essential output
--no-colorDisable ANSI colour
--non-interactiveNever prompt; fail instead of asking
--ciCI 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-bootingtan build / tan flash / tan size / tan image / tan clean / tan renode
Scaffolding, validating, compiling a model, checking the host, opening a console, decoding a faulttan init / tan validate / tan model / tan doctor / tan monitor / tan faultdecode
Writing the board-derived config files without a full buildtan generate (or west alp-emit <mode> from a west workspace)
Locking deps, migrating board.yaml, running the quality gateswest 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; tan executes them. Nothing in alp-sdk builds or flashes any more.
  • tan migrate / tan lock / tan quality forward their tail verbatim to west 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 generate renders from tan.planner, while west alp-emit and scripts/alp_project.py --emit are the SDK-side front doors onto the SDK's own emitters.
The extension commands need an alp-sdk-manifest workspace

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.

OptionMeaning
--templateProject template id
--from-exampleSDK example category/name to copy verbatim (conflicts with --template)
--nameProject name; creates a sub-directory when provided
--destinationDestination directory (default: the current directory or --project)
--somTarget SoM SKU written into the generated board.yaml, e.g. E1M-AEN701
--coresComma-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
--previewShow planned files without writing anything
--forceAllow 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.

OptionMeaning
--skuNew SoM SKU, e.g. E1M-NX9555
--soc-refSilicon triple-colon ref, e.g. nxp:imx9:imx95
--familyHuman-readable family slug, e.g. nxp-imx9
--vendorVendor display name for the SoC JSON (default: soc-ref vendor segment)
--display-namePreset display name (default derived from the SKU)
--inference-backendethos_u / drpai / deepx_dxm1 / tbd (default tbd)
--ethos-u-variantu55 / u65 / u85; required with --inference-backend ethos_u
--coresComma-separated canonical core ids (default: a tbd_core0 placeholder)
--default-boardStock carrier board (default E1M-EVK)
--default-hw-revDefault hardware revision (default r1)
--output-rootRoot to generate metadata/ under (default: the SDK checkout)
--dry-runValidate and print the planned files; write nothing
--forceOverwrite 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.

OptionMeaning
--planShow the build plan and exit without building
--plan-from FILERead the plan from a JSON file instead of invoking the SDK (implies --plan)
--materialiseWrite the plan's generated files to disk instead of just showing the plan
--nativeBuild natively (consume plan, materialise, run each slice's command). This is the default; the flag is an explicit opt-in
--manifestShow the system manifest — the post-build IDE/tool contract
--manifest-from FILERead 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:.

OptionMeaning
APP_PATH (argument, optional)App directory (default .); a non-. value overrides --project
--coreFlash only the slice with this core_id (skips every other slice and all helpers)
--helperFlash only the helper MCU with this name (skips all slices and every other helper)
--dry-runPrint the command each backend would run; spawn nothing. Also bypasses the required-tool PATH gate
--build-rootOverride the build root holding system-manifest.yaml (default <app_path>/build)
--skip-missing-toolsWarn + 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.

OptionMeaning
APP_PATH (argument, optional)App directory (default .)
--build-rootOverride the build root (default <app_path>/build)
--board SKUOverride the SoM SKU used to resolve the memory budget (default: hw_info.sku from the manifest). Distinct from the global --board-yaml
--fail-over-budgetExit 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.

OptionMeaning
APP_PATH (argument, optional)App directory (default .)
--board SKUOverride the SoM SKU used to pick the Renode platform descriptor (default: hw_info.sku from the manifest)
--build-rootOverride the build root (default <app_path>/build)
--log FILETee the console output to this file (default <build_root>/renode.log)
--timeout SECSWall-clock cap in seconds (default 120)
--expect STRStop early (exit 0) when this substring appears in any console line; exit 1 if the run ends without it
--image-bundle DIRDirectory of pre-built per-slice artefacts; accepted for parity with the dual-OS flow
--sim-modeStudio 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/
caution
Deferred in 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:

SKUConsole kindHow the console reaches the socket
E1M-AEN801uart (sysbus.uart5)The M55 has a real hardware UART console; Renode connects it to a socket terminal
E1M-V2N101ram_consoleThe 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:

ModeArtefactEmitted by
zephyr-confPer-core Zephyr alp.conf Kconfig fragmentalp_project.py
cmake-argsPer-core -D CMake argument listalp_project.py
yocto-confPer-core local.conf fragmentalp_project.py
dts-overlayBoard DTS overlay (bus aliases + pin array)alp_project.py
native-sim-overlaynative_sim overlay: alp,pin-array on zephyr,gpio-emulalp_project.py
hw-info-hBuild-time hw_info.h macro headeralp_project.py
west-librarieswest.yml fragment for libraries: depsalp_project.py
zephyr-boardPer-core Zephyr board tree (directory — see below)alp_project.py
composed-route-tableJSON route-table dump (demonstrator)alp_project.py
carrier-netlistStudio-facing carrier nets + BOM JSON handoffalp_project.py
os-topologyPer-core natural-vs-effective OS factsalp_project.py
scaffoldNew-project {path, contents}[] envelope for a templatealp_project.py
system-manifestFull-system manifest (slices, boot order)orchestrator
dts-reservationsDTS reserved-memory overlay (cross-core carve-outs)orchestrator
ipc-contract-hCross-core IPC contract headerorchestrator
dts-partitionsDTS fixed-partitions overlay (storage: entries)orchestrator
storage-mounts-cStatic C storage mount tableorchestrator
tfm-sysbuild-confTF-M sysbuild child-image overlay (security.psa:)orchestrator
build-planPer-slice build plan, JSON (IDE / CI / tan consumers)orchestrator
kconfigBoard-scoped, user-settable Kconfig symbol menu for the prj.conf LSP, JSONorchestrator

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.

Machine-readable mode registry

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:

FamilyGeneratedStill hand-authored
Alif Ensemble (E1M-AEN*)board.yml, Kconfig.alp_<board>, twister .yaml, -pinctrl.dtsi, _defconfig, Kconfig.defconfig, .dtsboard.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.

OptionMeaning
--offlineRun 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.

OptionMeaning
--target-kindzephyr-mcu / baremetal-mcu / yocto-userspace / native-host
--serverjlink / openocd / pyocd / gdbserver / none
--buildRun the build-readiness preflight instead of the debug-readiness checks
--fixWith --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.

OptionMeaning
--checkVerify against the workspace instead of writing; non-zero on drift
--workspaceWorkspace root (default: the west topdir)
--boardboard.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-migrateboard.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.

OptionMeaning
--check / --preview / --applyMode (exactly one, required)
--allEvery board.yaml under the repo
--boardA single board.yaml path (default: ./board.yaml)
--no-verifySkip the post-apply regeneration of derived files
The migration registry is empty — and that's the point

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.

ProfileRuns
prAll hard gates — exactly the gates CI's PR lane runs. The one to reach for before pushing
fullThe pr set plus the informational (non-gating) checks
releaseAll hard gates
quickAccepted by the parser, but no task currently opts into it — it runs nothing today
OptionMeaning
--profilequick / pr / full / release (required)
--jsonWrite a JSON report to this path
--junitWrite a JUnit XML report to this path
--sarifWrite 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

VariableEffect
ALP_SDK_ROOTExplicit 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_BASEThe 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

Questions about this page? Discuss in Community Forum