# gwm config — copy this to your repo root as `.gwm.toml`
#
# Global config (issue #190): the same schema may live at
# `~/.config/gwm/config.toml` ($XDG_CONFIG_HOME/gwm/config.toml). It is
# merged UNDERNEATH this repo file — set a preference once (e.g.
# `[theme] preset = "…"`) and every repo inherits it. The repo `.gwm.toml`
# always wins on conflict: scalars override, tables merge key-by-key,
# arrays ([[labels]], [[bootstrap.copy]], …) are replaced wholesale.
# Set GWM_NO_GLOBAL_CONFIG=1 to ignore the global file (repo-only).
#
# placeholders supported in paths/patterns:
# {repo} repo name {home} your home directory
# {repo_path} repo's absolute dir {repo_parent} the dir *containing* the repo
# {type} {issue} {desc}
# `{repo_parent}` lets the base sit next to the repo — e.g.
# base = "{repo_parent}/worktrees" → a sibling `worktrees/` dir,
# matching an editor's `../worktrees` convention (Zed git.worktree_directory).
#
# === TOFU trust gate (issue #95) ===========================================
# The first `gwm create` / `gwm bootstrap` against this file opens a
# one-shot trust prompt that summarises the bootstrap surface (copies,
# guards, no-symlinks, commands) and records the (origin URL, sha256)
# pair in `~/.config/gwm/trust.toml` on approval. Any byte change to this
# file re-prompts (whitespace matters — `rm -rf /tmp/` and
# `rm -rf /tmp /` differ by one byte and behave catastrophically
# differently). CI runners bypass the prompt with `--allow-bootstrap` or
# `GWM_ALLOW_BOOTSTRAP=1`. Forensic mode: `--deny-bootstrap` refuses
# even when trusted. Manage entries with `gwm trust list / revoke / show`.
# Full threat model: https://github.com/kbrdn1/gwm-cli/blob/main/src/trust.rs
# === forge (issue #419) ====================================================
# Which code-hosting platform backs issue / PR lookups: "github" (shells out
# to `gh`) or "gitlab" (shells out to `glab`, and says "MR" where GitHub says
# "PR"). Everything else — branch links, worktrees, bootstrap — is identical.
#
# Omit the key and gwm infers the forge from the `origin` host, which covers
# github.com, ghe.com and gitlab.com. A SELF-HOSTED instance lives on an
# arbitrary domain and cannot be detected from the URL alone, so name it:
#
# forge = "gitlab"
#
# The key always wins over inference, so it also forces GitHub on a host that
# would otherwise be read as GitLab.
#
# NOTE: on a host gwm does not recognise, this key names the BACKEND but does
# not authorise the HOST — it ships with the repo, so on its own it would let
# a clone point `gh` / `glab` at any server it likes, carrying whatever token
# is in your environment. Either approve this repo once with `gwm trust add`,
# or authorise the host in YOUR OWN ~/.config/gwm/config.toml (that file is
# never shipped with a repo, which is what makes it an answer):
#
# [forge_hosts]
# "gitlab.acme.com" = "gitlab"
# "ghe.acme.com" = "github"
#
# `forge_hosts` is read from the user-level config ONLY — setting it here in a
# repo's .gwm.toml has no effect, by design.
[worktree]
base = "{home}/cc-worktree/{repo}"
# Repo-relative alternative (sibling `worktrees/`, à la Zed `../worktrees`):
# base = "{repo_parent}/worktrees"
path_pattern = "{type}-{issue}-{desc}"
branch_pattern = "{type}/#{issue}-{desc}"
# --- file copies from main repo into the new worktree -----------------------
[[bootstrap.copy]]
from = ".env.testing"
to = ".env.testing"
required = true
fallback = "inline"
[[bootstrap.copy]]
from = ".env"
to = ".env"
required = false
guards = ["no-aws-rds"]
# --- regex guards on copied files -------------------------------------------
[[bootstrap.guard]]
name = "no-aws-rds"
deny_patterns = ["amazonaws\\.com", "\\.rds\\."]
on_match = "seed-from-example"
example_file = ".env.example"
# --- inline fallback when a required copy source is missing -----------------
[bootstrap.fallback.env_testing]
target = ".env.testing"
content = """
# Auto-generated by gwm. Safe sqlite :memory: defaults.
APP_ENV=testing
APP_DEBUG=true
APP_URL=http://localhost
APP_KEY=
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
CACHE_STORE=array
QUEUE_CONNECTION=sync
MAIL_MAILER=array
SESSION_DRIVER=array
BCRYPT_ROUNDS=4
"""
# --- refuse to inherit symlinks (e.g. vendor/ from main repo) ---------------
[[bootstrap.no_symlink]]
path = "vendor"
[[bootstrap.no_symlink]]
path = "node_modules"
# --- lifecycle hooks --------------------------------------------------------
# Phases: pre_create, post_create, pre_bootstrap, post_bootstrap, pre_remove,
# post_remove. Placeholders: {branch} {path} {type} {issue} {desc} {user}
# {owner} {repo}. Failure handling: on_fail = "abort" (default), "warn",
# or "ignore". Emergency bypass: --skip-hooks <phase,phase>; `gwm remove
# --force` implies --skip-hooks pre_remove,post_remove.
[[hooks.post_create]]
name = "composer install"
run = "composer install --no-interaction --prefer-dist"
when = "file_exists:composer.json"
on_fail = "warn"
env = { COMPOSER_IGNORE_PLATFORM_REQ = "ext-imagick" }
[[hooks.post_create]]
name = "direnv allow"
run = "direnv allow ."
when = "file_exists:.envrc"
on_fail = "ignore"
[[hooks.post_create]]
name = "npm install"
run = "npm install"
when = "file_exists:package.json"
# --- legacy post-create commands -------------------------------------------
# `[[bootstrap.command]]` is still accepted for compatibility. New configs
# should prefer `[[hooks.post_create]]`; if both legacy commands and any
# `[hooks.*]` entries are present, gwm warns and runs legacy commands as
# additional post_create hooks.
# --- composable when predicates --------------------------------------------
# Atoms: file_exists / cmd_exists / env_set / env_eq / glob_exists
# Operators: ! (NOT), && (AND), || (OR) — precedence: ! > && > ||
[[hooks.post_create]]
name = "install (bun if available)"
run = "bun install"
when = "file_exists:package.json && cmd_exists:bun"
[[hooks.post_create]]
name = "install (npm fallback)"
run = "npm ci"
when = "file_exists:package.json && !cmd_exists:bun"
[[hooks.post_create]]
name = "full local build"
run = "./scripts/full-build.sh"
when = "glob_exists:src/**/*.rs && !env_set:CI"
# --- branch types (issue #80) -----------------------------------------------
# Per-repo override of the allowed branch types. Absent (default) ⇒ the
# built-in list is used: feat, fix, hotfix, docs, test, refactor, chore,
# perf, ci, build. Present ⇒ only the listed types are accepted by
# `gwm create` / TUI create / `BranchSpec::validate()`. `gwm types`
# prints the resolved set with a footer noting the source
# (`(source: built-in defaults)` vs `(source: .gwm.toml)`).
#
# An empty array (`branch_types = []`) is treated as absent — the
# built-in list is restored so an accidental wipe doesn't lock you out
# of `gwm create`.
#
# [[branch_types]]
# name = "feat"
# description = "New feature implementation"
#
# [[branch_types]]
# name = "fix"
# description = "Bug fix"
#
# [[branch_types]]
# name = "migration"
# description = "Database migration"
# --- gitmoji mapping (issue #85) --------------------------------------------
# Per-repo override of the built-in `branch_type → :shortcode:` table used
# by `gwm commit-prefix`, `gwm types --gitmoji`, and the bundled
# `commit-msg` hook (`gwm hooks install commit-msg`). Absent (default) ⇒
# the built-in table is used:
#
# feat → :sparkles: ✨
# fix → :bug: 🐛
# hotfix → :ambulance: 🚑
# docs → :memo: 📝
# test → :white_check_mark: ✅
# refactor → :recycle: ♻
# chore → :wrench: 🔧
# perf → :zap: ⚡
# ci → :construction_worker: 👷
# build → :package: 📦
#
# `[gitmoji]` is **additive** — overriding one entry doesn't wipe the
# other nine. You can also map custom branch types (e.g. `migration`)
# without redeclaring the built-ins.
#
# Surface:
# gwm commit-prefix → ":sparkles: feat(#41):" (HEAD)
# gwm commit-prefix --unicode → "✨ feat(#41):"
# gwm commit-prefix --branch feat/#7-x → ":sparkles: feat(#7):" (named)
# gwm types --gitmoji → branch types + emoji columns
# gwm hooks install commit-msg → opt-in auto-prepend on `git commit`
#
# `--unicode` normalisation of overrides:
# With `feat = ":rocket:"`:
# gwm commit-prefix --branch feat/#1-x → ":rocket: feat(#1):"
# gwm commit-prefix --branch feat/#1-x --unicode → "🚀 feat(#1):"
# With `feat = ":foo:"` (shortcode unknown to gwm's built-in table):
# gwm commit-prefix --branch feat/#1-x --unicode → ":foo: feat(#1):"
# Unknown shortcodes fall through verbatim — no panic, no
# `:question:` substitution. The known-shortcode set covers the ten
# built-in mappings plus the most commonly-swapped Gitmoji entries
# (`:rocket:`, `:fire:`, `:lock:`, `:art:`, `:lipstick:`, `:hammer:`,
# `:bookmark:`, …).
#
# [gitmoji]
# feat = ":rocket:" # team uses 🚀 for new features instead of ✨
# migration = ":truck:" # custom branch type — emoji wins on `gwm types --gitmoji`
# --- doctor knobs -----------------------------------------------------------
# Trunk branches `gwm doctor`'s orphan-branch check treats as merge
# destinations. A gwm-style branch fully reachable from one of these is
# preserved per CONTRIBUTING ("never delete the source branch after
# merge") and is NOT flagged as orphan. Default: ["dev", "main"]. Repos
# with non-standard trunk conventions (e.g. release trains) opt in here.
# An empty list (`trunks = []`) disables the filter — every unclaimed
# gwm-style branch becomes an orphan warning.
#
# [doctor]
# trunks = ["master", "release-3.x", "release-4.x"]
# --- TUI knobs --------------------------------------------------------------
# Safety countdown (in seconds) applied to the delete-confirm overlay when
# `delete branch on remove` is armed (`p` in the TUI). The high-risk path
# becomes a two-step `y → countdown → auto-fire`; `y` during the countdown
# cancels. Range: 0..=5. `0` disables the countdown (classic single-
# keystroke confirm even when `p` is armed); values above `5` are clamped
# to `5` on read. Default: 3.
#
# [tui]
# confirm_countdown_secs = 3
#
# Auto-refresh the TUI worktree list/status every N seconds so Issue/PR
# markers and branch metadata do not stay stale while the session is open.
# `0` disables periodic refresh. Default: 60.
#
# auto_refresh_secs = 60
# --- TUI sidebar layout (issues #188 / #365) ---------------------------------
# `sidebar_position` — which side the worktree-details preview sidebar sits on
# in the side-by-side layout: "right" (default, pre-#188 behaviour) or "left".
# Toggle it live in the TUI with `v`. The stacked layout ignores this: there the
# sidebar is always at the bottom.
#
# `sidebar_orientation` — how the sidebar is arranged relative to the table:
# "stacked" table on top, sidebar below (default since #217)
# "side-by-side" always beside the table, even on a narrow terminal
# "auto" side-by-side at >= 120 columns, stacked below that
# Cycle it live with `Space`. Before #365 this reset on every launch; set it here to
# make the choice stick. An unknown value is a hard error at load time.
#
# [tui]
# sidebar_position = "left"
# sidebar_orientation = "side-by-side"
# --- TUI clipboard (issue #367) ----------------------------------------------
# How yanked text (path / branch / worktree name / command logs) reaches the
# clipboard:
# "auto" OSC52 when an SSH session is detected, host tools otherwise (default)
# "osc52" always emit the OSC52 escape sequence
# "tools" always use pbcopy / wl-copy / xclip / xsel / clip.exe
#
# Why "auto" matters over SSH: the host tools write to the clipboard of the
# machine gwm runs on. On a remote host `pbcopy` is found, succeeds, and reports
# success — while the text lands in the *remote* clipboard, out of reach. OSC52
# hands the text to your terminal emulator instead.
#
# Caveats, because OSC52 is never acknowledged by the terminal:
# - Under tmux, gwm wraps the sequence in DCS passthrough, but tmux needs
# `set -g allow-passthrough on` (off by default since tmux 3.3).
# - Inside GNU screen ($STY), gwm falls back to the host tools rather than
# emit a sequence screen would silently swallow.
# - Terminal support varies (kitty / WezTerm / Alacritty / iTerm2 yes,
# Terminal.app no). Use "tools" if your terminal drops it.
#
# [tui]
# clipboard = "auto"
# --- TUI keymap: `[tui.keys]` (issues #87 / #219 / #294) ---------------------
# Remap any TUI key. An override REPLACES the default for that verb; an empty
# array unbinds it. Run `gwm tui keys` for the full list of actions, contexts,
# and verbs, and `gwm doctor` to validate overrides. You can also edit every
# binding live from inside the TUI: open the Settings panel (`4`), tab to
# `Keys`, select a binding and press the activate key to capture a new key —
# it writes the same TOML shown below to the layer the panel targets (#294).
#
# Global list-view verbs are arrays keyed by action slug (multi-key chords
# like "g g" allowed here):
# [tui.keys]
# quit = ["q", "Esc"]
# down = ["j", "Down"]
# top = ["g g"]
#
# Modal / overlay verbs are nested under the dedicated `[tui.keys.modal]`
# namespace, keyed by context (issue #219). The separate namespace keeps a
# modal context from colliding with a same-named global action (create / help /
# command_logs / link are both): a global `create` array and a modal
# `[tui.keys.modal.create]` table can coexist. These are SINGLE keystrokes
# only — modals have no chord timeout. The same physical key can mean different
# verbs in different contexts:
# [tui.keys.modal.create]
# next_field = ["Tab"]
# prev_field = ["BackTab"]
# submit = ["Enter"]
# cancel = ["Esc"]
#
# [tui.keys.modal.confirm]
# confirm = ["y"]
# cancel = ["n", "Esc"]
# focus_confirm = ["Left", "h"]
# focus_cancel = ["Right", "l"]
# activate = ["Enter"]
#
# The link prompt and the settings editor are two-stage, addressed with a
# dotted context path:
# [tui.keys.modal.link.choose_target]
# issue = ["i"]
# pr = ["p"]
# next = ["j", "Down"]
# prev = ["k", "Up"]
# accept = ["Enter"]
# cancel = ["Esc"]
#
# [tui.keys.modal.link.input_number]
# submit = ["Enter"]
# cancel = ["Esc"]
#
# [tui.keys.modal.config.edit]
# submit = ["Enter"]
# cancel = ["Esc"]
# --- TUI launcher: `l` (git_tui) keybinding (issue #75) ----------------------
# The `l` key suspends the gwm TUI and launches a git TUI on the selected
# worktree. Placeholders: {path}. Default (no section) → `lazygit -p {path}`
# fullscreen=true, identical to the pre-issue-#75 hardcoded behaviour.
#
# Switch to gitui:
# [git_tui]
# command = "gitui -d {path}"
# fullscreen = true
#
# Or to a non-TUI tool (gwm stays visible, stderr first-line lands in the
# status bar):
# [git_tui]
# command = "code {path}"
# fullscreen = false
# --- TUI launcher: `R` (review) keybinding (issue #75) -----------------------
# The `R` key runs a code-review tool against the resolved base ref.
# Placeholders in `command`:
# {base} — resolved base (upstream → branch.<n>.gwm-base → default_base
# → "dev" → "main")
# {head} — selected worktree's branch name
# {path} — absolute path of the selected worktree
# {diff} — path to a temp file holding `git diff {base}..{head}`
# (lazily materialised; only created if the template uses it)
#
# Built-in presets — pick one with `tool = "<name>"` instead of `command`:
# lumen → "lumen diff {base}..{head}" · fullscreen=true
# claude → "claude --print 'review the diff {base}..{head}'" · fullscreen=false
# codex → "codex review {base}..{head}" · fullscreen=false
# aider → "aider --message 'review {base}..{head}'" · fullscreen=true
# gh → "gh pr view --web" · fullscreen=false
#
# When both `command` and `tool` are set, `command` wins (the TUI flags
# the shadow in the status bar). Setting neither leaves `R` inert with
# a status-bar hint.
#
# [review]
# tool = "lumen"
# skip_when_no_changes = true # default true — skip when `git rev-list --count {base}..{head} == 0`
# # default_base = "dev" # optional pin overriding the auto-discovery chain
#
# Explicit form (overrides any preset):
# [review]
# command = "lumen diff {base}..{head}"
# fullscreen = true
# skip_when_no_changes = true
# --- `gwm exec` profiles (issue #324) ----------------------------------------
# Saved commands for `gwm exec --profile <name>`. Each profile carries an
# argv ARRAY — run with NO shell, exactly like the inline `gwm exec -- <cmd>`.
#
# ⚠️ Divergence to note: `[exec.profiles.*].command` is an argv ARRAY
# (`["cargo", "test"]`), whereas the `command` of `[git_tui]` / `[review]`
# above is a single SHELL line (`"lazygit -p {path}"`). exec deliberately
# avoids a shell — no word-splitting, no globbing, no `{path}` placeholders;
# the program is run verbatim in each worktree. This is frozen for 1.0.
#
# `--profile` and an inline `-- <cmd>` are mutually exclusive (exit 1), and an
# unknown profile name exits 1.
#
# `jobs` is bounded parallelism: `[exec] jobs` is the global default, a
# profile's `jobs` overrides it, and the `--jobs <n>` flag wins over both
# (precedence: --jobs > profile.jobs > [exec] jobs > 1). `1`/absent runs
# sequentially with live output; `> 1` runs up to N worktrees at once and
# prints each one's captured output as a block at the end.
#
# [exec]
# jobs = 1
#
# [exec.profiles.test]
# command = ["cargo", "test"]
#
# [exec.profiles.fmt]
# command = ["cargo", "fmt", "--all"]
# jobs = 4
# --- `gwm clean` profiles (issue #324) ---------------------------------------
# Saved directory sets for `gwm clean --profile <name>`. A profile's `dirs` is
# a COMPLETE set that REPLACES the built-in target/node_modules/dist/build —
# it never adds to them. The safety gate (git-ignored + no tracked files +
# skip symlinks) still applies to every listed directory.
#
# The `default` profile is special: `gwm clean` WITHOUT `--profile` uses it
# when present, else falls back to the built-in four. An unknown `--profile`
# name exits 1.
#
# [clean.profiles.default]
# dirs = ["target", "node_modules", "dist", "build", "coverage", ".turbo"]
#
# [clean.profiles.deep]
# dirs = ["target", "node_modules", "dist", "build", ".cache", ".venv"]
# --- `o: open` dispatch (issue #73) -----------------------------------------
# How the `o` key behaves on the highlighted worktree. Three modes:
# "shell" — suspend the TUI and spawn $SHELL with cwd = worktree.
# Lazygit-style. Default. Exit the shell ⇒ TUI restores.
# "editor" — suspend the TUI and run $EDITOR <worktree-path>.
# "finder" — pre-#73 behaviour: hand off to the OS file manager
# (`open` / `xdg-open` / `explorer`).
# `shell_cmd` / `editor_cmd` override the env var when set (empty string
# = unset). An unknown `mode` is a hard config error at load time.
#
# [tui.open]
# mode = "shell"
# shell_cmd = "" # leave empty to read $SHELL
# editor_cmd = "hx" # example: Helix
# --- declarative GitHub labels (issue #81) ----------------------------------
# Each `[[labels]]` entry declares a GitHub label that `gwm labels push` will
# create or update on the `origin` remote via `gh label create --force`.
#
# Fields:
# name — required. The GitHub label name. Whitespace is preserved
# verbatim (e.g. "good first issue"); use `name = "…"`, not a
# bare key.
# description — optional. Empty / absent means "leave the description
# untouched on the remote".
# color — optional. 6-hex lowercase (no leading `#` — `#d73a4a` is
# accepted and normalised). When omitted, gwm picks a
# deterministic pastel from a hash of the name so the same
# label gets the same colour across repos. `--random-colors`
# overrides this to a random pastel per push.
#
# Workflow:
# gwm labels list — print the resolved set + the diff against remote
# (`+ create`, `~ update`, `= match`, `- extra`).
# gwm labels push — apply the diff (create + update).
# gwm labels push --dry-run — same as list, but with an explicit summary
# line ("would create 2, update 1, …").
# gwm labels push --prune — also delete labels on the remote that are
# NOT in `[[labels]]`. Destructive, off by
# default.
#
# Requires `gh` on $PATH (the same soft dependency as `gwm status`).
#
# [[labels]]
# name = "bug"
# description = "Something isn't working"
# color = "d73a4a"
#
# [[labels]]
# name = "enhancement"
# description = "New feature or request"
#
# [[labels]]
# name = "good first issue"
# description = "Good for newcomers"
# color = "7057ff"
# --- declarative GitHub milestones (issue #82) ------------------------------
# Each `[[milestones]]` entry declares a GitHub milestone that `gwm
# milestones push` will create or update on the `origin` remote via the
# GitHub REST API (no native `gh milestone` subcommand exists).
#
# Fields:
# title — required. The GitHub milestone title (unique per repo).
# description — optional. Empty / absent means "leave the description
# untouched on the remote".
# due_on — optional. Accepts `YYYY-MM-DD` (materialised as
# 23:59:59 UTC of that day — "due Friday" closes at end
# of day, not midnight) or full RFC3339
# (`2026-07-15T17:00:00Z`). Absent means no due date.
# state — optional. `"open"` (default) or `"closed"`. Use
# `"closed"` to archive a milestone declaratively.
#
# Workflow:
# gwm milestones list — print the resolved set + the diff against
# remote (`+ create`, `~ update`, `= match`,
# `- extra`).
# gwm milestones push — apply the diff (create + update).
# gwm milestones push --dry-run — same as list, but with an explicit
# summary line ("would create 2,
# update 1, …").
# gwm milestones push --prune — also delete milestones on the remote
# that are NOT in `[[milestones]]`.
# Destructive, off by default.
#
# Requires `gh` on $PATH (same soft dependency as `gwm labels` / `gwm
# status`).
#
# [[milestones]]
# title = "v0.7.0"
# description = "Configurability sprint"
# due_on = "2026-07-15"
# state = "open"
#
# [[milestones]]
# title = "v0.8.0"
# due_on = "2026-10-01T17:00:00Z"
#
# [[milestones]]
# title = "v0.6.0"
# state = "closed"
# --- issue templates for `gwm new` (issue #83) ------------------------------
# `gwm new <type> <desc>` renders one of `.github/ISSUE_TEMPLATE/*.yml`,
# creates the issue with `gh issue create`, captures the issue number, then
# delegates to `gwm create <type> <issue> <desc>`.
#
# [issue_template]
# default = "feature_request.yml"
#
# [issue_template.by_type]
# feat = { template = "feature_request.yml", surface = "cli", title_prefix = "[Feature]: ", labels = ["enhancement"] }
# fix = { template = "bug_report.yml", surface = "cli", title_prefix = "[Bug]: " }
# docs = { template = "task.yml", title_prefix = "[Docs]: " }
# hotfix = { template = "bug_report.yml", surface = "cli", title_prefix = "[Hotfix]: ", labels = ["priority: high"] }
# --- PR templates for `gwm pr` (issue #84) ----------------------------------
# `gwm pr [--draft] [--base <ref>] [--render]` renders a per-branch-type PR
# body and shells out to `gh pr create`. Bodies can be a workdir-relative
# Markdown file (`path = …`) or an inline string (`body = "…"`). Placeholders
# `{type}`, `{issue}`, `{desc}`, `{base}`, `{head}`, `{repo}`, `{commits}`
# (`- subject` lines from `git log base..head`), and `{files_changed}`
# (`git diff --stat base..head`, capped at 30 lines) are substituted before
# the body lands on the temp file `gh pr create --body-file` reads.
#
# `gwm pr --render` skips `gh` entirely and prints the rendered body to
# stdout so you can pipe it elsewhere: `gwm pr --render | gh pr create
# --body-file -`.
#
# [pr_template]
# default = ".github/pull_request_template.md"
#
# [pr_template.by_type]
# feat = { path = ".github/pr-templates/feat.md" }
# fix = { path = ".github/pr-templates/fix.md" }
# docs = { path = ".github/pr-templates/docs.md" }
#
# [pr_template.by_type.chore]
# body = """
# ## Summary
# {desc}
#
# Closes #{issue}
#
# ## Test plan
# - [ ] cargo test
# """
# --- CLI aliases (issue #86) ------------------------------------------------
# Mirror of `git config`'s `[alias]` block. Each entry maps a short
# name to an argv-substituted expansion run BEFORE clap parses, so
# `gwm wip` becomes `gwm create feat 0 wip` for the dispatcher.
#
# Resolution order (highest precedence first):
# 1. Built-in subcommands (`gwm list`, `gwm switch`, …) — never
# shadowable.
# 2. `visible_alias` set on built-in subcommands (`s → switch`,
# `cd → path`) — also never shadowable.
# 3. This `[aliases]` block (repo-level, follows the repo).
# 4. `~/.config/gwm/aliases.toml` (user-level, lowest precedence).
#
# Rules:
# - Aliases are argv substitution only — `gwm <alias> <extra>` becomes
# `gwm <expansion> <extra>`. NO shell pipelines: values containing
# `&&`, `||`, `|`, `;`, or backticks are refused at load time. Use a
# shell alias if you need shell semantics (`alias gwmwip='gwm wip &&
# lazygit'`).
# - Names that shadow a built-in subcommand or visible alias are a
# hard config error surfaced at `Config::load_for_repo` time.
# - Single-pass expansion — `wip = "ll"` followed by `ll = "list
# --format names"` expands ONCE and then dispatches.
#
# Surface the resolved chain (built-in + repo + user, with shadowing
# flagged inline) via:
#
# gwm aliases list
#
# Example:
#
# [aliases]
# wip = "create feat 0 wip"
# ll = "list --format names"
# sync = "bootstrap"