YAML-driven workflow orchestrator for Alpiq BESS local development. Replaces the 1,430-line Python monolith (optimization-universe-iac/local/run.py) with a generic, data-driven Rust CLI where all workflow logic lives in YAML recipe files.
Status: Active (updated 2026-03-15)
All 65 tests pass across 2 test suites. 10 step types implemented across 11 handler source files. 35 recipe YAML files covering full BESS local dev workflow. Recipes moved to poc/recipes/ (project root). Non-interactive execution via -e KEY=VALUE flag. Compound when conditions (&&, ||). Repo swap feature for development isolation. No git commits yet.
Overview
mando run <recipe> # Execute a recipe (e.g., mando run menu)
mando run <recipe> --replay # Re-run with saved user choices
mando run <recipe> -e KEY=VALUE -e K2=V2 # Non-interactive: preset variables
mando list # List all available recipes
mando swap <repo> --branch <branch> # Swap a repo directory via symlink
mando swap --restore # Restore all swapped repos
mando swap --list # Show active swaps
Key Principle: The engine is completely generic — it knows nothing about BESS, Docker, or Mando. All domain logic is encoded in YAML recipes. Recipes can be updated without rebuilding the binary.
Recipes moved from mando-cli/recipes/ to poc/recipes/ so they live at the project root alongside the compose files they reference. All compose file references in recipes now use optimization-universe-iac/local/ prefix. Recipe discovery walks up parent directories, so running from mando-cli/ still finds them.
Step Types
Step
Purpose
Key Fields
run
Execute shell command
command, name, cwd, ignore_errors, when
check
Verify dependency (abort on fail)
command, name
capture
Command stdout → variable
command, name, cwd, ignore_errors
prompt
User input (confirm/text)
name, message, type, default
choice
Menu selection → sub-recipe
name, message, options[] (label/value/recipe)
env
Load variables
file (.env/.toml), vars{} (inline), optional
compose
Docker compose wrapper
files[], profiles[], command, services[], cwd
include
Execute sub-recipe
recipe (shared context)
http
HTTP request
url, method, body, headers{}, capture
swap
Repo directory swap via symlink
repo, path, branch, from, restore_all, when
All steps support when for conditional execution.
Repo Swap (mando swap)
Swap POC repo directories with alternative directories, existing git worktrees, or ad-hoc created worktrees. Enables testing local changes in one repo against the rest of the POC stack without affecting the main checkout.
Motivation
Development isolation — test changes in one repo against the rest of the stack without touching the main checkout
Branch switching — run the POC stack with a specific repo on a different branch while everything else stays on its current branch
Mechanism
Symlink swap: the original directory is renamed to .mando-worktrees/<repo>.original, and a symlink is created at the original location pointing to the target. All existing tools (Docker Compose, build scripts, etc.) see the same paths transparently.
CLI Usage
# Swap with an external directorymando swap <repo> --path /some/other/directory# Swap with an existing worktreemando swap <repo> --worktree /existing/worktree/path# Swap with ad-hoc worktree from existing branchmando swap <repo> --branch feature/new-algo# Swap with ad-hoc worktree, new branch from basemando swap <repo> --branch feature/new-algo --from develop# Restoremando swap --restore # restore all swapped reposmando swap --restore <repo> # restore one specific repo# Statusmando swap --list # show active swaps
Recipe Step
The swap step type allows automating repo swaps within recipe workflows:
# Swap to a branch worktree (conditionally)- swap: repo: bess-optimization branch: feature/new-algo from: develop when: "{{isolate}} == true"# Swap to an external directory- swap: repo: mando path: /Users/andras/other-mando-checkout# Restore all swaps at the end of a workflow- swap: restore_all: true
Dirty-Repo Auto-Backup
When a repo has uncommitted changes before swapping:
Creates branch mando-backup/<repo>-<timestamp>
Commits all changes with message mando: auto-backup before swap
Returns to original branch
On restore, cherry-picks changes back and preserves the backup branch if there is a conflict
Backup branches persist
The mando-backup/* branches are not automatically deleted. If the cherry-pick on restore succeeds, you can safely delete them. If it conflicts, the backup branch is your safety net.
State
Swap registry stored in .mando-state.json alongside recipe state
Each active swap tracked as a SwapEntry with repo name, original path, target path, and swap type
Managed worktrees stored in .mando-worktrees/ (gitignored)
mando swap --list reads from the registry to show active swaps
Testing
6 integration tests covering:
Directory swap + restore roundtrip
Dirty repo auto-backup
Double-swap prevention (error if already swapped)
Nonexistent repo handling
Restore-all behavior
Branch worktree swap
Recipe Example
name: Start Mandosteps: - check: name: Docker available command: docker --version - env: file: .env optional: true - capture: name: mando_branch command: git -C ../../mando branch --show-current - prompt: name: confirm_branch message: "Mando is on branch '{{mando_branch}}'. Continue?" type: confirm - choice: name: mando_source message: "How to get the mando image?" options: - label: Build locally recipe: mando/build-local.yaml - label: Pull from registry recipe: mando/pull-registry.yaml - compose: files: [compose.yaml] profiles: [app] command: up -d
Recipe Ecosystem (35 files)
Entry Point
menu.yaml — Main hub with 5 choices: infra only, mando+mocked, mando+algos, service management, WireMock manager
mando/ (6 recipes) — Mando Image & Startup
Recipe
Purpose
start.yaml
Main orchestration (choose local build or registry, OTel toggle)
start-with-algos.yaml
Full stack with algo services (OTel toggle)
build-local.yaml
Build from source
build-local-with-algos.yaml
Build with algo overlay
pull-registry.yaml
Pull from GitLab registry
pull-registry-with-algos.yaml
Pull with algos
select-tag.yaml
Registry tag selection
OTel Toggle in Start Recipes
Both start.yaml and start-with-algos.yaml include an OTel toggle choice (enable_otel). When enabled, the compose.otel.yaml overlay is applied as a post-build step that layers Jaeger on top. See Jaeger Tracing for details.
# Non-interactive with OTel enabledcargo run -- run mando/start-with-algos -e enable_otel=true -e mando_source=local
Prior to this fix, && and || in when conditions silently failed (always evaluated to false). Now properly splits on &&/|| and evaluates each sub-expression independently.
The -e KEY=VALUE flag presets context variables before recipe execution, enabling fully non-interactive runs. Choice and Prompt steps check if their target variable already exists in context and skip user interaction if so.
# Preset a choice variable to skip the interactive menumando run mando/start -e mando_source=local# Multiple presetsmando run debug/memray-container -e debug_service=forecast -e memray_mode=flamegraph# Full stack with algos + OTel tracing, non-interactivecargo run -- run mando/start-with-algos -e enable_otel=true -e mando_source=local# Combine with --replay for fully automated re-runsmando run menu -e mode=2 --replay
For Agents
The -e flag makes recipes scriptable from CI or parent orchestrators. Any choice or prompt step whose name matches a preset key will use the preset value without prompting.
State Persistence
File: .mando-state.json (sibling of recipes/)
Stores all prompt/choice answers per recipe, plus the swap registry
--replay re-uses saved values; falls back to interactive if missing
Updated after successful recipe completion
Environment Forwarding
All context variables are exported as OS env vars to child processes. Critical for docker compose which reads ${VAR} from process environment.
Recipe Discovery
Look for recipes/ in current directory
Walk up parent directories (like .git discovery)
All *.yaml/*.yml under recipes/ are loadable
Recipe paths always relative to recipes/ root (not current file)
Dependencies
Crate
Version
Purpose
clap
4
CLI argument parsing (derive)
serde + serde_yaml
1 / 0.9
YAML deserialization
serde_json
1
State file (JSON)
toml
0.8
TOML config parsing
dialoguer
0.11
Interactive prompts (Confirm, Input, Select)
dotenvy
0.15
.env file loading
ctrlc
3
Signal handling
chrono
0.4
Timestamps (RFC3339)
anyhow
1
Error handling
ureq
3
HTTP requests (blocking)
tempfile
3
Test fixtures (dev-only)
Edition: Rust 2024
Test Coverage
65 tests total across 2 test suites — all passing.