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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
# Changelog

## 0.70.0

### Improved

- **`wt step prune` removes worktrees far faster**: Each removal ran a serial chain of ~17 git subprocesses under the scan write lock, re-preparing a plan the scan had already computed and re-stating the worktree right after the fsmonitor daemon stop. The chain is now one check per guarantee, reusing the scan-time plan, and removals run concurrently on the scan lock's read side — the write side is kept for the candidates that need it (hook-bearing, `--foreground`, metadata-pruning, and the current worktree). The documented rust-scale live prune of 24 candidates goes from ~12 s to ~0.6 s wall, and the `prune_e2e/live` benchmark from ~620 ms to ~400 ms. ([#3617](https://github.com/max-sixty/worktrunk/pull/3617), [#3631](https://github.com/max-sixty/worktrunk/pull/3631))

- **`wt step prune --format=json` is ordered, and a failed removal aborts the rest**: Live JSON output is now sorted by scan index, matching `--dry-run`, with the current worktree last. The first failing removal drains the remaining queue unexecuted, matching the serial loop it replaced; in-flight removals complete. ([#3631](https://github.com/max-sixty/worktrunk/pull/3631))

- **A worktree can be named by its path wherever a branch is accepted**: Every argument that takes a branch now also accepts the worktree's own path, resolved after the branch so a directory never shadows a branch sharing its name. A path names what a branch cannot — a detached worktree, or one of two checkouts of the same branch. Relative paths resolve against `-C` and a leading `~` against the home directory, so a path worktrunk printed can be pasted back. [Docs](https://worktrunk.dev/switch/#naming-a-worktree) ([#3607](https://github.com/max-sixty/worktrunk/pull/3607))

- **`wt list` flags a branch checked out in more than one worktree**: Such a branch resolves to whichever worktree git lists first, so every worktree on it now carries `⚑` — `worktree.state` `"duplicate_branch"` in schema 1, a `worktree.duplicate_branch` boolean in schema 2. The flag makes the ambiguity visible in the listing; resolving such a branch from any command warns separately and names a duplicate to drop. ([#3480](https://github.com/max-sixty/worktrunk/pull/3480), [#3606](https://github.com/max-sixty/worktrunk/pull/3606))

- **`wt switch --execute` computes only the template variables its command names**: The switch path built every variable the template context could hold before rendering; it now resolves just the ones the command references. On a clone with no `origin/HEAD` and no cached default branch, that removes a `git ls-remote` the command never asked for — 13 subprocesses and one remote query down to 8 and none. ([#3628](https://github.com/max-sixty/worktrunk/pull/3628))

### Fixed

- **A branch checked out in a second worktree is retained on removal, `-D` included**: `wt remove` and `wt step prune` now act on the worktree named rather than the branch's first checkout, and all three of `wt remove`, `wt step prune`, and `wt merge` keep the branch while another worktree still has it out — deleting the ref would leave that worktree unable to resolve `HEAD`, which is why `git branch -d` refuses the same delete. The retention is reported and names the surviving checkout rather than passing silently. ([#3533](https://github.com/max-sixty/worktrunk/pull/3533))

- **Removal reports what it took, not what it selected**: A removal's summary and JSON described the plan, so a worktree candidate whose branch was retained still counted as `✓ Pruned 1 branch`, and `wt remove --format=json` reported `"branch_deleted": true` beside a stderr line saying the branch was kept. Execution now returns the branch's fate; `wt step prune` counts executed outcomes (`--dry-run` included), both JSON payloads gained `branch_deleted`, and a declined orphan deletion drops out of the removed list rather than being reported as removed. ([#3633](https://github.com/max-sixty/worktrunk/pull/3633), [#3637](https://github.com/max-sixty/worktrunk/pull/3637))

- **Hook previews expand every variable except `vars.*`**: One `vars.` token disabled expansion for the whole command, so `wt hook show --expanded` and `wt hook <type> --dry-run` printed `{{ branch }}` and `{{ repo }}` raw in a listing whose job is to show the expansion. A preview now substitutes a stand-in that renders each `vars.*` reference back as itself, nested access included, while every other variable expands — and no longer spawns the git read that resolving `vars` required. The listing is also derived from the execution path itself, so a context key added there reaches the preview with no second edit. ([#3635](https://github.com/max-sixty/worktrunk/pull/3635), [#3638](https://github.com/max-sixty/worktrunk/pull/3638))

- **`wt config shell install` reclaims its own legacy wrapper paths**: Fish sources `conf.d` at startup, so a stale `conf.d/{cmd}.fish` was already loaded by the time fish would autoload the `functions/{cmd}.fish` the install had just written — the old definition won and the new wrapper never loaded. Install decided ownership by reading the file, and left anything unrecognized in place. Ownership now comes from the path: `conf.d/{cmd}.fish` and the stranded nushell `{cmd}.nu` candidates are paths worktrunk computes for the command being installed, so it takes them back whole, unread. Only that exact filename is touched — a neighbour under another name is not worktrunk's — and each removal is reported. `wt config shell uninstall` still reads the header, because it takes no `--cmd` and so cannot know the name; it prompts and previews every file first. (Breaking: install now removes a file at those exact paths regardless of its contents.) ([#3602](https://github.com/max-sixty/worktrunk/pull/3602))

- **Command timeouts actually bound wall-clock, and a default branch guessed while the remote was unreachable isn't cached**: A timeout killed only the direct child, so a surviving grandchild held the output pipe open and the call ran on regardless — a 3 s bound measured at 120 s. A timed command now runs in its own process group and the whole tree is torn down on expiry, which fixes every existing bound including the fsmonitor and reap probes. On top of that, nothing in git bounds `git ls-remote` (an unreachable host costs ~127 s per address on Linux), so default-branch detection abandons the query after 10 s and falls back to local inference — without caching the result, so an outage can't make an inferred default branch permanent. (Breaking: because a timed command gets its own process group, Ctrl-C no longer reaches it; the command waits out the remaining bound.) ([#3603](https://github.com/max-sixty/worktrunk/pull/3603))

- **`wt step relocate` no longer strands a worktree in its staging directory**: When worktree A's target was held by worktree B, and B was itself blocked by a non-worktree path without `--clobber`, the dependency loop read the stall as a cycle, temp-moved A into `.git/wt/staging/relocate/`, then failed moving it into the still-occupied target — leaving A at neither its original nor its expected path. A worktree blocked by an immovable occupant is now skipped. ([#3530](https://github.com/max-sixty/worktrunk/pull/3530))

- **Forge CLI failures are classified by response shape, not by the tool's prose**: `tea api` copies the response body to stdout and exits 0, so an HTTP error never tripped the exit-code gate — a Gitea `APIError` body deserialized into `{state: "", total_count: 0}`, indistinguishable from a commit with no CI statuses, while the PR-list path blamed an API change for what was an API error. Failures from `gh`, `glab`, and `tea` are now keyed on the response envelope, and a non-zero exit keeps meaning the tool itself failed; the CLI's own error text is forwarded rather than reworded, so a bad token surfaces as `gh: Bad credentials (HTTP 401)` instead of a suggestion to re-authenticate. `wt config show --full` reports the Azure DevOps CLI extension alongside the other forge tools. ([#3595](https://github.com/max-sixty/worktrunk/pull/3595), [#3597](https://github.com/max-sixty/worktrunk/pull/3597), [#3605](https://github.com/max-sixty/worktrunk/pull/3605))

- **`wt config plugins claude install-statusline` no longer mistakes another tool's statusline for its own**: The check for an existing worktrunk statusline matched the bare substring `wt `, which an unrelated command like `newt status` satisfies — so `wt config show` reported a foreign statusline as worktrunk's, and the installer early-returned "already configured" and refused to install. It now matches the adjacent `list statusline` token pair, so it works whether the binary is `wt`, `git-wt`, or an absolute path. ([#3595](https://github.com/max-sixty/worktrunk/pull/3595))

- **`[list] task-timeout-ms` is removed**: The per-command bound is gone; `[list] timeout-ms` bounds the whole collect phase. A config that still sets it warns, and `wt config update` strips the key — in the top-level, project-scoped, and inline-table forms. (Breaking: the setting no longer has any effect. It was also, until this release, ignored for a command that carried its own bound.) ([#3613](https://github.com/max-sixty/worktrunk/pull/3613), [#3615](https://github.com/max-sixty/worktrunk/pull/3615))

### Documentation

- **`wt remove` documents what retention protects**: The branch-cleanup section's six conditions all ask whether deleting loses work; a branch checked out in a second worktree fails a different test, and the page now says so, alongside a new "Naming a worktree" section on `wt switch`. The FAQ's advice to `git worktree lock` a worktree holding precious ignored data now says what the lock actually buys — it blocks removal, and nothing else; `wt merge` and `wt step push` will still overwrite an ignored file the incoming commits track. ([#3533](https://github.com/max-sixty/worktrunk/pull/3533), [#3601](https://github.com/max-sixty/worktrunk/pull/3601), [#3607](https://github.com/max-sixty/worktrunk/pull/3607))

- **Default-branch detection documents the remote-query bound**: The detection ladder records that the `git ls-remote` step is abandoned after 10 s, that local inference covers the abandoned case, and that this is the one result not cached. ([#3603](https://github.com/max-sixty/worktrunk/pull/3603))

- **`wt step push` records that destination-worktree safety matches git**: Pushing overwrites an ignored file in the destination whose path the incoming commits track, exactly as a `git merge` run there would; the spec now says the match is deliberate. ([#3614](https://github.com/max-sixty/worktrunk/pull/3614))

### Internal

- **skim is back on a current release**: The picker was held at skim 5.1.0 because 5.3.1 regressed the legacy Windows console — every picker PTY test failed on the keyboard progressive-enhancement path. 5.5.0 guards it, so the pin moves forward. ([#3626](https://github.com/max-sixty/worktrunk/pull/3626))

- **Coverage uploads on every `main` commit**: The coverage job shared one concurrency group with a sampled job that held it for ~2 h, so a queued coverage run was usually cancelled by the next merge — 29 of the last 40 `main` commits uploaded no report, which left later PRs comparing `codecov/patch` against a stale base. CI is now split by cadence into three workflows (`ci` every commit, `coverage` every commit keyed per-sha, `affected` sampled), so a merge train can't cancel a queued upload. ([#3608](https://github.com/max-sixty/worktrunk/pull/3608))

- **`/wt-switch-create` creates the worktree by name and cleans up an untouched one**: The shipped Claude skill now creates through `EnterWorktree({name})` for the common case, falling back to a path, which avoids a confirmation prompt on recent Claude Code versions. A worktree created that way and never touched — no changed files, no commits — is removed when the session ends, branch included; anything written into it keeps it, and a worktree entered by path always stays. ([#3636](https://github.com/max-sixty/worktrunk/pull/3636))

## 0.69.2

### Improved
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ filterset = "test(/readme_sync/)"

[package]
name = "worktrunk"
version = "0.69.2"
version = "0.70.0"
edition = "2024"
rust-version = "1.96"
build = "build.rs"
Expand Down
Loading