diff --git a/AGENTS.md b/AGENTS.md index 550c934bce..7a4769e558 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,9 +64,9 @@ they set disjoint environment variables, so check which one you are in: See `backend/AGENTS.md` to restart the backend with different cargo features. Under herdr, `herdr --skill` prints a full reference for inspecting and driving panes and agents, and the plugins that -provision worktrees live in `windmill-labs/windmill-herdr` — clone it and run `./setup.sh` to -install them and the keybindings they need. The commands below are for a plain checkout with -nothing running. +provision worktrees live in the private `windmill-labs/windmill-herdr` — clone it and run +`./setup.sh` to install them and the keybindings they need. The commands below are for a plain +checkout with nothing running. - **Backend**: `cargo run` from `backend/` (API at http://localhost:8000) - **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/` (port 3000+) @@ -78,9 +78,8 @@ nothing running. ### Per-worktree ports and database **`.env.local` in the worktree root is the portable answer** — `BACKEND_PORT`, -`FRONTEND_PORT`, `REMOTE`, `DATABASE_URL`, `CARGO_FEATURES`, `WM_DB_NAME`. Both managers write it -through the same helper (`wm_write_env_local` in `scripts/worktree-common.sh`), so it is correct -whichever one you are in, and it is readable despite the name. +`FRONTEND_PORT`, `REMOTE`, `DATABASE_URL`, `CARGO_FEATURES`, `WM_DB_NAME`. Both managers write +those fields, so it is correct whichever one you are in, and it is readable despite the name. webmux additionally writes `$(git rev-parse --git-dir)/webmux/runtime.env`, which every pane sources at startup and which carries the extras `.env.local` lacks: `WEBMUX_*`, `WM_CLONE_DB`, @@ -98,11 +97,12 @@ opt-in via `WM_CLONE_DB` (in `.webmux.yaml` under webmux, or the `windmill.workt The database is named after the **worktree directory, not the branch** (`scripts/worktree-common.sh`): `windmill_` + the directory basename with `-` → `_`, which Postgres then truncates at 63 -characters. Branch `hugo/win-2340-ai-agent-evals-standalone-agent-runs-and-eval-datasets` sits in -a worktree directory named `win-2340-…`, so its database is -`windmill_win_2340_ai_agent_evals_standalone_agent_runs_and_eval` — no `hugo_`, and the tail -chopped. Take `WM_DB_NAME` from `runtime.env` instead of reconstructing the name. Read those, or -discover from what is already running: +characters. The two managers lay worktrees out differently, so the same branch lands in different +directories and therefore different databases: under webmux, branch +`hugo/win-2340-ai-agent-evals-standalone-agent-runs-and-eval-datasets` sits in a directory named +`win-2340-…`, so its database is `windmill_win_2340_ai_agent_evals_standalone_agent_runs_and_eval` +— no `hugo_`, and the tail chopped. Take `WM_DB_NAME` from `.env.local` instead of reconstructing +the name. Read that, or discover from what is already running: ```bash psql postgres://postgres:changeme@localhost:5432/postgres -tAc \ diff --git a/scripts/worktree-common.sh b/scripts/worktree-common.sh index fd741f6312..41d4ed109e 100755 --- a/scripts/worktree-common.sh +++ b/scripts/worktree-common.sh @@ -13,46 +13,8 @@ wm_main_repo_root() { cd "$(git -C "$repo_root" rev-parse --git-common-dir 2>/dev/null)/.." && pwd } -wm_port_in_use() { - lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null -} - -# Prints " " for the lowest free slot. Slots are read from -# each worktree's .env.local rather than from position in `git worktree list`: removing a -# worktree in the middle would otherwise hand its slot to a new one while a live -# neighbour still listens on those ports. Slot 0 belongs to the main checkout. -wm_assign_ports() { - local repo_root=$1 - local slot=${WM_SLOT:-} - local used_slots=() wt_path backend_port frontend_port bp - - if [[ -z "$slot" ]]; then - while IFS= read -r wt_path; do - [[ "$wt_path" == "$repo_root" ]] && continue - [[ -f "$wt_path/.env.local" ]] || continue - bp=$(grep '^BACKEND_PORT=' "$wt_path/.env.local" | cut -d= -f2 || true) - if [[ -n "$bp" && "$bp" -gt 8000 ]]; then - used_slots+=("$(( (bp - 8000) / 10 ))") - fi - done < <(git -C "$repo_root" worktree list --porcelain | sed -n 's/^worktree //p') - - slot=1 - while [[ " ${used_slots[*]:-} " == *" $slot "* ]]; do - ((slot++)) - done - echo "Auto-assigned slot $slot (used: ${used_slots[*]:-none})" >&2 - fi - - backend_port=$((8000 + slot * 10)) - frontend_port=$((3000 + slot * 10)) - - if wm_port_in_use "$backend_port" || wm_port_in_use "$frontend_port"; then - echo "WARNING: slot $slot ports ($backend_port/$frontend_port) already in use" >&2 - fi - - printf '%s %s\n' "$backend_port" "$frontend_port" -} - +# Written by both worktree entry points — `worktree-env` by hand, `post-create.sh` from the +# webmux hook — and read by agents to find their own ports, so the two must not drift. wm_write_env_local() { local repo_root=$1 local backend_port=$2 diff --git a/scripts/worktree-env b/scripts/worktree-env index 390a12fd66..1977eae470 100755 --- a/scripts/worktree-env +++ b/scripts/worktree-env @@ -3,6 +3,43 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh" -read -r backend_port frontend_port < <(wm_assign_ports "$(pwd)") +port_in_use() { + lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null +} + +if [[ -z "${WM_SLOT:-}" ]]; then + # Scan .env.local files of existing worktrees to find which slots are claimed, + # then pick the lowest free slot. This avoids collisions when worktrees are + # removed and new ones created (position-based indexing would re-use slots + # still held by surviving worktrees). + used_slots=() + current_dir="$(pwd)" + while IFS= read -r wt_path; do + [[ "$wt_path" == "$current_dir" ]] && continue + if [[ -f "$wt_path/.env.local" ]]; then + bp=$(grep '^BACKEND_PORT=' "$wt_path/.env.local" | cut -d= -f2 || true) + if [[ -n "$bp" && "$bp" -gt 8000 ]]; then + used_slots+=("$(( (bp - 8000) / 10 ))") + fi + fi + done < <(git worktree list --porcelain | sed -n 's/^worktree //p') + + # Find lowest available slot (slot 0 = 8000/3000 is reserved for main) + WM_SLOT=1 + while [[ " ${used_slots[*]:-} " == *" $WM_SLOT "* ]]; do + ((WM_SLOT++)) + done + echo "Auto-assigned slot $WM_SLOT (used: ${used_slots[*]:-none})" +fi + +# Slot-based: predictable ports for SSH forwarding +# Slot 0 = 8000/3000, slot 1 = 8010/3010, slot 2 = 8020/3020, etc. +backend_port=$((8000 + WM_SLOT * 10)) +frontend_port=$((3000 + WM_SLOT * 10)) + +if port_in_use "$backend_port" || port_in_use "$frontend_port"; then + echo "WARNING: Slot $WM_SLOT ports ($backend_port/$frontend_port) already in use" >&2 +fi + wm_write_env_local "$(pwd)" "$backend_port" "$frontend_port" wm_shared_post_create "$(pwd)"