--- name: gwm description: Manage git worktrees across any repository with the `gwm` Rust binary (CLI + ratatui TUI). Use when the user asks to create / list / remove / bootstrap / switch / link worktrees, run a command across worktrees (`gwm exec`) or reclaim build artifacts (`gwm clean`), materialise a PR into a worktree (`gwm review`), drive a multi-repo workspace (`--workspace`), run the JSON daemon / statusline (`gwm daemon`, `gwm statusline`), list or pin AI-agent sessions per worktree (`gwm agents`, Claude Code / Codex / opencode / Mistral Vibe detection), seed config from a preset (`gwm init --preset`), diagnose with `gwm doctor`, drive tmux/zellij, or wire `gcd` via `gwm shell-init`. Also covers multi-forge (#419): GitHub via `gh` **or** GitLab via `glab`, the `forge` / `[forge_hosts]` keys, and `gwm trust add`. Triggers on `gwm`, `gwq`, `git worktree`, `.gwm.toml`, `forge`, `forge_hosts`, `glab`, `gwm trust add`, GitLab MR, `gwm create`, `gwm list`, `gwm exec`, `gwm clean`, `gwm review`, `gwm daemon`, `gwm statusline`, `gwm agents`, `gwm note`, `gwm bootstrap`, `gwm doctor`, `gwm switch`, `gwm tmux`, `gwm zellij`, `gwm link`, `gwm status`, `gwm shell-init`, `gcd`, `feat/#`, `fix/#`, GitHub issue/PR linking on a worktree. allowed-tools: Bash, Read, Edit, Write --- # gwm: git worktree manager (Rust CLI + TUI) Single-binary Rust tool that manages git worktrees with `libgit2`, a ratatui TUI, a declarative per-repo bootstrap (`.gwm.toml`), GitHub issue/PR linking, multiplexer hand-off (tmux / zellij), and a doctor command. Replaces project-specific bash wrappers with one portable binary that works in any git repo. Source: https://github.com/kbrdn1/gwm-cli, latest stable **`1.9.0`** (machine contracts frozen since 1.0.0, MSRV 1.95). **1.9.0 is the licensing line, and it changes nothing you call.** gwm is dual-licensed **MIT OR Apache-2.0**: MIT stays in full under `LICENSE-MIT`, `LICENSE-APACHE` joins it for its §3 patent grant, and a user picks either without having to say which. Both texts travel in every artefact (release archives, `.deb`, `.rpm`, AUR, the published crate). The libraries statically compiled in ship their notices under `third-party/`: libgit2 (GPLv2 with a linking exception, which is conditional on the notice travelling) and zlib. Behaviour-wise the line is what gwm prints: 165 em dashes rewritten across the binary's string literals and 49 more in `gwm --help`, so an error message or a completion script no longer carries a character the docs dropped; the Settings panel sizes to its active tab instead of a flat 60% of the frame; and a worktree path is spelled one way everywhere it is printed in full, `~` for `$HOME`, in the TUI header, the table and the sidebar alike (`gwm path --format=json` stays absolute). **1.8.0 is the density line, and it changes a default.** The TUI is **compact** unless told otherwise: panes and sidebar sections carry no box rule, each is delimited by a filled one-line header (uppercase title with its bracketed key, counter pinned right on the same line), a `muted` rule separates the two panes, and the worktrees pane sizes itself to its row count instead of reserving a share of the split. That is two rows and two columns back per section. Focus reads on the header, which takes the `focus` role and the `selection_bg` fill, so the border is no longer the focus cue. `[tui] layout = "bordered"` restores gwm's pre-1.8 frames verbatim and is left untouched by the compact refinements. Two knobs ride along, both applying under either layout: `[tui] status_one_line` (**on by default**) folds the sidebar Status block onto one row joined by ` · `, leaving the path its own row; `[tui] dim_unfocused` (off) dims the inactive pane's body via the terminal's `DIM` attribute. A new theme role, `section_bg`, paints the compact header fill: an indexed colour, not a translucent white, so the mode holds up without truecolor, and distinct from `selection_bg` in every preset because that difference is what separates a focused header from an unfocused one. Overlays and modals keep their border either way, and their width now comes from one policy (`modal_width(term_width, pct, min, max)`) rather than the four it used to be, two of which got narrower as the terminal widened. **Per-worktree notes, shipped in 1.7.0 (#515).** `N` opens the selected worktree's note in an editable modal (`Esc` writes and closes; `Ctrl+e` hands the same file to `$EDITOR`: `editor_cmd`, then `$EDITOR`, then `vi`, the handoff `o` uses in `mode = "editor"`), and the table carries a binary markdown marker (`nf-oct-markdown`, captioning its own column since #595) on the rows that have one. Notes are plain Markdown at `/.git/gwm/notes/.md`: greppable with gwm shut down, never committed, and they survive `gwm remove`, which is why they live in the main checkout rather than in the worktree. `gwm note show [slug]` prints one and exits 1 when there is none; the `--format=json` rows carry it in an additive `note` field. The note is keyed on the branch, so a detached row cannot carry one (and says so), a rename moves the file, and `gwm doctor` reports a note whose branch is gone (not `gwm clean`, whose `--yes` only ever removes directories git already ignores). Presence means non-blank, since `vi` over an empty buffer writes one byte. `N` was unbound before, so a `.gwm.toml` binding a chord starting with `N` is now a prefix conflict refused at load time. **Security, 1.6.0 (GHSA-fffq-vg6f-gxqm, high).** Every version up to and including 1.5.0 expanded lifecycle-hook placeholders into `sh -c` unescaped, so a branch name carrying `;` / `|` / `$` / backticks (all legal in a git ref, and a branch can arrive from someone else's push) ran arbitrary commands as the user, with no trust prompt in the path: the TOFU gate covers the repo's hooks, never the branch name flowing into them. Values are shell-escaped on expansion in 1.6.0, and hooks also get their context as `GWM_*` environment variables, which need no quoting at all. No backport: upgrade. **Free-form naming, shipped in 1.6.0 (#416 / #418).** `gwm create --name spike-redis` skips the ` ` triple; the name becomes the branch verbatim, so `branch_pattern` / `path_pattern` do not apply and branch-name-derived features (issue auto-linking, gitmoji) stay inactive on it, with `gwm link` as the way back. The TUI create and rename forms present the fields the repo's own patterns ask for, in pattern order, instead of the canonical triple, and move between the structured and free-form shapes in both directions. **Multi-forge, shipped in 1.5.0 (#419).** Issue / PR lookups go through a `Forge` trait with two backends: GitHub via `gh`, GitLab via `glab`. `forge = "github" | "gitlab"` in `.gwm.toml` names the backend; `[forge_hosts]` in the **user-level** config authorises a self-hosted host; `gwm trust add` is the per-repo alternative. Worktrees, bootstrap, branch naming and link storage are forge-neutral: only the network layer knows which forge is in play. See [Forge selection](#forge-selection-github--gitlab). Shipped since 1.0.0: **per-worktree notes** (`N`, #515), the **rich PR / issue view** (`I`, #420 + its inline review comments #528), **container execution** on an `exec` profile (#421), **multi-row selection** with a batch delete (`Space` + `d`, #484), the **`symfony` preset** (#392), and a TUI delete that finally runs the remove hooks and records the undo journal (#521 / #531), all in 1.7.0; **free-form naming** in 1.6.0 (#416 / #418, above) alongside its security fix; **multi-forge** in 1.5.0 (#419, above); the **help overlay** (`?`, complete by construction: every `Action` must appear, #453) and a trio of TUI polish items in 1.4.0; **agent session detection** in 1.3.0 (#408): per-worktree AI-agent sessions (Claude Code, Codex, opencode, Mistral Vibe) read from on-disk artefacts, no process scanning, surfaced as `gwm agents [attach|detach]`, a conditional AGENT column, an `a` overlay, a pinned-only Agents sidebar pane and multi-pins in branch config (`gwm-agent-pin`); plus the **Windows daemon** over a named pipe (#439). 1.0.0 shipped: `gwm exec` / `gwm clean` (#313), `gwm review ` (#308), the JSON API + `gwm daemon` + `gwm statusline` (#38 / #309), `gwm init --preset` (#37), multi-repo `--workspace` (#36), embedded PTY overlays (#35), a rebindable keymap incl. contextual modal keys + a live Settings panel with a Keys tab (#290 / #219 / #294), a Working Tree pane and a current-PR CI indicator (#300 / #299). ## When to use this skill - User runs or asks about any `gwm `: `init` (incl. `--preset`), `list` (incl. `--format json`), `create`, `remove`, `path` / `cd`, `bootstrap`, `sync`, `prune`, `doctor`, `types`, `completions`, `shell-init`, `switch` (alias `s`), `tmux`, `zellij`, `link`, `unlink`, `open`, `status`, `exec`, `clean`, `review`, `daemon`, `statusline`, `agents`, `new`, `pr`, `config`, `history`, `undo`, `trust`, `labels`, `milestones`, `hooks`, `commit-prefix`, `aliases`, `theme`, `tui keys`. `trust` takes `add | list | revoke | show`. - User wants to **fan a command out across worktrees** (`gwm exec -- `) or **reclaim build artifacts** (`gwm clean`). - User wants to **materialise a GitHub PR into a worktree** to review/test it (`gwm review `, cross-fork, safe-by-default). - User wants a **multi-repo workspace** (`gwm --workspace `), the **JSON API / daemon** (`--format=json`, `gwm daemon`, `gwm statusline` for shell prompts), or **stack presets** (`gwm init --preset laravel|node|rust|go|python-uv|generic`). - User asks **which AI agents are working where**: `gwm agents` (per-worktree sessions + an `unmatched` section), `gwm agents attach ` / `detach []` (multi-pins), the TUI `a` overlay (select `j`/`k`, pin `a`, unpin `d`, attach-by-id `i`), the pinned-only `Agents` sidebar pane, or the `GWM_AGENTS_HOME` test seam. - User opens the TUI by running `gwm` alone in a repo, or the picker via `gwm switch` / `gwm s`. - User wants to **delete several worktrees at once**: `Space` to mark rows in the TUI then `d` (#484), or `gwm remove a b c` from the shell. - User wants to **write down where they were** on a worktree: `N` in the TUI opens its note in an editable modal (#515), `gwm note show` reads it back, `gwm doctor` reports the ones whose branch is gone. - User asks about the redesigned TUI: PTY overlays (`l`/`L` lazygit, `r`/`R` review, `o`/`O` shell, #35), the Settings panel (`4`) and its live-editable Keys tab (#294), the Command Logs modal (`3`), the Working Tree file-explorer pane (#300), the current-PR CI indicator (#299), `[tui.keys.modal.]` rebindable overlay keys (#219), and `[tui.macro1]`/`[tui.macro2]` (`h`/`H`). - User mentions `.gwm.toml` (per-repo config) or any of its sections: `[worktree]`, `[doctor]`, `[tui]`, `[tui.open]`, `[git_tui]`, `[review]`, `[[bootstrap.copy]]`, `[[bootstrap.guard]]`, `[[bootstrap.no_symlink]]`, `[[bootstrap.command]]`, `[bootstrap.fallback.*]`. - User asks about composable `when` predicates (`file_exists:`, `cmd_exists:`, `env_set:`, `env_eq:`, `glob_exists:`) and the `!` / `&&` / `||` operators. - User wants to migrate a `tools/worktree-manager.sh` or `gwq`-based workflow to `gwm`. - User asks how to set up the AWS RDS guard, the safe `.env.testing` fallback, or the no-symlink invariant for `vendor/` / `node_modules/`. - User asks about the branch convention `/#-` or its overrides. - User wants to link a worktree to a GitHub issue / PR, refresh GitHub status from inside the TUI, or run `gwm doctor` to validate setup before pushing. - User mentions `gcd ` (shell wrapper from `gwm shell-init `). - User wants tmux / zellij hand-off (`gwm tmux `, `gwm zellij ` with optional `--split`). - User wants to configure the TUI `l` (git_tui) / `R` (review) launchers, the `o` (open dispatch: shell/editor/finder), or `y` (yank path to clipboard) keys. - User asks about the `branch..gwm-base` key (review base-resolution anchor) or any of the `lumen` / `claude` / `codex` / `aider` / `gh` review presets. ## Prerequisites ```bash command -v gwm # required — installed by `cargo install --path .` from the gwm-cli repo command -v cargo # required at install time (1.95+ — the crate MSRV) command -v git # required at runtime command -v gh # OPTIONAL — the GitHub backend: live `gwm status`, TUI state, `R: review` preset command -v glab # OPTIONAL — the GitLab backend (forge = "gitlab"); $GWM_GLAB overrides it command -v tmux # OPTIONAL — needed by `gwm tmux` command -v zellij # OPTIONAL — needed by `gwm zellij` (≥ 0.40 for `--cwd` on new-tab) command -v lazygit # OPTIONAL — default `[git_tui]` binary backing the TUI `l` key command -v lumen # OPTIONAL — `[review] tool = "lumen"` preset (default review tool) command -v claude # OPTIONAL — `[review] tool = "claude"` preset command -v codex # OPTIONAL — `[review] tool = "codex"` preset command -v aider # OPTIONAL — `[review] tool = "aider"` preset command -v pbcopy # OPTIONAL — TUI `y: yank` on macOS (wl-copy/xclip/xsel on Linux, clip on Windows) ``` `gwm` vendors `libgit2`, so no system git2 lib is needed. The binary is self-contained once compiled. ## Install (from source) ```bash git clone https://github.com/kbrdn1/gwm-cli.git cd gwm-cli cargo install --path . # → ~/.cargo/bin/gwm gwm --version ``` No Rust toolchain at hand? `cargo binstall gwm-cli` pulls the prebuilt binary from the matching GitHub Release (via `[package.metadata.binstall]`) and drops it in `~/.cargo/bin/` without compiling `git2`/vendored-libgit2 from source: much faster on first install. Prebuilt releases (Linux x86_64/aarch64, macOS Intel/Apple Silicon, Windows): https://github.com/kbrdn1/gwm-cli/releases. A Homebrew formula ships under `packaging/homebrew/` and a Nix `flake.nix` is at the repo root. ## Environment variables | Variable | Effect | |:---------|:-------| | `GWM_GH` / `GWM_GLAB` | Override the forge CLI binary (path or name). Read once, on the thread that resolves the forge, so the TUI's background fetch never races env mutation. | | `GWM_ALLOW_BOOTSTRAP=1` | Same as `--allow-bootstrap`: skip the TOFU prompt. Also satisfies the forge host gate ; same ledger, same decision. | | `GWM_TRUST_LEDGER` | Path to the trust ledger (default `~/.config/gwm/trust.toml`). | | `GWM_NO_GLOBAL_CONFIG=1` | Ignore the user-level `~/.config/gwm/config.toml`; repo-only config. | | `GWM_HISTORY_FILE` | Path to the destructive-op journal backing `gwm history` / `gwm undo`. | | `GWM_AGENTS_HOME` | Override the home scanned for agent session artefacts (test seam). | Two **global** flags apply before or after any subcommand: `--allow-bootstrap` (skip the TOFU prompt: for CI and other non-interactive runs) and `--deny-bootstrap` (refuse bootstrap even when the ledger says trusted: for a first look at an unfamiliar repo). `--workspace ` is global too. ## Default conventions | What | Default | Override | |:--------------------|:-------------------------------------|:-------------------------------| | Branch name | `/#-` | `.gwm.toml` `branch_pattern` | | Worktree dir name | `--` | `.gwm.toml` `path_pattern` | | Worktree base | `~/cc-worktree//` | `.gwm.toml` `base` | | Bootstrap | none (just `git worktree add`) | `.gwm.toml` `[bootstrap.*]` | | Doctor trunks | `["dev", "main"]` | `.gwm.toml` `[doctor] trunks` | | TUI confirm timer | `3` (clamped 0..=5) | `.gwm.toml` `[tui] confirm_countdown_secs` | Branch types: `feat`, `fix`, `hotfix`, `docs`, `test`, `refactor`, `chore`, `perf`, `ci`, `build`. Placeholders in patterns: `{home}`, `{repo}` (repo name), `{repo_path}` (main repo's absolute workdir), `{repo_parent}` (its parent dir), `{type}`, `{issue}`, `{desc}`. Tilde (`~/…`) is also expanded. ## CLI reference ```bash gwm # opens the TUI in the current repo gwm --workspace # TUI / list / create across every git repo one level under (#36) gwm init [--preset ] [--show] # write .gwm.toml (refuses overwrite); --show prints it instead gwm init --list-presets # built-in presets: laravel, node/nuxt, rust, go, python-uv, generic gwm types [--gitmoji] # list supported branch types (--gitmoji adds each type's emoji) gwm create # create branch + worktree + bootstrap # --skip-hooks on create / review / new / remove / bootstrap skips the [hooks.*] phases gwm create feat 123 "user-authentication" gwm create feat 123 foo --no-bootstrap # skip the .gwm.toml stages gwm create feat 123 foo --reuse-branch # attach to an existing local branch instead of erroring gwm create feat 123 foo --repo # workspace mode: which child repo gets the worktree gwm list [--format table|names|json] [--detect-pr] # `json` = stable schema (#38); names = completion gwm path [--format text|json] # print path (fuzzy match) → use $(gwm path auth) gwm cd # alias of `gwm path` gwm note show [] # print a worktree's note; exit 1 when there is none (#515) gwm bootstrap # re-run bootstrap on cwd worktree gwm bootstrap # ...or on a named worktree gwm sync # fetch + rebase the cwd worktree onto its upstream gwm sync # ...or a fuzzy-matched worktree gwm sync --merge # merge the upstream instead of rebasing gwm remove ... [--delete-branch] [--dry-run] [--force] # remove (fuzzy); -b drops the branch # several patterns = one batch (#484): every one is resolved BEFORE anything is # touched, so an unknown / ambiguous pattern removes nothing at all; duplicates # collapse; the destructive loop does not stop at the first error and exits non-zero gwm prune [--dry-run] # clean stale .git/worktrees entries gwm doctor [--format text|json] # diagnose setup. Exit: 0=green, 1=warn, 2=fail gwm completions # emit a shell-completion script on stdout gwm shell-init # emit a `gcd ` wrapper to eval/source gwm switch # interactive picker → prints chosen path to stdout (alias: s) gwm tmux [-p|--split] # open matched worktree in new tmux window (or split) gwm zellij [-p|--split] # open matched worktree in new zellij tab (or pane) # --- fan-out & disk hygiene (#313) --- gwm exec [...] [--jobs N] [--profile P] -- # run in each worktree; ✓/✗ rollup, non-zero on any fail # --jobs N runs N in parallel; --profile picks an [exec.profiles.*] entry # default = all non-main worktrees; slugs before `--`; cmd verbatim after `--` gwm clean [...] [--yes] [--profile P] # report (or with --yes reclaim) target/ node_modules/ dist/ build/ # --profile picks a [clean.profiles.*] dir set # --yes deletes ONLY git-ignored dirs holding no tracked files; never follows symlinks # --- forge: GitHub (`gh`) or GitLab (`glab`) --- gwm new # create issue from a repo template, then its worktree gwm pr [--draft] [--base ] [--render] # render [pr_template] body, then `gh pr create` gwm review [--name ] [--bootstrap] # materialise a PR into a worktree (cross-fork; safe-by-default) (#308) gwm link issue|pr [--worktree PAT] # bind a worktree to a GitHub issue or PR gwm unlink issue|pr [--worktree PAT] # remove the explicit link gwm open [issue|pr] [--print-url] [--worktree PAT] # open the linked URL in $BROWSER (--print-url just prints it) gwm status [--worktree PAT] [--json] # show link + live forge state (needs `gh` / `glab`) gwm labels list|push [--dry-run] [--prune] # sync the declarative [[labels]] set to origin gwm milestones list|push [--dry-run] [--prune] # sync the declarative [[milestones]] set to origin # --- daemon & shell consumers (#38 / #309) --- gwm daemon [--socket ] [--poll-ms ] # long-running JSON-RPC 2.0 over a unix socket (list/doctor/path/subscribe) gwm statusline [--socket ] [--watch] # one-line prompt summary off the daemon (degrades to blank when none) # --- AI-agent sessions (#408) --- gwm agents [--format table|json] # sessions per worktree (agent, freshness, last activity, id, name) # + an `unmatched` section for sessions no worktree claimed gwm agents attach # pin a session to a worktree (pins ACCUMULATE — several per worktree) gwm agents detach [] # unpin one session, or every pin when the id is omitted # detection reads on-disk artefacts only (Claude Code, Codex, # opencode incl. its opencode.db, Mistral Vibe) — no process scanning; # renames show (Claude live registry, Codex session_index, opencode titles); # GWM_AGENTS_HOME overrides the scanned home (test seam) # --- config / convention / introspection --- gwm config get|set|unset|list|validate|path|edit # git-config-style editing of .gwm.toml (comment-preserving) gwm history [--all] [--limit N] # recent destructive ops journal (newest first) gwm undo [--bootstrap] # reverse the last destructive op for this repo gwm trust add|list|show|revoke # TOFU trust ledger for .gwm.toml — `add` approves the # current repo without running anything (answers the forge # host gate, which is non-interactive by design) gwm aliases # resolved CLI aliases (built-in + repo + user) gwm commit-prefix [] [--unicode] [--branch ] # Gitmoji + Conventional prefix for the branch gwm hooks install commit-msg [--force] # opt-in git hook that auto-prepends the commit prefix gwm theme list|show [] # role-based [theme] presets gwm tui keys # resolved keymap (defaults + [tui.keys[.modal.*]] overrides) ``` ### `gwm doctor` Runs a structured set of checks across config, environment, and worktree state. Designed for CI / pre-commit hooks: | Exit | Meaning | |:-----|:-------------------------------------------------------------------------| | `0` | All checks green | | `1` | At least one **warning** (advisory, e.g. orphan gwm-style branch) | | `2` | At least one **failure** (broken config, prunable worktree, etc.) | Trunk branches the orphan-branch check treats as merge destinations come from `[doctor] trunks = [...]` (default `["dev", "main"]`). Setting `trunks = []` disables the filter (every unclaimed gwm-style branch is flagged). One of the warnings is an **orphan note** (#515): a note under `.git/gwm/notes/` whose branch no longer exists. This is the only place the note lifecycle is enforced, on purpose. `gwm clean` does not touch notes: its stated safety property is that `--yes` only removes directories git already ignores, and prose is neither regenerable nor ignored. ### `gwm shell-init` → `gcd ` The emitted wrapper defines a `gcd` function that resolves a worktree by fuzzy pattern and `cd`s into it in one keystroke. Install per shell: ```bash # zsh echo 'eval "$(gwm shell-init zsh)"' >> ~/.zshrc # bash echo 'eval "$(gwm shell-init bash)"' >> ~/.bashrc # fish gwm shell-init fish | source # also add the eval to ~/.config/fish/config.fish # powershell Invoke-Expression (& gwm shell-init powershell | Out-String) ``` `gcd` (no arg) launches `gwm switch` (the picker) and `cd`s into the chosen entry. ### Forge selection (GitHub / GitLab) Two backends implement the same `Forge` trait: **GitHub** via `gh`, **GitLab** via `glab` (which says *MR* where GitHub says *PR*). Everything else, worktrees, bootstrap, branch naming, the `branch..gwm-*` link storage: is forge-neutral. **Which backend.** Inferred from the `origin` host for the vendors' own domains (`github.com`, `ghe.com`, `gitlab.com`). A self-hosted instance lives on an arbitrary domain and cannot be detected from a URL, so name it: ```toml # .gwm.toml forge = "gitlab" # or "github" ``` The key always wins over inference, in both directions. **Which hosts may be called.** A separate question, with a separate answer: `forge` names the backend, it does not authorise a host. On a host gwm does not recognise, authorisation comes from one of: ```toml # ~/.config/gwm/config.toml — YOUR file, never shipped with a repo [forge_hosts] "gitlab.acme.com" = "gitlab" "ghe.acme.com" = "github" ``` ```bash gwm trust add # …or approve this one repo's .gwm.toml, in the repo, once ``` Per host (not one blanket key) so a mixed fleet: self-hosted GitLab *and* GitHub Enterprise: is describable in one config. Host matching is case-insensitive, and keyed on the host **without** the port. Why the split: `gh` and `glab` forward whatever token is in the environment to whatever host they are pointed at, with no scoping of their own. Reading a bare `forge` key as authorisation meant one global setting authorised every host in existence, including one an attacker put in a clone's `origin`. The refusal message prints the exact `[forge_hosts]` key to add. `GWM_ALLOW_BOOTSTRAP=1` satisfies this gate too: same ledger, same file, same decision. ### Forge linking (`link` / `unlink` / `open` / `status`) Links live in **per-branch git config**: - `branch..gwm-issue` ← `gwm link issue ` - `branch..gwm-pr` ← `gwm link pr ` - `branch..gwm-base` ← _written by `gwm create`_: anchors the `[review].{base}` resolution chain so the parent ref survives even when the branch has no upstream yet. Not user-facing; surfaces only via the `R: review` launcher. Local, per-branch, survives worktree moves. Issue numbers are auto-detected from the `/#-` convention when no explicit override is set; PR numbers are **not** auto-detected and must be linked explicitly. `gwm status` shells out to the resolved backend (`gh issue view` / `gh pr view`, or `glab issue view` / `glab mr view`) to fetch state, title, labels, and the CI rollup. GitLab hangs one `head_pipeline` off an MR rather than GitHub's per-check array, so the MR collapses to a single synthetic check. Without the backend CLI on `$PATH`, it prints only the local link. `--json` emits a stable schema for scripting. The backend a link was written under is recorded per branch; flipping `forge` drops the other backend's numbers rather than resolving issue #42 as merge request !42 on the other forge: a real page, the wrong one. ### Multiplexer hand-off (`tmux` / `zellij`) - `gwm tmux ` requires `$TMUX` to be set (i.e. you are already inside a tmux session): otherwise it exits non-zero with a clear error rather than spawning a stray server. `--split` opens a horizontal split of the current pane instead of a new window. - `gwm zellij ` requires `$ZELLIJ`. `--cwd` on `zellij action new-tab` needs zellij ≥ 0.40. `--split` opens a new pane in the current tab instead of a new tab. ## Status column The TUI table and `gwm list` both expose a `STATUS` column: | label | meaning | colour | |:-------------------|:----------------------------------------------------------------|:-------------| | `clean` | no upstream, no changes | green | | `✓ synced` | upstream set, no ahead/behind, no local changes | green | | `● dirty` | uncommitted changes (working tree or index) | yellow | | `↑N` | N commits ahead of upstream | cyan | | `↓M` | M commits behind upstream | yellow | | `↑N ↓M` | both | yellow | | `● dirty ↑N` | combined indicators | yellow | | `locked` | linked worktree is locked (git2 reports it) | magenta | | `prunable` | working tree dir is missing ; run `gwm prune` | red | | `unknown` | status couldn't be computed (detached HEAD, IO error, etc.) | dark gray | ## TUI key map Built-in defaults after the **v0.10 keymap redesign (#290)**, plus the `Space` / `z` swap that made room for the row mark (#484). Every binding is rebindable via `[tui.keys]` (list view) and `[tui.keys.modal.]` (overlays, #219). One upgrade note: `z` is now a shipped default, so a `.gwm.toml` binding a chord that *starts* with `z` (`top = ["z z"]`) is a prefix conflict and is refused at load time, and the same now goes for `N` (#515). Run `gwm tui keys` for the full resolved map incl. every modal context. | Key | Action | |:----------------|:----------------------------------------------------------------------------| | `j`/`↓` `k`/`↑` | move selection (scrolls the focused pane) | | `gg` / `G`·End | jump to first / last worktree | | `Tab` | swap focus between the worktree list and the Status sidebar | | `1` / `2` | focus the worktrees pane / the Status pane | | `3` / `4` | open the Command Logs modal / the Settings (config) panel (#290 / #294) | | `n` / `d` / `b` | new worktree modal / delete the marked rows (or the cursor row) / re-run bootstrap (TOFU gate) | | `Space` | mark / unmark the cursor row for the bulk delete (#484); only `d` reads the set | | `N` | edit the selected worktree's note in a modal (#515); a markdown glyph marks the rows that have one | | `D` | toggle "delete branch on remove" (arms the safety countdown when ON) | | `p` / `P` | pull / push the selected worktree's branch | | `s` / `f` | sync (fetch + rebase) / refresh the worktree list | | `c` / `e` | edit-worktree / exit-to-worktree (sets the picker's cd target) | | `l` / `L` | lazygit ; PTY overlay / fullscreen `[git_tui]` (#35) | | `r` / `R` | `[review]` launcher ; PTY overlay / fullscreen (#35 / #75) | | `o` / `O` | terminal (`$SHELL`) ; PTY overlay / fullscreen honouring `[tui.open]` (#35) | | `t` | open the worktree in a tmux/zellij pane (mux) | | `h` / `H` | run `[tui.macro1]` / `[tui.macro2]` (#290) | | `y` / `w` / `Y` | yank branch name / worktree name / absolute path to the clipboard | | `i` / `B` | link prompt (issue/PR) / browse-links menu (open linked issue·PR in browser)| | `a` | agent sessions overlay (#408) ; `j`/`k` select, `a` pin, `d` unpin, `i` attach-by-id, `Esc` close; the sidebar `Agents` pane shows pinned sessions only | | `I` | rich PR / issue view (#420) ; description, author, branch pair, diff size, CI rollup, submitted reviews, conversation, and the comments anchored to a diff hunk (#528, a second GraphQL request fired when the view opens). `Enter` opens the selected row's URL, `f` re-fetches. On GitLab: summary, description, author and branch pair only | | `x` | exec profile picker (#325) ; pick a `[exec.profiles.]` and run it in a PTY. A profile carrying a `[container]` block runs inside `docker run` / `podman run` with `-i -t` (#421) | | `F` | refresh GitHub issue/PR status via `gh` (off-thread; statusbar spinner) | | `V` / `v` | toggle the sidebar / flip its position left ↔ right | | `S` / `z` | sidebar Details mode (commits ↔ stashes) / cycle layout (auto→side→stacked; `Space` before #484) | | `.` / `?` | open the docs in `$BROWSER` / help overlay | | `:` / `/` | command palette (fuzzy-fire any action) / fuzzy filter bar (`Esc` clears) | | `Enter` | show path in status bar (picker mode: print path to stdout + exit) | | `q` / `Esc` | quit (`Esc` closes an overlay / clears a sticky filter first) | | `Ctrl-C` | force quit | PTY overlays (`l`/`r`/`o`) run the tool **inside** the TUI (no alt-screen swap; `Esc` closes); the uppercase variants (`L`/`R`/`O`) suspend the TUI for a fullscreen takeover. The Settings panel (`4`) edits `.gwm.toml` live across category tabs (Theme / Worktree / TUI / Keys / All) with a per-layer selector (`L`); the **Keys** tab captures a keystroke and writes it back validated (#294). ## Picker mode (`gwm switch` / `gcd`) `gwm switch` opens the same TUI minus the create / delete / bootstrap actions. The fuzzy filter bar opens immediately so typing narrows the list right away. `Enter` commits the highlighted pick (path → stdout). `Esc` / `Ctrl-C` / `q` exit non-zero without printing. ## Details sidebar When the sidebar is open (default ON, toggle with `v`), it shows a details panel for the selected worktree. The layout is responsive (issue #188): at ≥ 120 columns it sits **side-by-side** with the table; below that it **stacks** under the table (it is no longer hidden). `z` cycles `auto → side-by-side → stacked → auto` (`Space` before #484, `V` before #290); `v` flips the side-by-side position left ↔ right, with the default set by `[tui] sidebar_position = "left" | "right"` (default `right`). Since the lazygit-style redesign (issues #69 / #71 / #73) the panel is **four independent subsections** stacked vertically, with no outer `Details` frame and no inline `Label:` content headers. Under the default `compact` layout each subsection is delimited by a filled header line; under `layout = "bordered"` each carries a rounded border with its title on the rule, which is what the panel looked like up to 1.7. ``` ╭─ Worktree ──────────────────────╮ ● status dot tracks the linked PR / issue │ ● api-rest │ state (open=green, draft=darkgray, │ feat/#42-api-rest · 08d1029 │ merged=magenta, closed=red, white=link │ Created: 2d │ not yet fetched, darkgray=no link). │ ✓ synced ★ main │ Rebuilt fresh every frame so it tracks │ ~/Projects/Flippad/…/api-rest │ live `gh` fetches without invalidating ╰─────────────────────────────────╯ the cached git preview. ╭─ Issue / PR ────────────────────╮ Live `gh issue view` / `gh pr view` data: │ #42 · open · 3 labels │ state + checks rollup. Refresh with `F`. │ checks 7/8 │ Empty block hints "press L to link" when ╰─────────────────────────────────╯ the worktree has no link. ╭─ Working Tree ──────────────────╮ `git status --short` (`✓ clean` when empty). │ ✓ clean │ ╰─────────────────────────────────╯ ╭─ Recent Commits ────────────────╮ Full lazygit-style topology graph (issue #71): │ 08d1029 KB ○ feat: … │ per-row format ` │ 4d874e7 KB ○ fix: … │ `, `○` for commit, `◎` for merge, │ 2d1d3ae KB ◎ merge: … │ vertical pipes `│`, corners `╮ ╭ ╯ ╰`, │ … (300 commits, scrollable) │ junctions `┴ ┬`, horizontal strokes `─`. │ 7 of 14│ Subjects hard-clipped (no Wrap). Buffer = ╰─────────────────────────────────╯ 300 commits (matches lazygit's `log -300`). Bottom-right footer: ` of `. ``` Worktree block: name (bold) prefixed by the `●` dot · `branch · short-head` (branch coloured by `BranchStatus`, worst-state wins: dirty=red, ahead/behind=yellow, unpublished=magenta, synced=green, unknown=darkgray) · `Created: ` with freshness colour (green < 7d, yellow < 30d, darkgray ≥ 30d; `-` when undeterminable, e.g. trunk / detached HEAD) · status + flag badges (only the relevant ones: false flags stay invisible: `★ main`, `🔒 locked`, `⚠ prunable`) · tilde-compressed path. GitHub fetch state machine per worktree: `Idle → Loading → Loaded(T) | Error(String)`. Manual refresh = `F` (the legacy `R` was rebound to `R: review` in #75). An `Agents` block (issue #408) sits between Issue / PR and Working Tree when at least one session is **pinned** to the selected worktree: one line per pinned session (agent · freshness · last activity · name), collapsed entirely otherwise; the full detected list lives in the `a` overlay. The main table grows a conditional `AGENT` column (top session per worktree) only when a session is detected. `Tab` swaps focus between the worktree list and the sidebar. `j` / `k` (and arrows) scroll the Recent Commits block when the sidebar is focused: the small blocks above stay pinned. The focused panel's border turns cyan. ## Terminal open: `o` (PTY overlay) / `O` (fullscreen): issues #73 / #35 Since the keymap redesign, `o` opens an **embedded PTY terminal overlay** inside the TUI (no alt-screen swap; `Esc` closes), while `O` is the **fullscreen** variant that suspends the TUI and honours the `[tui.open]` dispatch below. Three fullscreen modes: | `mode = ` | Behaviour | |:----------|:-------------------------------------------------------------------------------------------| | `"shell"` _(default)_ | Suspend the TUI and spawn `$SHELL` with `cwd = ` ; lazygit-style. Exiting the shell restores the TUI. | | `"editor"` | Suspend the TUI and run `$EDITOR `. | | `"finder"` | Hand off to the OS file manager (`open` / `xdg-open` / `explorer`). | ```toml [tui.open] mode = "shell" # "shell" (default) | "editor" | "finder" shell_cmd = "" # override $SHELL when set ("" = read $SHELL) editor_cmd = "hx" # override $EDITOR when set ("" = read $EDITOR) ``` `shell_cmd` and `editor_cmd` win over the env var when non-empty. An unknown `mode` is a hard config-load error. ## Yank: `y` branch · `w` worktree name · `Y` path: issues #73 / #290 The yank keys copy to the system clipboard: `y` = the branch name, `w` = the worktree (dir) name, `Y` = the selected worktree's absolute path. Probe order (first hit wins on `$PATH`): | OS | Candidates (in order) | |:-----------|:-----------------------------------------------------------------------------------| | macOS | `pbcopy` | | Linux | `wl-copy`, `xclip -selection clipboard`, `xsel --clipboard --input` | | Windows | `clip` | Missing tool surfaces a status-bar hint, never a panic. No config knob: the probe list is built per-platform. ## Configurable launchers (`l`/`L` git_tui · `r`/`R` review): issues #75 / #35 Two configurable launchers share the same mini-API: take a `command` template from `.gwm.toml`, substitute placeholders, split with `shell-words`, and exec it with `cwd = `. Each has a **PTY-overlay** binding (lowercase: runs inside the TUI, no alt-screen swap, `Esc` closes) and a **fullscreen** binding (uppercase: suspends the TUI): | Keys (PTY / full) | Section | Default | Placeholders | Default `fullscreen` | |:------------------|:------------|:------------------------------|:--------------------------------------|:---------------------| | `l` / `L` | `[git_tui]` | `lazygit -p {path}` | `{path}` | `true` | | `r` / `R` | `[review]` | _(inert until configured)_ | `{base} {head} {path} {diff}` | `false` | `fullscreen = true` suspends the gwm TUI for a TUI-style takeover (same recipe as the pre-issue-#75 `l` → lazygit flow); `fullscreen = false` runs the command **synchronously in-place**: gwm stays in the alt-screen, `Command::output()` blocks the TUI until the child exits, and the first line of stderr lands on the status bar. Fine for quick print-only tools (`claude --print`, `gh pr view --web`); pick `fullscreen = true` for anything long-running so the TUI is properly suspended and restored. The `{diff}` placeholder is **lazy**: gwm only shells out to `git diff {base}..{head}` (into a tempfile) when the template references it. ### `[review]` base resolution chain (for `{base}`) 1. `branch..merge` (the branch's upstream, if any). 2. `branch..gwm-base` (recorded by `gwm create` so the parent ref survives `git push -u`). 3. `[review].default_base` from `.gwm.toml`. 4. `"dev"` (gwm's project convention). 5. `"main"` (universal git default). ### `[review].tool` built-in presets Sugar over `command + fullscreen`. Setting both `command` and `tool` makes `command` win (the TUI surfaces the shadow on next render). | `tool = "X"` | Resolves to | `fullscreen` default | |:-------------|:---------------------------------------------------------|:---------------------| | `lumen` | `lumen diff {base}..{head}` | true (TUI) | | `claude` | `claude --print 'review the diff {base}..{head}'` | false | | `codex` | `codex review {base}..{head}` | false | | `aider` | `aider --message 'review {base}..{head}'` | true (TUI) | | `gh` | `gh pr view --web` | false | ### Worked snippets ```toml # Switch the `l` key to gitui. [git_tui] command = "gitui -d {path}" # Review with lumen (TUI), skip when nothing to review. [review] tool = "lumen" skip_when_no_changes = true # default_base = "dev" # optional pin overriding the auto chain # Or a free-form shell line — `command` always wins over `tool`. [review] command = "my-review-bot --diff-file {diff} --owner kbrdn1" fullscreen = false ``` ### `gwm doctor` integration A configured `[review]` / `[git_tui]` binary that is not on `$PATH` surfaces as **Warning** (exit code `1`), never **Failed** (exit code `2`): both launchers are opt-in, so a CI pre-commit hook gated on `gwm doctor` keeps passing when the only red flag is a missing local-only tool. ## `.gwm.toml` schema Drop this at the repo root (or use `gwm init` for the annotated example). ```toml # Which backend drives issue / PR lookups. Omitted ⇒ inferred from the # `origin` host (github.com / ghe.com / gitlab.com). On any other host this # names the BACKEND but does not authorise the HOST — see `[forge_hosts]` in # your user-level config, or run `gwm trust add` in the repo. forge = "gitlab" # "github" | "gitlab" [worktree] base = "{home}/cc-worktree/{repo}" path_pattern = "{type}-{issue}-{desc}" branch_pattern = "{type}/#{issue}-{desc}" # --- file copies main → worktree (run in order) ----------------------------- [[bootstrap.copy]] from = ".env.testing" to = ".env.testing" required = true # if source missing AND no fallback → fail fallback = "inline" # "inline" (use [bootstrap.fallback.]) | "abort" | "skip" (default) [[bootstrap.copy]] from = ".env" to = ".env" required = false guards = ["no-aws-rds"] # references guard names # --- regex guards on copied files ------------------------------------------- [[bootstrap.guard]] name = "no-aws-rds" deny_patterns = ["amazonaws\\.com", "\\.rds\\."] on_match = "seed-from-example" # or "abort" example_file = ".env.example" # --- inline fallback when a required source is missing ---------------------- # Key is the destination filename normalised: ".env.testing" → "env_testing". [bootstrap.fallback.env_testing] target = ".env.testing" content = """ APP_ENV=testing DB_CONNECTION=sqlite DB_DATABASE=:memory: """ # --- symlinks to refuse (inherited from main) ------------------------------ [[bootstrap.no_symlink]] path = "vendor" [[bootstrap.no_symlink]] path = "node_modules" # --- shell commands after copies ------------------------------------------- [[bootstrap.command]] name = "composer install" run = "composer install --no-interaction --prefer-dist" when = "file_exists:composer.json" env = { COMPOSER_IGNORE_PLATFORM_REQ = "ext-imagick" } # --- composable when predicates -------------------------------------------- # Atoms : file_exists: | cmd_exists: | env_set: # env_eq:= | glob_exists: # Ops : ! (NOT) | && (AND) | || (OR) # Precedence: ! > && > || [[bootstrap.command]] name = "install (bun if available)" run = "bun install" when = "file_exists:package.json && cmd_exists:bun" [[bootstrap.command]] name = "install (npm fallback)" run = "npm ci" when = "file_exists:package.json && !cmd_exists:bun" [[bootstrap.command]] name = "full local build" run = "./scripts/full-build.sh" when = "glob_exists:src/**/*.rs && !env_set:CI" # --- branch types, commit prefixes, issue/PR templates ---------------------- # `branch_types` replaces the built-in list wholesale (feat, fix, hotfix, docs, # test, refactor, chore, perf, ci, build). `[gitmoji]` maps a type to the emoji # `gwm commit-prefix` (and the commit-msg hook) prepends. branch_types = ["feat", "fix", "chore", "spike"] [gitmoji] spike = "🧪" # `[issue_template]` / `[pr_template]` drive `gwm new` and `gwm pr` — the body # is rendered from the repo's own .github templates plus these defaults. [issue_template] labels = ["needs-triage"] [pr_template] base = "dev" draft = true # --- doctor knobs ----------------------------------------------------------- # Trunks the orphan-branch check treats as merge destinations. # Default: ["dev", "main"]. `trunks = []` disables the filter entirely. [doctor] trunks = ["master", "release-3.x", "release-4.x"] # --- TUI knobs -------------------------------------------------------------- # Safety countdown (seconds) applied to the delete-confirm overlay when # `delete branch on remove` (`p` in the TUI) is armed. Range 0..=5; # above 5 is clamped on read; 0 disables the countdown. Default: 3. # # `layout` (issue #545) picks how panes and sidebar sections are framed: # "compact" (default) one filled header line per section, no box rule # "bordered" the lazygit-style boxes gwm drew up to 1.7 # `status_one_line` (#547, default true) folds the sidebar Status block onto a # single row; false restores the labelled one-value-per-row block. # `dim_unfocused` (#545, default false) dims the body of the pane without # focus, under either layout. [tui] confirm_countdown_secs = 3 layout = "compact" status_one_line = true dim_unfocused = false # --- `o: open` dispatch (issue #73) ------------------------------------------ # Three modes: "shell" (default — $SHELL in the worktree), "editor" # ($EDITOR ), "finder" (pre-#73 OS file manager). `shell_cmd` / # `editor_cmd` override the env var when non-empty. [tui.open] mode = "shell" shell_cmd = "" editor_cmd = "hx" # --- TUI `l: git_tui` launcher (issue #75) ----------------------------------- # Default: `lazygit -p {path}` fullscreen=true (matches pre-#75 behaviour). # Placeholders: {path}. [git_tui] command = "lazygit -p {path}" fullscreen = true # --- TUI `R: review` launcher (issue #75) ------------------------------------ # Either a free-form `command` (placeholders: {base} {head} {path} {diff}) # or a `tool = ""` sugar (lumen / claude / codex / aider / gh). # `command` always wins when both are set. `{diff}` is lazy — only # materialised when the template references it. Base resolution chain: # upstream → branch..gwm-base → [review].default_base → "dev" → "main". [review] tool = "lumen" skip_when_no_changes = true # default true — `git rev-list --count {base}..{head} == 0` ⇒ skip # default_base = "dev" # optional pin overriding the auto-discovery chain # --- named exec profiles, and running one in a container (issues #325 / #421) -- # `gwm exec --profile test` runs this; `x` in the TUI picks from the same list. [exec.profiles.test] command = ["cargo", "test", "--all-features"] # A `[container]` block wraps THIS PROFILE's command in `docker run` / # `podman run`. It rides a profile only: an inline `gwm exec -- ` still # runs on the host whatever the config says, because that is the frozen 1.0 # surface. # # The mount is the substance, not the wrapper. A linked worktree's `.git` is # a FILE holding an absolute host path, so gwm mirrors host paths and mounts # the main checkout's gitdir alongside the worktree. Mounting the worktree # alone yields a container where `git status` answers # `fatal: not a git repository`. Every mounted path is declared # `safe.directory` through `GIT_CONFIG_*` (never the blanket `*`), since a # rootful Docker runs as uid 0 against a host-owned tree. [exec.profiles.test.container] image = "rust:1.90" # required # runtime = "docker" # auto-detected: docker first, then podman # extra_args = ["-e", "CI=1"] # spliced in before the image; `--name` is refused # selinux_relabel = true # suffixes gwm's own mounts with `:z`; off by default ``` `gwm exec` allocates no TTY (a terminal per container means nothing across a fan-out); the TUI overlay spawns into a real pty and runs with `-i -t`. The overlay also names its container (`gwm---`) and removes it on close, because killing a `docker run` client leaves the container running. Any Docker-compatible CLI works (OrbStack, Colima, Rancher Desktop, native Docker). Refused on Windows, and for a worktree path containing a `:`, which is the field separator of `-v source:destination`. ## Bootstrap report Every create / bootstrap run prints (or shows in the TUI) a per-step report: | Sigil | Status | Meaning | |:------|:---------|:-----------------------------------------------------------| | ✓ | Ok | step ran cleanly | | · | Skipped | conditional not met / dest already exists / optional miss | | ! | Warning | guard fired with fallback, or symlink removed | | ✗ | Failed | required step couldn't proceed | A run with any ✗ should be inspected before testing inside the worktree. ## Common workflows ### Migrating from `tools/worktree-manager.sh` + gwq 1. Install: `cargo install --path /path/to/gwm-cli` 2. In each repo: `gwm init` → edit `.gwm.toml` with the project-specific copies / guards / commands. 3. Replace `./tools/worktree-manager.sh create feat 123 foo` with `gwm create feat 123 foo`. 4. Replace `gwq list` / `gwq remove` / `gwq prune` with `gwm list` / `gwm remove` / `gwm prune`. 5. Drop `gwq` from the repo's prerequisites (gwm is self-contained). 6. Wire `gcd` into your shell: `echo 'eval "$(gwm shell-init zsh)"' >> ~/.zshrc`. 7. Add `gwm doctor` to CI / pre-commit to catch broken setups before push. ### Setting up the AWS RDS guard (Laravel / production-safe) `.gwm.toml`: ```toml [[bootstrap.copy]] from = ".env.testing" to = ".env.testing" required = true fallback = "inline" [bootstrap.fallback.env_testing] target = ".env.testing" content = """ APP_ENV=testing APP_KEY= DB_CONNECTION=sqlite DB_DATABASE=:memory: CACHE_STORE=array QUEUE_CONNECTION=sync MAIL_MAILER=array SESSION_DRIVER=array BCRYPT_ROUNDS=4 """ [[bootstrap.copy]] from = ".env" to = ".env" required = false guards = ["no-prod-rds"] [[bootstrap.guard]] name = "no-prod-rds" deny_patterns = ["amazonaws\\.com", "\\.rds\\.", "prod\\.flippad\\.com"] on_match = "seed-from-example" example_file = ".env.example" [[bootstrap.no_symlink]] path = "vendor" [[bootstrap.command]] name = "composer install" run = "composer install --no-interaction --prefer-dist" when = "file_exists:composer.json" env = { COMPOSER_IGNORE_PLATFORM_REQ = "ext-imagick" } ``` ### Node project (bun preferred, npm fallback) ```toml [[bootstrap.copy]] from = ".env" to = ".env" required = false [[bootstrap.no_symlink]] path = "node_modules" [[bootstrap.command]] name = "install (bun)" run = "bun install" when = "file_exists:package.json && cmd_exists:bun" [[bootstrap.command]] name = "install (npm fallback)" run = "npm ci" when = "file_exists:package.json && !cmd_exists:bun" ``` ### Quick create + cd ```bash gwm create feat 42 cool-thing cd "$(gwm path cool-thing)" # …or with the shell wrapper installed: gcd cool-thing ``` ### Picker → cd in one keystroke ```bash # After `eval "$(gwm shell-init zsh)"` gcd # opens the picker; cd into the chosen worktree on Enter ``` ### Hand-off into tmux / zellij ```bash # Inside tmux: gwm tmux api-rewrite # new window in current session gwm tmux api-rewrite --split # …or split the current pane # Inside zellij (>= 0.40): gwm zellij api-rewrite # new tab gwm zellij api-rewrite --split # …or new pane in current tab ``` ### Link a worktree to a GitHub issue / PR ```bash # Auto-detected from feat/#123-foo branches → no link needed. gwm status # shows the local link + (with gh) live state # Explicit linking: gwm link issue 456 # current worktree → issue #456 gwm link pr 789 --worktree api-rewrite # named worktree → PR #789 gwm open pr # open the PR URL in $BROWSER gwm unlink issue # drop the explicit issue link # Scripting: gwm status --json ``` ### Pre-push sanity check ```bash gwm doctor && git push # blocks the push on any warning/failure ``` ### Opt-in pre-commit hook (contributors) The repo ships an opt-in hook at `.githooks/pre-commit` that combines two gates: 1. **Env-dependent test pre-validation**: if any staged `tests/*.rs` file references ambient state (`assert_cmd`, `std::env::var`, `which::which`, `dirs::`, `Command::cargo_bin`), the suite is re-run under a stripped `PATH="$(dirname cargo):/usr/bin:/bin"` to catch tests that pass in a rich dev shell but fail on minimal CI. 2. **Local `gwm doctor`**: if staged files touch `.gwm.toml`, the bootstrap / doctor modules, the example config, or their tests, `gwm doctor` runs. Exit `0` is silent, `1` is advisory (commit proceeds), `2` blocks the commit. Unknown exits fail open. Enable per-clone: ```bash git config core.hooksPath .githooks git commit --no-verify # bypass for one commit, sparingly ``` ## Architecture (for skill agents asked to extend gwm) ``` src/ ├── lib.rs # public re-exports — tests import these ├── main.rs # bin entry, alias expansion, dispatches to cli::run ├── error.rs # GwmError (thiserror) + Result alias ├── config.rs # serde TOML → Config (worktree, bootstrap, hooks, doctor, tui, tui.open, │ # tui.keys[.modal], tui.macro1/2, git_tui, review, theme, gitmoji, aliases…) ├── config_cli.rs # `gwm config get/set/…` (comment-preserving toml_edit) ├── naming.rs # BranchSpec, kebab(), parse_branch() ├── worktree.rs # discover_repo, list, add, remove, prune, find_fuzzy, sync helpers (libgit2 + shell-out) ├── bootstrap.rs # run(BootstrapCtx) → BootstrapReport (copies / guards / commands / when DSL) ├── lifecycle.rs # [hooks.*] phases (pre/post create·bootstrap·remove) ├── sync.rs # fetch + rebase/merge onto upstream ├── review.rs # `gwm review ` — materialise a PR into a worktree (#308) ├── workspace.rs # multi-repo `--workspace` discovery (#36) ├── presets.rs # `gwm init --preset` stack templates (#37) ├── history.rs / trust.rs # destructive-op journal (undo) · TOFU trust ledger for .gwm.toml ├── forge.rs # `Forge` trait + forge-agnostic types, origin parsing, host authorisation (#419) ├── github.rs # GitHub backend (`gh`) + the shared link storage in git config, BranchLink ├── gitlab.rs # GitLab backend (`glab`) — parsers and argv builders (#419) ├── issue_templates.rs / pr_templates.rs / templating.rs # gwm new / gwm pr ├── labels.rs / milestones.rs / gitmoji.rs / aliases.rs # declarative GitHub sets, commit-prefix, CLI aliases ├── launcher.rs / multiplexer.rs # [git_tui]/[review] launcher pipeline · tmux/zellij hand-off ├── daemon.rs / json_api.rs # JSON-RPC 2.0 daemon + `--format=json` DTOs/schemas (#38) ├── statusline.rs # `gwm statusline` prompt one-liner off the daemon (#309) ├── agent_sessions.rs # AI-agent session detection — 4 artefact backends + pins overlay (#408) ├── command_log.rs # process-global command log (Command Logs modal, #290) ├── doctor.rs # `gwm doctor` checks (config, env, orphan branches, prunable wts, launcher PATH probes) ├── cli.rs # clap subcommands + handlers (incl. exec / clean, #313) └── tui/ ├── mod.rs # crossterm event loop (filter, overlays, launchers, clipboard, PTY) ├── app.rs # App state, transitions, Action dispatcher, GitHubFetchState, workspace ├── ui.rs # ratatui drawing — panes, bordered sidebar, modals, CI indicator (#299) ├── keymap.rs # [tui.keys] remappable list-view bindings + chords (#290) ├── modal_keymap.rs # [tui.keys.modal.] overlay bindings (#219) ├── palette.rs # `:` command palette ├── theme.rs # role-based colours + presets ├── wt_tree.rs # Working Tree file-explorer tree (#300) ├── commit_graph.rs # Recent Commits lazygit-style topology renderer └── state/ # create_form, filter, confirm, link_prompt, sidebar, github_fetch, spinner, # async_task, command_logs, config_panel, pty_overlay — one slice per overlay ``` Tests under `tests/` mirror this layout: 85 integration test files, one (or more) per module: e.g. `bootstrap_tests.rs`, `cli_binary.rs`, `config_tests.rs`, `doctor_tests.rs`, `daemon_tests.rs`/`daemon_integration.rs`, `json_api_tests.rs`, `review_tests.rs`/`review_integration.rs`, `statusline_tests.rs`, `workspace_tests.rs`, `presets_tests.rs`, `exec_tests.rs`, `clean_tests.rs`, `worktree_integration.rs`, plus the `tui_*` state-machine suites and `tests/common/` helpers. **TDD bar: any new behaviour ships with a matching test file or new assertions in an existing one** (project rule, enforced in `CLAUDE.md`). ## Differences vs. the bash + gwq stack | Capability | bash + gwq | gwm | |:----------------------------|:----------------------|:---------------------------------------------------------| | worktree engine | `gwq` CLI external | `libgit2` vendored | | bootstrap | hardcoded shell | declarative TOML + composable `when` predicates | | portability across repos | per-project script | one binary + per-repo config | | TUI | linear bash menu | full ratatui screen + filter + GitHub panel | | picker | none | `gwm switch` / `gcd` shell wrapper | | multiplexer hand-off | none | `gwm tmux` / `gwm zellij` (window/tab/split) | | GitHub linking | none | issue / PR per-branch git config + `gh` live status | | diagnostics | none | `gwm doctor` (exit 0/1/2, CI-ready) | | anti-RDS guard | hardcoded `grep` | configurable regex deny-list | | tests | 0 | large suite across 85 test files (config / bootstrap / when DSL / doctor / github / daemon + JSON API / review / statusline / workspace / presets / exec / clean / multiplexer / TUI state machines + commit graph / launcher / CLI / homebrew / flake / pre-commit hook) | | install | `chmod +x` per repo | `cargo install --path .` (or Homebrew / Nix / prebuilts) | ## Troubleshooting **`error: not inside a git repository`**: run `gwm` from inside a repo or pass a path explicitly. **`gwm create` fails with "branch ... already exists"**: the branch was created in a previous run that didn't finish. `git branch -D ` or pick another issue number, then retry. **`gwm remove` reports "pattern '...' is ambiguous"**: multiple worktrees match the substring. Pass a more specific pattern or the exact dir name from `gwm list`. **Bootstrap step shows ✗ on a `.env` copy with guard match + no example_file**: either set `example_file` in the guard, or change `on_match` to `"abort"` and rely on `.env.example`. Either way, the source `.env` is never copied past a guard match. **TUI shows `(prunable)` next to a worktree**: its working dir was deleted out-of-band. Run `gwm prune` (or hit `r` in the TUI after manual cleanup). **`gwm doctor` exits `2` complaining about an orphan branch**: the branch matches `/#-` but isn't reachable from any trunk in `[doctor] trunks`. Either delete the branch, merge it, or add its merge target to `trunks`. **`gwm tmux` says `not inside a tmux session`**: `$TMUX` is unset. Start tmux first; gwm refuses to spawn a stray server. **`gwm zellij` errors on `--cwd`**: your zellij is older than 0.40. Upgrade, or fall back to opening the path manually. **`gwm status` shows only the local link, no live data**: the backend CLI isn't on `$PATH`. Install `gh` (+ `gh auth login`) for GitHub, `glab` (+ `glab auth login`) for GitLab. **`origin host '…' is not one gwm recognises`**: a self-hosted forge on a domain gwm cannot read a forge from. gwm refuses rather than guessing, because guessing would send an authenticated call: and whatever token is in your environment: to whatever host the remote names. The message prints the exact key to add: ```toml # ~/.config/gwm/config.toml [forge_hosts] "gitlab.acme.com" = "gitlab" ``` Or name the backend in the repo's `.gwm.toml` and run `gwm trust add` there. **`this repo's .gwm.toml points gwm at '…' and the repo is not in the trust ledger`**: the repo names a backend for an unrecognised host, which only counts once you have approved that repo: `gwm trust add`. Editing `.gwm.toml` afterwards changes its hash and revokes the approval, by design. `gwm trust list` shows what you approved. **`cargo install --path .` fails to build libgit2**: install a C toolchain (`xcode-select --install` on macOS, `build-essential` on Debian/Ubuntu). The `git2` crate uses `vendored-libgit2` so it builds from source. **`.env` was copied even though it points to prod**: the guard's regex didn't match. Test it with `echo $YOUR_HOST | grep -E ''`. Regex syntax is Rust `regex` crate (PCRE-like, no lookaround). **`gcd` says command not found**: the shell-init wrapper isn't sourced. Re-run `eval "$(gwm shell-init )"` in your current shell and add it to your shell's rc file. **Pressing `r` / `R` in the TUI does nothing / shows a status hint**: `[review]` is opt-in. Either no `[review]` section exists in `.gwm.toml`, the resolved binary isn't on `$PATH` (`gwm doctor` flags it as Warning), or `skip_when_no_changes = true` (default) found 0 commits between `{base}..{head}`. Add a `[review] tool = "lumen"` (or another preset) to enable it. (`r` = PTY overlay, `R` = fullscreen.) **`R: review` resolves the wrong `{base}`**: the chain is upstream → `branch..gwm-base` → `[review].default_base` → `"dev"` → `"main"`. Pin it explicitly with `[review] default_base = ""` or set the per-branch override with `git config branch..gwm-base `. **`l` launches lazygit when the repo wants gitui (or vice versa)**: `[git_tui]` defaults to `lazygit -p {path}`. Override: ```toml [git_tui] command = "gitui -d {path}" fullscreen = true ``` **Pressing `o` opens a shell when you want the file manager (or vice versa)**: `[tui.open] mode = "shell"` is the new default since issue #73. Set `mode = "finder"` for the pre-#73 OS file manager hand-off, or `mode = "editor"` to spawn `$EDITOR `. **Pressing `y` does nothing / status bar says "no clipboard tool found"**: install a per-OS clipboard helper: `pbcopy` (macOS, built-in), `wl-copy` (Wayland), `xclip` / `xsel` (X11), `clip` (Windows, built-in). The probe list is platform-fixed; first hit on `$PATH` wins. ## Quick reference card ``` gwm # TUI (gwm --workspace = multi-repo) gwm init [--preset ] # scaffold .gwm.toml (optionally from a preset) gwm create <#> # create + bootstrap gwm list [--format json] # list worktrees (json = stable schema) gwm path|cd # print path gwm switch | gwm s | gcd # interactive picker (cd via shell wrapper) gwm bootstrap [pat] # re-run bootstrap gwm sync [pat] [--merge] # fetch + rebase (or merge) onto upstream gwm remove ... [-b] # remove one or several (-b drops branch); resolve-all-first gwm prune # clean stale refs gwm exec [slug...] -- # run in each worktree (✓/✗ rollup) gwm clean [slug...] [--yes] # report / reclaim build artifacts gwm review # materialise a PR into a worktree gwm new # create issue from template, then its worktree gwm pr [--draft] [--render] # render [pr_template], then gh pr create gwm daemon # JSON-RPC 2.0 over a unix socket gwm statusline [--watch] # one-line prompt summary off the daemon gwm agents [attach|detach] # AI-agent sessions per worktree + manual pins (#408) gwm note show [slug] # print a worktree's note (exit 1 = none) (#515) gwm types # show branch types gwm doctor [--format json] # diagnose setup (exit 0/1/2) gwm config get|set|list # edit .gwm.toml (comment-preserving) gwm history | gwm undo # destructive-op journal / undo gwm completions # emit shell completion script (bash/elvish/fish/powershell/zsh) gwm shell-init # emit gcd wrapper to eval (bash/fish/powershell/zsh) gwm tmux [-p] # tmux window / split hand-off gwm zellij [-p] # zellij tab / pane hand-off gwm link issue|pr # bind to GitHub issue / PR gwm unlink issue|pr # drop the link gwm open [issue|pr] # open URL in $BROWSER gwm status [--json] # local link + live forge state (gh / glab) gwm trust add # approve this repo's .gwm.toml (forge host gate + bootstrap) ``` ## Related - Repo: https://github.com/kbrdn1/gwm-cli - Bash predecessor: `tools/worktree-manager.sh` (skill: `worktree-wrapper`), replaced by `gwm` across repos. - Naming convention: `CONTRIBUTING.md` (per repo): matches `gwm` defaults. - Project rules for contributors / AI agents: `CLAUDE.md` (TDD mandatory, `gwm doctor` before PRs touching `.gwm.toml` / bootstrap schema / doctor).