diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..2aad807
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,78 @@
+name: Bug report
+description: Something doesn't work the way the docs say it should
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for filing. The most useful thing you can include is the output of
+ `ecluse validate` and `ecluse ls --json` — most issues are visible there.
+
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened
+ description: What you expected, and what you got instead.
+ validations:
+ required: true
+
+ - type: textarea
+ id: repro
+ attributes:
+ label: Steps to reproduce
+ placeholder: |
+ 1. ecluse init --mode hybrid
+ 2. ecluse up feat-foo
+ 3. ...
+ validations:
+ required: true
+
+ - type: textarea
+ id: config
+ attributes:
+ label: .ecluse.toml
+ description: Redact anything sensitive.
+ render: toml
+ validations:
+ required: true
+
+ - type: textarea
+ id: diagnostics
+ attributes:
+ label: Diagnostics
+ description: Output of `ecluse validate` and `ecluse ls --json`.
+ render: shell
+
+ - type: input
+ id: version
+ attributes:
+ label: ecluse version
+ description: Output of `ecluse --version`
+ validations:
+ required: true
+
+ - type: dropdown
+ id: os
+ attributes:
+ label: OS
+ options:
+ - macOS (Apple Silicon)
+ - macOS (Intel)
+ - Linux
+ - Windows (WSL2)
+ validations:
+ required: true
+
+ - type: dropdown
+ id: runtime
+ attributes:
+ label: Container runtime
+ options:
+ - OrbStack
+ - Docker Desktop
+ - Colima
+ - Podman
+ - Native Docker (Linux)
+ - None (host mode)
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..cd73f08
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Question or idea
+ url: https://github.com/hefgi/ecluse/discussions
+ about: Questions, workflow ideas, and "how would I do X" go in Discussions.
+ - name: Documentation
+ url: https://ecluse.ai
+ about: Configuration reference, command reference, and known limits.
diff --git a/.github/ISSUE_TEMPLATE/stack_support.yml b/.github/ISSUE_TEMPLATE/stack_support.yml
new file mode 100644
index 0000000..f4cc480
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stack_support.yml
@@ -0,0 +1,45 @@
+name: Stack didn't work
+description: ecluse init detected the wrong thing, or your stack needs config it can't express
+labels: ["stack-support"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Detection getting a repo wrong is treated as a bug here, not user error. The more
+ stacks reported, the better detection gets — so these are genuinely welcome.
+
+ - type: input
+ id: stack
+ attributes:
+ label: Stack
+ placeholder: Rails 7 + Postgres + Sidekiq, or Next.js monorepo with Turborepo
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: What you expected ecluse to do
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual
+ attributes:
+ label: What it actually did
+ description: Include the `ecluse init` output if detection was wrong.
+ validations:
+ required: true
+
+ - type: textarea
+ id: config
+ attributes:
+ label: Generated .ecluse.toml (and your corrected version, if you fixed it)
+ render: toml
+
+ - type: textarea
+ id: compose
+ attributes:
+ label: Relevant docker-compose.yml
+ description: Only the parts that matter. Redact secrets.
+ render: yaml
diff --git a/README.md b/README.md
index edeb22f..655cd6e 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,11 @@
-**Ephemeral local environments for coding agents — any stack.**
+**Your coding agent can't verify its work if it has nowhere to run it.**
-Each git worktree gets its own slot — isolated ports, isolated services, isolated data.
-Works whether your stack runs in Docker, on the host, or a mix. No collisions, clean teardown.
+ecluse gives every agent its own full stack — isolated ports, isolated services,
+isolated database. Run 8 agents in parallel, each verifying against a real running
+system. No collisions, clean teardown.
[](https://github.com/hefgi/ecluse/actions/workflows/ci.yml)
[](https://crates.io/crates/ecluse)
@@ -13,9 +14,13 @@ Works whether your stack runs in Docker, on the host, or a mix. No collisions, c
[](LICENSE)
[](https://ecluse.ai/)
+
+
+
---
-**Built for coding agents running tasks in parallel.**
+**Works with every agent that runs in a terminal.**


@@ -23,21 +28,37 @@ Works whether your stack runs in Docker, on the host, or a mix. No collisions, c


-and any agent that can run shell commands.
-
## The problem
-You're running 4 Claude Code sessions in parallel. Each agent finishes its task and wants to verify — run the test suite, spin up the app, hit the endpoints. But port 3000 is taken. Agent 2 kills agent 1's server. Agent 3 waits. The verification loop that was supposed to run in parallel is now sequential. You're paying for 4 agents and getting the throughput of one.
+AI writes the code. It can't check the code.
-ecluse gives each agent its own slot: isolated ports, its own services, its own infra. All 4 agents spin up, verify, and tear down independently. The full AI verification loop — build, migrate, test, e2e — runs in parallel, without collisions, without waiting.
+Writing code needs a context window. *Verifying* code needs a running system — a
+database to migrate, a port to bind, an endpoint to hit, a browser to click through.
+An agent with no environment can only do the two weakest kinds of verification:
+re-read its own diff, and run unit tests that mock away the parts where bugs live.
-
+So you get plausible-looking code that nobody ran. And it lands in a human's review
+queue, which is the one part of the pipeline that doesn't parallelize.
-**Create worktree → Spin up env → Do work → Verify → PR → Teardown**
+Now try to fix that by running four agents at once:
-
+```
+Agent A → needs Postgres, port 3000, a migrated schema
+Agent B → needs Postgres, port 3000, a migrated schema
+Agent C → needs Postgres, port 3000, a migrated schema
+Agent D → needs Postgres, port 3000, a migrated schema
+```
+
+Port 3000 is taken. Agent B drops Agent A's database. Agent C waits. The verification
+loop that was supposed to run in parallel is now sequential, and you're paying for four
+agents to get the throughput of one.
+
+## What ecluse does
+
+One command per agent. Each gets its own slot: its own ports, its own services, its own
+data, torn down cleanly when it's done.
```bash
ecluse up feat-foo # new worktree, isolated ports, isolated services
@@ -45,7 +66,17 @@ ecluse up fix-bar # parallel session, different slot, zero collisions
ecluse down feat-foo # clean teardown, nothing left behind
```
-> ecluse is French for "canal lock" — each session gets its own chamber, everything is isolated, nothing leaks between them.
+All four agents spin up, run the full loop — build, migrate, test, e2e, hit the real
+endpoints — and tear down independently.
+
+
+
+**Create worktree → Spin up env → Do work → Verify → PR → Teardown**
+
+
+
+> ecluse is French for "canal lock" — each session gets its own chamber, everything is
+> isolated, nothing leaks between them.
## Install
@@ -61,13 +92,14 @@ brew install hefgi/tap/ecluse
cargo install ecluse
```
-Then install the agent skill:
+Then install the agent skill, so your agent knows how to drive it:
```bash
npx skills add hefgi/ecluse -g
```
-Requires Rust 1.85+. For container and hybrid modes, [OrbStack](https://orbstack.dev) is recommended over Docker Desktop on macOS — faster, less memory.
+Requires Rust 1.85+. For container and hybrid modes, [OrbStack](https://orbstack.dev) is
+recommended over Docker Desktop on macOS — faster, less memory.
## Get started
@@ -79,7 +111,7 @@ ecluse shell feat-foo # drops into worktree with env loaded
npm run dev # PORT already set — app binds to its own port
```
-`ecluse init` writes a `.ecluse.toml` at repo root. Here's what a typical one looks like:
+`ecluse init` writes a `.ecluse.toml` at repo root. A typical one:
```toml
mode = "hybrid" # container | host | hybrid
@@ -100,7 +132,17 @@ run = "docker"
base_port = 6379 # slot 1 → ECLUSE_REDIS_PORT=6380, slot 2 → 6381
```
-Each `ecluse up` picks the next free slot, starts isolated services, and writes all ports to `.env.ecluse` in the worktree. Type `exit` (or `ecluse down`) to tear everything down.
+Each `ecluse up` picks the next free slot, starts isolated services, and writes all ports
+to `.env.ecluse` in the worktree. Type `exit` (or `ecluse down`) to tear everything down.
+
+Migrations and seeding go in [hooks](https://ecluse.ai/hooks.html) — `post_up` runs with
+the full environment available, so each agent gets a migrated database of its own.
+
+📖 **[Full configuration reference →](https://ecluse.ai/configuration.html)**
+· [Commands](https://ecluse.ai/commands.html)
+· [Port allocation](https://ecluse.ai/ports.html)
+· [Agent workflow](https://ecluse.ai/agent-workflow.html)
+· [Known limits](https://ecluse.ai/limits.html)
## Choosing a mode
@@ -114,18 +156,20 @@ Each `ecluse up` picks the next free slot, starts isolated services, and writes
## How it works
-The central concept is a **slot** — an integer from 1 to `max_slots`. Every resource is derived from the slot:
+The central concept is a **slot** — an integer from 1 to `max_slots`. Every resource is
+derived from it:
-- Per-service port: `base_port + slot` (e.g. `api` at `base_port=3000`, slot 1 → 3001, slot 2 → 3002)
+- Per-service port: `base_port + slot` (`api` at `base_port=3000` → slot 1 gets 3001, slot 2 gets 3002)
- Compose project name: `_`
- Named volumes: `__`
-Three thin mode implementations share this slot primitive. Mode is selected once at `init` time and stored in `.ecluse.toml`.
+Three thin mode implementations share this one primitive. Mode is selected once at `init`
+time and stored in `.ecluse.toml`.
-**How services are started depends on mode:**
+**How services start depends on mode:**
- `container` — everything runs via Docker Compose. ecluse generates a per-slot overlay and calls `docker compose up`.
-- `host` / `hybrid` — native services are spawned using your system's process manager. ecluse uses **tmux** if available (one detached session per slot, one window per service), falling back to **nohup** otherwise (background processes with logs at `.ecluse/logs//`). Docker data services in hybrid mode still go through Compose. Set `command` on a `[[services]]` entry to opt in; services without `command` are not spawned.
+- `host` / `hybrid` — native services are spawned using your system's process manager: **tmux** if available (one detached session per slot, one window per service), falling back to **nohup** (logs at `.ecluse/logs//`). Docker data services in hybrid mode still go through Compose. Set `command` on a `[[services]]` entry to opt in; services without `command` are not spawned.
## Commands
@@ -141,14 +185,15 @@ ecluse flush [--yes]
ecluse ls [--json]
ecluse validate [--ports]
ecluse status [] [--json] [--quiet]
+ecluse whose-pid [--json]
```
-**Env** — get the worktree path and all env vars for a running session as JSON:
+`ecluse up` is idempotent, auto-detects the slug from your cwd, auto-registers existing
+worktrees, and accepts branch names directly (`ecluse up feat/add-auth`). `ecluse env`
+emits the worktree path and every `ECLUSE_*` variable as JSON, which is how agents
+discover their own environment.
-```bash
-ecluse env feat-foo # full JSON: worktree_path, slot, all ECLUSE_* vars
-ecluse env # auto-detects session if run from inside a worktree
-```
+📖 **[Full command reference →](https://ecluse.ai/commands.html)**
**Port discovery** — `ecluse ls` and `ecluse status` also report the port each service is
*actually* listening on next to the one ecluse assigned, so a service that bound the wrong
@@ -168,25 +213,6 @@ Assignment stays the source of truth — a discovered port is reported, never wr
over it. Under parallel sessions the process on a neighbouring port is almost always
another agent's working service, so the remedy is `down --keep-worktree` + `up`, not `kill`.
-**Branch names as argument** — pass your git branch name directly; ecluse sanitizes it to a valid slug and uses the original as the branch:
-
-```bash
-ecluse up feat/add-auth # slug=feat-add-auth, branch=feat/add-auth
-ecluse up # inside a git worktree → auto-detects branch from cwd
-ecluse up # in repo root / main worktree → prompts for branch name
-```
-
-**Auto-register existing worktrees** — running `ecluse up` from inside any git worktree (even one not created by ecluse) auto-detects the branch, registers the session, and starts services. No `--reuse-worktree` flag needed.
-
-**Idempotent up** — `ecluse up` is safe to run on an existing session. It reuses the worktree and slot, checks which services are running, and starts only the ones that are down. Slug is auto-detected from cwd:
-
-```bash
-ecluse up feat-foo # existing session: starts only downed services, skips running ones
-ecluse up # same, slug auto-detected from cwd
-ecluse up --force # kill all running services on allocated ports, restart all
-ecluse up --skip api # skip api; start everything else
-```
-
**Soft restart** — tear down services without losing your worktree, then spin them up fresh:
```bash
@@ -196,121 +222,61 @@ ecluse up feat-foo # resumes at the same slot; ports are re
A stopped session keeps its slot until its worktree is deleted. Running `--keep-worktree` again does not free it. If you hit "all N slots are in use", check `ecluse ls` for `(stopped)` sessions and run `ecluse down --delete-worktree` on the ones you no longer need. When you are done with a branch, use `--delete-worktree` rather than `--keep-worktree`.
-**Port override** — pin a specific service to a port for this session (useful when the auto-assigned port conflicts with something ecluse can't detect):
-
-```bash
-ecluse up feat-foo --port api=4001 --port postgres=5444
-```
-
-## Configuration
-
-`.ecluse.toml` lives at repo root, written by `ecluse init`:
-
-```toml
-mode = "hybrid"
-max_slots = 8
-prefix = "ecluse"
-worktree_dir = ".ecluse/worktrees"
-
-# Env file inheritance — materialized from repo root into each worktree (default: both symlinked)
-# inherit_env = [".env", ".env.local"] # set to [] to opt out
-# inherit_env = [".env", { file = ".env.local", mode = "copy" }] # .env.local copied once,
-# # per-worktree edits stay local
-
-# Port collision handling (both optional)
-# strict_port = false # default: search for a free port on collision
-# port_search_range = 10 # how many alternatives to try (bump by max_slots each time)
-
-# One [[services]] block per service. port = base_port + slot.
-# Native services run on the host; docker services run in containers.
-# The first native entry also sets the PORT alias for framework compatibility.
-# Add command = "..." to have ecluse spawn the process on ecluse up.
-
-[[services]]
-name = "api"
-base_port = 3000 # slot 1 → ECLUSE_API_PORT=3001 + PORT, slot 2 → 3002
-command = "npm run dev" # optional — ecluse spawns this on ecluse up
- # omit for port-allocation-only (start process yourself)
-# port_env = "DJANGO_PORT" # also inject the port under a custom var name
-# port_env = ["DJANGO_PORT", "APP_PORT"] # or multiple aliases
-
-[[services]]
-name = "postgres"
-run = "docker"
-base_port = 5432 # slot 1 → ECLUSE_POSTGRES_PORT=5433, slot 2 → 5434
-
-# Optional: lifecycle hooks — shell commands run in the worktree.
-# Order: pre_up (nothing exists yet, no env) → pre_spawn (env written,
-# services not started) → post_up (everything up) → pre_down (before
-# teardown) → post_down (after teardown).
-[hooks]
-post_up = "npx prisma migrate deploy" # full ECLUSE_* env available
-pre_down = "npx prisma migrate reset --force"
-```
-
-`ecluse init` writes `~/.config/ecluse/config.toml` with the detected process manager (`tmux` if installed, otherwise `nohup`). Services with `command` are spawned on `ecluse up` and killed on `ecluse down`. Set `process_manager = "none"` to opt out.
-
-**`[[services]]` for monorepos and multi-service stacks:** define one block per service. Each gets a stable, collision-free port per slot (`base_port + slot`). Omit `[[services]]` entirely for single-service projects — ecluse falls back to a single `PORT = 3000 + slot`.
+## How ecluse compares
-**Multiple compose files in a monorepo:** point each docker service at its own compose file with the `compose` field (path relative to repo root). Services without `compose` fall back to the root compose file. ecluse generates one overlay per compose file and brings them all up under the same project name.
-
-```toml
-[[services]]
-name = "api"
-base_port = 3000 # native — no compose needed
+There's a healthy ecosystem of tools for running coding agents in parallel. Most of them
+solve *orchestration* — giving each agent a directory, a terminal, a session. ecluse
+solves the layer underneath: giving each agent a **running system** to verify against.
-[[services]]
-name = "postgres"
-run = "docker"
-base_port = 5432 # uses root docker-compose.yml (default)
+| | Worktree per agent | Isolated ports | Isolated services + DB | Clean teardown | Cross-platform |
+|---|---|---|---|---|---|
+| **ecluse** | ✅ | ✅ | ✅ | ✅ | ✅ macOS/Linux/WSL2 |
+| [Superset](https://github.com/superset-sh/superset) | ✅ | ✅ detection | ⚠️ setup scripts | — | macOS (Linux exp.) |
+| [claude-squad](https://github.com/smtg-ai/claude-squad) | ✅ | — | — | — | ✅ |
+| [Gastown](https://github.com/gastownhall/gastown) | ✅ | — | — | — | ✅ |
+| [ccmanager](https://github.com/kbwo/ccmanager) | ✅ | — | — | — | ✅ |
+| [cmux](https://github.com/craigsc/cmux) | ✅ | — | — | — | macOS |
+| plain `git worktree` | ✅ | — | — | — | ✅ |
-[[services]]
-name = "worker-queue"
-run = "docker"
-base_port = 6379
-compose = "services/worker/docker-compose.yml" # its own compose file
-```
+**They're complements, not competitors** — and ecluse is designed to compose with them:
-**Port collision handling** — by default ecluse searches for a free port if the nominal one is taken, trying `nominal + i × max_slots` to stay out of other slots' territory. Set `strict_port = true` to fail immediately instead. Run `ecluse validate` to check your config and preview the full port allocation table.
+- **[Superset](https://github.com/superset-sh/superset)** gives you a polished agentic IDE with a diff viewer, in-app browser, and 100+ parallel agents. It detects ports but leaves service and database provisioning to your own setup scripts. Point those scripts at `ecluse up` and each workspace gets a real stack.
+- **[Gastown](https://github.com/gastownhall/gastown)** coordinates 20–30 agents with git-backed work tracking. ecluse uses tmux as a process manager too, so the two sit naturally side by side.
+- **[cmux](https://github.com/craigsc/cmux)**'s docs recommend pairing it with a worktree manager. That's this.
+- **[nono](https://github.com/nolabs-ai/nono)** confines what an agent is *allowed* to touch, at the kernel. ecluse provisions what an agent *needs to run*. Security and capability are different problems — use both.
-Hooks run as shell commands with all `.env.ecluse` variables pre-loaded (except `pre_up`, which runs before any env exists). `pre_up` and `post_down` run from the repo root, the rest inside the worktree. Use them for migrations, seeding, or teardown. ecluse doesn't manage databases directly — your app's own tooling handles that via `pre_spawn` (if the app needs the schema at boot) or `post_up`. The old `on_up`/`on_down` names still work as deprecated aliases for `pre_up`/`pre_down`.
+If you want a GUI, agent orchestration, or session multiplexing, use one of the above.
+Use ecluse when you need the agents to have somewhere real to verify.
## Known limits
-**Ports are checked, not reserved.** ecluse finds a free port at `ecluse up` time and writes it to `.env.ecluse`. There is a small window between the check and when your process actually binds — if something else takes the port in between, the port in `.env.ecluse` will be wrong. The fix is to tear down and recreate the session:
-
-```bash
-ecluse down feat-foo --keep-worktree # session shows as `feat-foo (stopped)` in `ecluse ls` until the next up
-ecluse up feat-foo # resumes the stopped session and re-probes for free ports
-```
-
-Or pin a specific port manually:
+Three things worth knowing before you rely on this:
-```bash
-ecluse up feat-foo --port api=4001
-```
+**Ports are checked, not reserved.** ecluse finds a free port at `ecluse up` time and
+writes it to `.env.ecluse`. There's a small window before your process binds — if
+something else takes the port in between, the value in `.env.ecluse` is wrong. Fix by
+recreating the session (`ecluse down --keep-worktree && ecluse up`) or pinning
+(`ecluse up feat-foo --port api=4001`).
-**Process management is spawn-and-kill only.** For `host` and `hybrid` modes, services with `command` are spawned on `up` and killed on `down`. ecluse does not monitor or restart crashed processes — `ecluse ls` warns if a nohup-managed process has died. For a fresh start, use `ecluse down feat-foo --keep-worktree && ecluse up feat-foo` (the stopped session is auto-detected on the next `up`).
+**Process management is spawn-and-kill only.** Services with `command` are spawned on
+`up` and killed on `down`. ecluse doesn't monitor or restart crashed processes, though
+`ecluse ls` warns if a nohup-managed process has died.
-**`command` only works if the app reads its port from the environment.** ecluse injects the full `.env.ecluse` contents (all `ECLUSE_*` vars, `PORT`, `port_env` aliases) directly into the spawned process environment — no separate sourcing needed. It cannot help if:
-- The port is **hardcoded in source code** — the app must be changed to read `$PORT`.
-- The port is **set in a config file** (e.g. `config/puma.rb`, `vite.config.ts`, `.env`) — ecluse does not modify app config files; update the config to read from the environment instead.
+**`command` only works if your app reads its port from the environment.** ecluse injects
+the full `.env.ecluse` into the spawned process, but it can't help if the port is
+hardcoded in source or set in a config file. Use `port_env` for custom variable names, or
+pass it through the command (`command = "next dev --port $PORT"`).
-If the app reads a custom env var, use `port_env` to inject it under that name:
-```toml
-port_env = "DJANGO_PORT" # single alias
-port_env = ["DJANGO_PORT", "APP_PORT"] # multiple aliases
-```
-
-If the framework accepts a CLI flag, pass the var through the command:
-```toml
-command = "next dev --port $PORT"
-command = "bundle exec rails s -p $PORT"
-```
+📖 **[Full details and workarounds →](https://ecluse.ai/limits.html)**
## Contributing
-Issues and PRs are welcome. Check the [open issues](https://github.com/hefgi/ecluse/issues) for ideas — good first issues are tagged. If you're adding a new isolation mode or provider, open an issue first to discuss the approach.
+Issues and PRs are welcome — check the [open issues](https://github.com/hefgi/ecluse/issues),
+where good first issues are tagged. If you're adding an isolation mode or an execution
+provider, open an issue first so we can talk through the approach.
+
+Questions and ideas are welcome in
+[Discussions](https://github.com/hefgi/ecluse/discussions).
## License
diff --git a/docs/DEMO-SCRIPT.md b/docs/DEMO-SCRIPT.md
new file mode 100644
index 0000000..4ee9b20
--- /dev/null
+++ b/docs/DEMO-SCRIPT.md
@@ -0,0 +1,208 @@
+# Demo recording scripts
+
+Two assets, two jobs. Record them in this order.
+
+1. **`demo.gif`** — 30s, silent, loops. Goes at the top of the README. Job: make a
+ stranger understand the product in 10 seconds.
+2. **`caught-bug.mp4`** — 90s, narrated. Goes in launch posts, the pitch deck, and
+ the HN first comment. Job: prove agents catch real bugs when they have somewhere
+ to verify.
+
+The GIF sells the feature. The video sells the thesis. You need both, and the video
+is the more valuable of the two.
+
+---
+
+## Setup (once, for both)
+
+### Tooling
+
+```bash
+brew install asciinema agg ffmpeg # asciinema → cast, agg → gif
+# GUI alternative for the GIF: Kap (https://getkap.co) — brew install --cask kap
+```
+
+`asciinema` + `agg` gives crisp, small, text-perfect GIFs and is the right choice for
+a terminal-only demo. Use Kap or QuickTime if you want to show a browser window.
+
+### Terminal appearance
+
+- **Font size 16–18pt.** GIFs get downscaled in the README; small text turns to mush.
+- **Window 100×30 max.** Wider than that and the README render is unreadable.
+- Dark theme, high contrast. Hide any personal information in the prompt.
+- Simplify your prompt for the recording — a long path or git status eats horizontal space:
+ ```bash
+ PS1='$ '
+ ```
+- Clear scrollback right before you start: `clear && printf '\033[3J'`
+
+### Demo repo
+
+Do **not** record against a private or client repo. Build a small purpose-made repo
+so the demo is reproducible and you can re-record when the CLI changes.
+
+Requirements: Next.js or Express API + Postgres + Redis, one migration, a test suite
+that runs in <10s, and one e2e test that hits a real endpoint. Commit a working
+`.ecluse.toml` with `[[services]]` for api/postgres/redis and a `post_up` migration hook.
+
+Suggested: `hefgi/ecluse-demo`, public, MIT. Link it from both assets so people can
+reproduce.
+
+---
+
+## Asset 1 — `demo.gif` (30s, README hero)
+
+**The single point to land:** four agents, four real stacks, one laptop, zero collisions.
+
+### Shot list
+
+| Time | Screen | Purpose |
+|---|---|---|
+| 0–3s | `ecluse up feat-auth` → slot 1, ports listed | Establish the primitive |
+| 3–6s | `ecluse up feat-search` → slot 2, *different* ports | The isolation is visible |
+| 6–9s | `ecluse up fix-cache` → slot 3 | Repetition = pattern |
+| 9–12s | `ecluse up chore-deps` → slot 4 | Four is enough to make the point |
+| 12–18s | `ecluse ls` — four sessions, four port sets, four DBs | **The money shot** |
+| 18–24s | Split panes: 4 agents working simultaneously | Real parallelism |
+| 24–28s | `curl localhost:3001/health` and `:3002/health` both 200 | Proof they're actually running |
+| 28–30s | `ecluse shutdown` → clean | Teardown is part of the value |
+
+### Recording
+
+```bash
+cd ~/dev/ecluse-demo
+git checkout main && ecluse shutdown --delete-worktrees # clean slate
+clear && printf '\033[3J'
+
+asciinema rec demo.cast --cols 100 --rows 30 --idle-time-limit 1.5
+# ... perform the shot list ...
+# exit
+
+agg demo.cast demo.gif \
+ --font-size 18 \
+ --speed 1.4 \
+ --theme monokai \
+ --last-frame-duration 2
+
+# Keep it under ~3 MB or GitHub throttles the render
+ls -lh demo.gif
+```
+
+If it's over 3 MB: raise `--speed`, cut the split-pane section, or drop to 3 agents.
+
+### Rules
+
+- `--idle-time-limit 1.5` collapses your thinking pauses. Don't try to type fast.
+- Never show a failure or a stray error in the GIF. Re-record instead.
+- No typos. Viewers read every character in a loop.
+- Test the render at 800px wide before committing — that's the README width.
+
+### Install
+
+```bash
+cp demo.gif ~/dev/ecluse/docs/demo.gif
+# then remove the TODO(demo) comment in README.md
+```
+
+---
+
+## Asset 2 — `caught-bug.mp4` (90s, the thesis)
+
+**The single point to land:** the agent found its own bug, because it had somewhere
+to run the code. Same model, same prompt — the only difference is the environment.
+
+This is the most valuable asset in the whole GTM plan. It's simultaneously the launch
+video, the pitch-deck demo, and the answer to "why is this not just a worktree manager."
+
+### The bug to plant
+
+Pick a bug that **passes unit tests and fails against a real system**. That asymmetry
+*is* the argument. Good candidates:
+
+1. **N+1 query** — correct results, unit tests green with 3 seeded rows, obvious against a real DB with 10k rows.
+2. **Missing migration** — code references a new column; unit tests mock the DB; the real Postgres 500s.
+3. **Cache-key collision** — two endpoints share a Redis key. Passes in isolation, wrong data with a real Redis.
+4. **Text overflow** — the Claude Code Desktop bug from Theo's video: renders fine in a
+ unit test, visibly truncated in a browser. Best option if you can show a browser.
+
+**Recommended: #2 (missing migration).** Fastest to stage, most legible in 90 seconds,
+and impossible to catch by reading the diff — which is precisely the point.
+
+### Structure
+
+**Act 1 — the counterfactual (0:00–0:30)**
+```
+Agent, no environment. Task: "add a status column to orders and filter by it."
+→ writes the code
+→ runs unit tests: PASS (the DB is mocked)
+→ opens a PR
+Narration: "Tests pass. Code looks right. Nobody ran it. This is now in a human's queue."
+```
+
+**Act 2 — same task, with ecluse (0:30–1:10)**
+```
+ecluse up feat-order-status
+→ post_up runs migrations against its own Postgres
+→ agent writes the same code
+→ unit tests: PASS
+→ agent runs the e2e suite against its own live stack
+→ FAIL: 500 on GET /orders?status=open — column "status" does not exist
+→ agent reads the error, writes the missing migration, re-runs
+→ PASS
+Narration: "Same model. Same prompt. The only difference is that this one had
+somewhere to check its work."
+```
+
+**Act 3 — the scale point (1:10–1:30)**
+```
+ecluse ls → 4 sessions, 4 stacks
+Narration: "Four agents, four stacks, one laptop. Verification runs in parallel with
+generation instead of queueing behind one human."
+```
+
+### Recording
+
+```bash
+# Rehearse until it runs clean end to end. Script the agent prompts
+# in a file so they're identical across takes.
+
+# Terminal-only:
+asciinema rec caught-bug.cast --cols 110 --rows 32
+
+# With a browser (recommended if using bug #4) — Kap or:
+ffmpeg -f avfoundation -i "1" -r 30 -c:v libx264 -crf 18 caught-bug-raw.mp4
+```
+
+Add narration afterwards rather than talking live — you'll re-record less. A clean
+subtitle track works too and travels better on X and LinkedIn, where most people watch
+muted.
+
+### Rules
+
+- **Do not fake the failure.** The bug must genuinely fail e2e and genuinely pass units.
+ Someone will try to reproduce it, and the demo repo makes that easy on purpose.
+- Show the actual error text. `column "status" does not exist` is more persuasive than
+ any claim you could write.
+- Keep the agent's self-correction visible but trimmed — cut long thinking blocks.
+- End on `ecluse ls`. The parallelism is the business case.
+
+### Derivative cuts
+
+From the same footage:
+- **20s** — Act 2 only, for X and LinkedIn (muted-friendly, subtitles required)
+- **10s GIF** — the FAIL → fix → PASS moment, for the blog posts and HN comment
+- **Still frame** of the failing e2e output — the pitch-deck slide
+
+---
+
+## Verification checklist
+
+Before either asset ships:
+
+- [ ] Renders legibly at 800px wide (README) and on a phone
+- [ ] Under 3 MB for the GIF
+- [ ] No personal info: paths, hostnames, tokens, client names, real branch names
+- [ ] No typos, no stray errors, no unexplained pauses
+- [ ] Demo repo is public and linked
+- [ ] The bug in the video genuinely reproduces from a clean clone
+- [ ] Re-watched once at full speed without wincing
diff --git a/docs/src/commands.md b/docs/src/commands.md
index f395363..898c1b5 100644
--- a/docs/src/commands.md
+++ b/docs/src/commands.md
@@ -84,15 +84,17 @@ If a session for the slug already exists in `state.json`, sync refreshes its por
## ecluse down
-Tears down services, frees the slot, and prompts before removing the worktree. Slug is auto-detected from cwd when omitted.
+Tears down services and prompts before removing the worktree. Slug is auto-detected from cwd when omitted.
The worktree prompt always appears; if the worktree has uncommitted changes an extra warning is shown. Pass `--delete-worktree` to skip the prompt and delete, or `--keep-worktree` to skip the prompt and keep.
+Deleting the worktree frees the slot. Keeping it marks the session **stopped**: it shows as ` (stopped)` in `ecluse ls` and keeps its slot, so the next `ecluse up ` (or a bare `ecluse up` from inside the worktree) resumes at the same slot, re-probing ports. `env`, `shell`, `status`, and `sync` refuse a stopped session until it is brought back up. A stopped session holds its slot until its worktree is deleted. If `ecluse up` reports that all slots are in use, run `ecluse down --delete-worktree` on stopped sessions you no longer need.
+
| Flag | Description |
|---|---|
| `--keep-volumes` | Preserve named Docker volumes |
| `--keep-branch` | Keep the git branch (no-op — branches are never deleted by ecluse) |
-| `--keep-worktree` | Skip prompt, keep the worktree on disk |
+| `--keep-worktree` | Skip prompt, keep the worktree on disk; the session is marked stopped and keeps its slot |
| `--delete-worktree` | Skip prompt, delete the worktree (for CI/agents) |
| `--quiet` | Suppress step output |
@@ -103,7 +105,7 @@ Tears down all active sessions at once. Prompts before removing each worktree (s
| Flag | Description |
|---|---|
| `--keep-volumes` | Preserve named Docker volumes |
-| `--keep-worktrees` | Skip prompt, keep all worktrees on disk |
+| `--keep-worktrees` | Skip prompt, keep all worktrees on disk; each session is marked stopped and keeps its slot |
| `--delete-worktrees` | Skip prompt, delete all worktrees (for CI/agents) |
| `--quiet` | Suppress step output |
diff --git a/docs/src/configuration.md b/docs/src/configuration.md
index ae63dad..ae71b30 100644
--- a/docs/src/configuration.md
+++ b/docs/src/configuration.md
@@ -76,6 +76,7 @@ Each `[[services]]` block defines one service. Each gets a stable, collision-fre
| `name` | string | Service name — becomes `ECLUSE__PORT` |
| `base_port` | integer | Port formula: `base_port + slot` |
| `run` | string | `"docker"` to run in a container; omit for native |
+| `compose` | string | Path to the compose file for this service, relative to repo root. Docker services without `compose` use the root compose file. Use in monorepos where services live in separate compose files — ecluse generates one overlay per compose file and brings them all up under the same project name. |
| `command` | string | Shell command ecluse spawns on `ecluse up` and uses to identify the process during `ecluse sync`. Managed by your global `process_manager` setting. Omit to use **port-allocation-only mode** — ecluse allocates the port and injects env vars; you start the process yourself (e.g. via a task runner). |
| `port_env` | string or array | Extra env var names to set to this service's allocated port — accepts a single string or an array |
| `extra_ports` | array | Additional per-slot port allocations. Each entry has `base_port` and `port_env`: `port_env` is set to `base_port + slot` in the process environment and (for docker services) published as a host→container port binding in the compose overlay. Use for debugger ports, auxiliary listeners, and any secondary port a service exposes. Example: `extra_ports = [{ base_port = 9229, port_env = "NODE_INSPECT_PORT" }]` |
@@ -85,6 +86,27 @@ The first native (non-docker) service entry also sets `PORT` for framework compa
Omit `[[services]]` entirely for single-service projects — ecluse falls back to `PORT = 3000 + slot`.
+### Monorepos with multiple compose files
+
+Point each docker service at its own compose file with `compose` (path relative to repo root). Services without `compose` fall back to the root compose file. ecluse generates one overlay per compose file and brings them all up under the same project name.
+
+```toml
+[[services]]
+name = "api"
+base_port = 3000 # native — no compose needed
+
+[[services]]
+name = "postgres"
+run = "docker"
+base_port = 5432 # uses root docker-compose.yml
+
+[[services]]
+name = "worker-queue"
+run = "docker"
+base_port = 6379
+compose = "services/worker/docker-compose.yml" # its own compose file
+```
+
## Global config (`~/.config/ecluse/config.toml`)
Controls how ecluse spawns native service processes. Written by `ecluse init` based on what's installed.
diff --git a/docs/src/introduction.md b/docs/src/introduction.md
index f5e57e0..73c5042 100644
--- a/docs/src/introduction.md
+++ b/docs/src/introduction.md
@@ -1,14 +1,20 @@
# ecluse
-**Ephemeral local environments for coding agents — any stack.**
+**Your coding agent can't verify its work if it has nowhere to run it.**
-Each git worktree gets its own slot — isolated ports, isolated services, isolated data. Works whether your stack runs in Docker, on the host, or a mix. No collisions, clean teardown.
+ecluse gives every agent its own full stack — isolated ports, isolated services, isolated database. Run 8 agents in parallel, each verifying against a real running system. No collisions, clean teardown.
---
-You're running 4 Claude Code sessions in parallel. Each agent finishes its task and wants to verify — run the test suite, spin up the app, hit the endpoints. But port 3000 is taken. Agent 2 kills agent 1's server. Agent 3 waits. The verification loop that was supposed to run in parallel is now sequential. You're paying for 4 agents and getting the throughput of one.
+AI writes the code. It can't check the code.
-ecluse gives each agent its own slot: isolated ports, its own services, its own infra. All 4 agents spin up, verify, and tear down independently. The full AI verification loop — build, migrate, test, e2e — runs in parallel, without collisions, without waiting.
+Writing code needs a context window. *Verifying* code needs a running system — a database to migrate, a port to bind, an endpoint to hit, a browser to click through. An agent with no environment can only do the two weakest kinds of verification: re-read its own diff, and run unit tests that mock away the parts where bugs live.
+
+So you get plausible-looking code that nobody ran. And it lands in a human's review queue, which is the one part of the pipeline that doesn't parallelize.
+
+Now try to fix that by running four agents at once. Each needs Postgres, port 3000, and a migrated schema. Port 3000 is taken. Agent B drops Agent A's database. Agent C waits. The verification loop that was supposed to run in parallel is now sequential, and you're paying for four agents to get the throughput of one.
+
+ecluse gives each agent its own slot: isolated ports, its own services, its own data. All four spin up, run the full loop — build, migrate, test, e2e, hit the real endpoints — and tear down independently.
```
Create worktree → Spin up env → Do work → Verify → PR → Teardown
@@ -31,3 +37,9 @@ The central concept is a **slot** — an integer from 1 to `max_slots`. Every re
- Named volumes: `__`
Three thin mode implementations share this slot primitive. Mode is selected once at `init` time and stored in `.ecluse.toml`.
+
+## Where ecluse sits
+
+There's a healthy ecosystem of tools for running coding agents in parallel — [Superset](https://github.com/superset-sh/superset), [Gastown](https://github.com/gastownhall/gastown), [claude-squad](https://github.com/smtg-ai/claude-squad), [cmux](https://github.com/craigsc/cmux), [ccmanager](https://github.com/kbwo/ccmanager). Most of them solve *orchestration*: giving each agent a directory, a terminal, a session.
+
+ecluse solves the layer underneath — giving each agent a **running system** to verify against. They compose well together: use an orchestrator for the agents, ecluse for the environments those agents need. And [nono](https://github.com/nolabs-ai/nono) confines what an agent is *allowed* to touch at the kernel level, which is a different problem again — use both.