aifw is a thin operator layer, not a framework. It wraps four existing tools:
- tmux — user interface (windows, panes, session management)
- LXD — container isolation (one container per mission)
- Claude Code — AI worker runtime (multiple instances per container)
- Git — version control (one checkout per repo)
The glue between them is files on disk. No database, no message broker, no daemon.
src/aifw/
├── cli.py # argparse-based CLI entry point
├── config.py # Configuration loading, defaults, env overrides
├── events.py # Structured event logging
├── lxd.py # LXD adapter (container lifecycle via lxc CLI)
├── tmux.py # tmux session/window management
├── mission.py # Mission lifecycle, directory structure, state
├── claude.py # Claude Code session launch and prompt injection
├── workers.py # Worker assignment, briefs, status
└── status.py # Status display, doctor, tail
cli.py
├── config.py (standalone)
├── events.py (standalone)
├── lxd.py ← config
├── tmux.py ← config
├── mission.py ← config, events, lxd
├── claude.py ← config, tmux
├── workers.py ← config, mission, claude, events, tmux
└── status.py ← config, mission, workers, lxd, tmux
No circular dependencies. Each module has a clear responsibility.
All workers share a single LXD container. This avoids:
- Container proliferation
- Cross-container networking
- Duplicate mounts
The container is created once during aifw start and shared by all workers via lxc exec.
Repository paths and the mission directory are mounted at the same path in the container as on the host. This means:
- No path translation needed
- Claude Code's path references work everywhere
- File-based coordination "just works"
Workers communicate through structured files in .ai/:
- Status files (JSON) — machine-readable, polled by
aifw status - Handoff notes (Markdown) — human-readable, written when work is complete
- Contracts (Markdown) — cross-repo interface agreements requiring escalation
- Events log (text) — append-only audit trail
This is deliberate. Files are:
- Inspectable (
cat,jq,grep) - Versioned (if the mission dir is in a git repo)
- Debuggable (no hidden state)
- Simple (no serialisation framework)
tmux provides:
- Multiple windows for different concerns
- Persistent sessions (survive terminal disconnects)
- Standard keybindings terminal users already know
- Scriptable window/pane management
The overview window runs watch aifw status for an auto-refreshing dashboard.
When assigning work, aifw sends a prompt to Claude Code through tmux:
- Short prompts:
tmux send-keys - Long prompts:
tmux load-buffer+paste-buffer
This is transparent and inspectable. The prompt points Claude to the brief file rather than inlining the full task.
aifw does not replace the user's existing LXD scripts. The lxd.py module mirrors the logic of work-it.sh but adds:
- Support for multiple disk mounts
- Non-interactive operation
- Configurable fallback to external scripts
If the built-in adapter doesn't fit, set lxd_bootstrap_script in config.
Priority order:
AIFW_*environment variables (highest)- Config file (
~/.config/aifw/config.toml) - Built-in defaults (lowest)
All Claude-related paths are centralised in config.py — no scattered path assumptions.
A mission transitions through these states:
(none) → created → running → stopped → destroyed
↑ │
└──────────┘ (aifw start --id <existing>)
- created: directory exists, no container
- running: container up, tmux session active
- stopped: container stopped, mission files preserved
- destroyed: container deleted, files removed (unless
--keep-files)
- LXD operations raise
LXDErrorwith the fulllxcstderr - tmux operations raise
TmuxError aifw doctorvalidates the environment before things break- Operations are idempotent — re-running is safe
- The CLI catches exceptions and prints clear messages
- Unit tests: config, events, mission, workers — all testable without LXD/tmux
- Mocking: LXD and tmux operations are mocked at the module boundary
- No integration tests requiring LXD: these would need a running LXD daemon
- Tests run with
pytestand usetmp_pathfor isolation