From b3ba04ec819b9f5f2295d1f8a6efa4effe3231b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 2 Sep 2026 21:21:23 +0100 Subject: [PATCH 1/5] docs: lead with the verification problem, add comparison table The README opened with an infrastructure description ("ephemeral local environments") which reads as a category nobody searches for. Lead with the problem instead: agents can't verify work they can't run. - Rewrite README and docs intro around the verification framing - Add an honest comparison table vs Superset/claude-squad/Gastown/ccmanager/cmux, positioning them as complements rather than competitors - Move the full config reference to the docs site; keep a typical example inline - Document the previously undocumented per-service `compose` field, plus a monorepo example, so nothing is lost from the README slim-down - Add GIF placeholder above the fold --- README.md | 255 ++++++++++++++++---------------------- docs/src/configuration.md | 22 ++++ docs/src/introduction.md | 20 ++- 3 files changed, 148 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index edeb22f..2d62a4c 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ 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. [![CI](https://github.com/hefgi/ecluse/actions/workflows/ci.yml/badge.svg)](https://github.com/hefgi/ecluse/actions/workflows/ci.yml) [![Crates.io](https://img.shields.io/crates/v/ecluse.svg)](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://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) [![Docs](https://img.shields.io/badge/docs-ecluse.ai-blue)](https://ecluse.ai/) + +Four coding agents, four isolated stacks, one laptop + --- -**Built for coding agents running tasks in parallel.** +**Works with every agent that runs in a terminal.** ![Claude Code](https://img.shields.io/badge/Claude_Code-d97706?style=flat-square) ![Cursor](https://img.shields.io/badge/Cursor-000?style=flat-square) @@ -23,21 +28,37 @@ Works whether your stack runs in Docker, on the host, or a mix. No collisions, c ![OpenCode](https://img.shields.io/badge/OpenCode-6366f1?style=flat-square) ![Pi](https://img.shields.io/badge/Pi-333?style=flat-square) -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 @@ -143,12 +187,12 @@ ecluse validate [--ports] ecluse status [] [--json] [--quiet] ``` -**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 +212,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 +221,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/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. From 954f50c7bbcf47cea7df8bf9426dd722b7f8f3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 2 Sep 2026 21:22:36 +0100 Subject: [PATCH 2/5] docs: add recording scripts for the demo GIF and caught-bug video MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two assets: a 30s silent GIF for the README hero, and a 90s narrated video showing an agent catching its own bug because it had an environment to verify in. Includes tooling setup, shot lists, the bug to plant (missing migration — passes unit tests, fails e2e), and a verification checklist. --- docs/DEMO-SCRIPT.md | 208 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 docs/DEMO-SCRIPT.md 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 From 8730cc00dd65d645d73eaf62f422cf0c1fc3077f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 4 Sep 2026 16:37:32 +0100 Subject: [PATCH 3/5] docs(readme): add missing whose-pid to the command list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2d62a4c..655cd6e 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,7 @@ ecluse flush [--yes] ecluse ls [--json] ecluse validate [--ports] ecluse status [] [--json] [--quiet] +ecluse whose-pid [--json] ``` `ecluse up` is idempotent, auto-detects the slug from your cwd, auto-registers existing From 7fef542f43633b06b6bddc926f90b84f50d13e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 4 Sep 2026 16:39:33 +0100 Subject: [PATCH 4/5] chore: add issue templates for bugs and stack-support reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detection getting a repo wrong is a bug, not user error — the stack_support template makes those reports easy to file and easy to act on. Points questions at Discussions rather than issues. --- .github/ISSUE_TEMPLATE/bug_report.yml | 78 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/ISSUE_TEMPLATE/stack_support.yml | 45 ++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/stack_support.yml 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 From ced0975c58347b1a91ee808972460c21670011a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 2 Oct 2026 16:04:25 +0100 Subject: [PATCH 5/5] docs(commands): explain stopped sessions on down --keep-worktree --- docs/src/commands.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 |