github max-sixty/worktrunk v0.37.0
0.37.0

16 hours ago

Release Notes

Improved

  • Concurrent table form across hooks and aliases: post-* hooks already ran table form concurrently; aliases in table form ([[aliases.deploy]]) now do too, with output prefixed by a colored {label} │ and line-atomic writes. pre-* table form ([[pre-merge]]) is still forced serial but will follow in a future release — it's deprecated now so the serial→concurrent switch is explicit. Run wt config update to migrate to pipeline form. (#2089, #2135, #2145, #2151)

  • --KEY=VALUE shorthand for alias and hook variables: wt step deploy --env=staging and wt hook pre-start --branch=feature/test now work without the --var prefix. --my-var=value becomes {{ my_var }} in templates. Hooks also accept custom variable names (previously a fixed list; now matches alias behavior) and warn when a --var isn't referenced by any template — catching typos like --branch=feature. (#2091, #2096, #2117)

  • wt step discovers configured aliases: Running wt step (or wt step --help) now lists user and project aliases alongside the built-in subcommands, each with a one-line template summary. Aliases that shadow a built-in are flagged (shadowed by built-in). (#2131, #2141)

  • Shell completions for external wt-* subcommands: Tab completion now discovers wt-* binaries on PATH and forwards completion requests to them, so wt sync --<tab> shows the external command's flags. Builds on the git-style external subcommand dispatch in 0.36.0. (#2074, thanks @pablospe)

  • Persistent on-disk cache for expensive git operations: Five SHA-keyed probes that previously ran live on every wt list and wt switch — merge-tree conflict checks, the integration/add-probe, is-ancestor, has-added-changes, and branch diff stats — are now cached to disk under .git/wt/cache/. Because commit SHAs are content-addressed, cached results never go stale; an LRU bound (5000 entries per kind) keeps disk usage bounded. User-visible effects:

    • wt list and the wt switch picker open much faster on big repos, especially those with many stale branches. A warm cache skips the expensive probes entirely; a cold cache still benefits from the faster per-worktree check below.

    • Dirty-worktree conflict check is ~10× faster on cold cache. Swapped git stash create for git write-tree as the ephemeral tree snapshot — same answer, far less plumbing per worktree. (#2119)

    • The picker now shows the same status info as wt list. The old "skip stale branches" shortcut hid conflict and ahead/behind info on branches 50+ commits behind main to keep the picker responsive. The cache makes the shortcut unnecessary, so stale branches now display full status.

    • Consistent results during in-progress rebases. Tasks now track the branch ref rather than the worktree's transient HEAD, so rows no longer contradict themselves mid-rebase (e.g., is_ancestor=true alongside 1 ahead / 1 behind).

    Cache is cleared by wt config state clear. (#2085, #2090, #2098, #2119)

  • Lower-priority background operations: Extends the CPU/IO priority throttling already used by wt step copy-ignored to the background rm -rf in wt remove and the trash sweep, so cleanup doesn't compete with foreground work. On macOS this now uses taskpolicy -b, which throttles disk I/O as well as CPU; Linux uses nice -n 19 with best-effort ionice. User hooks are unchanged. (#2130, #2133)

  • Pipeline structure in alias announcements: Aliases now announce their pipeline structure: Running alias deploy: install; build, lint rather than the bare alias name. (#2092)

  • Graceful per-layer config degradation: A bad env var or a broken user config file no longer wipes the entire config to defaults. Each layer (system, user, env vars) degrades independently — valid layers apply, invalid layers are skipped with a warning. (#2120)

  • Per-variable env var type resolution: When multiple WORKTRUNK_* env vars target fields of different types (e.g., a numeric and a string field), each is resolved independently against the file config. Previously one incompatible var would drop every env override and the file config. (#2111)

  • Clearer deprecation warnings: Structural deprecation warnings follow a consistent {label}: X is deprecated in favor of Y pattern with a single proposed-diff preview — no more redundant current-config dump. Template variable renames and the approved-commands removal use the same pattern. Every command (not just wt config show) now emits the same per-kind warnings, with a single dedup'd hint per process pointing to wt config show for details and wt config update to apply. Deprecation warnings are suppressed in non-diagnostic contexts (tab completion, picker, wt list statusline) to keep prompts quiet. (#2147, #2148, #2153, #2171)

  • Structured JSON for wt config state logs: --format is now a global flag on state logs, state hints, state ci-status, and state marker — ordering no longer matters. Logs JSON entries gain first-class branch, source, hook_type, name, size, modified_at, and absolute path fields alongside the existing relative file, so filtering works with jq directly. (Breaking: the --hook and --branch filters on wt config state logs get were removed in favor of jq; piping the JSON through jq 'select(.branch == "...")' replaces them.) (#2156, #2161)

  • Cleaner log filenames: Background hook log files skip the collision-avoidance hash suffix when the input is already a safe filename. main/project/post-merge/clippy.log instead of main-vfz/project/post-merge/clippy-vif.log. Names containing invalid path characters still get the hash. (#2157)

  • wt list stall visibility: When wt list hangs for 5+ seconds, the progressive footer now names the blocked task and worktree (e.g. ○ Showing 13 worktrees (253/254 loaded, no recent progress; waiting on ci-status for feat)), with a pending count when multiple tasks are outstanding. On full timeout, the warning joins the blocked-tasks list onto a single gutter-prefixed line: ▲ wt list timed out after 120s (151 results received); blocked tasks: …. (#2203, #2205, #2207)

  • -vv logs full subprocess output to disk; drop -vvv: Captured subprocess stdout/stderr now fan out to two log targets — a bounded preview on stderr mirrored to .git/wt/logs/trace.log (renamed from verbose.log), and the uncapped body to a new .git/wt/logs/output.log. Large captures (e.g. git log -p | patch-id during wt list) no longer flood stderr with elision markers and force a rerun — the full body is always on disk. Any -v count above 2 collapses to -vv. (#2201)

  • Clap-native errors for unrecognized subcommands: wt s and wt step sqush now show clap's formatted error: unrecognized subcommand 'X' with typo suggestions and Usage block, rather than a custom git-style single-line message. The #[command(external_subcommand)] path added in 0.36.0 for wt-<name> dispatch is preserved. (#2212, #2215)

  • Quieter wt list loading placeholders: The · loading indicator no longer appears for commands that finish within 200ms — short renders never flash the dots. The Status column's loading/timeout glyph collapses from to a dim ·, and the working-tree gate's loading placeholder collapses from ··· to a single ·, matching the visual weight of neighbouring gates. (#2177, #2181, #2199)

  • Fewer wt list statusline subprocesses: Statusline rendering dropped four duplicate git subprocesses per render (rev-parse --git-common-dir ×2, --show-toplevel ×3, --git-dir ×2) by adding a process-wide rev-parse --git-common-dir cache and canonicalizing input paths in Repository::worktree_at(). (#2209)

  • Signals named in background pipeline errors: Killed hook children now report which signal: pipeline step terminated by signal 15 (SIGTERM): <step> instead of the generic command failed with signal. (#2193)

  • Nested config typos surface as warnings: Mistyped keys nested inside a known table (e.g. [merge] squas = true) now produce Unknown field merge.squas rather than going unnoticed. Built on a unified top-level + nested unknown-key analysis that also powers on-save preservation. (#2195)

  • sanitize_hash minijinja filter: New template filter that wraps sanitize_for_filename — produces a filesystem-safe name with a 3-char hash suffix so distinct originals never collide, while already-safe inputs pass through unchanged. Useful for matching on-disk hook log filenames from wt config state logs --format=json. (#2172)

Fixed

  • wt --help and wt --version write to stdout: Both previously wrote to stderr, breaking version=$(wt --version) and pipelines like wt --help | grep …. If you have scripts redirecting 2>&1 as a workaround, drop the redirect. Fixes #2072. (#2073, thanks @koralowiec for reporting; #2155)

  • Directive file passes through wt step aliases: Inside a wt step <alias> body, an inner wt switch --create now writes its cd directive to the parent shell instead of dropping it. This was the last blocker for "move staged changes into a new worktree" alias recipes. (#2077)

  • Detect AA and DD unmerged status codes: The working-tree conflict check caught 5 of 7 unmerged states but missed AA (both added) and DD (both deleted). Worktrees with these conflict types now fall back to the commit-based check as intended. (#2124)

  • Squash detection no longer silently misses branches when git diff-tree fails: The patch-id pipeline used for squash-merge detection didn't check whether git diff-tree exited cleanly — a failed source command fed git patch-id a truncated stream, producing a bogus patch-id and reporting "not squashed" when the branch may have been. Pipeline now bails on source-exit non-zero, and streams directly between commands via an OS pipe rather than buffering in-process. (#2136)

  • Nushell multi-line --execute payloads: The nushell wrapper was executing the exec directive file line-by-line, so multi-line payloads ran as separate shell sessions — cd and variable assignments didn't persist across lines. Now matches bash/zsh/fish source semantics. (#2134)

  • Redundant "To configure" hint for outdated shell wrappers: When a shell's integration file exists but is stale, wt config show no longer prints both a specific wt config shell install <shell> hint and the generic "To configure" summary. The summary now appears only when a shell is genuinely not configured. (#2152)

  • Ctrl-C aborts wt command loops: Signal-derived child exits (SIGINT/SIGTERM) now abort hook pipelines, alias steps, concurrent groups, and the wt step for-each worktree loop. Previously, wt's signal handler forwarded SIGINT/SIGTERM to the current child but wt itself survived, and FailureStrategy::Warn silently swallowed each interrupt — a single Ctrl-C against wt merge could charge through remaining hook steps. (#2174, #2182)

  • Nested unknown config keys preserved on save: Any unknown key nested inside a known table (e.g. future-option = true under [merge]) was silently deleted on any config save triggered by other mutations (first-run prompt, interactive path customization). Preservation is now computed recursively, so unknown keys survive at every nesting level. (#2180)

  • wt step --help honors -C and --config: Help previously resolved aliases before applying global flags, so wt -C other step --help listed aliases from the process cwd and --config custom.toml was ignored. Globals are now parsed in a single early pass. (#2176)

  • wt step --help no longer triggers config side effects: Rendering the alias listing in help output no longer emits deprecation warnings to stderr or writes a migration file next to the user config. (#2179)

  • wt step <alias> --dry-run with lazy vars: Dry-run previously expanded every command eagerly, so pipelines that read {{ vars.foo }} set by an earlier step failed with an "undefined vars" error even when the non-dry-run command would succeed. Dry-run now mirrors the hook pattern: templates that reference vars.* are syntax-validated (catching typos like {{ vars..foo }}) and shown raw, while other templates expand eagerly. (#2170)

  • wt config show fish completions and false-negative gating: A missing fish completions file used to print a confusing nested hint under "Already configured shell extension" and flip the generic "To configure" summary. It now prints a warning with specific remediation, mirroring the "Outdated shell extension" pattern. The "report a false negative" link is no longer gated on !has_any_configured, so a detector miss in one shell still offers the link when other shells are detected. (#2163)

  • Nix build meets Rust 1.93 MSRV: flake.lock updated to ship a newer nixpkgs rustc. (#2185, thanks @Lysanleo)

Documentation

  • "Extending Worktrunk" page: Dedicated docs page collecting recipes for custom workflows via hooks and aliases, including a "move staged changes to a new worktree" recipe closing #938. (#2079, #2083, #2088, #2094)

  • OpenCode in agent handoffs: Skill documentation now covers OpenCode alongside other agent CLIs. (#2108, thanks @vinicius507 for the suggestion in #2076)

  • Hook pipeline documentation: wt hook --help and web docs now teach pipelines as [[hook]] blocks with TOML notes in config commands, and the aliases docs teach [[aliases.NAME]] pipeline blocks. (#2144, #2149, #2154)

  • FAQ updates: Qualified the "no background processes" claim; clarified coverage includes shell-integration-tests; config key location uses git config worktrunk.*. (#2080, #2086, #2126)

  • Troubleshooting: wt list fsmonitor hang: Noted the interaction with core.fsmonitor daemons. (#2194)

  • README installation command formatting: Fixed code-block formatting around installation commands. (#2187, thanks @MahmoudMabrok)

Internal

  • Shell wrapper directive file split: The shell integration now writes cd paths to a separate file from --execute shell payloads, with the cd path read literally (cd -- "$(< file)", no shell parsing) and the exec file scrubbed from alias and hook child environments. Hardens against shell injection from hook/alias bodies into the parent session. The legacy single-file form is honored through 0.38; nushell users need wt config shell install to pick up the new wrapper. (#2118)

  • MSRV bumped to Rust 1.93: Per the "latest stable − 1" policy. (#2125)

  • Centralized [wt-trace] emitter: Trace records are now owned by src/trace/emit.rs rather than ad-hoc log::debug! format strings, and -vv log verbosity is fixed. (#2146)

  • Unified hook and alias execution paths: Hooks and aliases share the same foreground execution, shell invocation, template expansion, and priority-spawning code. (#2089, #2128, #2140, #2095, #2113)

  • Config migration is now in-memory; no more .new files: wt config show renders the deprecation diff from in-memory migrated content rather than writing a .new file next to the user's config. wt config update owns the sole filesystem mutation; a new --print flag emits migrated TOML to stdout without writing. (#2184)

Install worktrunk 0.37.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/download/v0.37.0/worktrunk-installer.sh | sh && wt config shell install

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/max-sixty/worktrunk/releases/download/v0.37.0/worktrunk-installer.ps1 | iex"; git-wt config shell install

Install prebuilt binaries via Homebrew

brew install worktrunk && wt config shell install

Download worktrunk 0.37.0

File Platform Checksum
worktrunk-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
worktrunk-x86_64-apple-darwin.tar.xz Intel macOS checksum
worktrunk-x86_64-pc-windows-msvc.zip x64 Windows checksum
worktrunk-aarch64-unknown-linux-musl.tar.xz ARM64 MUSL Linux checksum
worktrunk-x86_64-unknown-linux-musl.tar.xz x64 MUSL Linux checksum

Install via Cargo

cargo install worktrunk && wt config shell install

Install via Winget (Windows)

winget install max-sixty.worktrunk && git-wt config shell install

Install via AUR (Arch Linux)

paru worktrunk-bin && wt config shell install

Don't miss a new worktrunk release

NewReleases is sending notifications on new releases.