Infrastructure-as-code based on Tailscale mesh network. Auto-detects platform (Linux server, macOS, Android/Termux, iOS/iSH, Windows/WSL) and installs + configures everything needed for a multi-device, multi-agent development environment.
| Layer | Technology | Purpose |
|---|---|---|
| Networking | Tailscale (WireGuard) | Zero-config mesh VPN — device-to-device connectivity |
| Remote access | Mosh + tmux | Resilient mobile shell + persistent sessions |
| Proxy | autossh + privoxy | Auto-reconnecting SOCKS5 tunnel + HTTP bridge |
| AI agents | Claude Code · Gemini CLI · Codex CLI | Agentic coding — dispatched to isolated worktrees |
| Orchestration | Custom Python (conductor, orchestrator, task DB) | Task generation, dispatch, monitoring, auto-merge |
| Webapp | FastAPI + Docker | Web control panel — device dashboard, job launcher |
| Database | SQLite (per-device) | Tasks, subtasks, organizations, repos, jobs |
| Package mgmt | uv | Fast Python venv + package management |
| Config | Hydra YAML | Device registry, org/repo settings, CLI config |
| Linting | Ruff + pytest | Linting and test suite |
| Shell | Bash/Zsh aliases, SSH config | 30+ universal functions across all mesh devices |
| CI | Webhook listener (ntfy.sh) + validation pipeline | Automated lint → test → merge on completion |
| Git isolation | Git worktrees | Parallel agent work without branch conflicts |
| Browser access | VibeTunnel (muxtree) | Access tmux sessions through the browser |
| Platforms | macOS · Linux · WSL2 · Termux · iSH | Cross-platform with auto-detection (detect.sh) |
┌────────────────────────────────────────────────────────────┐
│ Tailscale Mesh VPN │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ server1 │ │ server2 │ │ laptop │ │ phone │ │
│ │ (Linux) │ │ (Linux) │ │ (Mac) │ │(Android) │ │
│ │ webapp │ │ agents │ │ conduct │ │ │ │
│ │ Docker │ │ dispatch │ │ develop │ │ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └─────────────┴─────────────┴─────────────┘ │
└────────────────────────────────────────────────────────────┘
Each device has its own SQLite database (~/.tenai/devices/{name}.db) storing organizations, repos, tasks, subtasks, and jobs. The webapp (Docker container on the configured device) provides a web control panel for managing all devices.
📖 See
docs/for detailed guides: setup, CLI config, conductor, skills, testing, webapp, troubleshooting
- One-command setup —
make onboardauto-detects platform, registers device, installs everything - Remote bootstrap —
make new-server HOST=xsyncs code, propagates.env, installs tools via SSH - Code sync —
make sync HOST=xrsync + Docker rebuild + alias push, ormake sync-allfor fleet - Tool verification —
make check [HOST=x]verifies all tools installed across devices - Reset device —
make reset-deviceinteractive wizard (reset config, walk through.envand API keys, optionally full uninstall) - Idempotent — every script is safe to re-run on any platform
- Task generation — Gemini conductor analyzes codebases, generates ATC-compliant tasks
- Parallel dispatch — agents work in isolated git worktrees with auto-approval flags
- Orchestation loop — polls DB, dispatches, monitors, marks done, auto-merges (every 30s)
- Merge safety — conflict detection, per-worktree validation, integration testing, sequential merge
- GitHub Issues — bidirectional sync between task DB and GitHub Issues
- Webhook triggers — ntfy.sh notifications for completion and remote dispatch
- Device dashboard — status, disk, uptime, Docker, tmux sessions
- Job launcher — shell commands, conductor sessions, agent dispatch from browser
- Live monitoring — real-time tmux session interaction
- Remote DB sync — automatically syncs device databases when switching views
- SOCKS5 + HTTP bridge — route specific CLI tools through Tailscale exit nodes
- Persistent daemon — autossh + launchd/systemd for auto-reconnecting tunnel
- Per-command routing —
tenai_claudegoes through exit node,claudestays direct - Network-aware — macOS
KeepAlive.NetworkStateauto-restarts on Wi-Fi/VPN changes
- Skills — reusable agent instructions synced across Claude/Gemini/Codex (
.agents/skills/) - Extensions — Gemini CLI extensions (MCP servers, tools) with idempotent install
- Settings — per-CLI settings merge (
settings.jsonmanagement) - Rules — global rules (
CLAUDE.md,GEMINI.md,AGENTS.md) deployed to all devices
- Named .env files —
~/.tenai_envs/{org}--{repo}.envwith push/pull across devices - Hydra config —
config/defaults.yamlfor devices, orgs, repos, proxy, webapp, CI - SSH key management — per-org GitHub SSH keys with automatic git URL rewrites
- Shell aliases — 30+ universal functions that work from any directory on any device
| Tool | Purpose |
|---|---|
| Tailscale | Mesh VPN — device-to-device connections |
| Mosh | Resilient mobile shell (survives network switches) |
| tmux | Persistent terminal sessions with mouse support |
| uv | Fast Python package manager (replaces pip) |
| autossh | Auto-reconnecting SSH tunnel monitor |
| privoxy | HTTP→SOCKS5 protocol bridge for proxy routing |
| Claude Code | Agentic coding CLI (Anthropic) |
| Gemini CLI | Agentic coding CLI (Google) |
| Codex CLI | Agentic coding CLI (OpenAI) |
| muxtree | Automated worktree + tmux session manager |
| VibeTunnel | Access tmux sessions from browser (private mode) |
| SSH config | Pre-configured entries for all tailnet devices |
| Shell aliases | Universal functions for the entire mesh |
| Platform | Prerequisites | Setup command |
|---|---|---|
| macOS | Homebrew, Git | make onboard |
| Linux (Ubuntu/Debian) | apt, Git |
make onboard |
| WSL2 (Windows) | WSL2 installed, Git | make onboard |
| Termux (Android) | Termux, Git | make onboard |
| Windows native | — | WSL2 required (see below) |
Windows users: tenai-infra requires WSL2. Run
wsl --installin PowerShell (Admin), then work inside WSL. For initial device bootstrap (Git, SSH, Tailscale, AI CLIs), you can also run:powershell -File scripts/install/bootstrap_windows.ps1
- Tailscale account — tailscale.com (free for personal use)
- Tailscale API and AUTH keys
TAILSCALE_API_KEY- for registering devicesTAILSCALE_AUTH_KEY- for authenticating devices
- SSH access between devices (the setup will configure this)
- Git installed on all devices
- API keys (optional, for AI agent CLIs):
ANTHROPIC_API_KEY— for Claude CodeGEMINI_API_KEY— for Gemini CLIOPENAI_API_KEY— for Codex CLIGITHUB_TOKEN— for GitHub integration
# 1. Clone on your first device
git clone <repo-url> ~/tenai-infra && cd ~/tenai-infra
# 2. Create your .env with API keys
cp .env.example .env
$EDITOR .env # set TAILSCALE_AUTH_KEY, TAILSCALE_TAILNET, GITHUB_TOKEN
# 3. Onboard this device (auto-detects platform + installs everything)
make onboard
# 4. Reload shell to activate aliases
source ~/.zshrc # or source ~/.bashrc on Linuxmake onboard auto-detects your device type, registers it in config/local.yaml (gitignored — personal config), and runs the internal _setup command (install + configure). Devices added by onboard look like:
# config/local.yaml (gitignored — your personal overrides)
tailscale:
tailnet: yourname@
devices:
myserver: # Added by make onboard
ip: "100.x.y.z" # Tailscale IP (auto-detected)
user: ubuntu # SSH user
type: server # server | mac | android | ios_ish | windows
capabilities: [conductor, dispatch, claude, gemini]
webapp:
device: "myserver" # which device hosts the webapp Docker containerRun make help to see all available targets.
The setup targets build on each other. Use the one that matches your situation:
graph TD
ON["make onboard"] -->|"local: runs"| S["make _setup"]
ON -->|"remote: runs"| NS["make new-server HOST=x"]
S --> I["make install"]
S --> C["make configure"]
I --> ID["install-deps<br/>uv + venv + Python deps"]
I --> IT["setup.py install_all<br/>Tailscale, Mosh, tmux, tools"]
C --> CS1["SSH config (per-device entries)"]
C --> CS2["Shell aliases (~/.tenai_aliases)"]
C --> CS3["CLI skills/extensions/settings"]
C --> CS4["Proxy (autossh + privoxy)"]
C --> CS5["Data directories (~/.tenai, ~/.tenai_envs)"]
NS --> NS1["rsync code to remote"]
NS --> NS2["propagate .env"]
NS --> NS3["make setup on remote"]
NS --> NS4["distribute SSH keys"]
NS --> NS5["git-ssh per-org keys"]
| Target | What it does | When to use |
|---|---|---|
make onboard |
Registers device in config + runs make _setup (local) or full remote bootstrap (remote) |
Primary command — adding a device to the mesh |
make install |
Installs uv, Python venv + deps, Tailscale, Mosh, tmux, AI CLIs, autossh, privoxy | Need to install/update tools |
make configure |
SSH config, shell aliases, CLI setup, proxy, data dirs | Need to regenerate config after changing defaults.yaml |
make _setup |
install + configure (internal full local setup) |
Standard underlying setup command |
make new-server HOST=x |
Syncs repo via rsync, propagates .env, runs make _setup on remote, distributes SSH keys, sets up git-ssh |
Bootstrap a remote device (called by make onboard for remote targets) |
make reset-device |
Interactive wizard: backs up config, resets devices, walks through .env and tailnet setup. FULL_RESET=1: also uninstalls tracked tools. NONINTERACTIVE=1: automated/CI mode. |
Recovery — broken config, or guided setup when unsure what keys to set |
All steps are idempotent — safe to re-run at any time.
cp .env.example .env # 1. Create and fill in your API keys
$EDITOR .env # (TAILSCALE_AUTH_KEY, TAILSCALE_TAILNET are required)
make onboard # 2. Registers this device + runs internal setup
source ~/.zshrc # 3. Load new aliases (or ~/.bashrc)Guided setup: If you're unsure what keys to set, run
make reset-deviceinstead — it walks through each key interactively.
For subsequent devices:
make onboard IP=100.x.y.z # remote by IP
make onboard TYPE=server NAME=myserver IP=100.x.y.z # explicit type + name
make onboard TYPE=android NAME=myandroid # Android (Termux)
make onboard TYPE=ios NAME=iphone TERMIUS=1 # iOS (Termius)The remote wizard auto-discovers the device on Tailscale, establishes SSH, installs tools, registers it in config/local.yaml, and pushes aliases.
If the device already has Tailscale configured by another user, tenai detects this and skips Tailscale to avoid disrupting their access (a device can only belong to one tailnet). To onboard without touching Tailscale:
make onboard HOST=myhost SKIP_TOOLS=tailscaleWhen you specify HOST=ipm, the system resolves through: config/local.yaml →
~/.ssh/config → raw hostname. When resolved from SSH config, the original alias
is preserved for all connections, ensuring IdentityFile, ProxyCommand, etc. are
respected.
See HOWTO_setup.md for shared instances, SSH resolution, and firewall details.
To reset your configuration and walk through setup again:
make reset-device # interactive config reset
make reset-device FULL_RESET=1 # config reset + uninstall all tracked tools
make reset-device NONINTERACTIVE=1 TAILNET=name@ # automated/CI mode (no prompts)Config-only reset (default) backs up defaults.yaml and .env into .backup/ (with date suffix, never overwriting existing backups), clears the devices list, and walks through API key setup. It never touches config/local.yaml or removes existing backups.
Full reset (FULL_RESET=1) additionally runs make uninstall to surgically reverse all tracked changes — tools, SSH config blocks, aliases, CLI skills — using the state manifest.
To preview what would be removed without changing anything:
make uninstall DRY_RUN=1 # preview uninstall plan
make state # display current state manifest
make state-audit # reconstruct manifest from filesystemmake sync HOST=myserver # rsync + rebuild Docker webapp + push aliases + CLI setup
make sync HOST=myserver GIT_PULL=1 # git pull on remote instead
make sync-all # sync to all remote devicesmake configure # SSH config + aliases + CLI skills/extensions/settings + data dirsThis is the local equivalent of make sync.
The webapp runs as a Docker container on the device specified in config/defaults.yaml under webapp.device:
webapp:
device: "server1" # device that hosts the webapp Docker container
port: 7700make sync HOST=myserver # deploys and auto-rebuilds Docker container if needed
make webapp # start locally (foreground)
make webapp-bg # start in tmux (background)
make webapp-docker # start in DockerAccess at http://<device-ip>:7700. Features: device dashboard, org management, repo browser, job launcher (shell, conductor, dispatch), job monitoring with live tmux interaction.
When switching between devices in the webapp, it automatically syncs the remote device's database to ensure fresh data.
Named .env files are stored in ~/.tenai_envs/ with the convention {org}--{repo}.env:
# Push .env to a device
make push-env HOST=myserver # pushes .env as ~/.tenai_envs/{org--repo}.env
make push-env HOST=myserver ENV_FILE=.env.prod NAME=my-app # custom file + name
# Sync all env files to devices
make sync-envs HOST=myserver # single device
make sync-envs # all devices
# Pull env into current repo (shell alias — works from any directory)
tenai_env_pull # prompts before overwriting
tenai_env_pull -f # force overwriteWorktrees auto-populate .env from ~/.tenai_envs/ during setup.
Skills, extensions, settings, and rules for Claude Code, Gemini CLI, and Codex CLI:
make cli-setup # install all CLI config locally
make cli-setup HOST=myserver # install on remote device
make cli-setup HOST=all # install on all devices
make cli-skills HOST=all # deploy skills only
make cli-extensions CLI=gemini # extensions for specific CLI
make cli-list # list all installed CLI assetsSkills live in .agents/skills/ and are symlinked to each CLI directory. See HOWTO_skills.md and HOWTO_cli_setup.md.
Each device has its own SQLite database (~/.tenai/devices/{name}.db) storing organizations, repos, tasks, subtasks, and jobs. The webapp (Docker container on the configured device) has a separate global DB for device registry and settings. When switching devices in the webapp, it automatically syncs the remote device's database via SCP.
create task → dispatch agent → monitor → validate proof → merge
make task-add REPO=my-repo TITLE="Fix login bug" # add a task
make task-list REPO=my-repo # list active tasks
make task-list REPO=my-repo STATUS=completed # completed tasks
make task-import REPO=my-repo # import TASKS.md into DB
make task-import REPO=my-repo HOST=myserver # import from remote device
make task-import REPO=my-repo HOST=myserver FORCE=1 # clear + re-import
make task-register REPO=my-repo TITLE="..." CLI=claude # register + standardize
make task-sync HOST=myserver # sync remote task DB to local
make task-delete IDS=1,2,3 # delete tasks by ID
make task-query REPO=x PATTERN="auth" SINCE=2026-03-01 # rich query with filtersTasks live in the per-device SQLite DB. TASKS.md is an ephemeral view rendered from the DB, not the source of truth.
make dispatch |
make orchestrate |
|
|---|---|---|
| Scope | Single task | All active tasks (loop) |
| Input | You specify REPO + TASK/BRANCH | Polls DB, TASKS.md, and GitHub Issues |
| Agent launch | Creates worktree + starts agent in tmux | Same, but for every dispatchable task |
| Monitoring | Manual (tmux attach) |
Automatic (polls for completion, checks PROOF.md) |
| Task status | Manual | Auto-updates DB: active → dispatched → done |
| TASKS.md | Unchanged | Re-rendered from DB after each cycle |
| Merge | Manual | Auto-runs merge safety pipeline when all agents complete |
| Notifications | None | ntfy.sh alerts on completion |
| Mode | One-shot | Daemon (runs every 30s until all done) |
make dispatch REPO=my-repo TASK="Fix login" CLI=claude # local
make dispatch REPO=my-repo TASK="Fix login" HOST=myserver # remote
make dispatch REPO=my-repo BRANCH=feat/auth CLI=gemini # explicit branchWhat dispatch does:
- Creates a git worktree in
.trees/{branch}/ - Copies
.env,CLAUDE.md/GEMINI.md/AGENTS.md, and.claude//.gemini//.codex/dirs - Writes
WORKTREE.mdwith task instructions - Opens a tmux window in the
{repo}-agentssession - Launches the CLI with auto-approval flags from
config/cli/{cli}.yaml:- Claude:
--dangerously-skip-permissions - Gemini:
--yolo - Codex:
--full-auto
- Claude:
- Sends the agent prompt: "Read WORKTREE.md, implement, test, commit, push, create PROOF.md, exit"
make orchestrate REPO=my-repo # dispatch all, monitor, auto-merge
make orchestrate REPO=my-repo CLI=gemini HOST=myserver # remote, Gemini agents
make orchestrate REPO=my-repo GH_REPO=org/repo # also pull from GitHub Issues
make orchestrate REPO=my-repo IDS=5,12 # specific task IDs only
make orchestrate REPO=my-repo PATTERN=auth # filter by title pattern
make orchestrate REPO=my-repo ONE_SHOT=1 # single pass (no loop)
make orchestrate-webhook # listen for ntfy.sh dispatch triggersThe orchestrator loop (every 30s):
- Load tasks — DB first, TASKS.md fallback, optionally GitHub Issues
- Dispatch — new active tasks → worktree + agent
- Monitor — check tmux sessions for completion
- Mark done — update DB + TASKS.md when PROOF.md exists
- Auto-merge — when all agents finish: check-conflicts → validate → integration-test → merge-sequential
make conductor REPO=my-repo # start Gemini conductor for task planning
make conductor REPO=my-repo HOST=myserver # on remote device
make conductor-send REPO=my-repo PROMPT="..." # send prompt to running conductorThe conductor is a Gemini session that analyzes a codebase and generates ATC-compliant tasks (Self-Contained, Verifiable, Bounded, Parallelizable, Resume-safe).
After agents complete, the merge pipeline validates before merging:
make check-conflicts REPO=x # detect file overlap between branches
make validate-worktrees REPO=x # run tests in every worktree
make integration-test REPO=x # merge all → test branch → validate
make merge-sequential REPO=x # merge branches one-by-one with test after eachmake monitor-agents REPO=x HOST=y # watch agent sessions for completion
make agent-history REPO=x [FORMAT=json] # session historySee HOWTO_conductor.md, CONCEPT_agent_system.md, and DATA_FLOW_tasks.md.
After make configure, these functions work from any directory:
| Command | Description |
|---|---|
<device> [session] |
Mosh into device, attach/create tmux session |
ssh_<device> |
Plain SSH to device |
send_<device> <file> |
Tailscale file send |
tenai_status |
Mesh + repos overview |
tenai_check [device] |
Verify tools on device |
tenai_devices |
List all mesh devices |
tenai_resolve <device> |
IP/user/type lookup |
tenai_conductor |
Start conductor session |
tenai_dispatch [repo] "title" [cli] [host] |
Launch agent task |
tenai_worktree [repo] "title" |
Create git worktree (no agent) |
tenai_tmux_worktree [repo] "title" [cli] |
Worktree + tmux + agent |
tenai_task_list [repo] [status] |
Query tasks |
tenai_task_add [repo] "title" [host] |
Add a task |
tenai_push_env <device> [file] [name] |
Push .env to device |
tenai_env_pull [-f] |
Pull .env from ~/.tenai_envs/ |
tenai_tmux_list [host] |
List tmux sessions (local, remote, or all) |
tenai_tmux_clean [host] |
Kill stale tmux sessions (keeps main) |
tenai_tmux_kill_all [host] |
Kill ALL tmux sessions (destructive) |
tenai_proxy_start [node] |
Start SSH SOCKS5 tunnel (autossh) |
tenai_proxy_stop |
Stop tunnel + privoxy |
tenai_proxy_status |
Check proxy services |
tenai_proxy_test |
Compare real vs proxied IP |
tenai_proxy_daemon enable|disable|status |
Manage persistent SOCKS5 daemon |
All aliases support --help. Commands that accept repo auto-detect it from git remote when called inside a repo.
make git-ssh # set up all orgs locally
make git-ssh HOST=myserver # set up on remote (copies your local key)
make git-ssh ORG=myorg HOST=myserver # single org
make git-ssh HOST=myserver GENERATE_GIT_SSH_KEY=1 # generate new key on device
make distribute-keys # distribute SSH keys across all devicesEach org gets its own SSH host alias (github-<org>) with automatic git URL rewrites.
Route CLI tools through a Tailscale exit node using autossh SOCKS5 + privoxy HTTP bridge:
tenai_proxy_start # start SSH tunnel (autossh, auto-reconnects)
tenai_proxy_stop # stop both
tenai_proxy_status # check if running
tenai_proxy_test # compare real vs proxied IP
tenai_proxy_daemon enable # enable persistent daemon (survives reboots)
tenai_proxy_daemon status # check daemon stateProxied aliases (tenai_claude, tenai_gemini, tenai_codex) route traffic through the exit node while keeping your other traffic direct:
claude "help" # direct (local ISP)
tenai_claude "help" # via exit node (server1's ISP)Configure in defaults.yaml:
proxy:
enabled: true
autossh: true # use autossh for auto-reconnecting tunnel
socks_port: 1055 # SSH SOCKS5 tunnel port
http_port: 8118 # privoxy HTTP bridge port
exit_node: "server1" # device with advertise_exit_node: true
proxied_tools: [claude, gemini, codex]See CONCEPT_tailscale_proxy.md for architecture details, alternatives evaluated, and KeepAlive.NetworkState deep-dive.
make release-notes # generate .release_notes/vX.Y.Z.md
$EDITOR .release_notes/vX.Y.Z.md # review and edit
make tagged-release PATCH=1 AUTO_MESSAGE=1 # tag + push with notesCreates an isolated worktree, sanitizes config (removes real devices/secrets), tags, and pushes. Your main branch keeps your real device configuration intact.
| Directory | Purpose |
|---|---|
scripts/install/ |
Idempotent tool installers (tailscale, mosh, tmux, proxy, tools) |
scripts/configure/ |
SSH config, shell aliases, git-ssh-setup, key distribution |
scripts/conductor/ |
Task DB, orchestrator, conductor, CI daemon, GitHub Issues |
scripts/entrypoints/ |
Orchestration scripts (onboard, new-server, sync, git-ssh) |
scripts/repos/ |
Git worktree management, merge safety, multi-agent dispatch |
webapp/ |
FastAPI control panel (runs in Docker on servers) |
config/ |
Hydra YAML configs (devices, orgs, repos, CLI settings) |
config/cli/ |
Per-CLI config (extensions, skills, settings, MCP servers) |
.agents/skills/ |
Reusable agent instructions (single source → symlinked to CLIs) |
docs/ |
Categorized docs (CONCEPT_, DATA_FLOW_, DEBUG_, EXAMPLE_, HOWTO_) |
tests/ |
pytest suite — unit + integration |
tenai uses a 3-layer configuration model:
| Layer | File | Tracked | Purpose |
|---|---|---|---|
| Defaults | config/defaults.yaml |
✅ git-tracked | Core settings, tool defaults, empty device/org scaffolds |
| Personal | config/local.yaml |
❌ gitignored | Your devices, orgs, proxy, tailnet — deep-merged over defaults |
| Secrets | .env |
❌ gitignored | API keys, auth tokens, device name |
Personal config is deep-merged on top of defaults. Set TENAI_CONFIG in .env to use
a custom overlay path.
Example config/local.yaml:
tailscale:
tailnet: yourname@
devices:
myserver:
ip: "100.x.y.z"
user: ubuntu
type: server
capabilities: [conductor, dispatch, claude, gemini]
organizations:
myorg:
github_url: "github.com"
ssh_host_alias: "github-myorg"
ssh_key: "~/.ssh/tenai-ssh-key"Supported device types: server, mac, android, ios_ish, windows, wsl.
Onboarding never enables a host-level firewall and never touches SSH port configuration. On cloud instances (AWS/GCP/Azure), host-level firewall is skipped entirely — use Security Groups instead. See HOWTO_setup.md § Firewall Safety.
git clone <repo-url> ~/tenai-infra && cd ~/tenai-infra
make install-deps # create .venv with all dependencies
make lint && make test # verify everything worksTest the full onboarding pipeline in an isolated VM before releasing changes:
| Engine | Platform | VM Type | When to use |
|---|---|---|---|
| Multipass | macOS, Linux, Windows | Ubuntu (Linux) | Testing Linux server onboarding |
| Tart | macOS (Apple Silicon) | macOS (ARM64) | Testing macOS onboarding |
# Multipass (Linux VM)
make test-sandbox # interactive — SSH in and test manually
make test-sandbox AUTO=1 # automated — runs full pipeline
# Tart (macOS VM)
make test-sandbox ENGINE=tart # interactive macOS sandbox
make test-sandbox ENGINE=tart AUTO=1 # automated macOS pipeline
make test-sandbox ENGINE=tart AUTO=1 KEEP=1 # keep VM for iteratingThe automated pipeline runs exactly what a real user would do:
make reset-device → make onboard → make check → make test
Flags:
| Flag | Description |
|---|---|
ENGINE=multipass|tart |
VM engine (default: multipass) |
AUTO=1 |
Run full pipeline automatically |
KEEP=1 |
Keep VM after test (skip teardown) |
TART_IMAGE=ghcr.io/... |
Custom Tart base image |
UBUNTU_VERSION=24.04 |
Specific Ubuntu version |
UPDATE_PACKAGES=1 |
Run apt update before test |
NAME=my-node |
Custom VM name |
Tart image caching: If a VM with the same name already exists, the script reuses it (skipping the 20+ minute Homebrew/Xcode setup). To iterate quickly: run with KEEP=1, then re-run without — the cached VM boots instantly.
Accessing VMs:
ssh admin@$(tart ip <vm-name>) # Tart (password: admin)
ssh ubuntu@<multipass-ip> # Multipass
tart run <vm-name> # Tart GUI desktopLogs: Saved to sandbox-logs/<date>_<vm-name>/
See HOWTO_testing.md for full details.
Use Conventional Commits — release notes are auto-generated from these:
| Prefix | Section |
|---|---|
feat: |
✨ New Features |
fix: |
🐛 Bug Fixes |
refactor: / perf: / chore: |
🔧 Improvements |
docs: |
📖 Documentation |
- Create a task:
make task-add REPO=tenai-infra TITLE="Fix X" - Dispatch to worktree:
make dispatch REPO=tenai-infra TITLE="Fix X" CLI=claude - Agent works in isolated worktree, creates
PROOF.md - Verify:
make lint && make test(always before committing) - PR & merge — the conductor monitors and merges sequentially
make release-notes # generate .release_notes/vX.Y.Z.md
$EDITOR .release_notes/vX.Y.Z.md # review and edit
make tagged-release PATCH=1 AUTO_MESSAGE=1 # tag + pushSee HOWTO_releases.md for full details.
- All scripts must be idempotent and cross-platform
- Run
make lint && make testafter every code change - Never install Python packages system-wide — use
uv pip install -p .venv - Never hard-code device IPs, hostnames, or secrets
make help # show all targets (~80 commands)
make onboard # register + full setup (this device)
make _setup # install + configure (internal, no registration)
make install # install tools only (uv, Tailscale, Mosh, autossh, etc.)
make configure # configure only (SSH, aliases, CLI, proxy, dirs)
make reset-device # recovery / guided wizard (.env, tailnet, API keys)
make reset-device FULL_RESET=1 # full reset (uninstall + reconfigure)
make uninstall # interactive uninstallation and reversal wizard
make state # display local state tracking manifest
make state-audit # reconstruct missing manifest from filesystem
make check # verify tools installed
make status # mesh + repos overview
make status HOST=x # remote device status
make proxy-daemon ACTION=enable # enable persistent SOCKS5 daemon
make release-notes # generate changelog since last tag
make tagged-release PATCH=1 # create tagged release