Development

Building

cargo build                    # Debug build
cargo build --release          # Release build (with LTO)
cargo build --profile dev-release  # Optimized build without LTO (faster compile)

The release binary is at target/release/aoe.

The web dashboard needs the serve feature and Node.js: cargo build --release --features serve. See Web Dashboard Development.

Faster rebuilds across worktrees (kache)

AoE’s normal workflow keeps several git worktrees in flight at once (one per branch). Each worktree has its own target/, so a cold cargo build in a fresh worktree recompiles all ~400 dependency crates from scratch, even when Cargo.lock is identical to a worktree that built them minutes ago. That is both slow (minutes of dependency compilation) and wasteful on disk (each target/ holds its own multi-gigabyte copy of the same artifacts).

kache is an optional, opt-in fix for this. It is a content-addressed rustc wrapper: it compiles each crate once, stores the artifact in a shared local store (~/Library/Caches/kache on macOS, ~/.cache/kache on Linux), and shares it into each worktree’s target/ without copying by reflinking it (a copy-on-write clone, on APFS/btrfs/xfs) or hardlinking it (on filesystems without reflink support, such as ext4). The first build of a given Cargo.lock populates the store; every later build, in any worktree, restores the dependency artifacts instead of recompiling them. Each worktree still produces its own target/debug/aoe binary, and the shared blocks mean one physical copy of each dependency artifact backs all worktrees, so both build time and disk usage drop.

kache is not required and is deliberately not committed to the repo (no rustc-wrapper in .cargo/config.toml): a plain cargo build with no extra setup works exactly as it does today, and CI, the Nix build, and release builds never touch kache, so shipped release binaries stay compiled by plain rustc. Each developer turns it on for themselves with two environment variables.

Opting in

Install a prebuilt binary (the prebuilt avoids compiling kache; see the bootstrap caveat below):

cargo binstall kache         # prebuilt binary, recommended
# or
mise use -g github:kunobi-ninja/kache@latest

Enable it for your shell (for example in ~/.zshrc or ~/.bashrc):

export RUSTC_WRAPPER=kache
export CARGO_INCREMENTAL=0   # kache and incremental compilation are mutually exclusive

Then build as usual; rustc now routes through kache and your per-worktree target/debug/aoe is unchanged:

cargo build --all-features

Watch cache hits and deduplicated bytes live with kache monitor, or print a non-interactive summary with kache stats. To go back to plain rustc, unset RUSTC_WRAPPER (or point it at another wrapper, e.g. export RUSTC_WRAPPER=sccache for a time-only cache without disk dedup).

Caveats

  • Same filesystem only. Reflinks and hardlinks cannot span filesystems, so the kache store and your worktrees’ target/ dirs must live on one volume. A worktree on a different mount falls back to copying (still cached, no disk dedup).
  • Native-linking crates still rebuild. Dependencies with build scripts that link C libraries (git2, openssl-sys, ring) are not cacheable and recompile each time. They are a minority of total build time.
  • --features serve and stale assets. The web dashboard embeds web/dist at compile time via rust-embed (the debug-embed feature embeds in debug builds too). build.rs emits rerun-if-changed=web/src (and the other web inputs), so rebuilding the frontend dirties the crate and kache recompiles it; a cache hit therefore should not serve stale embedded assets. If you ever suspect a stale bundle, cargo clean -p agent-of-empires forces a rebuild.
  • macOS: kache excludes its store from Spotlight indexing and Time Machine automatically.
  • Bootstrap. Prefer the prebuilt install above. If you do build kache from source with cargo install while RUSTC_WRAPPER=kache is already exported, cargo tries to use kache to compile kache and fails; unset the variable for that one command.

You can confirm the dependency artifacts are actually shared and deduplicated across two target dirs with scripts/verify-shared-target.sh (see the script header for usage).

Running

cargo run --release            # Run from source
AGENT_OF_EMPIRES_DEBUG=1 cargo run  # Debug logging (writes to debug.log in app data dir)
AOE_LOG_LEVEL=trace cargo run        # Pick the log level explicitly
AOE_ACP_TRACE=1 cargo run            # Plus raw ACP JSON-RPC firehose; useful for
                                     # verifying sub-agent linkage
                                     # (`_meta.claudeCode.parentToolUseId` round-trip)
                                     # and other adapter-side _meta fields. Structured view
                                     # also logs a `acp.protocol.tool_dispatch` debug line whenever
                                     # it links a child tool call to a parent Task.
AOE_TERMINAL_TRACE=1 cargo run       # Plus per-message bytes for the web terminal WS (spammy)
aoe logs                       # View debug.log via lnav/bat/less (auto-detects)
aoe logs --path                # Print the resolved log file path

Requires tmux to be installed.

Web dashboard dev server

cargo xtask dev    # Unix only

Builds the serve-enabled binary, then runs aoe serve (8081) and the Vite dev server (5173) together with hot module reload. Open http://localhost:5173; Vite proxies /api and the AoE session WebSocket relays to the backend (via VITE_PROXY). One Ctrl-C stops both. Ports are overridable with --serve-port / --web-port. See Web Dashboard Development for the manual two-shell alternative.

Add --watch to auto-rebuild the Rust backend on source edits:

cargo xtask dev --watch

It watches src/**, Cargo.toml, and Cargo.lock; on a change it runs cargo build --features serve and, if that succeeds, restarts aoe serve. A failed build leaves the running backend in place and prints the error. The Vite dev server is never restarted, so frontend HMR keeps working and the browser reconnects through the proxy once the backend is back. Note that the backend restart drops all live terminal and cockpit WebSocket connections.

Dev namespace

Debug builds use an isolated namespace so a local cargo run shares no state with an installed release aoe; run them side-by-side without colliding on sessions, settings, tmux, or aoe serve. debug.log lives in the app dir, so it’s isolated too. The dev namespace starts empty (nothing migrates from your real dir); wipe it any time with rm -rf ~/.agent-of-empires-dev (Linux: the XDG equivalent).

ReleaseDebug (cargo run)
App dir (macOS / Windows)~/.agent-of-empires~/.agent-of-empires-dev
App dir (Linux)~/.config/agent-of-empires~/.config/agent-of-empires-dev
tmux session prefixaoe_aoe_dev_
aoe serve default port80808081

cargo build --profile dev-release counts as a release build for namespacing (shares app dir, tmux prefix, serve port); use the default dev profile for the isolated -dev namespace.

Testing

cargo test       # Unit + integration tests
cargo fmt        # Format code
cargo clippy     # Lint
cargo check      # Fast type-check

Some integration tests require tmux to be available and will skip if it’s not installed.

Demo GIFs (rarely touched)

All three demos show the same flow on a different surface: create a real Claude Code session, send a message, and watch its status update in the sidebar. They are recorded against a live aoe (no mocks) by driving it with Playwright and converting the WebM to GIF via ffmpeg. Both recorders document their full setup recipe (isolated $HOME/XDG_CONFIG_HOME with Claude credentials, a scratch git repo, the claude-agent-acp adapter) at the top of the file.

TUI demo (docs/assets/demo.gif): web/scripts/record-tui-demo.mjs runs aoe inside ttyd and creates a session that launches into live mode. Needs the profile’s default_attach_mode = "live_send".

Web dashboard GIFs (docs/assets/web-{desktop,mobile}.gif): web/scripts/record-web-demo.mjs --viewport desktop|mobile --project <repo> drives the structured (ACP) view against a real aoe serve --no-auth.