Repository navigation
feat(config)!: split the Pi and oh-my-pi plugin installers - #4135
Conversation
`wt config plugins pi` targeted oh-my-pi: it wrote a `HookAPI` hook to `~/.omp/agent/hooks/pre/worktrunk.ts`. Pi (earendil-works/pi) is a different agent — it loads `ExtensionAPI` extensions from `~/.pi/agent/extensions/`, has no profile concept, and reads none of the config-root variables the omp installer honors — so one command cannot serve both. - `wt config plugins pi` writes an extension to `~/.pi/agent/extensions/worktrunk.ts`, honoring `$PI_CODING_AGENT_DIR` and nothing else. Verified end-to-end against pi 0.85.1: pi discovers the extension at that path and fires `agent_start` → 🤖, `agent_end` → 💬, `session_shutdown` → clear, with the worktree as cwd. - `wt config plugins omp` takes over the hook installer unchanged, including `$PI_CONFIG_DIR`, `$OMP_PROFILE`/`$PI_PROFILE`, and `$PI_CODING_AGENT_DIR` (all four confirmed against oh-my-pi 18.1.16). - `wt config show` reports PI and OH-MY-PI as separate sections, each detected by its own binary (`pi`, `omp`), so an installed oh-my-pi no longer reads as an installed Pi. BREAKING CHANGE: `wt config plugins pi` installed the oh-my-pi hook. That installer is now `wt config plugins omp`, and hooks already installed at the same path keep working; `wt config plugins pi` installs the Pi extension.
Before the split, `wt config plugins pi uninstall` removed the oh-my-pi hook. Anyone repeating that command now looks at Pi's extension path, finds nothing, and gets a bare "Plugin not installed" — a no-op that reads as a completed removal while their hook sits untouched under oh-my-pi. Name the command that removes it.
…function Splitting the Pi-family installer left `wt config show` with a third copy of the same twenty lines: heading, then installed / outdated / not installed, differing only in the heading and the command that writes the file. Fold OpenCode, Pi, and oh-my-pi onto one renderer they all call. Output is unchanged — the config_show snapshots are untouched.
worktrunk-bot
left a comment
There was a problem hiding this comment.
Four fixes going onto this branch next, plus two things left open. The renderer consolidation in cc9eb96c reads clean and its config show output is byte-identical (the snapshots pass untouched); it also deleted the two lines that were codecov/patch's only misses on the previous head, so that check needs re-reading rather than the arithmetic I'd worked out.
Fixing
The Pi extension sets the "waiting for input" marker on the wrong event. Pi's extension docs say of agent_end: "Pi may still auto-retry, auto-compact and retry, or continue with queued follow-up messages. Use agent_settled for status integrations that need to know Pi will not continue running automatically." With agent_end, an auto-retry or an auto-compact-and-retry flips wt list to 💬 and then straight back to 🤖 while the agent never stopped working — the one distinction the marker exists to make. Pi's own status-integration example, examples/extensions/titlebar-spinner.ts, uses agent_start / agent_settled / session_shutdown: the same three-event shape as this extension. agent_settled has no counterpart in oh-my-pi (zero hits in can1357/oh-my-pi), which is why the hook carried agent_end over and why the divergence is Pi-only.
The FAQ's delete inventory missed omp. "What can Worktrunk delete?" → Other cleanup still reads wt config plugins opencode uninstall / wt config plugins pi uninstall. The create-side table and the "To remove:" line above it both picked up the new command; this bullet is the one CLAUDE.md → Data Safety names as the review checklist for a change to the deletion surface, so it's the one that matters.
A doc comment points at code that moved. opencode.rs's opencode_plugins_dir docstring says the empty-value handling matches "PI_CONFIG_DIR in config::pi" — config::pi no longer reads PI_CONFIG_DIR; config::omp does.
config plugins pi uninstall --help had no snapshot while all three omp pages got one. The comment above those cases in help.rs gives the reason they exist ("The uninstall pages describe when each command skips, which depends on what the harness's config still holds"), and pi uninstall is the one handler in this PR that gained conditional output. Adding the case, plus a line in its after_long_help naming the oh-my-pi redirect, so the page describes the behavior the snapshot pins.
Open
Install has no redirect, and a misfire there is invisible. uninstall got a hint; install is the command that sits in dotfiles, and it flips outright. An oh-my-pi user who reruns wt config plugins pi install now writes ~/.pi/agent/extensions/worktrunk.ts, and wt config show never mentions it — the PI section is gated on which pi, which they don't have. The description's argument that config show routes them ("OH-MY-PI … Plugin outdated") holds for the hook they still need, but the stray file under ~/.pi has no surface at all. The symmetric fix is a hint on install when omp resolves and pi doesn't, which also bears on the deprecation-release question the description leaves open — so not mine to pick.
The default test env pins WORKTRUNK_TEST_{CLAUDE,CODEX,OPENCODE,GEMINI}_INSTALLED=0 but neither PI nor OMP, so config show snapshots fall through to the host's PATH. That was a latent dependency on which omp; this PR adds which pi, and anyone who verified this branch against pi 0.85.1 has pi installed. Pinning both rewrites the recorded env: block in every affected snapshot, which is more churn than the flake warrants without your say.
Pi's extension docs say `agent_end` is not the end — Pi may auto-retry, auto-compact and retry, or pick up a queued follow-up message — and direct status integrations at `agent_settled`, which fires once Pi will not continue on its own. On `agent_end` the marker flips to 💬 and back to 🤖 mid-run, reporting "waiting for input" while the agent is still working. Pi's own status-integration example (examples/extensions/titlebar-spinner.ts) uses agent_start / agent_settled / session_shutdown, the shape this extension already has. oh-my-pi has no `agent_settled`, so dev/omp-hook.ts keeps `agent_end`.
The FAQ's "What can Worktrunk delete?" inventory still listed only opencode and pi uninstall, though the split gave omp its own removal — and that section is the one CLAUDE.md names as the review checklist for a change to the deletion surface. opencode.rs's path-resolution docstring cited `PI_CONFIG_DIR` in `config::pi`, which no longer reads it; `config::omp` does. `config plugins pi uninstall --help` had no snapshot while all three omp pages did, and it is the one handler here that gained conditional output — so its page now describes the oh-my-pi redirect and the snapshot pins it.
|
The re-read of 100 of 103 patch lines hit, 97.1%. All three misses are in Per CLAUDE.md → Coverage that leaves this in the "no deterministic trigger" bucket: it needs your approval before merge rather than a test.
|
`wt config plugins pi install` meant oh-my-pi until the split, so anyone running it out of muscle memory who has oh-my-pi and no Pi now writes an extension nothing on their machine reads. Nothing downstream tells them: `wt config show` gates its PI section on `which pi`, so the stray file has no surface at all. Warn before writing, and name the oh-my-pi installer. The section renderer that check exposed becomes infallible while it is being touched — it writes into a `String`, where `fmt::Write` cannot fail, so returning the section instead of threading `&mut String` and `anyhow::Result` drops an error arm no caller could reach. `test_pi_install_writes_extension` pins both CLIs rather than letting the developer's PATH decide whether the new warning appears in its snapshot.
worktrunk-bot
left a comment
There was a problem hiding this comment.
Both tests that reach the new pi install redirect pass --yes, so the eprintln!() that separates the warning from the install prompt never runs under test — the spacing has no regression pin, and the line would land as a fresh codecov/patch miss on a PR that already needs a coverage decision. Pushing test_pi_install_redirect_precedes_the_prompt: a snapshot of the declined path, in the shape test_opencode_install_prompt_declined already uses, which covers the line and pins the two-message layout.
The deprecation-vs-flip decision in the description is still open.
Both tests reaching the new redirect pass `--yes`, so the blank line that separates the warning from the install prompt never ran under test: its rendering had no regression pin, and the line would have landed as a fresh `codecov/patch` miss. The new case declines the prompt (no `--yes`, piped stdin), the shape `test_opencode_install_prompt_declined` already uses, and snapshots warning, blank, and prompt together.
|
That coverage exemption is no longer needed —
|
worktrunk-bot
left a comment
There was a problem hiding this comment.
d0b22229 is clean — it covers the one branch the earlier heads left uncovered and pins the two-message layout. No findings on it.
One thing this PR has never had named, though, and it is the reason to hold it for a human rather than anything wrong with the code.
The uninstall surface was restructured, and .claude/skills/running-tend/references/review-pr.md → Data-Loss Surface holds a PR that does that. Before this branch, wt config plugins pi uninstall removed $PI_CONFIG_DIR|~/.omp/agent[/profiles/<name>]/hooks/pre/worktrunk.ts. After it, that command removes ~/.pi/agent/extensions/worktrunk.ts and the new wt config plugins omp uninstall owns the old path. Both reach std::fs::remove_file through uninstall_file_plugin in src/commands/config/mod.rs.
What I checked, so the hold is about the decision rather than a suspected bug:
omp.rs'somp_agent_dir/plugin_pathare a faithful move of the pre-splitpi.rslogic — samePI_CODING_AGENT_DIRprecedence, sameOMP_PROFILE/PI_PROFILEhandling, same.ompdefault. A named-profile user's hook is still reachable.uninstall_file_pluginkeys its removal ontarget.exists(), not on a content match, so the rename of the hook source (dev/pi-plugin.ts→dev/omp-hook.ts) does not strand an already-installed old hook —omp uninstallstill removes it.- Each target is a worktrunk-authored file the paired
installregenerates byte-for-byte frominclude_str!, which is the reference's own "everything under its target can be regenerated" exemption.
So the contents test points at exempt, but the command that deletes a user's file changed meaning in the same release that the command that writes one did, and that pairing is exactly what the hold exists to put in front of a person. Requesting @max-sixty.
Both changes the fork carried landed upstream: the Pi/oh-my-pi split (max-sixty/worktrunk#4135 — the bot's version of our branch, so our proposal; upstream closed #4133 by merging it) and the `stdenv.hostPlatform.isDarwin` fix (#4132). Neither is in a release yet: v0.77.0 is still the latest tag and main runs ahead of it under release-please. So the pin is no longer a fork necessity, just a hold until a release ships both — and pinning `main` would re-inherit the unreleased-commit churn the pin exists to avoid. Record that, name BOX-190 as the ticket carrying the removal steps, and fix the branch description now that `pi-omp-split` exists alongside the `feat/` branch. Comment only; no functional change.
Version bump to 0.78.0 plus the changelog section for this release, a comment-preservation fix the release review turned up, a PTY test-filter fix, and merges of `main` (commits that landed during the review, including #4124, #4122, #4141, #4140 and #4145). **Bump level**: minor. `cargo semver-checks` reports 2 breaking library changes — a new `leftover_branch` field on the `GitError::WorktreeCreationFailed` struct variant, and `DEPRECATED_TEMPLATE_VARS` removed. Pre-1.0, so a breaking change takes a minor bump. ## Comment-preservation fix A config save — declining the commit-generation offer, say — dropped the trailing comment on every value it rewrote. That was already true in v0.77.0 for a line the save itself changed (`skip-commit-generation-prompt = false # note` → `true`, reproduced on the v0.77.0 binary). #4080 widened it to lines the command never touched: retired template variables now migrate on every load, so the next unrelated save rewrote any line naming one. #4080's own PR said this "needs a call before merge" and merged without one. - `replace_keeping_decor` keeps the value's decor (spacing after `=`, trailing comment) when `merge_tables` replaces a scalar. Comments *above* a line sit on the key and were never at risk. - `main`'s #4120 independently reworked the inline-table arm and moved a key's leading comment onto the rewritten header, but documented that a trailing comment after the closing brace "is still dropped". `replace_inline_with_table` now carries it after the header's `]`, so both of its callers keep it. - Tests: the existing comment-preservation test goes back to a retired name (the only fixture that exercises the replacement) and adds a comment on the mutated line; a new test snapshots and re-parses an inline `[projects]` entry. Each fails without its half of the fix. Not fixed, and identical on v0.77.0: a save deletes an explicitly written *default* value (e.g. `skip-commit-generation-prompt = false # note`) as a stale key, comment included. ## `--help` correction #4104 moved the OpenCode plugin path to etcetera's `Xdg` strategy, which uses `$XDG_CONFIG_HOME` only when absolute — a deliberate, documented choice — but `wt config plugins opencode --help` still said install mirrors OpenCode's precedence. OpenCode reads a relative value as-is, so the help now says an empty `$OPENCODE_CONFIG_DIR` and an empty or relative `$XDG_CONFIG_HOME` are ignored. ## Data-loss surface review Independent finders swept the full diff, including the five commits merged from `main`. Nothing holds the tag; candidates were adjudicated against v0.77.0 binaries: - **Fixed in this release, reproduced on v0.77.0**: #4104 stops `wt config shell install` (with Nushell) deleting a `nushell/vendor/autoload/wt.nu` under the current directory when `$XDG_CONFIG_HOME` is empty or relative; #4120 stops a save or migration from writing an inline section's comment inside `[commit]` brackets, which made the config unparseable, and from dropping unknown keys. - **`wt step prune --min-age` doesn't apply where no reflog exists** — pre-existing, proven by building the pre-#4077 binary. `git clone --bare` and `git fetch` into `refs/heads/*` write no reflog, so in the bare layout `tips-patterns.md` recommends every branch starts unprotected. Docstring corrected here; behavioral fix is follow-up. - **`wt config update --output <path>` replaces any named file whole**, no existence check or prompt — pre-existing, follow-up. - **`wt config shell install fish --yes` replaces a hand-written wrapper at the XDG path** — accepted ("ownership is the path", predates #3602), newly reachable only for users with `$XDG_CONFIG_HOME` set. The FAQ's delete inventory now cross-references it. - **`--var <retired-name>` binds a name nothing references** after #4080 — inert, no shipped usage; follow-up. - **#4135 migrates nothing**: an existing oh-my-pi hook stays put, `pi uninstall` points at `omp uninstall`. `PI_CODING_AGENT_DIR='~/x'` is used literally (Pi expands `~`) — creates a stray directory, deletes nothing; follow-up. ## Changelog 37 entries; each new or edited entry was re-verified against v0.77.0 until a pass came back clean (six passes in all). Passes caught, among others: the OpenCode `-x` recipe was wrong since 0.76.0, not 0.75.0; #4080's JSON reaches every hook, not just `pre-start`/`post-start`; a quoted git error string git never prints; a copy-button overlap that existed only inside this release; #4104's relative-`XDG_CONFIG_HOME` OpenCode change described as a fix when OpenCode reads that path as-is; and which commands a broken inline section stopped. **Fixed** leads with the data-loss entries. ## Open PRs checked against v0.77.0 - **#4124** fixed a bug new in this release, caused by #4080, and has landed and been merged in here. The hand-rolled template-variable scanner it extends is slated for removal; see its commit message. - **#4112** — no data-loss regression vs v0.77.0; two small non-safety regressions from #4111 (an uncapped auto-staging file list, a narrow `Nothing to commit` refusal under `status.showUntrackedFiles = no`). Not merged, by decision. - **#4122** (merged in) and **#4123** — pre-existing bugs, identical on v0.77.0. ## PTY filter fix `test (macos)` failed once on a shell's failed-`setpgid` diagnostic that landed straight after the capture's trailing `\x1b[0m`, with no newline between, so #4136's line-anchored filter missed it. The filter now also matches right after an SGR escape and keeps the escape; its test gains that shape and fails on the old pattern. ## Validation - `cargo run -- hook pre-merge --yes` — 4930 tests passed, 1 skipped; clippy, fmt, doc-sync, snapshot and lockfile checks green - Nightly on the original cut-from tip (https://github.com/max-sixty/worktrunk/actions/runs/35060617020): green across `full-tests` on linux/macos/windows, `feature-powerset`, all `release-target`s, `nix-flake`, `minimal-versions`, `link-check` > _This was written by Claude Code on behalf of max-sixty_ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
) The Agent Integration page contradicts itself about oh-my-pi. Its capability table marks activity tracking ✓ for oh-my-pi and the Installation section documents `wt config plugins omp install`, but the Activity tracking section below still reads "The Claude Code, Codex, OpenCode, Pi, and Gemini plugins track agent sessions…" and "All five plugins clear the marker when a session ends." Both predate #4135, which split the Pi and oh-my-pi installers and added the sixth column without updating the prose. oh-my-pi does track and does clear: `dev/omp-hook.ts` sets 🤖 on `agent_start`, 💬 on `agent_end`, and clears on `session_shutdown`. Rather than adding a sixth name and bumping the count to six, both sentences now quantify instead of enumerating — "Every plugin tracks…" and "Every plugin clears…". The duplicated host list is what went stale, and the capability table directly above already carries it, so removing it means the next host doesn't have to be added in two places. Quantifying also reads correctly for someone arriving on the `#activity-tracking` deep link from `tips-patterns.md`, who never sees that table. No test: the change is prose. `test_docs_are_in_sync` covers the two generated mirrors, which are regenerated here. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com>
Both changes the fork carried shipped in upstream's v0.78.0 — the Pi/oh-my-pi split (max-sixty/worktrunk#4135, which upstream landed after closing our #4133) and the `stdenv.hostPlatform.isDarwin` fix (#4132) — so the fork pin is gone. `nix-pin` is retired; nothing points at a fork branch again. Pinned to the tag rather than `main`: `main` runs ahead of releases with release-please, and release-please commits, dependabot bumps, and in-flight features all land on it, so `main` pins an arbitrary unreleased commit. `v0.78.0` is upstream's newest *published* release — a `Release v0.79.0` commit exists on `main` but the tag is not pushed, so there is nothing newer to point at. Adds `just update-worktrunk`, which bumps the ref itself instead of re-locking in place. `nix flake update worktrunk` alone would re-resolve whatever ref flake.nix names, which cannot express "newest tagged release" — the ref IS the version selector. The recipe finds the highest semver tag upstream publishes, rewrites the URL if it moved, then locks. The rewrite happens in flake.nix rather than via `--override-input` so the committed file stays the source of truth for the pinned version; an override would bump the lock while flake.nix still claimed the old tag. Two failure modes the recipe guards against, both found by testing: - A bare-substring rewrite also edited prose comments citing an older release, silently changing documentation. Now anchored to the `url = "..."` line. - With no matching url line, the rewrite was a no-op that still exited 0, so the recipe reported success after `nix flake lock` had dropped the worktrunk input entirely. It now fails with the lock intact. The whole pin and the overlay in lib/overlays/worktrunk.nix drop once nixpkgs unstable ships >= 0.78.0. refs BOX-145, BOX-190
worktrunk 0.78.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 - **Hook scripts read renamed context keys**: the JSON piped to every hook drops `worktree`, `repo_root`, `main_worktree`, and `main_worktree_path` in favour of `worktree_path`, `repo_path`, `repo`, and `primary_worktree_path`. Config templates migrate on load, so hooks written with the old names keep working; scripts reading the JSON, `--execute` templates, and `--var` overrides need updating. (Breaking.) ([#4080](max-sixty/worktrunk#4080)) - **`wt config plugins pi` installs for Pi; oh-my-pi moves to `wt config plugins omp`**: `pi install` wrote an oh-my-pi hook, which Pi never loads; Pi users re-run it to get a Pi extension. Existing oh-my-pi hooks keep working, listed as outdated until `wt config plugins omp install`. (Breaking: scripts that run `wt config plugins pi` for oh-my-pi need `omp`.) [Docs](https://worktrunk.dev/claude-code/#oh-my-pi) ([#4135](max-sixty/worktrunk#4135), thanks @ashebanow) - **Interactive prompts no longer open with a blank line**: `wt config shell install`, `wt config plugins claude install`, and the commit-message setup offer in `wt merge` began with one. ([#4059](max-sixty/worktrunk#4059)) - **`wt config show` gives a single `wt config shell install` hint**: an outdated wrapper, a fish wrapper at the deprecated `conf.d` path, and missing fish completions each printed their own hint. The zsh `compinit` snippet is now syntax-highlighted. ([#4059](max-sixty/worktrunk#4059)) - **`wt config update --output <path>` confirms the write**: it now prints `✓ Wrote user config migration @ ~/migrated.toml` where it printed nothing. `--output=-` stays silent. ([#4053](max-sixty/worktrunk#4053)) ### Fixed - **Removal no longer discards untracked files hidden by `status.showUntrackedFiles = no`**: under that setting, a worktree holding only untracked files looked clean, so `wt remove`, `wt merge`, and `wt step prune` removed it without `--force`. Safety checks now always count untracked files, and auto-staging warnings list every file they stage. ([#4111](max-sixty/worktrunk#4111), thanks @Duang777) - **`wt remove` and `wt merge` check the right worktree under an inherited `GIT_DIR`**: run from a `!wt` git alias or a git hook, which pass `GIT_DIR` down, `wt remove` could delete a dirty worktree without `--force`, and `wt merge` could refuse a clean merge. Both now check the worktree they act on. ([#4082](max-sixty/worktrunk#4082), thanks @Duang777) - **`wt merge` no longer deletes a worktree locked with `git worktree lock`**: the merge completes and prints `Worktree preserved (locked)`. Every Worktrunk removal refuses a locked worktree, even with `--force`; run `git worktree unlock <path>` first. ([#4073](max-sixty/worktrunk#4073), thanks @Duang777) - **`wt config update` no longer overwrites a config edited while its prompt is open**: it wrote the pre-prompt snapshot over the newer file and reported success. It now fails with the path and leaves the newer file; re-running previews the current contents. ([#4127](max-sixty/worktrunk#4127), thanks @zach-hammad-vs for reporting) - **`wt step prune` no longer deletes a branch created minutes ago from an older commit**: `--min-age` aged a branch without a worktree by its commit's date, so a new branch off a day-old commit was pruned on the next run. Age now comes from the branch's oldest reflog entry. ([#4077](max-sixty/worktrunk#4077)) - **A comment or blank line above an inline config section no longer breaks the config**: a save or migration that rewrote `commit = { … }` as `[commit]` put that line inside the brackets. `wt switch`, `wt merge`, `wt remove`, and several `wt step` commands then failed with `Failed to load config`; `wt list` warned and ran without the config. Saves also dropped unrecognized keys from inline sections. ([#4120](max-sixty/worktrunk#4120), thanks @zach-hammad-vs for reporting) - **Config saves keep the trailing comment on a value they rewrite**: a save such as answering the commit-generation offer dropped the comment after each value it rewrote, including one after an inline section's closing brace. ([#4138](max-sixty/worktrunk#4138)) - **Ignored project-config keys warn from every worktree**: an unknown key in `.config/wt.toml`, or one that belongs in user config such as `[merge]`, warned only in the main worktree, so `wt merge` from a linked worktree ignored it silently. ([#4145](max-sixty/worktrunk#4145), thanks @ee-prog for reporting) - **`wt merge` skips `post-commit` when it removes that hook's worktree**: before, the hook ran in the primary worktree when the removed one sat inside the repository, and otherwise didn't run. The merge now prints `▲ Skipped post-commit`; move work that must finish there to `pre-remove`. ([#4049](max-sixty/worktrunk#4049)) - **`wt switch` recovers from a removed worktree in a bare repository**: `wt switch` and the picker surfaced git's raw `fatal: Unable to read current working directory` instead, and the removed-directory message lacked `wt switch ^`. ([#4067](max-sixty/worktrunk#4067)) - **`wt config shell install fish` puts the wrapper where fish reads it under a custom `$XDG_CONFIG_HOME`**: the wrapper always went to `~/.config/fish/functions/`, so install reported success while `wt switch` changed no directory. On Windows, the fish completion moves from `%APPDATA%\fish\completions` to the wrapper's directory. ([#4107](max-sixty/worktrunk#4107)) - **`wt config shell install zsh` honours `$ZDOTDIR` only when it is absolute**: an empty or relative value resolved against the current directory, so install appended to a `.zshrc` under the current directory and reported success while the file zsh reads went untouched. A non-absolute value now falls back to `$HOME`. ([#4085](max-sixty/worktrunk#4085)) - **`wt config shell install` for Nushell no longer deletes a `wt.nu` under the current directory**: an empty or relative `$XDG_CONFIG_HOME` resolved against it, so install removed a `nushell/vendor/autoload/wt.nu` beneath it unread, as a stale wrapper. It now falls back to `~/.config`, which also fixes unread `tea` logins that let `wt switch pr:<n>` treat a Gitea host as GitHub. ([#4104](max-sixty/worktrunk#4104)) - **`wt switch --create` names the branch a failed worktree add leaves behind**: `git worktree add -b` writes the branch ref before populating the worktree, so a failure in between left the branch with no worktree, and the next attempt reported a name collision. The error now says how to delete or reuse it. ([#4109](max-sixty/worktrunk#4109), thanks @technicalpickles for reporting) - **`wt config shell install` no longer tells an already-wrapped shell to restart**: reinstalling from a shell with the wrapper loaded, such as after a version bump, printed `↳ Restart shell to activate shell integration`. ([#4059](max-sixty/worktrunk#4059)) - **`wt config shell install` migrates a deprecated `conf.d` fish wrapper when `~/.config/fish/functions` is missing**: before, only `wt config shell install fish` migrated it. `wt config show` now reports that wrapper even when fish isn't on `PATH`. ([#4059](max-sixty/worktrunk#4059)) - **Plugin status and uninstall checks ask the agent CLIs**: `wt config show` could list an installed Claude Code plugin or Gemini CLI extension as not installed, and `wt config plugins claude|codex uninstall` could report success over a marketplace still configured. A repeat Claude uninstall now runs both removals instead of stopping at `Plugin not installed`. ([#4048](max-sixty/worktrunk#4048), [#4054](max-sixty/worktrunk#4054)) - **`wt config plugins opencode install` no longer writes the plugin into the current directory**: with `OPENCODE_CONFIG_DIR` set but empty, it wrote `plugins/worktrunk.ts` there, typically inside a repository, reporting success though OpenCode never saw it. An empty value now counts as unset. ([#4084](max-sixty/worktrunk#4084)) - **The OpenCode plugin clears its activity marker when OpenCode 1 disposes it**: only explicit session deletion cleared it, so a finished session kept its `💬` and `wt list` showed it as active until `wt config state marker clear` was run by hand. ([#4129](max-sixty/worktrunk#4129), thanks @drewdas for reporting) - **`wt config update` migrates `[ci] platform` into a `[forge]` section that only sets `hostname`**: that config, common with GitHub Enterprise and self-hosted GitLab, kept the deprecated key with no deprecation warning. ([#4061](max-sixty/worktrunk#4061)) - **Deprecated keys in an inline `[projects]` entry now migrate**: `"example.com/org/repo" = { merge = { no-ff = true } }` skipped the migration the section form got, so the per-project setting was ignored. ([#4122](max-sixty/worktrunk#4122), thanks @zach-hammad-vs for reporting) - **A deprecated config section the migration can't rewrite now warns**: `select = "not a table"`, or a `[select]` whose `[switch.picker]` destination is already set, was ignored without a message. It now reports `unknown field select (will be ignored)`. ([#4121](max-sixty/worktrunk#4121), thanks @zach-hammad-vs for reporting) - **`wt config update` no longer changes what a template renders**: it could rename `{{ repo_root }}` inside a quoted s See merge request: Harmonybrew/homebrew-core!20435
wt config plugins piinstalled an oh-my-pi hook. It wrote aHookAPImodule to~/.omp/agent/hooks/pre/worktrunk.tsand read$PI_CONFIG_DIR/$OMP_PROFILE/$PI_PROFILE— all oh-my-pi's. Pi (earendil-works/pi) loadsExtensionAPIextensions from~/.pi/agent/extensions/, so a Pi user who ran that command got a file Pi never reads, andwt config showprinted a PI section offwhich omp.This splits them into two commands, because they are two agents:
wt config plugins ompwt config plugins pi~/.omp/agent/hooks/pre/worktrunk.ts~/.pi/agent/extensions/worktrunk.tsdev/omp-hook.ts(HookAPI)dev/pi-extension.ts(ExtensionAPI)$PI_CONFIG_DIR,$OMP_PROFILE/$PI_PROFILE,$PI_CODING_AGENT_DIR$PI_CODING_AGENT_DIRwhich ompwhich piwt config shownow renders PI and OH-MY-PI as separate sections, each gated on its own binary.The
pirename is the breaking partwt config plugins pi installhas meant oh-my-pi since 0.77.0, and it now means Pi. Already-installed hooks keep working — the path is unchanged — so what breaks is the command name in scripts and dotfiles. Both halves of that now say so rather than failing quietly:pi installwould write a Pi extension for someone who has only oh-my-pi, and nothing downstream would mention it: the PI section ofwt config showis gated onwhich pi, which they don't have. It now warns before writing —▲ oh-my-pi is on PATH and Pi is not; for the oh-my-pi hook, run wt config plugins omp install.pi uninstallwould look at~/.pi/agent/extensions/, printPlugin not installed, and exit 0 — a no-op that reads as a completed removal while the hook sits untouched under~/.omp/. It now adds↳ An oh-my-pi hook is installed; to remove it, run wt config plugins omp uninstall.An oh-my-pi user who reruns
pi installdespite the warning is still routed home bywt config show: the hook source text changed in the rename, so the byte-compare misses and the OH-MY-PI section printsPlugin outdated. To update, run wt config plugins omp install.The open decision is whether that's enough, or whether
pishould warn-and-redirect for one release before it changes meaning. The flip is what's implemented here; the deprecation is a small change on top if preferred.Provenance and verification
The first commit is @ashebanow's, from
feat/pi-and-omp-plugin-targets, cherry-picked unmodified — this PR exists because #4133 asked for the implementation and the branch was already there and already verified. @ashebanow, if you'd rather carry this yourself, say so and I'll close it.That verification is the part CI can't do: the commit message records an end-to-end run against pi 0.85.1 (extension discovered at
~/.pi/agent/extensions/,agent_start→ 🤖,agent_end→ 💬,session_shutdown→ clear, worktree as cwd) and oh-my-pi 18.1.16 for all four config-root variables. I checked the static half against pi's extensions doc andcore/exec.ts: the install path is auto-discovered, the import matches, andExecOptionscarriescwd.The remaining four commits are mine. One moves the Pi extension's "waiting for input" marker from
agent_endtoagent_settled, which is what pi's own docs direct status integrations at — underagent_endan auto-retry or auto-compact flipswt listto 💬 and back to 🤖 while the agent never stopped.agent_settledhas no oh-my-pi counterpart, which is why the hook keepsagent_end. The rest are the two redirects above, the FAQ's delete inventory and a staleopencode.rsdocstring the split left behind, and a consolidation:wt config showhad two copies of the same twenty-line "installed / outdated / not installed" section renderer and the new OH-MY-PI section would have made a third, so all three now call one function that differs only in the heading and the install command. Itsconfig showoutput is byte-identical — those snapshots are untouched.Local test run
cargo clippy --all-targets --all-featuresis clean;cargo test --test integrationis 2178 passed / 2 failed andcargo test --lib --binsis 1543 passed / 1 failed. All three failures are sandbox limitations unrelated to this change, incopytests that bind a unix socket (PermissionDeniedonsocket(AF_UNIX, …)) or assert a0644mode the sandbox's umask writes as0664. Snapshots were regenerated withINSTA_UPDATE=always—cargo-instaisn't available in this sandbox.One finding from the review is deliberately not fixed here: the default test env pins
WORKTRUNK_TEST_{CLAUDE,CODEX,OPENCODE,GEMINI}_INSTALLED=0and neitherPInorOMP, soconfig showsnapshots fall through to the host'sPATH. Pinning both insrc/testing/mod.rsrewrites the recordedenv:block in all 1027 snapshots that carry one, which is a separate change from this one. The two snapshots this PR's newpi installwarning would have made PATH-dependent pin the CLIs themselves instead.Closes #4133.