Imported from kbrdn1/gwm-cli (
skills/SKILL.md). Install upstream withnpx skills add kbrdn1/gwm-cli --skill skills. Copyright stays with the author.
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 <main-checkout>/.git/gwm/notes/<branch>.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 <type> <issue> <desc> 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.
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 <PR#> (#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 <subcommand>:init(incl.--preset),list(incl.--format json),create,remove,path/cd,bootstrap,sync,prune,doctor,types,completions,shell-init,switch(aliass),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.trusttakesadd | list | revoke | show. - User wants to fan a command out across worktrees (
gwm exec -- <cmd>) or reclaim build artifacts (gwm clean). - User wants to materialise a GitHub PR into a worktree to review/test it (
gwm review <PR#>, cross-fork, safe-by-default). - User wants a multi-repo workspace (
gwm --workspace <dir>), the JSON API / daemon (--format=json,gwm daemon,gwm statuslinefor 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 + anunmatchedsection),gwm agents attach <wt> <id>/detach <wt> [<id>](multi-pins), the TUIaoverlay (selectj/k, pina, unpind, attach-by-idi), the pinned-onlyAgentssidebar pane, or theGWM_AGENTS_HOMEtest seam. - User opens the TUI by running
gwmalone in a repo, or the picker viagwm switch/gwm s. - User wants to delete several worktrees at once:
Spaceto mark rows in the TUI thend(#484), orgwm remove a b cfrom the shell. - User wants to write down where they were on a worktree:
Nin the TUI opens its note in an editable modal (#515),gwm note showreads it back,gwm doctorreports the ones whose branch is gone. - User asks about the redesigned TUI: PTY overlays (
l/Llazygit,r/Rreview,o/Oshell, #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.<context>]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
whenpredicates (file_exists:,cmd_exists:,env_set:,env_eq:,glob_exists:) and the!/&&/||operators. - User wants to migrate a
tools/worktree-manager.shorgwq-based workflow togwm. - User asks how to set up the AWS RDS guard, the safe
.env.testingfallback, or the no-symlink invariant forvendor//node_modules/. - User asks about the branch convention
<type>/#<issue>-<desc>or its overrides. - User wants to link a worktree to a GitHub issue / PR, refresh GitHub status from inside the TUI, or run
gwm doctorto validate setup before pushing. - User mentions
gcd <pattern>(shell wrapper fromgwm shell-init <shell>). - User wants tmux / zellij hand-off (
gwm tmux <pat>,gwm zellij <pat>with optional--split). - User wants to configure the TUI
l(git_tui) /R(review) launchers, theo(open dispatch: shell/editor/finder), ory(yank path to clipboard) keys. - User asks about the
branch.<n>.gwm-basekey (review base-resolution anchor) or any of thelumen/claude/codex/aider/ghreview presets.
Prerequisites
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)
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 <DIR> is global too.
Default conventions
| What | Default | Override |
|---|---|---|
| Branch name | <type>/#<issue>-<desc> |
.gwm.toml branch_pattern |
| Worktree dir name | <type>-<issue>-<desc> |
.gwm.toml path_pattern |
| Worktree base | ~/cc-worktree/<repo>/ |
.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
gwm # opens the TUI in the current repo
gwm --workspace <dir> # TUI / list / create across every git repo one level under <dir> (#36)
gwm init [--preset <stack>] [--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 <type> <issue> <desc> # 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 <name> # workspace mode: which child repo gets the worktree
gwm list [--format table|names|json] [--detect-pr] # `json` = stable schema (#38); names = completion
gwm path <pattern> [--format text|json] # print path (fuzzy match) → use $(gwm path auth)
gwm cd <pattern> # alias of `gwm path`
gwm note show [<slug>] # print a worktree's note; exit 1 when there is none (#515)
gwm bootstrap # re-run bootstrap on cwd worktree
gwm bootstrap <pattern> # ...or on a named worktree
gwm sync # fetch + rebase the cwd worktree onto its upstream
gwm sync <pattern> # ...or a fuzzy-matched worktree
gwm sync <pattern> --merge # merge the upstream instead of rebasing
gwm remove <PATTERN>... [--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 <bash|elvish|fish|powershell|zsh> # emit a shell-completion script on stdout
gwm shell-init <bash|fish|powershell|zsh> # emit a `gcd <pattern>` wrapper to eval/source
gwm switch # interactive picker → prints chosen path to stdout (alias: s)
gwm tmux <pattern> [-p|--split] # open matched worktree in new tmux window (or split)
gwm zellij <pattern> [-p|--split] # open matched worktree in new zellij tab (or pane)
# --- fan-out & disk hygiene (#313) ---
gwm exec [<slug>...] [--jobs N] [--profile P] -- <cmd> # run <cmd> 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 [<slug>...] [--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 <type> <desc> # create issue from a repo template, then its worktree
gwm pr [--draft] [--base <b>] [--render] # render [pr_template] body, then `gh pr create`
gwm review <PR#> [--name <b>] [--bootstrap] # materialise a PR into a worktree (cross-fork; safe-by-default) (#308)
gwm link issue|pr <N> [--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 <path>] [--poll-ms <n>] # long-running JSON-RPC 2.0 over a unix socket (list/doctor/path/subscribe)
gwm statusline [--socket <path>] [--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 <pat> <session-id> # pin a session to a worktree (pins ACCUMULATE — several per worktree)
gwm agents detach <pat> [<session-id>] # 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 [<branch>] [--unicode] [--branch <b>] # 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 [<name>] # 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 <pattern>
The emitted wrapper defines a gcd function that resolves a worktree by fuzzy pattern and cds into it in one keystroke. Install per shell:
# 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 cds 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.<n>.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:
# .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:
# ~/.config/gwm/config.toml — YOUR file, never shipped with a repo
[forge_hosts]
"gitlab.acme.com" = "gitlab"
"ghe.acme.com" = "github"
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.<name>.gwm-issue←gwm link issue <N>branch.<name>.gwm-pr←gwm link pr <N>branch.<name>.gwm-base← written bygwm 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 theR: reviewlauncher.
Local, per-branch, survives worktree moves. Issue numbers are auto-detected from the <type>/#<N>-<slug> 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 <pat>requires$TMUXto 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.--splitopens a horizontal split of the current pane instead of a new window.gwm zellij <pat>requires$ZELLIJ.--cwdonzellij action new-tabneeds zellij ≥ 0.40.--splitopens 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.<context>]
(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.<name>] 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 `<hash> <initials> <node>
│ 4d874e7 KB ○ fix: … │ <subject>`, `○` 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: `<bottom> of <total>`.
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: <age> 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 = <worktree> ; lazygit-style. Exiting the shell restores the TUI. |
"editor" |
Suspend the TUI and run $EDITOR <worktree-path>. |
"finder" |
Hand off to the OS file manager (open / xdg-open / explorer). |
[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 = <selected-worktree>. 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})
branch.<name>.merge(the branch's upstream, if any).branch.<name>.gwm-base(recorded bygwm createso the parent ref survivesgit push -u).[review].default_basefrom.gwm.toml."dev"(gwm's project convention)."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
# 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).
# 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.<key>]) | "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:<path> | cmd_exists:<bin> | env_set:<VAR>
# env_eq:<VAR>=<value> | glob_exists:<pattern>
# 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 <path>), "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 = "<preset>"` 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.<n>.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 -- <cmd>` 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-<worktree>-<pid>-<n>) 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
- Install:
cargo install --path /path/to/gwm-cli - In each repo:
gwm init→ edit.gwm.tomlwith the project-specific copies / guards / commands. - Replace
./tools/worktree-manager.sh create feat 123 foowithgwm create feat 123 foo. - Replace
gwq list/gwq remove/gwq prunewithgwm list/gwm remove/gwm prune. - Drop
gwqfrom the repo's prerequisites (gwm is self-contained). - Wire
gcdinto your shell:echo 'eval "$(gwm shell-init zsh)"' >> ~/.zshrc. - Add
gwm doctorto CI / pre-commit to catch broken setups before push.
Setting up the AWS RDS guard (Laravel / production-safe)
.gwm.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)
[[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
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
# After `eval "$(gwm shell-init zsh)"`
gcd # opens the picker; cd into the chosen worktree on Enter
Hand-off into tmux / zellij
# 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
# 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
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:
- Env-dependent test pre-validation: if any staged
tests/*.rsfile references ambient state (assert_cmd,std::env::var,which::which,dirs::,Command::cargo_bin), the suite is re-run under a strippedPATH="$(dirname cargo):/usr/bin:/bin"to catch tests that pass in a rich dev shell but fail on minimal CI. - Local
gwm doctor: if staged files touch.gwm.toml, the bootstrap / doctor modules, the example config, or their tests,gwm doctorruns. Exit0is silent,1is advisory (commit proceeds),2blocks the commit. Unknown exits fail open.
Enable per-clone:
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 <PR#>` — 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<T>, 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.<context>] 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
*Truncated - read the full file at https://github.com/kbrdn1/gwm-cli/blob/b0752388196ff4728943655681b1fe20caf49d17/skills/SKILL.md