Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion skills/worktree-isolation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,12 @@ Runtime hazards when several members run at once:
secrets) is never created in a fresh worktree, but the services read it - so a
new member boots mis-configured until its config is seeded from the repo's
main checkout. Provide an idempotent seed step and run it before starting the
stack.
stack. Write those files with an explicit `0600` mode rather than a plain
copy, which inherits the caller's umask and can leave secrets world-readable.
- **Dependencies not carried either.** A worktree has no `node_modules` /
`.venv`, so a new member starts and dies. Declare per role the dep dir and the
command that builds it, and build at creation time - creation should leave a
runnable tree, not one that fails on first run.
- **Port collisions.** Give each group a port slot and derive every service's
port from it (e.g. `base + 10*slot`). If a service hardcodes its port in
source instead of reading an env var, the slot must be applied on the launch
Expand Down Expand Up @@ -263,6 +268,8 @@ be a shell launcher, not a startup hook.
| No package.json/Cargo.toml | Skip dependency install |
| Feature spans multiple repos | One worktree per repo, bind them in a group registry |
| Branch names differ per repo | Bind explicitly - never auto-match by branch |
| New member starts then dies | Its config and dep dir were never seeded - both are gitignored |
| Seeding config with secrets | Write with an explicit `0600` mode, not a umask-dependent copy |

## Cleanup

Expand Down
72 changes: 54 additions & 18 deletions skills/worktree-isolation/references/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,45 @@ declare -A WTG_PORT=( # role -> base port (final = base + 1
[api]=8080 [web]=3000 )
declare -A WTG_RUN=( # role -> launch command written into dev.sh
[api]='SERVER_PORT=%PORT% go run ./cmd/server'
[web]='VITE_API_URL=http://localhost:%PRIMARY_PORT% npm run dev -- --port %PORT%' )
[web]='VITE_API_URL=http://localhost:%API_PORT% npm run dev -- --port %PORT%' )
declare -A WTG_WORKDIR=( # role -> run subdir inside the worktree (optional)
[web]='apps/web' )
declare -A WTG_ENV=( # role -> env files to seed (space-sep); default ".env"
[web]="apps/web/.env" )

WTG_PRIMARY_ROLE="api" # the OAuth-bearing role; the tree `wtg go` opens in
declare -A WTG_DEPS=( # role -> dep dir a fresh worktree lacks
[web]='node_modules' )
declare -A WTG_SETUP=( # role -> how to build it, run at the worktree root
[web]='npm install' )

WTG_PRIMARY_ROLE="api" # the tree `wtg go` opens in (siblings are --add-dir'd)
WTG_API_ROLE="api" # backend role %API_PORT% resolves to (what a frontend calls)
WTG_OAUTH_ROLES="api" # roles that get the OAuth port-swap prelude in dev.sh
WTG_OAUTH_PORT=8080 # port your OAuth redirect_uris are registered against
WTG_OAUTH_VARS="PUBLIC_BASE_URL OAUTH_REDIRECT_URL" # host:port URLs to port-swap for the primary
WTG_OAUTH_VARS="PUBLIC_BASE_URL OAUTH_REDIRECT_URL" # host:port URLs to port-swap
WTG_WORKROOT="$HOME/work" # where worktrees are created
WTG_BASE_BRANCH="origin/main" # branch new worktrees fork from
WTG_BASE_BRANCH="main" # start-point for new worktrees; per-run: --base <ref>
WTG_LAUNCHER_ARGS="" # extra argv for `wtg go` (word-split)
```

Run-command placeholders, substituted when `dev.sh` is generated:

| placeholder | becomes |
|---|---|
| `%PORT%` | this member's port for the group's slot |
| `%PRIMARY_PORT%` | the primary role's port (e.g. wire a frontend to its backend) |
| `%API_PORT%` | `WTG_API_ROLE`'s port (e.g. wire a frontend to its backend) |
| `%OAUTH_PORT%` | the OAuth-registered port |

Only `WTG_PRIMARY_ROLE` receives the OAuth port-swap prelude and is the tree
`wtg go` opens in; every other role simply runs its command.
`WTG_PRIMARY_ROLE`, `WTG_API_ROLE` and `WTG_OAUTH_ROLES` are often the same role.
Split them when they differ - e.g. the agent opens in a worker service while the
UI calls a separate api, and only the api carries the OAuth redirect.

`WTG_BASE_BRANCH` takes a bare name: repos are fetched first and the name
resolves to `origin/<name>`, so a new worktree is always cut from the latest
remote tip rather than a stale local branch. `origin/x`, a tag or a sha are used
as given, and `--base <ref>` overrides per run.

> Seeded config is written with `install -m 600`, not `cp` - these files carry
> live secrets, and `cp` would take the caller's umask and leave them 0644.

## 4. Registry

Expand All @@ -76,19 +94,30 @@ source of truth; safe to hand-edit. Back it up if you like; it is small.
Using the example roles `api` / `web` from the config above (substitute yours):

```bash
wtg new checkout api web # a worktree + feat/checkout branch per repo, bound, with dev.sh
wtg env checkout # seed each repo's base env into the new worktrees
wtg new checkout api web # worktree + feat/checkout branch per repo, bound, dev.sh,
# .env seeded and deps installed - a RUNNABLE tree
wtg up checkout # run the whole stack (tmux windows, or background)
wtg st checkout # branch / dirty / ahead-behind / port per member
wtg st checkout # full table + a warnings block with the fix for each
wtg go checkout # open one agent session across the worktrees
wtg down checkout # stop the stack
```

`new` and `add` seed config and build deps for you, so there is no separate
setup step; `--no-env` / `--no-deps` opt out, and `wtg env` / `wtg deps` re-run
either on demand (`--force` to overwrite / rebuild).

Second concurrent feature gets the next port slot automatically:

```bash
wtg new billing api web # slot 1 -> api :8090, web :3010
wtg claim-oauth billing # when you need billing's OAuth: move it onto the primary port
wtg claim-oauth billing # when you need billing's OAuth: move it onto the registered port
```

Cut from somewhere other than the default base:

```bash
wtg new hotfix --base release/2.1 api # bare name resolves to origin/release/2.1
wtg add hotfix web # grow an existing group, reusing its slot
```

## 6. Optional: Claude Code statusline integration
Expand Down Expand Up @@ -122,16 +151,23 @@ exit; it never touches your shell prompt.

| command | does |
|---|---|
| `wtg new <group> <role...>` | create a new group: worktree + branch + bind + dev.sh per role |
| `wtg add <group> <role...>` | add member(s) to an existing group (reuses its slot) |
| `wtg <group> [role]` | bind the current worktree into a group |
| `wtg env <group> [--force]` | seed base `.env` into the group's worktrees |
| `wtg new <group> [--base <ref>] [--no-env] [--no-deps] <role...>` | create a group: worktree + branch + bind + dev.sh + env + deps per role |
| `wtg add <group> [--base <ref>] <role...>` | add member(s) to an existing group (reuses its slot) |
| `wtg <group> [role]` | bind the current worktree into an **existing** group |
| `wtg bind <group> [role]` | same, but may create a new group (explicit, so typos cannot) |
| `wtg env <group> [--force]` | (re)seed base config into the group's worktrees |
| `wtg deps <group> [--force]` | (re)build each member's dep dir |
| `wtg up <group>` / `wtg down <group>` | run / stop the whole stack |
| `wtg st [group]` | cross-repo branch / dirty / ahead-behind / port |
| `wtg st [group] [--fetch]` | full table: branch, clean/DIRTY, upstream, behind base, port, env, deps + warnings |
| `wtg go [group]` | launch one agent session across the set (menu if no group) |
| `wtg claim-oauth <group>` | move a group onto the OAuth-registered port |
| `wtg rm` | unbind the current worktree (keeps the checkout) |
| `wtg` / `wtg ls` | current group status / list all groups |
| `wtg` / `wtg ls` / `wtg help` | current group status / list all groups / command reference |

A bare `wtg <name>` only binds into a group that already **exists**. That guard
matters: without it a typo like `wtg downfoo` silently creates a group `downfoo`,
rebinds the current tree away from its real group, and overwrites its `dev.sh`.
Use `wtg bind` to create one deliberately.

## 8. Testing your config safely

Expand Down
26 changes: 24 additions & 2 deletions skills/worktree-isolation/references/multi-repo-feature-groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,22 @@ subdirectory.

| verb | does |
|---|---|
| `new <group> <role...>` | create a worktree + feature branch per role, bind them, assign the next free slot, generate a run script each |
| `new <group> <role...>` | create a worktree + feature branch per role, bind them, assign the next free slot, generate a run script, seed config, build deps |
| `add <group> <role...>` | add member(s) to an existing group, reusing its slot |
| `bind <group> [role]` | fold the current worktree in, inheriting the group's slot |
| `env <group>` | seed gitignored config (`.env`, secrets) from each repo's main checkout into the worktrees |
| `deps <group>` | build each member's dependency dir (see "Dependencies" below) |
| `up` / `down <group>` | run / stop every member's run script as one stack |
| `st <group>` | cross-repo status: branch, dirty, ahead/behind, port per member |
| `st <group>` | cross-repo status: branch, dirty, ahead/behind, behind-base, port, config, deps |
| `rm` | unbind the current worktree (keep the checkout) |
| `claim-oauth <group>` | move a group onto the OAuth-registered port (see below) |
| `go [group]` | open one agent/editor session across the whole set |

Bind-by-bare-name must resolve to an **existing** group. Otherwise a mistyped
verb (`wtg downfoo`) silently creates a group, rebinds the current tree away from
its real one, and overwrites its run script. Creating a group from the current
tree should be its own explicit verb.

## Port slots (running several groups at once)

Give each group an integer `slot`. Derive every service's port from it so groups
Expand Down Expand Up @@ -91,6 +97,22 @@ local secrets) is never created in a fresh worktree, but services read it - so a
new member boots mis-configured until you seed its config from the repo's main
checkout. Make the seed step idempotent (skip existing files unless forced).

Write seeded config with an explicit mode (`install -m 600`), not a plain copy.
These files hold live credentials, and a copy inherits the caller's umask - on a
common `0002` umask that leaves secrets world-readable at `0644`.

## Dependencies are not carried either

The same gap applies to build output: a worktree has no `node_modules`,
`.venv`, or equivalent, so a freshly created member starts and immediately dies.
Declare per role both the directory that must exist and the command that builds
it, then run it at creation time. Skip a dir that is already present unless a
force flag is passed, and log failures with the tail of the build log - a silent
dependency failure looks identical to a broken run script later.

Creation should therefore leave a **runnable** tree: worktree + branch + run
script + config + dependencies. Anything less defers a failure to first run.

## Grouped session (working the set as one)

Open one agent/editor session in the group's primary worktree and grant it
Expand Down
Loading
Loading