Skip to content
Open
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Changed
- `ecluse down --keep-worktree` (and `shutdown --keep-worktrees`) no longer drops the session from state — it marks it `Stopped`, keeping the slot reserved so the next `ecluse up` resumes at the same slot instead of allocating a new one (which would change all ports). `ecluse up <slug>` auto-detects a stopped session and resumes it (no `--reuse-worktree` needed), re-probing for free ports. `ecluse ls` shows `<slug> (stopped)`. While stopped, `ecluse env`/`shell`/`status`/`sync` error with a hint to run `ecluse up`, instead of surfacing stale, no-longer-running port values.

### Fixed
- `ecluse down` in tmux mode now kills the entire pane process group, not just the pane's foreground shell. Previously, multi-level child chains (`sh → pnpm → node → vite`, plus anything that calls `setsid()` like Cloudflare workerd) survived as orphans adopted by `launchd`/`init`, holding their ports indefinitely. Each orphan held 4-8 ports; after a few `up`/`down` cycles the next `ecluse up` would silently land on a port already held by a zombie, serving a different worktree's content. The same TERM→KILL grace pattern that was applied to the nohup path in PR #18 now applies to tmux. (#30)
- `ecluse flush` now sweeps every process whose cwd is inside a worktree (`lsof +d <worktree>`) AND every listener on a configured port (`base_port + slot*slot_stride` and `extra_ports[].base_port + slot*slot_stride` across all `max_slots`), killing each with TERM→KILL grace. The flush confirmation prompt warns that editors/shells with files open in worktrees will be killed; `--yes` bypass for CI is unchanged. (#30)
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ ecluse up --skip api # skip api; start everything else
**Soft restart** — tear down services without losing your worktree, then spin them up fresh:

```bash
ecluse down feat-foo --keep-worktree # services torn down, worktree + branch kept
ecluse up feat-foo --reuse-worktree # new slot, fresh ports, worktree reused
ecluse down feat-foo --keep-worktree # services torn down, worktree + branch kept, slot reserved
ecluse up feat-foo # resumes at the same slot; ports are re-probed (stopped session auto-detected)
```

**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):
Expand Down Expand Up @@ -260,8 +260,8 @@ Hooks run as shell commands inside the worktree directory with all `.env.ecluse`
**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
ecluse up feat-foo --reuse-worktree
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:
Expand All @@ -270,7 +270,7 @@ Or pin a specific port manually:
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 --reuse-worktree`.
**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`).

**`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`.
Expand Down
12 changes: 6 additions & 6 deletions skills/ecluse/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ The canonical fix for misbehaving services is `ecluse down` + `ecluse up`, not `

```bash
ecluse down <your-slug> --keep-worktree
ecluse up <your-slug> --reuse-worktree
ecluse up <your-slug> # stopped session auto-detected; resumes at the same slot
```

This tears down only **your** services and respawns them with the right env. Idempotent, safe under parallel sessions, never touches another agent's work. Reach for this 95% of the time.
Expand Down Expand Up @@ -573,7 +573,7 @@ Persistent conflict: change `base_port` in the relevant `[[services]]` block, or
ecluse down <your-slug> --keep-worktree

# 2. Restart with ecluse spawning the services directly
ecluse up <your-slug> --reuse-worktree
ecluse up <your-slug> # stopped session auto-detected; resumes at the same slot

# 3. Verify ports are correct
ecluse status <your-slug>
Expand Down Expand Up @@ -766,7 +766,7 @@ inspect it with `ecluse whose-pid <pid>` and kill it manually if intended.

What ecluse intentionally does not do in v0. These are design decisions, not bugs.

- **Ports are checked, not reserved** — ecluse finds a free port at `up` time and writes it to `.env.ecluse`. There is a small window between that check and when your process actually binds. If another process takes the port in between, the value in `.env.ecluse` will be wrong. Fix: `ecluse down feat-foo --keep-worktree` then `ecluse up feat-foo --reuse-worktree`, or pin a specific port with `--port name=value`.
- **Ports are checked, not reserved** — ecluse finds a free port at `up` time and writes it to `.env.ecluse`. There is a small window between that check and when your process actually binds. If another process takes the port in between, the value in `.env.ecluse` will be wrong. Fix: `ecluse down feat-foo --keep-worktree` then `ecluse up feat-foo` (stopped session auto-detected, re-probes for a free port), or pin a specific port with `--port name=value`.
- **No process lifecycle management beyond spawn/kill** — ecluse can spawn native services on `up` (via `command` + `process_manager`) and kill them on `down`, but cannot auto-restart a crashed process. If a service dies, `ecluse up` (idempotent — slug auto-detected from cwd) starts only the downed services. `ecluse up --force` kills everything on allocated ports and restarts fresh. `ecluse ls` and `ecluse env` warn about dead nohup processes.
- **`command` requires the app to expose a port entry point** — ecluse injects the full `.env.ecluse` contents (`PORT`, `ECLUSE_SLOT`, `ECLUSE_SLUG`, `ECLUSE_MODE`, all `ECLUSE_<NAME>_PORT` vars, and any `port_env` aliases) directly into the spawned process environment — no separate sourcing step needed. If the port is hardcoded or set in a config file, resolve it via `.ecluse.toml` first: pass it as a CLI flag (`command = "vite --port $ECLUSE_WEB_PORT"`), or use `port_env` to inject it under the var name the app already reads. Modifying app source code is the last resort — see [Port wiring](#port-wiring--exhaust-eclusetoml-options-before-touching-app-code) above.
- **Mode is set at `init`, not re-detected on `up`** — to change: `ecluse init --mode <new>`
Expand Down Expand Up @@ -913,11 +913,11 @@ ecluse status --quiet # exit-code only (0 = all up, 1 = any down)
**Soft restart** — tear down services without losing the git worktree, then spin up fresh:

```bash
ecluse down feat-foo --keep-worktree # stops services, removes session from state, keeps worktree on disk
ecluse up feat-foo --reuse-worktree # allocates a new slot, skips worktree creation
ecluse down feat-foo --keep-worktree # stops services, marks the session Stopped (slot reserved), keeps worktree on disk
ecluse up feat-foo # resumes at the same slot (stopped session auto-detected; --reuse-worktree not needed)
```

Use this when a service failed to bind after `up` and you want a fresh start without losing changes in the worktree.
Use this when a service failed to bind after `up` and you want a fresh start without losing changes in the worktree. While stopped, `ecluse env`, `ecluse shell`, and `ecluse status` on that session error with a hint to run `ecluse up` — they will not surface stale, no-longer-running ports.

**Port override** — pin a service to a specific port for this session:

Expand Down
Loading
Loading