Skip to content

fix(config): let uninstall succeed when the marketplace is already gone - #4033

Merged
max-sixty merged 5 commits into
mainfrom
fix/uninstall-idempotent
Sep 7, 2026
Merged

max-sixty merged 5 commits into
mainfrom
fix/uninstall-idempotent

Conversation

@max-sixty

@max-sixty max-sixty commented Sep 7, 2026 •

Copy link
Copy Markdown
Owner

wt config plugins codex uninstall fails when run twice. #4019 made both harnesses' uninstall remove the marketplace after the plugin, and plugin marketplace remove exits non-zero when the marketplace is not configured — which is the state uninstall is trying to reach. So the second run reports a failure having nothing left to do. The Claude side has the same shape and is only masked by its is_plugin_installed early return, which returns before the marketplace step.

Measured against the real CLIs, each in a throwaway config dir:

command target absent exit
claude plugin marketplace remove worktrunk Marketplace 'worktrunk' not found 1
claude plugin uninstall worktrunk@worktrunk not found in installed plugins 1
codex plugin marketplace remove worktrunk is not configured or installed 1
codex plugin remove worktrunk@worktrunk reports removed 0

codex plugin remove tolerating an absent plugin is what #4019's "Superseded during review" note established; the marketplace removal beside it does not, which is what this fixes.

run_plugin_removal runs the removal and, when it fails, asks the harness's own config whether the marketplace is still there. The answer is three-way. A config the harness has never written records no marketplaces, so its absence is a confident Some(false) and the command succeeds. A marketplace still present is Some(true), a genuine failure, and the harness's stderr surfaces in the gutter exactly as before. A config that exists and will not read or parse is None, and the error stands there too: a reader that cannot see the marketplace has not established that the removal worked.

That third answer is what keeps the fix from failing open, and it has to rest on the file's shape rather than on parsing alone. A key lookup answers "absent" for any JSON that simply lacks the key, so known_marketplaces.json reshaped the way the installed_plugins.json beside it wraps its map in a version key would parse, miss, and report a confident no — every genuine failure silently reported as success for as long as the shape held. The Claude reader therefore requires every value to be a marketplace object; the Codex reader rules out the one shape available to it, a marketplaces that is not a table, and claims nothing more, since a fresh config.toml legitimately has no such key. Both ask try_exists rather than exists, which reports false for a directory it cannot stat.

The removal runs in every case, so the ? preview stays an accurate list of the commands the uninstall spawns, which test_plugins_claude_prompt_previews_commands pins.

is_marketplace_configured reads known_marketplaces.json for Claude, beside the installed_plugins.json that is_plugin_installed already reads, and Codex's [marketplaces.<name>] table in config.toml.

Tests, and one existing test whose setup changed

Each harness gets five cases, differing only in what the config holds: no config file, a config holding a different marketplace, a config holding worktrunk, a config that will not parse, and a config reshaped so it parses but no longer means what the reader expects. The middle one is the state a second uninstall actually lands in — Codex keeps its model and other settings in config.toml, and a Claude user with any other marketplace still has a known_marketplaces.json — so it, not the missing-file case, is the branch this fix exists for. Mock helpers fail only the marketplace removal, so the cases differ in config state alone, and the fixtures take the file body.

test_plugins_claude_uninstall_second_step_fails needed a configured marketplace added to its setup. Without one it had become the already-absent case, so it would have asserted success while its doc comment says the error surfaces. With the fixture its snapshot is unchanged from before this branch.

Tests drop an ambient CODEX_HOME rather than pinning one, so the Codex config lookup stays inside the temp home the way CLAUDE_CONFIG_DIR already does. Removing it keeps the variable out of every snapshot's env block, where pinning it would have churned each one and needed a new redaction. A separate test then points CODEX_HOME at a directory holding the only copy of the marketplace record, so the lookup has to follow the variable to find it; it asserts on the exit status rather than a snapshot, for that same reason.

The lookups carry no unreachable arms. codex_config_dir does not expand a leading ~/, which was mirrored from claude_config_dir — where the comment explains a literal ~ only arrives when the variable is set outside a shell — and nothing sets Codex's that way here. Both readers fold their early returns into the paths the tests drive: resolving the config dir and reading the file are one and_then, so the missing-file case covers the else arm, and a malformed file falls out of is_ok_and rather than a let ... else of its own.

Local gate: cargo fmt --check, cargo clippy --all-targets --all-features, cargo test --lib --bins, cargo test --test integration (2090 passed), and pre-commit run --all-files.

This was written by Claude Code on behalf of max-sixty

🤖 Generated with Claude Code

`plugin marketplace remove` exits non-zero on both harnesses when the
marketplace is not configured — the state uninstall is trying to reach — so
`wt config plugins codex uninstall` failed on a second run, and the Claude
half would have too but for its `is_plugin_installed` early return.

Uninstall now reads the harness's own config after a failed removal. A
marketplace that is gone is the goal met, so the command succeeds; one that
survives is a genuine failure and still surfaces the harness's stderr. The
removal runs either way, so the `?` preview stays the list of commands the
uninstall spawns.

`is_marketplace_configured` reads `known_marketplaces.json` for Claude, beside
the `installed_plugins.json` that `is_plugin_installed` already reads, and
Codex's `[marketplaces.<name>]` table in `config.toml`. Tests drop an ambient
`CODEX_HOME` so that lookup stays inside the temp home, and
`test_plugins_claude_uninstall_second_step_fails` gains a configured
marketplace, which is what makes its removal failure the genuine kind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nmkMJwiwdvBeB7WY1TsLe

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two-step shape — run the removal, then ask the harness's own config whether the goal state was reached — is the right call, and it's a real improvement over a blanket best-effort. Three things I'd want resolved before this lands.

codecov/patch is red at 90.63% (9 of 96 patch lines missed), and six of those are one branch nothing can reach. The misses are src/commands/config/codex.rs 113 and 115–118 (the whole CODEX_HOME arm of codex_config_dir), 131 and 139 (the no-home and TOML-parse arms), and src/commands/config/show.rs 251 and 260 (the no-home and JSON-parse arms). The CODEX_HOME block is the interesting one: set_temp_home_env now env_removes CODEX_HOME, so by construction no integration test can enter that branch. The PR body argues for removing over pinning on snapshot-churn grounds, which is fair for the existing tests — but it also means the newly added env-var support ships with nothing exercising it. One test that sets CODEX_HOME to a relocated dir (and one that sets it to a ~/-prefixed value) would both close most of the gap and be the only check that the branch works at all. Per CLAUDE.md → Coverage, this gates the merge either way.

The predicate answers "is it configured?" with false for both "definitely not" and "I couldn't tell", and run_plugin_removal turns the second into a reported success. Detail inline on show.rs.

Smaller: codex_config_dir is a near-verbatim copy of claude_config_dir (env var, empty-check, ~/ expansion, home-joined default), differing only in the two names — and, less visibly, in which home_dir it calls. claude_config_dir uses show::home_dir, which prefers HOME then USERPROFILE; the new copy uses worktrunk::path::home_dir. On Windows with a Git-Bash-style HOME that differs from %USERPROFILE% those resolve differently. I think the new one is actually the better match for Codex, which resolves its own home the same way — but that makes it worth stating rather than leaving as an artifact of the copy. A shared config_dir_from_env(var, default_subdir) would collapse both and keep the two harnesses from drifting again.

Comment thread src/commands/config/show.rs Outdated
Comment thread tests/integration_tests/config_show.rs
Comment thread src/commands/config/codex.rs
`codecov/patch` read 90.62% against a 98.26% target, all nine misses in
defensive arms the tests cannot reach.

`codex_config_dir` no longer expands a leading `~/` in `CODEX_HOME`. It was
mirrored from `claude_config_dir`, where the comment explains a literal `~`
only arrives when the variable is set outside a shell; nothing sets Codex's
that way here, so the branch was untested surface.

Both `is_marketplace_configured` bodies fold their early returns into the
paths the tests already drive. Resolving the config dir and reading the file
become one `and_then`, so the missing-file case the absent-marketplace tests
exercise covers the `else` arm, and a malformed file falls out of `is_ok_and`
on a line those tests run rather than a `let ... else` of its own.

A new test points `CODEX_HOME` at a directory holding the only copy of the
marketplace record, so the lookup has to follow the variable to find it. It
asserts on the exit status rather than a snapshot, which would put the
variable's temp path in the snapshot's env block and trip the host-path guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CODEX_HOME gap is closed properly: test_plugins_codex_uninstall_reads_codex_home records the marketplace under the relocated root and nowhere else, so a lookup that ignored the variable would read the failed removal as "already gone" and exit 0 — the assertion discriminates in the right direction, and dropping the ~/ expansion removes surface that had no caller rather than papering over it.

One thing the commit changed that the code doesn't say: codex_config_dir's docstring still claims parity with claude_config_dir, which this commit is exactly what broke. Inline.

Comment thread src/commands/config/codex.rs Outdated
max-sixty and others added 2 commits September 6, 2026 22:05
The previous commit dropped the `~/` expansion but left the docstring
claiming parity with `claude_config_dir`, so the asymmetry read as an
omission. Say it is a decision and why, where someone comparing the two — or
copying this one for a third harness — will see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… gone

`is_marketplace_configured` returned `false` for four different situations —
no home directory, a file that would not read, a file that would not parse,
and the marketplace genuinely absent — and `run_plugin_removal` swallowed the
harness's error on all four. So a config we could not read turned a real
`plugin marketplace remove` failure into `Plugin & marketplace removed` and
exit 0. That fails open on the case that would be silent: if
`known_marketplaces.json` ever grew a wrapper the way the
`installed_plugins.json` beside it has one, every genuine failure would report
success and nothing would notice.

Both readers now answer three ways. A config the harness has never written
records no marketplaces, so its absence stays a confident `Some(false)`. A
config that exists and will not read or parse gives `None`, and
`run_plugin_removal` drops the error only on `Some(false)`, so the harness's
own stderr stands wherever the answer is unknown.

The `tolerates_absent` tests were also exercising the wrong arm: both wrote no
config file at all, which is the missing-file answer, while a second
`uninstall` lands on a config that exists — Codex keeps its model and other
settings in `config.toml`, and a Claude user with any other marketplace still
has `known_marketplaces.json` — with only worktrunk's entry gone. Four tests
now cover that key-absent path and the unparseable path, per harness. The
fixtures take the file body so those three states differ only in what they
write.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Option<bool> shape is the right answer to the last round, and the four-state-per-harness test matrix pins it properly. One thing the new code doesn't actually do, though, is the thing its docstring says it does — inline on show.rs.

Separately, on whether the Claude half reaches the case the PR is named for. handle_claude_uninstall returns early on !is_plugin_installed(), so a genuine second wt config plugins claude uninstall prints Plugin not installed and never gets to the marketplace step; what the new tolerance covers on Claude is the narrower state where the plugin is still recorded but the marketplace was removed by hand. That leaves the partial-failure path with no way out: claude plugin uninstall succeeds, the marketplace removal fails for a transient reason, wt exits 1 — and every re-run now short-circuits at the early return, so the marketplace stays configured and no wt command will remove it. The user's recourse is claude plugin marketplace remove worktrunk by hand. Pre-existing and outside this diff, so not something I'd hold the PR on, but it's the state this fix's own failure path drops users into, and gating the early return on plugin-or-marketplace rather than plugin alone would close it.

Comment thread src/commands/config/show.rs Outdated
The previous commit's `None` arm named a drift case it did not actually
catch. `{"version":2,"marketplaces":{...}}` — the shape the
`installed_plugins.json` beside it already uses — is valid JSON, so the parse
guard passes, `json.get("worktrunk")` misses, and the reader answers a
confident `Some(false)`. `run_plugin_removal` then drops a genuine removal
failure and prints `Plugin & marketplace removed`, which is the fail-open
direction the guard existed to close. The parse guard only ever fired on a
corrupt file, not a reshaped one.

The Claude reader now requires every value to be a marketplace object, so a
reshaped file is `None` rather than a silent success, while the three states
it already answered are unchanged. The Codex reader rules out the one shape
available to it — `marketplaces` present but not a table — and its docstring
now claims only that, since a fresh `config.toml` legitimately has no
`marketplaces` key and a renamed one is indistinguishable from a user with no
marketplaces configured.

Both readers ask `try_exists` rather than `exists`, which reports `false` for
a directory it lacks permission to stat and would put that on the confident-no
side. Written as `try_exists().ok()?` so the unknown case needs no arm of its
own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@max-sixty
max-sixty merged commit 74ad58a into main Sep 7, 2026
46 checks passed
@max-sixty
max-sixty deleted the fix/uninstall-idempotent branch September 7, 2026 06:16
max-sixty added a commit that referenced this pull request Sep 7, 2026
…4034)

`wt config plugins claude uninstall` cannot finish a half-done
uninstall. It returns early on `!is_plugin_installed()`, so once the
plugin is gone the command stops there — and the state where the plugin
is gone but the marketplace is not is exactly what a first run leaves
behind when the plugin uninstall succeeds and the marketplace removal
then fails. Re-running prints `Plugin not installed` and exits 0 with
the marketplace still configured, and nothing in `wt` will remove it.

The early return now requires both halves to be gone, and takes only a
confident `Some(false)` for the marketplace, matching what
`run_plugin_removal` is willing to trust. The plugin step becomes
conditional, and the `?` preview lists it only when it will run, so the
preview stays equal to the set of commands the uninstall spawns — the
property `test_plugins_claude_prompt_previews_commands` pins.

The marketplace removal still runs unconditionally. Gating it on the
config reading `Some(false)` was the first thing I tried, and it is
wrong: it makes `run_plugin_removal`'s tolerance unreachable in the
cases that exercise it, so
`test_plugins_claude_uninstall_tolerates_absent_marketplace` and its
siblings would pass while never attempting the removal they exist to
tolerate. Leaving the removal unconditional keeps that tolerance the
thing that decides whether an absent marketplace is a failure.

`test_plugins_claude_uninstall_removes_marketplace_left_without_plugin`
covers the recovered state: no `installed_plugins.json`, a
`known_marketplaces.json` holding worktrunk, and a run that skips the
plugin step and removes the marketplace. Every other uninstall snapshot
is unchanged, which is what confirms this reaches only the state that
was previously unreachable.

`wt config plugins claude uninstall --help` promised the opposite of the
new behavior — "Skips gracefully if the plugin is not installed, leaving
the marketplace in place" is precisely the state the command no longer
skips — so it now states the real condition. Nothing caught that: the
string is in no generated mirror and no snapshot, so
`test_docs_are_in_sync` and the help snapshots both stayed green while
the sentence described behavior that was gone. Both uninstall pages are
`test_help` cases now, since what each says about skipping depends on
what the harness's config still holds.

The Codex uninstall has no equivalent stranded state, which is
measurable rather than a judgment call: `codex plugin remove` exits 0 on
an absent plugin, so its first step never blocks the re-run from
reaching the marketplace step. `claude plugin uninstall` exits 1 there,
which is what made the `is_plugin_installed` guard necessary on that
side and gave it a state to strand.

Raised as a non-blocking observation on #4033. It is pre-existing rather
than introduced there, but it is in the function that PR was editing.

> _This was written by Claude Code on behalf of max-sixty_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@max-sixty max-sixty mentioned this pull request Sep 8, 2026
max-sixty added a commit that referenced this pull request Sep 8, 2026
Version bump to 0.77.0 plus the changelog section for this release.

**Bump level**: minor. `cargo semver-checks` reports 8 breaking library
changes (removed `DeprecationInfo::has_deprecated_patterns`,
`Repository::recent_commit_subjects`, `Repository::diff_stats_summary`,
`config::detect_deprecations`; arity changes on
`compute_migrated_content` and `Progress::record`). Pre-1.0, so a
breaking change takes a minor bump. `wt list --format=json` defaulting
to schema 2 is a user-facing break on its own.

**Validation**

- `cargo run -- hook pre-merge --yes` — 4831 tests passed, 1 skipped;
clippy, fmt, doc-sync, snapshot and lockfile checks green
- Nightly dispatched on the cut-from tip
(https://github.com/max-sixty/worktrunk/actions/runs/34272636821):
full-tests on linux, macos, windows and git-2.43 all green, alongside
release-target ×3, nix-flake, minimal-versions, crate-build, unused-deps
and link-check

**Changelog**

29 entries. Each was verified against its commit's diff by a separate
pass, which caught two uncovered fixes (#4033/#4034 uninstall
idempotency), one entry in the wrong section (#4018 is a fix, not an
improvement), and three unmentioned machine-visible breaks (`wt list`
JSON `marker`/`state`, `copy-ignored --dry-run` dropping
`files`/`bytes`, the hidden-column footer wording).

One correction to the previous release's notes: the `wt config update
--output` entry was filed under `## 0.76.0` by #4021, which landed after
v0.76.0 was tagged. It moves to 0.77.0, where it actually ships.

**Data-loss surface review**

Four independent finders swept the cumulative diff. The destructive core
is untouched — no changes in `src/commands/process.rs`,
`src/git/remove.rs`, `src/utils.rs`, or `src/commands/remove.rs`. This
release removes a loss path: #3984 deletes the fork-PR `git branch -D`
rollback that force-deleted a pre-existing branch, and its reflog, when
the branch name was already taken.

Three follow-ups, none blocking: `run_plugin_removal` reporting success
over a failed marketplace removal; the documented-not-fixed
`post-commit` window in `wt merge`; and `merge_args_into_command`'s
`args` precondition, now dead code since `drop_unsupported_keys` removes
the key regardless.

> _This was written by Claude Code on behalf of max-sixty_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0134XuJoGCzwoZ21XXbWujdY

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
max-sixty added a commit that referenced this pull request Sep 9, 2026
`wt config plugins claude uninstall` and `codex uninstall` could print
`Plugin & marketplace removed` and exit 0 over a marketplace that was
still configured.

`plugin marketplace remove` exits non-zero when the marketplace is not
configured, which is the state uninstall is trying to reach, so a second
run would otherwise fail with nothing left to do
([#4033](#4033)). To tell
that from a removal that genuinely failed, `run_plugin_removal` asked
the harness's own config file. That reader could not fail loudly: a
record the harness renamed or moved is the same missing key or missing
file as a record it never wrote, so a confident absence was also the
answer for every genuine failure.

For Codex this isn't hypothetical. `codex plugin marketplace remove`
deletes the whole `marketplaces` key along with the last entry under it,
leaving an empty `config.toml`:

```console
$ codex plugin marketplace add max-sixty/worktrunk
$ cat $CODEX_HOME/config.toml
[marketplaces.worktrunk]
source_type = "git"
source = "https://github.com/max-sixty/worktrunk.git"
$ codex plugin marketplace remove worktrunk
$ cat $CODEX_HOME/config.toml
$
```

So an absent `marketplaces` key had to read as "already removed" for a
second uninstall to succeed, and that same reading swallowed every real
failure. Making the reader distinguish "no key at all" from "key
present, worktrunk absent" would just trade the false success back for
the failure it was introduced to fix.

Both harnesses publish their marketplaces as structured output, so this
asks them instead:

```console
$ claude plugin marketplace list --json
[{"name":"worktrunk","source":"github","repo":"max-sixty/worktrunk"}]
$ codex plugin marketplace list --json
{"marketplaces":[{"name":"worktrunk","root":"…"}]}
```

An empty list is the harness saying it holds nothing, which is a
different fact from a file that isn't where wt looked. A harness that
won't answer — the subcommand gone, output that isn't JSON, or entries
that aren't named marketplace objects — yields `None`, and the removal's
error stands. The shape check now fails closed rather than open. The
cost is that a repeat uninstall against a harness with no `--json` loses
its clean exit and shows the harness's own "not configured" message
instead.

Reading Codex's config also meant wt carried its own copy of the
`CODEX_HOME` resolution rule. The child reads the variable itself, so
`codex_config_dir` goes with the reader.

<details><summary>Verification and tests</summary>

Verified end-to-end against the real `claude` (2.1.265) and `codex`
(0.153.4), with a shim that fails only `plugin marketplace remove` and
forwards everything else. With the marketplace really configured both
uninstalls surface the error and exit 1; with nothing configured both
exit 0.

The mock-driven tests move from config-file fixtures to the mock's
`plugin marketplace list --json` stdout, which is now the whole variable
between the cases. Per harness: an empty list and a list naming only
another marketplace both tolerate the failed removal; a list naming
worktrunk surfaces it; and three unusable answers — the subcommand
rejecting `--json`, stdout that isn't JSON, and a list reshaped so it
parses and means something else — each leave the error standing. The
three unusable cases assert the exit status and stderr directly rather
than snapshotting, since they share one outcome that the single-case
tests either side already pin.

Both guards were checked by mutation: making `plugin_marketplace_list`
return an empty list on a non-zero exit fails `case_1_unsupported`, and
dropping the entry-shape `?` in `marketplace_listed` fails
`case_3_reshaped`.

Local gate: `cargo run -- hook pre-merge --yes` — 4832 tests passed,
`cargo fmt`, clippy, and `pre-commit run --all-files` clean.

</details>

> _This was written by Claude Code on behalf of max-sixty_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
worktrunk 0.77.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>## Release Notes


### Improved

- **`wt list` sizes and aligns its columns to the terminal width**: one long branch name no longer sizes the Branch column for every row — it caps at 32 characters and elides, so a narrow terminal keeps its other columns instead of degenerating into a branch list. `Remote⇅` stops holding blank space open in a repo with no remote, and alignment now follows the value type. `--format=json` still carries the whole name. ([#3998](max-sixty/worktrunk#3998))

- **`wt list` names what it hid and marks detached worktrees**: the summary reads `hidden: Path, Commit` instead of counting columns, a detached worktree shows `⊘` rather than borrowing `⚑`, and a prunable row leaves its cells blank instead of showing loading dots that never resolve. ([#3998](max-sixty/worktrunk#3998))

- **`wt list --format=json` gains a `marker` field and stops reporting `detached` twice**: the branch marker set by `wt config state marker` is now readable without parsing it out of `symbols`, and `state` no longer reports a detached worktree as `branch_worktree_mismatch` — the sibling `detached` boolean carries it. ([#3998](max-sixty/worktrunk#3998))

- **`wt list --format=json` now defaults to schema 2**: callers get the envelope with repository metadata and orthogonal per-item facts without configuring `[list] json-schema`. `wt list statusline --format=json` follows the same key. Set `json-schema = 1` to retain the original bare-array format. (Breaking.) ([#4038](max-sixty/worktrunk#4038))

- **`wt config show` fails on a broken config**: it exits 1 on an unreadable or invalid config, an invalid `[list] columns`, or an invalid `approvals.toml`. Unknown keys and deprecations stay warnings and still exit 0. A new `APPROVALS` section counts project commands awaiting approval. (Breaking: it always exited 0 before.) ([#3999](max-sixty/worktrunk#3999))

- **Config migration output names its destination**: `wt config update --output <path>` writes the migration artifact to that file instead of applying it in place, and `--output=-` writes it to stdout. Output mode includes project config when run from a linked worktree. (Breaking: `--print` was removed.) ([#4021](max-sixty/worktrunk#4021))

- **Pi joins the agent integrations**: `wt config plugins pi install` writes an activity hook to `~/.omp/agent/hooks/pre/worktrunk.ts`, so Pi sessions show 🤖/💬 markers in `wt list` like Claude Code, Codex, OpenCode, and Gemini. `$PI_CONFIG_DIR`, `$PI_CODING_AGENT_DIR`, and named `$OMP_PROFILE`/`$PI_PROFILE` profiles are honored. [Docs](https://worktrunk.dev/claude-code/) ([#3594](max-sixty/worktrunk#3594), thanks @adity982, and @ortonomy for the request)

- **`wt config plugins codex install` installs the plugin too**: it previously registered the marketplace and left you to run `/plugins` in Codex yourself. Uninstall follows for both Codex and Claude Code, removing the plugin and then the marketplace, so the two commands are inverses. (Breaking: Codex uninstall previously left an installed plugin alone; Claude uninstall now also removes the marketplace.) ([#4019](max-sixty/worktrunk#4019))

- **`wt step copy-ignored` says whether it reflinked or copied**: the summary now reads `(reflinked, no extra disk)`, `(full copy)`, or a partial count, so the same `29.5 GB` line distinguishes a free copy from one that actually wrote the bytes. `--format=json` gains `reflinked` and `written`; a `--dry-run` plan no longer reports `files`/`bytes`. ([#4025](max-sixty/worktrunk#4025))

- **`wt config update` removes keys its destination cannot hold**: migrating `[select]` or `[commit-generation]` drops keys the new section has no field for, reporting each one. Otherwise the key landed at a path the user never typed and warned on every command, with no way to clear it. ([#3994](max-sixty/worktrunk#3994))

### Fixed

- **A failed `wt switch pr:<n>` against a fork no longer deletes a branch it didn't create**: the rollback force-deleted the branch on any setup error, including the one case where the branch wasn't its own — a `pre-switch` hook or a concurrent session claiming the name in the window after the forge answered. Branch and worktree are now created in one `git worktree add -b`, which writes nothing when the name is taken. ([#3984](max-sixty/worktrunk#3984))

- **Codex on Windows: the activity hooks no longer fail on every event**: each hook led with a bare `bash`, which `cmd.exe` resolves to the WSL launcher rather than Git Bash, so every event raised a `Hook failed` banner. Hooks now run `wt.sh` through a shim that finds Git Bash by path. ([#4008](max-sixty/worktrunk#4008), fixes [#4007](max-sixty/worktrunk#4007), thanks @McNultyyy for reporting and diagnosing)

- **Templates leave `branch` unset in a detached worktree**: aliases and hooks rendered `{{ branch }}` as the literal `HEAD`, which git resolves, so an unguarded template ran against the wrong ref — the reported case ended in `git push origin --delete HEAD`, which git refused. `base` and `target` follow. (Breaking: an unguarded `{{ branch }}` now errors.) ([#4010](max-sixty/worktrunk#4010), fixes [#4009](max-sixty/worktrunk#4009))

- **The OpenCode plugin loads under OpenCode 2**: OpenCode 2's loader dropped the old bare-function export silently, so activity markers stopped appearing. The plugin now exports one object carrying both the v2 `setup` and v1 `server` entry points, and spawns `wt` through `node:child_process` rather than the Bun shell. Re-run `wt config plugins opencode install` to pick it up; the one file covers OpenCode 2 and OpenCode 1.16 or later. ([#4018](max-sixty/worktrunk#4018), thanks @pragmaticivan for the request)

- **`wt step commit --branch` builds its prompt from the worktree being committed**: prompt-building git plumbing ran in the invoking worktree, so with nothing staged there the generated message described none of the changes it committed. `wt step relocate --commit` had the same bug, and `--dry-run`/`--show-prompt` now preview the worktree `--branch` names. ([#3996](max-sixty/worktrunk#3996))

- **LLM commit messages keep diffs for quoted paths**: a file whose name git quotes in the `diff --git` header — any non-ASCII name under the default `core.quotePath` — had its whole diff section dropped from the prompt once the diff exceeded the 100 KB budget. ([#4041](max-sixty/worktrunk#4041))

- **An unparsable user config is reported, not a panic**: `wt config show --format=json` and `--full`, plus `wt step prune`, `relocate`, `eval`, and `for-each`, panicked on a debug build and printed a bare `✗ Command failed` on a release one. They now report `✗ Failed to load config` with the parser's caret diagram. ([#4002](max-sixty/worktrunk#4002))

- **`wt switch --execute` names the directory the program actually starts in**: the `Executing (--execute) @ …` header rendered the worktree the background hooks run in, not the program's own directory — which differs under `--no-cd` or a switch from a subdirectory. The path is omitted when the shell already stands there. ([#4043](max-sixty/worktrunk#4043), fixes [#4042](max-sixty/worktrunk#4042), thanks @yajo for reporting)

- **`wt config plugins claude|codex uninstall` is safe to re-run**: removing an already-absent marketplace reported a failure, and a Claude uninstall that removed the plugin and then failed left the marketplace with no way to clear it. Both now finish a half-done uninstall and succeed when there is nothing left to remove. ([#4033](max-sixty/worktrunk#4033), [#4034](max-sixty/worktrunk#4034))

- **`wt config state marker` set and clear no-op outside a git repository**: they exited 1 with a git error, so an agent plugin running outside a repository printed one every turn (the hook's `|| true` kept the session going). ([#3981](max-sixty/worktrunk#3981), thanks @mahirhir)

- **`wt list` produces output in a bare repo with no worktrees**: it printed nothing at all and exited 0. It now reports `○ No worktrees` with a hint, `--branches` lists the branches, and `--format json` emits the envelope with an empty `items`. ([#3992](max-sixty/worktrunk#3992))

- **`wt list` warns when it can't parse the project config** rather than ignoring it silently; the listing still succeeds. ([#3993](max-sixty/worktrunk#3993))

- **Relayed command output no longer prints ahead of the delayed-stream progress line**, and delayed-stream output routes through anstream so color is stripped consistently when redirected. ([#4037](max-sixty/worktrunk#4037), [#3995](max-sixty/worktrunk#3995))

- **A hook filter that matches nothing names the unconfigured source**, rather than reporting no match without saying which source was empty. ([#3997](max-sixty/worktrunk#3997))

### Documentation

- **`wt hook` says that `post-*` hooks from the two sources run concurrently**, so dependent commands belong in one source; `wt merge` records that `post-commit` can't run when the merge removes the worktree it anchors on. ([#4020](max-sixty/worktrunk#4020), [#4026](max-sixty/worktrunk#4026))

- **`wt step` help pages describe what the commands actually do**: `wt step prune` says it removes br

See merge request: Harmonybrew/homebrew-core!18763
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants