Skip to content

fix(shell): reclaim the legacy wrapper paths instead of inspecting them - #3602

Merged
max-sixty merged 1 commit into
mainfrom
signpath-code-signing
Jul 25, 2026
Merged

max-sixty merged 1 commit into
mainfrom
signpath-code-signing

Conversation

@max-sixty

@max-sixty max-sixty commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

wt config shell install decided whether the file at a legacy location was worktrunk's by reading it — a substring test for the wrapper header, falling back to "every code line looks like an integration line". Anything that didn't match was left in place. Fish sources conf.d at startup, so whatever conf.d/{cmd}.fish defines is already loaded by the time fish would autoload the functions/{cmd}.fish the install just wrote — the stale definition wins and the new wrapper may never load.

Ownership is now the path. conf.d/{cmd}.fish and the stranded nushell {cmd}.nu candidates are paths worktrunk itself computes for the command name being installed, so install takes them back whole without reading them. Only that exact filename is touched: a neighbour under another name is not worktrunk's, and each removal is still reported.

wt config shell uninstall still reads the header, because it takes no --cmd and so genuinely doesn't know the name — it lists the shell-owned directories and has to tell our {cmd}.fish from the user's own files beside it. That is the one place ownership can't come from the path, and it prompts and previews every file before removing it, which install does neither of. is_worktrunk_managed_content's docstring now says so; is_worktrunk_managed_nushell existed only to gate the install-time deletion and is gone.

Audited the rest of the surface for the same principle: the OpenCode plugin (~/.config/opencode/plugins/worktrunk.ts) is already removed by path with no inspection, the Claude plugin delegates to claude plugin uninstall, Gemini installs through gemini extensions install and leaves nothing of ours on disk, and fish is the only shell with a separate completion file — which uninstall already covers. No other gaps.

Data safety

This deliberately widens what install deletes, so it's worth naming: two tests asserted the old conservatism and now assert the new boundary — test_configure_shell_fish_reclaims_conf_d_path (was ..._preserves_user_conf_d_file, from #3589) and test_nushell_install_reclaims_only_the_command_name (was ..._keeps_unmanaged_legacy_file, from #2992). Both put a neighbour file in the same directory to pin that only {cmd}.{ext} is taken. The FAQ's "What can Worktrunk delete?" inventory is updated to describe path-based ownership rather than the old content-marker rule.

This was written by Claude Code on behalf of max

`wt config shell install` decided whether the file at a legacy location was
worktrunk's by reading it — a substring test for the wrapper header, falling back
to "every code line looks like an integration line". Anything that didn't match
was left in place. Fish sources `conf.d` at startup, so whatever
`conf.d/{cmd}.fish` defines is already loaded by the time fish would autoload the
`functions/{cmd}.fish` the install just wrote — the stale definition wins and the
new wrapper may never load.

Ownership is now the path. `conf.d/{cmd}.fish` and the stranded nushell `{cmd}.nu`
candidates are paths worktrunk itself computes for the command name being
installed, so install takes them back whole without reading them. 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 genuinely doesn't know the name — it lists the shell-owned directories and
has to tell our `{cmd}.fish` from the user's own files beside it. That is the one
place ownership can't come from the path, and it prompts and previews every file
before removing it, which install does neither of.

The two tests that pinned the old conservatism now pin the new boundary: a
headerless file at the reclaimed path goes, a differently-named neighbour stays.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

@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.

Holding this for a human on the deletion surface, not because I found a problem — the diff removes the content guards in front of fs::remove_file in cleanup_legacy_fish_conf_d and cleanup_stranded_nushell, which is exactly the class of change the repo's review policy says an agent shouldn't sign off on. I traced it and the blast radius holds up:

  • Bounded to the exact filename. Both cleanups only ever touch a path worktrunk itself computes for the install command — legacy_fish_conf_d_path(cmd) → conf.d/{cmd}.fish, and Nushell.config_paths(cmd) → {autoload_dir}/{cmd}.nu. A neighbour under any other name in the same directory is never a candidate, and the new tests (aliases.fish / helpers.nu) pin that.
  • Uninstall recognition is unaffected. Dropping is_worktrunk_managed_nushell is safe — it had no other callers, and uninstall's nushell scan goes through is_worktrunk_managed_content, whose WRAPPER_MARKER branch ("worktrunk shell integration for") already matches the nushell header, so the marker-based recognition it removed is subsumed.
  • The report path is real. On success each removal surfaces via output/shell_integration.rs as Removed … (deprecated; now using …), which is what the new stderr.contains("deprecated") assertion keys on.

The one thing genuinely worth a human's eye is the deliberate tradeoff the PR already names: a user-authored conf.d/{cmd}.fish — named exactly after the command being installed, no worktrunk marker — is now deleted rather than preserved. The shadowing argument for it is sound (a conf.d definition wins over the new functions/ autoload, so leaving it breaks the install), and running wt config shell install for wt is a reasonable point to claim the wt filename. That's a defensible call, just one for the maintainer to own rather than me. No code changes requested.

@max-sixty
max-sixty merged commit 9645e3e into main Jul 25, 2026
39 checks passed
@max-sixty
max-sixty deleted the signpath-code-signing branch July 25, 2026 21:10
max-sixty added a commit that referenced this pull request Jul 27, 2026
…#3608)

## What prompted this

Getting #3605 green ran into codecov reporting a `base_commit` three
commits
older than the real merge-base. This audits whether our config causes
that.

## The cause

Codecov picks a PR's base by walking back to the newest ancestor that
has a
coverage report. It used the real merge-base for PRs #3480, #3532 and
#3602,
and a stale one for #3603 and #3605. The difference is whether the
merge-base
uploaded a report. **29 of the last 40 main commits did not.**

`ci` had one concurrency group for main pushes, and GitHub cancels the
*pending* run in a group whenever a newer one joins, even with
`cancel-in-progress: false`. So the question is how long a run holds the
group,
and a run isn't done until its slowest job is:

| job | duration on main |
|-----|------------------|
| `fast-checks` | 2 min |
| `code-coverage` | 3-4 min |
| `test (windows)` | 11 min |
| `collect affected coverage (windows)` | 110-129 min |

Each main run held the group for ~2 hours, so nearly every subsequent
main push
was cancelled while queued, taking the 4-minute coverage job with it.
Every
cancelled main run's `updated_at` lands within a second of the next
push's
`created_at`.

The 2 hours is real work, not queue: 2-5s from `created_at` to
`started_at`,
then 108 minutes inside `cargo affected collect` — 4181 tests under
`-C instrument-coverage` with a per-test LLVM profile, ~5 GB of profraw.

## The fix: one workflow per cadence

The three groups of jobs have incompatible needs, and one group was
serving all
of them.

| workflow | cadence on main | why |
|----------|-----------------|-----|
| `ci` | every commit, ~11 min | required gate + fast checks |
| `coverage` | every commit, keyed per-sha | a skipped upload leaves
later PRs on a stale base |
| `affected` | sampled, ~2 h | a DB a few commits old still anchors a
correct superset |

`affected` keeps exactly the grouping it has today, so its sampling is
unchanged and deliberate. It just no longer drags the other two along.

### Scope of the impact

The posted `codecov/patch` check scopes to the PR's own GitHub diff, so
a stale
base did **not** score PRs against other people's lines. On #3605 the
posted
91.66% is exactly `github.rs`'s 11/12, while the stale-base compare
object
reported 64/65 across 13 files. What a stale base costs:

- `codecov/project` reports "compared to \<stale sha\>"
- the patch `auto` target is the stale base's project coverage (0.02pp
here)
- the compare API object widens to `base..head`, which is what made the
  investigation look like silence

Separately, `test`/`lint`/`fast-checks` also stopped completing on main.
Nothing
load-bearing rode on that (they already ran on the PR), but it left
`tend-ci-fix` with nothing to watch, since it doesn't fire on cancelled
runs.

## Two smaller fixes

- `ignore: "**/tests/**"` compiles to `.*/tests/.*` (confirmed against
codecov's validator), which needs a leading directory and so never
matched
`tests/` itself. Inert today since `cargo llvm-cov` reports only `src/`
(verified against a downloaded `cobertura.xml`), but now correct if that
  changes. Now `tests/**`.
- `fail_ci_if_error` gated on `github.repository_owner`, which is the
*base*
repo's owner on a fork PR too, so the soft-fail its comment describes
never
  applied. It keys off the head repo now.

## Docs

The API behaviour was ours to misuse, not codecov's to explain. Three
traps,
all confirmed against the live API:

- `file_report/<path>/` 404s with `coverage info not found` because the
route
swallows the trailing slash into the path. Without it the endpoint
returns
  `line_coverage`.
- `?pullid=N` always compares the PR's **current** head. `?base=&head=`
asks
  about an earlier commit.
- the compare response has no `patch_totals` key, and `.name` is
`{base, head}` rather than a string, so a filename lookup silently
matches
  nothing.

A working recipe already existed in `running-tend`, but that skill is
scoped to
CI. `tests/CLAUDE.md` owns coverage investigation, so the queries go
there and
`running-tend` points at them instead of keeping a second copy.

Re-running the corrected query against #3605's failing commit reproduces
the
miss exactly: `src/git/remote_ref/github.rs:164`, the `gh repo
set-default`
hint, matching what the session eventually found by hand.

## This PR demonstrates it

It changes no Rust at all, only YAML and markdown. Codecov still
reported a
**10-file, 111-line patch** on its first commit, because it based the
comparison on `203603909` rather than the real merge-base `32f380a27`.
Every
main commit in between has no report:

| commit | ci run | report |
|--------|--------|--------|
| `32f380a27` | queued | no |
| `9645e3e13` | cancelled | no |
| `bcd1ffdfd` | cancelled | no |
| `8865f20ab` | cancelled | no |

Every one of those 111 patch lines belongs to somebody else's merged
commit. It
passed at 100% only because those commits are well covered.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: worktrunk-bot <[email protected]>
@max-sixty max-sixty mentioned this pull request Jul 29, 2026
max-sixty added a commit that referenced this pull request Jul 29, 2026
Cuts 0.70.0 — version bump plus the changelog for the 45 commits since
v0.69.2.

**Minor bump, not patch.** `cargo semver-checks` reports 6 breaking
library changes (`set_command_timeout` and `ListConfig::task_timeout_ms`
removed, `WorkingTree::stage` arity changed, three enum variants added
to exhaustive enums). Worktrunk ships breaking library changes freely,
but semver still puts a break at minor while pre-1.0.

## Release validation

- Local gate green on the release commit: 4631 tests, lints, doctests.
- `nightly` dispatched on the cut-from tip (`a27cbd42`) for the full
cross-platform suite — `full-tests` green on linux, macOS, and Windows,
plus minimal-versions, nix-flake, crate-build, and the release targets.

## Data-loss surface review

The cumulative diff was audited against the deletion surface. One
deliberate widening, signed off for this release with follow-ups to
file:

- **#3602** removes the content check from `wt config shell install`'s
legacy cleanup, so `conf.d/{cmd}.fish` and the stranded nushell
`{cmd}.nu` are now deleted by path, unread. Only that exact filename is
touched and each removal is reported, but the deletion is absent from
both `--dry-run` and the confirmation prompt, and the already-configured
path skips the prompt entirely.

Also noted, none blocking:

- The `!path.exists()` check precedes the lock guard on the
`Path`/`Current` removal arm, so a locked worktree whose directory is
absent loses its branch. This already governed the branch-targeted route
in v0.69.2; #3533 unified the other arms onto it. The FAQ's "Neither
`git worktree remove` nor `wt remove` (even with `--force`) will delete
them" is absolute where the behavior isn't.
- On the default background removal path, `ensure_clean` and
`stop_fsmonitor_daemon` swapped order, so the safety gate is now
answered by the live fsmonitor daemon rather than a full re-stat.
Bounded by trash staging with 24-hour retention, and the foreground and
picker paths were already daemon-served.

Net *improvements* to the same surface: shared-branch retention across
remove/prune/merge (#3533), outcome-accurate removal reporting (#3633,
#3637), and the removal of the thread-local command timeout that could
kill in-flight git commands on worker threads (#3615).

## Changelog accuracy

Entries were verified against the actual diffs rather than commit
messages, which corrected several drafts: the prune figures were one PR
stale (~2.9 s → the real ~0.6 s), `wt merge` takes no worktree argument
so it only gained the retention half of #3533, the Azure DevOps report
is behind `--full`, #3608 never touched `nightly.yaml`, and #3601
inverted what the FAQ change actually said. Two omissions were added —
the `install-statusline` foreign-statusline fix (#3595) and the shipped
`/wt-switch-create` skill change (#3636).

> _This was written by Claude Code on behalf of Maximilian_
@max-sixty max-sixty mentioned this pull request Sep 16, 2026
max-sixty added a commit that referenced this pull request Sep 16, 2026
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)
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
worktrunk 0.70.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 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](max-sixty/worktrunk#3617), [#3631](max-sixty/worktrunk#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](max-sixty/worktrunk#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](max-sixty/worktrunk#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](max-sixty/worktrunk#3480), [#3606](max-sixty/worktrunk#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](max-sixty/worktrunk#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](max-sixty/worktrunk#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](max-sixty/worktrunk#3633), [#3637](max-sixty/worktrunk#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. `wt config alias dry-run` shares the renderer, so its help text — which still described the all-or-nothing behavior — was corrected to match. ([#3635](max-sixty/worktrunk#3635), [#3638](max-sixty/worktrunk#3638), [#3639](max-sixty/worktrunk#3639))

- **`wt hook show` no longer prints a bare heading for an empty command list**: A hook type declared as `post-switch = []` has a config entry but no commands, and the section decided it had printed something from the entry rather than from the rows — so it emitted its heading and stopped, and the `(none configured)` line never appeared. Both the user and project sections carried the bug, since the loop and the fallback were duplicated; they now share one renderer that reports whether it wrote any rows. The execution path was already correct: an empty list announces nothing and is omitted from JSON. ([#3641](max-sixty/worktrunk#3641))

- **`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](max-sixty/worktrunk#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](max-sixty/worktrunk#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](max-sixty/worktrunk#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](max-sixty/worktrunk#3595), [#3597](max-sixty/worktrunk#3597), [#3605](max-sixty/worktrunk#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](max-sixty/worktrunk#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 — i

See merge request: Harmonybrew/homebrew-core!15439
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