worktree · diff

git:20260714.2a3c4ed to git:20260926.e57a6cf

27 added, 10 removed. Audit A to A.

---
name: worktree
description: Create an isolated git worktree + branch + port for this session so concurrent kube-coder sessions and builds don't conflict. Use at the START of any change you want isolated, when two sessions touch the same repo, or before running a dev server/build that would collide on ports or output dirs. Also lists and tears down worktrees.
user-invocable: true
allowed-tools: Bash, Read
argument-hint: "[slug] | list | rm <slug> [--force] (default: new, slug from task id)"
---
- # Per-session git worktree (Phase 0)
+ # Per-session git worktree
kube-coder isolates sessions at the *terminal* level (each task gets its own
tmux session + `~/.claude-tasks/<id>/`) but everything still runs in the **same
`/home/dev` filesystem**. So two sessions on the same repo collide three ways:
- **git tree** — branch/checkout/index thrash, a dirty working tree
- **build output** — both write `./dist`, `./build`; Kaniko reads its context from the shared path
- **dev-server ports** — `EADDRINUSE` on `:3000` (ports are per-*pod*, not per-session)
This skill fixes all three for the current session with **no infra change**: it
gives you a dedicated `git worktree` (own checkout + branch) at a path outside
the repo, and leases a free port for any server you start. Worktrees share the
main clone's object store, so they're cheap on disk — only working files are
duplicated.
The helper is `worktree.sh` next to this file. Invoke it as
`bash "$CLAUDE_SKILL_DIR/worktree.sh" …` (or with its absolute path under
`/home/dev/kube-coder/.claude/skills/worktree/`).
## Start an isolated session (the common path)
Run from **inside the repo** you want to work on (`cd` there first). `$ARGUMENTS`
is an optional human slug; if omitted it derives one from the current task id
(`KC_TASK_ID`) or a random token, so it's always unique.
```bash
WT=/home/dev/kube-coder/.claude/skills/worktree/worktree.sh
eval "$(bash "$WT" new "$ARGUMENTS")" # creates worktree+branch+port, sets KC_WT / PORT
cd "$KC_WT" # <-- this cd PERSISTS across your Bash calls
```
After this, **do all work for the change from `$KC_WT`.** The human summary
(path / branch / port / preview URL) is printed to stderr — note the **port**,
because exported env vars do *not* survive between separate Bash tool calls (the
`cd` does). The worktree also records everything in `$KC_WT/.kc-worktree.json`,
so you can re-read the port later:
```bash
jq -r .port "$KC_WT/.kc-worktree.json" # or read .branch / .path
```
### What you get
- worktree dir: `/home/dev/.worktrees/<repo>/<slug>/`
- branch: `kc/<slug>` (branched from current HEAD, or a base-ref: `new <slug> <base>`)
- - leased port in 3000–3999 (skips kube-coder's reserved ports and other leases)
+ - leased port in 3100–3999 (skips kube-coder's reserved ports, other leases and
+ anything already listening)
+ - `new` with a slug that already exists **reuses** that worktree (same path,
+ branch and port) instead of failing
## Running a dev server / preview
Bind the leased port explicitly (env doesn't persist between calls), then open
it through the app-proxy — no port config in the dashboard needed:
```bash
PORT=$(jq -r .port "$KC_WT/.kc-worktree.json")
cd "$KC_WT" && PORT=$PORT npm run dev # or: python -m http.server "$PORT", etc.
# preview at: /api/app-proxy/<port>/
```
## Builds
Because the worktree is its own path, `docker-build` (Kaniko) and local build
output (`./dist`, `./build`) no longer collide with other sessions. Run builds
with the worktree as context — e.g. `docker-build -t img:tag "$KC_WT"`.
## List / tear down
```bash
bash "$WT" list # worktrees + leased ports for the current repo
bash "$WT" rm <slug> # remove the worktree DIR; the branch is KEPT
bash "$WT" rm <slug> --force # also drop uncommitted changes in it
```
`rm` deliberately keeps `kc/<slug>` — that branch *is* your change. Push/PR it as
usual, then delete with the `git branch -D` line `rm` prints. `rm` refuses to
delete a dirty worktree unless you pass `--force`.
## Footguns
1. **Uncommitted work stays behind.** A worktree branches from **HEAD** (the
committed state). Anything uncommitted on your original checkout is *not*
carried in. Commit or stash first if you need it.
2. **`cd` persists, env does not.** In this harness the working directory
survives between Bash calls but exported vars don't. Rely on `cd "$KC_WT"`
and re-read the port from `.kc-worktree.json` when you need it; don't assume
`$PORT` is still set in a later call.
3. **Disk.** Worktrees share `.git`, but a fresh `node_modules` / `.venv` /
build tree per worktree is *not* shared and can fill the 20–50Gi PVC. Keep a
handful live at once; `rm` ones you're done with.
4. **One repo, one clone.** All worktrees for a repo register under that clone's
`.git/worktrees`. Don't `rm -rf` a worktree dir by hand — use `rm` (which runs
`git worktree remove` + `prune`) so git's bookkeeping stays consistent.
5. **Not for the boot-time kube-coder pull.** `start.sh` does
`git pull --ff-only origin main` on `/home/dev/kube-coder` at boot. Working in
a `kc/<slug>` worktree keeps `main` clean so that pull still fast-forwards —
which is the point. Don't check out `main` inside a worktree.
- ## Scope (why this is "Phase 0")
+ ## The dashboard does this too (#701)
- Pure userland, works today, teaches us the naming/cleanup ergonomics. It does
- **not** yet integrate with the Task API or dashboard — the worktree/branch/port
- aren't shown in the task UI and aren't auto-created when a task starts, and the
- port lease is advisory (first-come). Phase 1 moves this into `server.py`'s
- `create_task()` (record worktree in `task.json`, clean up in `delete_task`,
- surface it in `NewTaskForm`/task detail). Sanity-check the helper before relying
- on it:
+ A Build can now be launched isolated without this skill: the **New Build**
+ form's *Isolated worktree* toggle, a Board run's *Isolated worktree per item*,
+ `create_task(isolate=true)` from the dashboard MCP, and `spawn_agent(isolate=
+ true)` for sub-agents. All of them — and this script — share one
+ implementation, `charts/workspace/worktrees.py` (in the pod at
+ `/tmp/browser/worktrees.py`): the same `/home/dev/.worktrees/<repo>/<slug>`
+ layout, the same `.kc-worktree.json` manifest, the same port lease and the same
+ lock. So a worktree made here shows up in **Settings → Worktrees**, counts
+ toward `KC_MAX_WORKTREES`, and its port is never handed out twice.
+ When `/tmp/browser/worktrees.py` is present this script delegates to it
+ (override with `KC_WORKTREES_PY`); otherwise it falls back to its own bash
+ implementation, which takes the same `flock` and writes the same manifest.
+
+ A worktree you make by hand has no owning Build, so the dashboard's cleanup
+ sweep only removes it once it is pristine (nothing committed, nothing dirty)
+ **and** older than `KC_WORKTREE_GC_DAYS` (7). Anything with work in it is yours
+ to remove.
+
+ Sanity-check the helper before relying on it:
+
```bash
bash -n /home/dev/kube-coder/.claude/skills/worktree/worktree.sh
+ python3 /tmp/browser/worktrees.py --api-version
```