fix(env): scrub GIT_* discovery vars from for-each and --execute fallback - #3400
Conversation
…back Extends the hook-spawn scrub from #3374 to the remaining spawn sites that relocate a user command into a wt-chosen worktree: wt step for-each and the --execute no-integration fallback. Restates the classification once on scrub_git_discovery_env_vars (scrub iff wt chose the child's cwd) and pins all three branches with tests, including alias pass-through. Ref #3373 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
The change looks correct and well-scoped. The gating is exactly right: target_dir is only set by change_directory(), so target_dir.is_some() precisely means "wt chose the child's cwd" — the same condition that governs the hook and for-each scrubs. Both platform variants of execute_command apply it consistently, and the keep-side sites (aliases, commit.generation, internal Cmd plumbing) are left untouched, matching the classification now centralized on scrub_git_discovery_env_vars. I confirmed the "completes the sweep" claim: the fourth relocating site, the background pipeline runner, already scrubs (run_pipeline.rs spawn_shell_command), so hooks / for-each / --execute fallback / pipeline are now uniformly covered. Tests are thorough — both scrub-side regressions plus the alias pass-through pin the keep side.
Not approving only because of the red check below, which isn't this PR's fault.
The failing lint check is pre-existing and unrelated to this change. It's a lychee 404, not a formatting/clippy failure (cargo fmt and cargo clippy both pass). The dead link is https://docs.anthropic.com/en/docs/build-with-claude/claude-code in docs/content/llm-commits.md and skills/worktrunk/reference/llm-commits.md — files this PR doesn't touch — and lint also fails on the base commit on main. The Anthropic docs moved; I verified both the old URL and its redirect target now 404, while https://docs.claude.com/en/docs/claude-code/overview returns 200. That's a separate link-rot fix (main-branch CI), out of scope for this PR.
The Claude Code docs moved; docs.anthropic.com/en/docs/build-with-claude/claude-code now 404s (also failing lint on main). Point at the live docs.claude.com/en/docs/claude-code/overview instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # docs/content/llm-commits.md # skills/worktrunk/reference/llm-commits.md
worktrunk-bot
left a comment
There was a problem hiding this comment.
codecov/patch is failing on this PR, and after investigating I believe it's a structural false positive rather than a real coverage gap — flagging it here and dismissing my auto-approval, since per CLAUDE.md a failing codecov/patch gates merge and shouldn't be rubber-stamped.
What's uncovered: the two new lines in the Unix execute_command — if target_dir.is_some() { scrub_git_discovery_env_vars(&mut cmd); } in src/output/global.rs.
Why it can't be covered on the Linux run: that function ends in cmd.exec(), which replaces the process image before llvm-cov flushes its counters, so the entire Unix execute_command body reads as uncovered — the pre-existing lines above the new ones (current_dir/stdin/stdout/stderr setup) are in the same miss list in the coverage artifact. The new lines just extend an already-uncoverable function.
The path is tested — test_switch_execute_fallback_does_not_inherit_git_discovery_vars drives wt switch feature --execute … as a subprocess and asserts the payload sees empty GIT_DIR/GIT_WORK_TREE. It genuinely exercises the scrub; the subprocess's counters just die with the exec(). On non-Unix the same test drives the spawn variant (which does register), but the coverage run is Linux-only, so codecov never sees that side.
The repo has no coverage-exclusion convention (GRCOV_EXCL / #[coverage(off)] etc.), so there's no in-tree way to mark these lines. Options if you want green: pull the if target_dir.is_some() decision into a small helper that a unit test can call directly (coverable), or ignore codecov/patch here as a known exec-path artifact. Since CLAUDE.md requires explicit maintainer approval to merge over a failing codecov/patch, this is your call.
The change itself still looks correct and well-scoped (my earlier review stands) — this is purely about the coverage gate.
codecov/patch failing — dismissing auto-approval per CLAUDE.md; it's a structural exec-path false positive (analysis in follow-up comment), merge decision is the maintainer's.
Release v0.67.0: version bump and changelog. Highlights since v0.66.0: experimental `wt remove --reap` (#3396), `wt switch -x` opening the picker (#3394), the termimad panic containment in the picker's PR-comments preview (#3408), the `wt config show` SIGTTOU fix (#3327), the `GIT_*` discovery-var scrub for `wt step for-each` and the `--execute` fallback (#3400), and the `wt list` prompt-reserve fix (#3409). Seven commits landed on `main` after the initial cut; `origin/main` is merged back in and the two user-facing ones (#3394, #3411) plus the BY CONTEXT profile table (#3403) are folded into the changelog. Pre-release validation: local pre-merge gate green on the merged tree (4389 tests); nightly cross-platform suite green on the initial cut ([run 29086956095](https://github.com/max-sixty/worktrunk/actions/runs/29086956095)), with the seven post-cut commits each validated by their own PR CI; `cargo semver-checks` reports no breaking changes; data-loss surface review of the cumulative diff (including the drift commits) found one new destructive capability (`--reap`, explicit opt-in, adjudicated acceptable). > _This was written by Claude Code on behalf of max_
commit 550d6641b6e1888b8a0712b637308ee00639e3ff
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Sat Jul 11 03:04:10 2026 -0700
skills(running-tend): drop case-specific example from root-cause section (#3421)
Follow-up to #3414: drops the example paragraph citing PR #3390 / issue
#3389 from the "Weigh the root-cause fix" section. Skills state the
recurrence pattern generically; per-incident references require
institutional memory the reader lacks and age into noise. The rule
paragraph stands alone; the case remains documented in #3414 and the
#3390 discussion.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit 69d6afb1fe7cd441278c02e57941208c3425b3f8
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Fri Jul 10 20:37:21 2026 -0700
fix: restore Claude plugin hooks to conventional hooks/hooks.json (#3418)
## Problem
Since [#3382](https://github.com/max-sixty/worktrunk/pull/3382)
("Claude-scope the shared hooks payload"), the Claude plugin's hooks
stopped loading in Claude Code. Skills and commands still load, so the
plugin looks healthy — but `/hooks` shows none of worktrunk's handlers
and the 🤖/💬 activity markers never update. It fails silently.
Root cause (confirmed by the reporter on Claude Code 2.1.207): Claude
Code discovers plugin hooks by **convention** at `hooks/hooks.json`. It
does **not** honor `plugin.json`'s string-path `"hooks":
"./hooks/claude-hooks.json"` override for plugin loads. The #3382 rename
moved the file off the only path Claude's loader actually reads.
The rename was intended to stop Codex's convention discovery from
surfacing Claude's events
([#3362](https://github.com/max-sixty/worktrunk/issues/3362)). But the
Codex manifest already defined its hooks **inline** (`Some(Inline)`
branch of `resolve_manifest_hooks`), which overrides Codex's convention
discovery — and that inline definition **predates** the rename (verified
against `2677f05^`). So the rename was always redundant for the Codex
scoping, while it broke Claude's discovery.
## Solution
Restore the file to the conventional
`plugins/worktrunk/hooks/hooks.json` and point `plugin.json`'s `hooks`
back at it. `plugin.json` reverts byte-for-byte to its pre-#3382 state.
This does not resurface the #3362 collision: the Codex manifest's inline
`hooks` object keeps Codex off the shared `hooks/hooks.json` regardless
of the filename. The two toolchains now coexist on one file — Claude
discovers it, Codex ignores it via the inline override. The
`test_plugin_layout_is_consolidated` assertions covering the Codex
inline manifest are unchanged and still pass.
Also updates `plugins/worktrunk/CLAUDE.md` (layout diagram + Known
Limitations) and drops the now-reverted #3382 CHANGELOG entry, replacing
it with a Fixed entry (both were in the unreleased 0.67.0, so the net
change is nil).
## Testing
Reproduction is captured in `test_plugin_layout_is_consolidated`
(`tests/integration_tests/config_show.rs`), whose invariant was inverted
from "the Claude file must be Claude-scoped" to "the Claude file must
sit at the conventional `hooks/hooks.json` that Claude Code's loader
discovers." Against pre-fix production it fails (`left:
"./hooks/claude-hooks.json"`); after the rename it passes.
- `test_plugin_layout_is_consolidated` ✅
- `test_claude_hook_commands_parse_in_all_shells` ✅ (reads the renamed
file; shell-integration-tests)
- Full `integration_tests::config_show` module: 136 passed ✅
- `test_docs_are_in_sync` ✅
**Not verified end-to-end from CI** (per running-tend "Don't ship fixes
you can't verify"): the bot cannot drive a live Claude Code or Codex
session to observe the hooks firing. The fix rests on the reporter's
confirmed symptom + repro (symlinking `hooks/hooks.json` →
`claude-hooks.json` immediately restores the hooks) and on Claude Code's
[documented convention
path](https://code.claude.com/docs/en/plugins-reference.md)
(`hooks/hooks.json` in plugin root). The Codex-stays-clean claim rests
on the existing code analysis in CLAUDE.md and the inline manifest
predating the rename, not a live Codex run.
---
Closes #3417 — automated triage
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
commit f2508771d28aa98140c6c56add8769243d5c0a3d
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Fri Jul 10 16:26:20 2026 -0700
skills(running-tend): weigh root-cause fix over lossy config workaround (#3414)
Adds a triage rule to `running-tend` capturing a maintainer-flagged bad
case from the daily review-runs analysis.
## The bad case
In [#3390](https://github.com/max-sixty/worktrunk/pull/3390) (triage of
[#3389](https://github.com/max-sixty/worktrunk/issues/3389)), triage of
the `⚑` `branch_worktree_mismatch` flag shipped a **docs-only** PR
recommending users reconfigure `worktree-path = "{{ branch | basename
}}"`. On [#3390 the maintainer pushed
back](https://github.com/max-sixty/worktrunk/pull/3390#issuecomment-4928586504)
— "why do we drop the prefix?" — then [explicitly asked to log it as a
bad
case](https://github.com/max-sixty/worktrunk/pull/3390#issuecomment-4928926873)
and closed the PR.
Two problems the rule targets:
- **Lossy workaround presented as *the* fix.** `basename` collapses
`alice/foo` and `bob/foo` to `foo`, colliding on one directory, whereas
the default `{{ branch | sanitize }}` keeps them distinct (`alice-foo` /
`bob-foo`). The collision downside only surfaced after the maintainer
challenged it.
- **Jumped to a docs workaround over the root-cause code fix.** The
proportionate fix was teaching the path-match check to tolerate a
dropped namespace prefix, not asking every affected user to adopt a
lossy template. The "docs-only, no risk" framing masked a bad
recommendation — low code risk ≠ good guidance.
## The rule
A new `### Weigh the root-cause fix before shipping a config/docs
workaround` subsection under Issue Triage, placed right after
"Suggesting Aliases for Niche Feature Requests" (which encourages
config-based deflection) as a tempering counterpoint: check whether a
configurable workaround is lossy/foot-gunny, weigh a proportionate
root-cause code fix first, and surface any recommended config's
downsides up front.
## Gate assessment
- **Gate 1 (confidence)**: Critical — directly flagged by @max-sixty
("log this as a bad case"). 1 occurrence is sufficient at Critical.
- **Gate 2 (magnitude)**: Targeted one-subsection addition; evidence bar
met by the Critical maintainer flag.
Already recorded in the [review-runs-tracking evidence
log](https://github.com/max-sixty/worktrunk/issues/3349#issuecomment-4923383823).
Kept as a separate atomic PR from the open cargo-PATH skill note
([#3361](https://github.com/max-sixty/worktrunk/pull/3361)) — different
concern, non-overlapping section.
<sub>Run
[29081037464](https://github.com/max-sixty/worktrunk/actions/runs/29081037464)
· daily review-runs</sub>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
commit abf9155546c84e65a09118f4b82e7a7c545a8d66
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Fri Jul 10 06:10:26 2026 -0700
Release v0.67.0 (#3416)
Release v0.67.0: version bump and changelog.
Highlights since v0.66.0: experimental `wt remove --reap` (#3396), `wt
switch -x` opening the picker (#3394), the termimad panic containment in
the picker's PR-comments preview (#3408), the `wt config show` SIGTTOU
fix (#3327), the `GIT_*` discovery-var scrub for `wt step for-each` and
the `--execute` fallback (#3400), and the `wt list` prompt-reserve fix
(#3409).
Seven commits landed on `main` after the initial cut; `origin/main` is
merged back in and the two user-facing ones (#3394, #3411) plus the BY
CONTEXT profile table (#3403) are folded into the changelog.
Pre-release validation: local pre-merge gate green on the merged tree
(4389 tests); nightly cross-platform suite green on the initial cut
([run
29086956095](https://github.com/max-sixty/worktrunk/actions/runs/29086956095)),
with the seven post-cut commits each validated by their own PR CI;
`cargo semver-checks` reports no breaking changes; data-loss surface
review of the cumulative diff (including the drift commits) found one
new destructive capability (`--reap`, explicit opt-in, adjudicated
acceptable).
> _This was written by Claude Code on behalf of max_
commit bd504f09982f4bababfcb33d649490312fad6819
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Fri Jul 10 04:30:14 2026 -0700
feat(switch): run --execute against the picked worktree (#3394)
commit 4f6f6d8dc28efce94f97d06a6767631e2b5dce14
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Fri Jul 10 04:27:29 2026 -0700
fix(test): tolerate the prune race in picker alt-x branch assertions (#3412)
commit dac0f80608b909b6b0d254b793e046267d193e60
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Fri Jul 10 03:56:40 2026 -0700
feat(config): wt config update pins [list] json-schema while unset (#3411)
Wires the `[list] json-schema` nag into the standard config-update
machinery, so the warning comes with the standard one-command fix.
`wt config update` now pins `json-schema = 1` (the behavior-preserving
choice) when the key is unset, via a new `PendingDefault` variant in
`DEPRECATION_RULES`: applied on the update pass only — the load path
must not pin, or an in-memory `Some(1)` would silence the nag — and
scoped to user config through a `ConfigFileKind` enum threaded through
detection in place of the old string labels. Two guards keep the pin
honest: it stays inert when the system config layer already defines the
key (a user-file pin would override a system-level `= 2` and flip
resolved output), and the nag's hint offers `wt config update` only when
the same detection update runs would actually write the pin — a missing,
unreadable, or malformed user config falls back to naming the manual
setting.
The detection-equals-migration invariant holds with the warning
relocated: the pin's warning fires at the JSON-emitting surface
(`resolve_json_schema`) exactly when update would change the file, while
config load stays quiet (`DeprecationKind::is_pending_default` filters
it, and pin-only configs skip the warning-dedup machinery entirely), so
`wt switch` users never see it. `wt config show` renders the pending
pin's diff — including for empty config files — but keeps its TOML dump:
a pin is additive, unlike a deprecation diff that supersedes the dump.
This departs from the plan reviewed in `design/list-json-v2.md` (#3357),
which deferred the `wt config update` integration to the default flip.
Deliberate tradeoff: users who run update during the window land pinned
on schema 1 and will see the `= 1` deprecation round after the flip; in
exchange, the warning ships with its fixer.
**Testing:** unit tests pin the rule's iff (unset ⟺ update changes the
file), kind scoping (System/Project inert, load pass inert), the
PendingDefault-rule/kind coupling, and placement in existing or implicit
`[list]` sections; integration tests cover both hint variants, the
update flow end-to-end (pin applied, second run clean), `--print`, and
the system-config deferral. The invariant battery runs with an explicit
pin appended so each case exercises only its own rule. Full pre-merge
gate green (4386 tests) plus `--features shell-integration-tests`
clippy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit 18d2bef84b989b4186d8bb1b2a9398d62d22a00e
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Fri Jul 10 03:44:18 2026 -0700
refactor(perf): canonicalize the benchmark / wt-perf system (#3403)
Implements the design proposal from this branch's first commit
(b22aced85, `design/simplify-bench-perf.md` — reviewable there; dropped
from the merge candidate per the `design/` convention). The perf
tooling's emission side was already canonical; this consolidates the
consumption side around one split: capture/harness work in `wt-perf`,
analysis in `worktrunk::trace` with `wt config state logs profile` as
its one CLI surface.
The seven design items:
1. **`wt-perf cache-check` deleted** — its output was the `cache` field
of `logs profile --format=json`. The tend statusline recipe now uses
`logs profile` (its old pipe also fed human-format stderr into a JSON
parser, so it's now correct as well as canonical).
2. **Timeline renderer moved into `worktrunk::trace`**
(`src/trace/timeline.rs`), sharing
`command_label`/`render_table`/`fmt_dur` with the profile so the two
views can't drift (they already had); drops wt-perf's `tabwriter` and
`insta` deps. Durations now render in the profile's fixed-point format.
3. **`Profile.by_context`** (BY CONTEXT table + JSON array):
per-worktree subprocess totals, the one analysis that previously
required trace_processor SQL. The SQL sections in `benches/CLAUDE.md`
and the `trace` module docs are replaced by a `logs profile` pointer;
Perfetto stays for visual critical-path work.
4. **One warm/cold bench runner** (`wt_perf::bench_wt`) owns the
`BatchSize::PerIteration` rationale once, replacing five inline copies.
All runs now assert child exit status (`benches/list.rs` previously
ignored it).
5. **Bench matrix pruned**: `real_repo` keeps only the 8-worktree
variants (scaling shape stays on synthetic `worktree_scaling`),
`cow_copy` deleted (benchmarked a hand-written serial copy against
production rayon — a settled choice), `remove_e2e/first_output` deleted
(duplicate of `first_output/remove`). Ends three gist series with this
PR as the single discontinuity point; saves roughly 15–20 min of the
~80-min daily bench run.
6. **wt-perf CLI tests moved in-package** (native
`CARGO_BIN_EXE_wt-perf`): the dummy `builds.rs` is gone and the nextest
setup script builds only `mock-stub`. Note `cargo test --test
integration` no longer runs these four tests; full-workspace runs still
do.
7. **`parse_config` returns a `SetupConfig` enum** covering `mixed-W-B`;
the two fixture builders share `init_bench_repo`.
**Merge with main (#3401):** the new prune benches and fixtures landed
mid-flight in exactly these files; the resolution routes them through
the consolidated API — `prune-M-U` is a `SetupConfig::Prune` variant,
the pair parsing is one shared `parse_pair`, and `prune-real` keeps its
own cache-managed path (no `--path`, self-repairing). `benches/prune.rs`
keeps its custom `iter_batched` arms deliberately: every variant asserts
the candidate count from stdout, which `bench_wt`'s shape doesn't
express — the exact carve-out the cache-handling docs describe.
Two review passes (correctness + subtraction) then folded in:
`CacheReport` trimmed to its same-context fields (the dropped six
duplicated `Profile` fields or counted cross-context noise the text
report never showed), `render_table` pads by display width (replacing an
ASCII-only invariant the new BY CONTEXT table violated), the timeline's
instant-event row gained test coverage, and Chrome args dropped the
redundant `duration_ms`.
**Decisions surfaced, not taken** (from the review passes): (a) `wt-perf
setup` could become `--persist`-only, dropping the interactive
Enter-to-cleanup path — a UX preference; (b) whether the daily benchmark
cron's gist time series justifies its remaining ~60 min/day is a
cost/value call. Also deferred: measuring whether `skeleton/cold` adds
signal over `skeleton/warm` before cutting it.
Gate green throughout (4374 tests); `clippy --features
shell-integration-tests` clean; `setup`/`timeline` (warm, cold,
chrome)/`logs profile` (text + JSON)/`trace` all driven end-to-end on a
scratch `mixed-4-8` repo.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit fbf380b4471a580256f8da251de0aec9309828e3
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri Jul 10 03:43:43 2026 -0700
chore: bump taiki-e/install-action from 2.82.11 to 2.83.0 (#3405)
Bumps
[taiki-e/install-action](https://github.com/taiki-e/install-action) from
2.82.11 to 2.83.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/releases">taiki-e/install-action's
releases</a>.</em></p>
<blockquote>
<h2>2.83.0</h2>
<ul>
<li>
<p>Support <code>cargo-about</code>. (<a
href="https://github.com/taiki-e/install-action/pull/1924">#1924</a>,
thanks <a
href="https://github.com/ruffsl"><code>@ruffsl</code></a>)</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.11.28.</p>
</li>
<li>
<p>Update <code>martin@latest</code> to 1.12.0.</p>
</li>
<li>
<p>Update <code>kingfisher@latest</code> to 1.106.0.</p>
</li>
<li>
<p>Update <code>biome@latest</code> to 2.5.3.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md">taiki-e/install-action's
changelog</a>.</em></p>
<blockquote>
<h2>[2.83.0] - 2026-07-09</h2>
<ul>
<li>
<p>Support <code>cargo-about</code>. (<a
href="https://github.com/taiki-e/install-action/pull/1924">#1924</a>,
thanks <a
href="https://github.com/ruffsl"><code>@ruffsl</code></a>)</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.11.28.</p>
</li>
<li>
<p>Update <code>martin@latest</code> to 1.12.0.</p>
</li>
<li>
<p>Update <code>kingfisher@latest</code> to 1.106.0.</p>
</li>
<li>
<p>Update <code>biome@latest</code> to 2.5.3.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/taiki-e/install-action/commit/c7eb1735f09259a5035e8e5d44b1406b1cddc0fb"><code>c7eb173</code></a>
Release 2.83.0</li>
<li><a
href="https://github.com/taiki-e/install-action/commit/84d4171800adc3ccfe039969324217bde4ca0f25"><code>84d4171</code></a>
Update changelog</li>
<li><a
href="https://github.com/taiki-e/install-action/commit/21e5d859bea44c4a9c990a48a63fed2203e01d68"><code>21e5d85</code></a>
Support cargo-about (<a
href="https://github.com/taiki-e/install-action/issues/1924">#1924</a>)</li>
<li><a
href="https://github.com/taiki-e/install-action/commit/786ded99bea7500a913f1f19b18bfcfdf782fb97"><code>786ded9</code></a>
Update <code>uv@latest</code> to 0.11.28</li>
<li><a
href="https://github.com/taiki-e/install-action/commit/1c1bbcf779fa11c2ef8cf0bda69bba43465230e6"><code>1c1bbcf</code></a>
Update rclone manifest</li>
<li><a
href="https://github.com/taiki-e/install-action/commit/4e5eb09101d68b0b5f52740d1abc011ae003ac6f"><code>4e5eb09</code></a>
Update <code>martin@latest</code> to 1.12.0</li>
<li><a
href="https://github.com/taiki-e/install-action/commit/dc84fc5f1a7ad878e27656eb3ed173e393c62867"><code>dc84fc5</code></a>
Update <code>kingfisher@latest</code> to 1.106.0</li>
<li><a
href="https://github.com/taiki-e/install-action/commit/cf10e546be4ee50afcfd94a851bcd9eb64cba30d"><code>cf10e54</code></a>
Update <code>biome@latest</code> to 2.5.3</li>
<li>See full diff in <a
href="https://github.com/taiki-e/install-action/compare/v2.82.11...v2.83.0">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 9283179d1176a56985d38f1a3c92a05e6015c68b
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri Jul 10 03:43:38 2026 -0700
chore: bump max-sixty/tend from 0.1.9 to 0.1.10 (#3404)
Bumps [max-sixty/tend](https://github.com/max-sixty/tend) from 0.1.9 to
0.1.10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/max-sixty/tend/releases">max-sixty/tend's
releases</a>.</em></p>
<blockquote>
<h2>0.1.10</h2>
<h3>Improved</h3>
<ul>
<li><strong>The system prompt gains an explicit priority
ordering.</strong> A new <code>Priorities</code> section in the shared
system prompt, loaded by every harness, ranks (1) being pro-social, (2)
making the project excellent, and (3) helping individual users — so when
an individual's workaround and the durable project-level fix pull apart,
the bot foregrounds the durable fix. The triage skill adds a matching
"apply the project lens" step before replying. (<a
href="https://github.com/max-sixty/tend/pull/758">#758</a>)</li>
<li><strong>The <code>tend-outage</code> failure reporter renders every
entry as one table.</strong> Follow-up failures on an open outage issue
now append the same one-row <code>When | Run | Trigger</code> table the
issue body uses, instead of a bespoke one-liner, so a single outage
issue reads uniformly. (<a
href="https://github.com/max-sixty/tend/pull/748">#748</a>)</li>
<li><strong>Both Claude harnesses update to claude-code
2.1.201.</strong> (<a
href="https://github.com/max-sixty/tend/pull/755">#755</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><strong>Bot commits and PRs are attributed solely to the tend
bot.</strong> Both Claude harness actions now set <code>attribution:
{commit: "", pr: ""}</code> in the agent's settings,
emptying Claude Code's auto-added <code>Co-Authored-By</code> commit
trailer and "Generated with Claude Code" PR footer, and the
triage, ci-fix, and review skill templates drop their hard-coded
<code>Co-Authored-By</code> lines. (<a
href="https://github.com/max-sixty/tend/pull/760">#760</a>)</li>
<li><strong><code>tend-mention</code> no longer runs a no-op session on
the bot's own APPROVED review.</strong> The bot's empty-body approval is
terminal, but its <code>pull_request_review</code> event passed the
engagement check (which counts that very review) and spun up a session
with nothing to do. The verify job now skips when the review is the
bot's own approval with an empty body — a bot review requesting changes,
commenting, or approving with body text still fires. (<a
href="https://github.com/max-sixty/tend/pull/749">#749</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/max-sixty/tend/blob/main/CHANGELOG.md">max-sixty/tend's
changelog</a>.</em></p>
<blockquote>
<h2>0.1.10</h2>
<h3>Improved</h3>
<ul>
<li><strong>The system prompt gains an explicit priority
ordering.</strong> A new <code>Priorities</code> section in the shared
system prompt, loaded by every harness, ranks (1) being pro-social, (2)
making the project excellent, and (3) helping individual users — so when
an individual's workaround and the durable project-level fix pull apart,
the bot foregrounds the durable fix. The triage skill adds a matching
"apply the project lens" step before replying. (<a
href="https://github.com/max-sixty/tend/pull/758">#758</a>)</li>
<li><strong>The <code>tend-outage</code> failure reporter renders every
entry as one table.</strong> Follow-up failures on an open outage issue
now append the same one-row <code>When | Run | Trigger</code> table the
issue body uses, instead of a bespoke one-liner, so a single outage
issue reads uniformly. (<a
href="https://github.com/max-sixty/tend/pull/748">#748</a>)</li>
<li><strong>Both Claude harnesses update to claude-code
2.1.201.</strong> (<a
href="https://github.com/max-sixty/tend/pull/755">#755</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><strong>Bot commits and PRs are attributed solely to the tend
bot.</strong> Both Claude harness actions now set <code>attribution:
{commit: "", pr: ""}</code> in the agent's settings,
emptying Claude Code's auto-added <code>Co-Authored-By</code> commit
trailer and "Generated with Claude Code" PR footer, and the
triage, ci-fix, and review skill templates drop their hard-coded
<code>Co-Authored-By</code> lines. (<a
href="https://github.com/max-sixty/tend/pull/760">#760</a>)</li>
<li><strong><code>tend-mention</code> no longer runs a no-op session on
the bot's own APPROVED review.</strong> The bot's empty-body approval is
terminal, but its <code>pull_request_review</code> event passed the
engagement check (which counts that very review) and spun up a session
with nothing to do. The verify job now skips when the review is the
bot's own approval with an empty body — a bot review requesting changes,
commenting, or approving with body text still fires. (<a
href="https://github.com/max-sixty/tend/pull/749">#749</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/max-sixty/tend/commit/bdfbdd40ac9455db7082b8abca040225503e933e"><code>bdfbdd4</code></a>
chore: release 0.1.10 (<a
href="https://github.com/max-sixty/tend/issues/763">#763</a>)</li>
<li><a
href="https://github.com/max-sixty/tend/commit/6e58d2ef510bd51898cbb14dec0ffd09fd5dc28d"><code>6e58d2e</code></a>
fix: attribute bot commits and PRs solely to the tend bot (<a
href="https://github.com/max-sixty/tend/issues/760">#760</a>)</li>
<li><a
href="https://github.com/max-sixty/tend/commit/75240861f0ac25ac54713b77395f36fb3dcb4f1b"><code>7524086</code></a>
prompt: make project excellence an explicit priority (<a
href="https://github.com/max-sixty/tend/issues/758">#758</a>)</li>
<li><a
href="https://github.com/max-sixty/tend/commit/1a505d4649980668d5082440d3465a5c2aca19ff"><code>1a505d4</code></a>
chore(review-reviewers): bump action pin to claude-interactive@0.1.9 (<a
href="https://github.com/max-sixty/tend/issues/754">#754</a>)</li>
<li><a
href="https://github.com/max-sixty/tend/commit/668dce805f035f4b3b14ddb15922f92dbd5b6144"><code>668dce8</code></a>
chore: bump claude_version to 2.1.201 (<a
href="https://github.com/max-sixty/tend/issues/755">#755</a>)</li>
<li><a
href="https://github.com/max-sixty/tend/commit/891df36b1d7ec493733644e616dde6946c639763"><code>891df36</code></a>
fix: skip no-op tend-mention session on bot's own APPROVED review (<a
href="https://github.com/max-sixty/tend/issues/749">#749</a>)</li>
<li><a
href="https://github.com/max-sixty/tend/commit/8bcb1537181c78c252d463e2b0fb8541e61f00af"><code>8bcb153</code></a>
refactor(report-failure): render every outage entry as one table (<a
href="https://github.com/max-sixty/tend/issues/748">#748</a>)</li>
<li><a
href="https://github.com/max-sixty/tend/commit/a28e618205c3f740c9a3b2c9f6a09cea00439156"><code>a28e618</code></a>
chore: regenerate workflows with tend 0.1.9 (<a
href="https://github.com/max-sixty/tend/issues/746">#746</a>)</li>
<li>See full diff in <a
href="https://github.com/max-sixty/tend/compare/0.1.9...0.1.10">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 103e9a70abd418b34969358a8bafceb8c5e2e741
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Fri Jul 10 03:43:30 2026 -0700
chore: update tend workflows (0.1.9 → 0.1.10) (#3413)
Automated nightly regeneration of tend's workflow files, picking up tend
0.1.10.
**tend version:** 0.1.9 → 0.1.10
**Notable changes:**
- Skip a no-op `tend-mention` session when the only trigger is the bot's
own empty-body *approved* review — the review is terminal, so there's
nothing to act on (a bot `CHANGES_REQUESTED`/`COMMENTED` review, or an
approval carrying nits in its body, still fires). This is the
substantive body change in this regen (`tend-mention.yaml`)
(max-sixty/tend#749).
- Attribute bot commits and PRs solely to the tend bot account
(max-sixty/tend#760).
- `report-failure` now renders every outage entry as a single table in
`tend-outage` issues (max-sixty/tend#748).
- Prompt update: make project excellence an explicit priority for the
bot (max-sixty/tend#758).
Compare: https://github.com/max-sixty/tend/compare/0.1.9...0.1.10
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
commit 26f66585228c9aa627fcf6d82a1073c059c1935f
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Fri Jul 10 03:37:13 2026 -0700
fix: contain termimad panic on ragged tables in PR comments (#3408)
## Problem
Running `wt switch` (the interactive picker) crashes with:
```
thread '<unnamed>' panicked at termimad-0.34.1/src/tbl.rs:176:30:
index out of bounds: the len is 7 but the index is 7
Rayon: detected unexpected panic; aborting
```
The panic originates in the `termimad` crate (latest version, 0.34.1 —
no upstream fix available), reached via the picker's PR-comments preview
pane: `picker::prs::render_comment_blocks` →
`md_help::render_table_with_termimad` → `MadSkin::text`.
**Root cause (upstream bug):** termimad's column fitter,
`Table::fix_columns`, panics on a *ragged* table — one where some row
has more cells than the header — when it's rendered at a width too
narrow to fit the widest row. In that case the fitter's `TblFit::new`
fails and it takes an error branch that **skips** padding the short rows
to the column count, then the alignment loop indexes past them
(`cells[ic]` for `ic in 0..nbcols`). Equal-column tables never hit this
at any width; only ragged ones do.
PR/MR comment markdown is untrusted and can contain such a table.
Because the comments pane renders on a **rayon worker**, the escaped
panic aborts the entire process rather than just failing one render.
## Solution
Contain the upstream panic at the single termimad call site
(`render_table_with_termimad` in `src/md_help.rs`) with
`std::panic::catch_unwind`, falling back to the plain preprocessed table
lines so the pane still shows the table's text. This keeps the panic
from escaping to the rayon worker and taking down `wt switch`, and
guards the whole class of termimad table panics rather than one specific
input.
## Testing
Added `test_render_table_ragged_narrow_does_not_panic` in
`src/md_help.rs`, which renders a ragged table (`| Key | Value |` header
over a six-column data row) at a narrow width. Verified it panics at
`tbl.rs:176:30` without the fix (same location as the report) and passes
with it. Full `md_help` suite (26 tests) and `cargo clippy` are green.
---
Closes #3407 — automated triage
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
commit 57b303fda253dc80340133807f6d8833f6b47ec0
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Thu Jul 9 23:29:40 2026 -0700
fix(nix): provide lsof to the flake's test sandbox (#3410)
The nix-flake job has failed on main since #3396:
`test_remove_reap_kills_process` discovers its child via `lsof -d cwd`,
but the flake's `worktrunk-tests` check didn't provide `lsof`, so
`processes_under` fails to spawn it, returns an empty list forever, and
the test's 5s discovery poll panics with `child <pid> never discovered`.
Failing main run:
https://github.com/max-sixty/worktrunk/actions/runs/29066326142.
Three changes:
- `flake.nix`: add `pkgs.lsof` to the test check's inputs, the same
pattern as `git`/`python3`/`procps`. `ps` (procps) already works in the
sandbox, so /proc-based process inspection is available there and the
test should be genuinely exercised rather than excluded.
- `.github/workflows/nightly.yaml`: add `flake.nix`, `flake.lock`, and
`nix/**` to the trigger paths (push and PR gate). A flake change
couldn't previously trigger the nix-flake job that validates it; this PR
relies on the fixed trigger for its own validation.
- `tests/integration_tests/remove.rs`: point the discovery-timeout panic
message at lsof, so the next environment missing it doesn't need
re-diagnosing.
Validation: nix isn't available locally, so the proof is this PR's own
nightly run going green on the nix-flake job.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit 0b413ae3b21bb4976474c1b28e9db94dcd268017
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Thu Jul 9 22:35:47 2026 -0700
fix(list): reserve prompt rows so the table doesn't jump at exit (#3409)
`wt list`'s progressive table sits stable on screen through the loading
phase, then jumps up at an unpredictable moment: the command exits and
the shell prints its multi-line prompt, and the terminal scrolls to make
room. Every other command scrolls too, but there the output scroll and
the prompt scroll merge into one Enter-time event; the progressive
table's delay between paint and exit is what makes the jump read as a
jerk.
The renderer now emits two blank rows below the footer with the skeleton
and moves the cursor back over them. The prompt renders into those
pre-scrolled rows, so the settled table doesn't move at exit; the scroll
happens at Enter-time with the skeleton paint, where scroll is expected.
The exact prompt height is unknowable, but the failure modes are
asymmetric, so a constant works: extra reserved rows are consumed
invisibly by whatever prints next (verified in tmux: a 1-line prompt
leaves scrollback byte-identical to before), while a prompt taller than
3 rows scrolls by just the difference. Two rows absorb fish's default
(1), starship's default (2), and tide/powerlevel10k (typically 3).
Details:
- The viewport budget goes from `h − 4` to `h − 6` so a bottom-pinned
skeleton plus reserve can't scroll its own header off, which would break
the `MoveUp` redraw math. Short terminals show two fewer skeleton rows;
the overflow finalize still prints the full table at the end.
- The overflow finalize path re-emits the reserve after its reprint.
- The resting cursor position (line after the footer) is unchanged, so
the in-place update code needed no changes.
- The PTY test harness now exposes the final cursor position; the
overflow test asserts the cursor rests two reserved rows above the
bottom, and the fast-command test pins the resting position (catching an
emit-without-`MoveUp` mismatch).
Verified end-to-end in tmux (fish, 3-row prompt, full screen, 11
worktrees): the header row stays fixed across run, exit, and prompt,
where the released binary jumps two rows. The error path (post-table
error blocks printed after finalize) consumes the reserve and keeps
today's behavior, which is fine: new content appearing at completion
legitimately grows the screen.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit 958b3082de22ad9f518eec7aab78bd9e4508b7f5
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Thu Jul 9 22:08:33 2026 -0700
fix(env): scrub GIT_* discovery vars from for-each and --execute fallback (#3400)
#3374 scrubbed inherited `GIT_*` discovery vars from user-hook spawns,
but two more spawn sites relocate a user command into a wt-chosen
worktree and had the same bug: `wt step for-each` and the `--execute`
payload when wt executes it directly (no shell integration — including
every `!wt`-alias invocation, which bypasses the wrapper). Git resolves
`GIT_DIR`/`GIT_WORK_TREE` before walking up from the cwd, so the
inherited value silently overrode the worktree wt placed the command in:
`git w step for-each -- git rev-parse --show-toplevel` from a linked
worktree printed the invoking worktree's path for every iteration while
the headers named each worktree. The linked-worktree alias case is the
common trigger — git exports an absolute `GIT_DIR` pinned to the
invoking worktree's private gitdir there (verified on git 2.54; from a
main-worktree root it exports none).
The rule is now stated once, on `scrub_git_discovery_env_vars`: **scrub
exactly when wt chose the child's cwd** (hooks, for-each, the
`--execute` fallback); keep when the command runs in the user's own
context (aliases, `commit.generation`, and wt's internal plumbing, which
keeps the absolutize-and-forward behavior from #1914). A CLAUDE.md rule
points there, mirroring the `CommandTrace` any-new-spawn-site
convention.
Testing: regression tests for both new scrub sites (each validated by
reverting the fix and watching it fail) plus an alias pass-through test
pinning the keep side. The `--execute` test is cross-platform, so
Windows CI drives the non-unix `spawn` variant and unix the `exec`
variant; the for-each test follows the file's unix `sh -c` precedent.
Ref #3373 (already closed by #3374; this completes the sweep).
> _This was written by Claude Code on behalf of max-sixty_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit 5e58827a2858972080d47840b649ab70852bfaf7
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Thu Jul 9 21:35:28 2026 -0700
bench(prune): rust-scale prune fixture, criterion benches, and removal trace spans (#3401)
Makes `wt step prune` / `wt remove` staging performance measurable and
reproducible. Users report prune taking many seconds while the synthetic
benches scan in ~150 ms; this adds the observability and fixtures that
reproduce the gap and attribute it. No `wt` runtime behavior changes —
the `src/` diff is ~29 lines of trace spans.
## What a reviewer needs
- **Trace spans** (`src/commands/step/prune.rs`,
`src/commands/process.rs`, `src/git/fsmonitor.rs`): `prune-gather` /
`prune-scan` / `prune-check:<ref>` / `prune-remove:<label>` plus
`internal-sweep` around `wt remove`'s end-of-command janitor. A
`prune-remove` span starts before the write-lock acquisition, so it
reads as "how long this removal stalled the run" (documented in
benches/CLAUDE.md).
- **Fixtures** (`tests/helpers/wt-perf/src/lib.rs`, the bulk of the
diff): `prune-M-U` — M squash-merged candidate pairs
(content-integrated, the post-PR-squash shape) against a
two-sided-diverged backdrop forked across history; `prune-real[-M-U]` —
the same shape on a rust-lang/rust clone, default 12+24 → 36 linked
worktrees. The real fixture is cache-managed under `target/bench-repos/`
(~15 GiB, minutes to build): `ensure_prune_real_repo` classifies it
Intact/Consumed/Broken on reuse, re-creates candidates consumed by a
live prune in ~1 min (round derived from the surviving squash commits,
no sidecar state), and heals invalidated worktree indexes.
- **Benchmarks** (`benches/prune.rs`): criterion groups `prune_e2e`
(dry-run cold/warm + live with per-iteration candidate re-creation) and
`prune_real_repo` (warm dry-run only). The real group is gated behind a
new `real-repo-benches` cargo feature so the nightly benchmarks workflow
— plain `cargo bench` on a hosted runner — never builds a fixture bigger
than the runner's disk and the actions cache cap.
## Measured (M-series Mac, in benches/CLAUDE.md)
Live prune on the rust-scale fixture: **~12 s wall** — a 5.4 s stat-cold
scan (36 `git status` at ~4.5 s each, absorbed by the rayon pool) plus
24 removals serialized under the scan write lock (~0.5–1.7 s per
worktree candidate). Warm re-scan: 0.25–0.8 s. This reproduces the
reported "prune takes seconds" experience and points optimization at
status cost and removal overlap, not the integration probes.
Rider fix picked up en route:
`docs.anthropic.com/en/docs/build-with-claude/claude-code` now 404s
(failing lychee on every push, including on main), so the llm-commits
installation link and the `wt config plugins` install-hint now point at
`code.claude.com/docs/en/setup`.
Also fixes a latent fixture bug: `history_spread_shas` divided by its
hardcoded 5000-commit cap, so on short synthetic histories every
"history-spread" fork silently collapsed to the tip.
## Testing
wt-perf unit tests cover the fixture lifecycle (state classification,
derived repair rounds, squash content-integration); every
`ensure_prune_real_repo` path (build, cached, repair, index-heal,
foreign cwd) was exercised end-to-end locally, as were both criterion
groups and the one-shot timelines. An integration test asserts the `wt
remove -vv` trace surfaces the fsmonitor sweep (span plus daemon count),
pinning the sweep's observability contract. The rust-scale numbers are
I/O-bound and ambient-load-sensitive — documented as shape, not
thresholds.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit c8de0d872a90f318d98e9303a0d44fed3d9358aa
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Thu Jul 9 20:12:03 2026 -0700
ci: move lychee link check to the nightly workflow (#3402)
Moves the lychee link check out of PR CI into the `nightly` workflow.
Link health depends on external sites (429s, bot-blocking, link rot),
which made `lint` the flakiest PR check, and breakage isn't correlated
with the PR that trips it: the most recent main run's lint job went red
on the docs.anthropic.com restructure (fixed in #3399) with no code
change involved.
The hook stays defined once in `.pre-commit-config.yaml`, moved to the
`manual` stage, so the PR `lint` job, local `pre-commit run
--all-files`, and the `wt merge` gate all skip it (the wt.toml Windows
conditional existed only to skip lychee and collapses). The new
`link-check` nightly job runs that stage with the pinned lychee and is
wired into `create-issue-on-nightly-failure`, so breakage lands in the
nightly-failure issue for tend instead of blocking unrelated PRs.
Running through pre-commit rather than `git ls-files | xargs lychee`
keeps file selection at one definition: pre-commit's `types: [file]`
filter excludes the 16 tracked `.md`/`.txt` symlinks, which would
otherwise false-positive on relative links resolved against the
symlink's directory.
The trade: a PR that introduces a genuinely wrong URL now merges green
and is caught up to a day later, asynchronously.
Verified locally that the default stage no longer selects the hook and
that `pre-commit run lychee-system --all-files --hook-stage manual`
reproduces the CI lint run's selection exactly (same two flagged
inputs). The nightly job itself first runs on the next cron or a
`workflow_dispatch`.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit 9b2cbc6a8c2852a30e7fff393f8343f3558c10ed
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Thu Jul 9 19:42:57 2026 -0700
docs(llm-commits): fix 404 Claude Code docs link (#3399)
## Problem
The `lint` CI job (its `lychee-system` link-check step) fails on the
default branch:
```
[404] https://docs.anthropic.com/en/docs/build-with-claude/claude-code | Rejected status code: 404 Not Found | Followed 2 redirects: --[301]--> platform.claude.com/docs/en/docs/build-with-claude/claude-code --[307]--> platform.claude.com/docs/en/build-with-claude/claude-code
```
Anthropic migrated its docs and the old `build-with-claude/claude-code`
path now redirects to a dead `platform.claude.com` URL. The link was
green when it last ran on `main`; the upstream migration broke it since,
so the next push to the default branch fails `lint`.
## Solution
Point the link at
`https://docs.anthropic.com/en/docs/claude-code/overview` (verified
`200`), which is the same URL form already used elsewhere in the repo.
The link lives in `docs/content/llm-commits.md` (the primary source for
this non-command doc); `skills/worktrunk/reference/llm-commits.md` is
the generated mirror, regenerated via `test_docs_are_in_sync`.
## Testing
- `curl -sIL https://docs.anthropic.com/en/docs/claude-code/overview` →
`200`.
- `cargo test --test integration test_docs_are_in_sync` passes (source
and skill mirror in sync).
- No `build-with-claude/claude-code` occurrences remain under `docs/` or
`skills/`.
---
Separated from the [PTY-flake CI fix
(#3398)](https://github.com/max-sixty/worktrunk/pull/3398) to keep each
concern atomic. Surfaced while fixing [run
29053905721](https://github.com/max-sixty/worktrunk/actions/runs/29053905721).
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
commit 1d46c0083b6680f22a1e6306b38e326ef9b7183c
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Thu Jul 9 19:42:48 2026 -0700
fix(test): treat Linux PTY EIO-on-close as EOF in read helpers (#3398)
## Problem
The `code-coverage` job failed on [run
29053905721](https://github.com/max-sixty/worktrunk/actions/runs/29053905721)
(default branch, commit f593438) with a panic in a PTY test:
```
thread '...test_branch_name_with_dashes_underscores::case_3' panicked at tests/common/pty.rs:44:41:
```
Line 44 was `reader.read_to_string(&mut buf).unwrap()` in the Unix
branch of `read_pty_output`. Only `case_3` (fish) panicked;
`case_1`/`case_2` (bash/zsh) passed in the same run — the flaky
signature.
**Root cause:** On Linux, a `read` on a PTY master returns `EIO` once
the child exits and closes the slave side, instead of the clean 0-byte
EOF macOS returns. `read_to_string` propagates that `EIO` as an
`io::Error`, and `.unwrap()` panics. Whether the read observes the `EIO`
or a clean EOF depends on whether it raced ahead of or behind the
child's exit — hence the intermittent, per-case failure.
This is the Linux face of the same fragile read that #3144 diagnosed as
a macOS read-to-EOF timeout; that issue explicitly flagged a more robust
read in this path as the escalation if the flake recurred.
## Solution
Extract a shared `read_pty_master_to_string` helper that reads to
end-of-stream and treats `EIO` as EOF on Unix (a plain `read_to_string`
on Windows/ConPTY, behavior unchanged). Route both PTY-master reads that
shared the fragile `read_to_string().unwrap()` pattern through it:
- `read_pty_output` (`tests/common/pty.rs`) — the shell-wrapper and
README-example PTY path.
- `execute_shell_script` (`tests/common/shell.rs:108`) — the e2e-shell
path, which had the identical pattern and the same latent flake.
`libc` (already present transitively via `portable-pty`) is added to
`[dev-dependencies]` for the canonical `EIO` constant.
## Testing
- `cargo test --test integration --features shell-integration-tests` for
`test_branch_name_with_dashes_underscores` and
`test_source_flag_forwards_errors` (bash/zsh/fish cases) — all pass.
`case_4` (nu) is only skippable locally because nushell isn't installed
in the CI-fix sandbox; it fails at *spawn*, not the read path, and
passed in the original CI run.
- `e2e_shell::*` (7 tests exercising `execute_shell_script`) — all pass.
- `cargo clippy --tests --features shell-integration-tests` and `cargo
fmt --check` — clean.
---
Automated fix for [failed
run](https://github.com/max-sixty/worktrunk/actions/runs/29053905721)
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
commit 745f6a90df0b15f437983493ad4e6ef58980bc63
Author: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Date: Thu Jul 9 18:11:48 2026 -0700
refactor(hook): share command-label rendering with approvals surfaces (#3397)
Follow-up to #3380: `wt hook show` still hand-built the `{phase} name:`
command label that `ApprovableCommand::label()` renders for the approval
prompt and `wt config approvals`. This extracts the format into a
`command_label` free function in `project_config.rs` and calls it from
both. It's free-standing rather than a method on `ApprovableCommand`
because hook show also labels user hooks, which are never approvable.
Output is byte-identical — no snapshot changes.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
commit f593438e7c1b4898c82ed222e235f6d116974408
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Thu Jul 9 15:13:46 2026 -0700
feat(remove): add experimental --reap to kill worktree processes (#3396)
Implements the design agreed in #3365: an opt-in, experimental flag on
`wt remove` that reaps processes left running in the worktree.
## The name
`--reap` — it's the term the whole thread and the issue title already
use, it's concise (matching worktrunk's flag style), and the user-facing
messages read naturally (`◎ Reaping 2 processes under feature
worktree`).
## What it does
```console
$ wt remove --reap feature
◎ Reaping 2 processes under feature worktree
┃ 51234 node
┃ 51240 esbuild
✓ Reaped 2 processes
◎ Removing feature worktree & branch in background (same commit as main, _)
```
Processes are discovered by working directory (`lsof -d cwd`): any
process whose cwd is at or under the worktree path. Termination reuses
the existing `SIGTERM`→wait→`SIGKILL` escalation (`escalate_terminate`,
shared with the fsmonitor sweep).
## Data-safety posture
Killing a process the user didn't mean to kill — a terminal editor with
unsaved buffers — is exactly the silent loss-of-work the project refuses
without consent, so two guards keep `--reap` conservative:
- **Controlling-terminal exclusion.** A process holding a controlling
terminal (an interactive shell, or `vim`/`nvim`/`emacs -nw`) is never
reaped (`ps -o tty=`). Only detached processes — the dev servers and
watchers this issue is about — remain candidates. This also spares the
shell `wt remove` was run from.
- **Self-exclusion.** The current `wt` process is never a candidate.
The flag itself is the explicit opt-in; the list is printed before
signalling for transparency.
## Scope / limitations (matching the #3365 discussion)
- **Under-inclusive by design.** cwd discovery misses a daemon that
forked and `chdir`'d away, or one that reparented to `init` — they no
longer report a cwd under the path. Those are what [`wt step
tether`](https://worktrunk.dev/step/#wt-step-tether) is built to reap
(whole process group). `--reap` and `tether` cover different gaps and
are complementary, not substitutes — the docs say so.
- **Ordering.** Reaping runs before the worktree directory is
staged/renamed (cwd matching needs the directory in place), so it's
independent of foreground/background removal, trash-vs-delete, and
`--force`.
- **Unix only.** Windows has no cheap per-process cwd; `--reap` is
rejected there with a clear error.
## Tests
- Pure parsers for `lsof`/`ps` output (`parse_lsof_cwd`,
`parse_ps_tty`).
- End-to-end against the real `lsof`/`ps`: spawns a child with a cwd
under a tempdir, asserts `processes_under` discovers it, asserts the
controlling-terminal guard keeps-or-drops it in agreement with the
child's *actual* TTY state (so the test is host-independent — CI has no
TTY, a dev box does), then reaps it and confirms `SIGTERM`.
- CLI snapshot (`test_remove_reap_no_processes`) covering the
no-candidates path, deterministic whether or not `lsof` is installed on
the runner.
Help text, `docs/content/remove.md`, and the skill reference mirror are
regenerated and in sync.
One thing worth a maintainer's eye: I chose to print-then-signal rather
than add an interactive confirm/`--dry-run` in this first cut — the
opt-in flag + TTY exclusion + printed list felt like enough for an
experimental flag, and a confirm step is easy to layer on if you'd
prefer it.
Closes #3365.
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
commit d96eedb747d315949a6d884280010bb2b88a83b0
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Thu Jul 9 12:48:51 2026 -0700
refactor(test): extract truncate_details_section helper (#3393)
Follow-up to #3328, per @max-sixty's
[request](https://github.com/max-sixty/worktrunk/pull/3328#discussion_r3554171596).
`normalize_report` in `tests/integration_tests/diagnostic.rs` had three
near-identical truncation blocks — for the **Environment variables**,
**Performance profile**, and **Trace log** `<details>` sections. Each
did the same find-summary → find-`</details>` → splice-placeholder
dance, differing only in the section title and placeholder string.
This extracts a `truncate_details_section(&mut result, title,
placeholder)` helper and collapses all three call sites to one-liners.
Net −9 lines, and adding a fourth truncated section is now one call
instead of a copied block.
Behaviour is unchanged: the existing
`test_diagnostic_report_file_format` snapshot (which runs
`normalize_report`) and `test_diagnostic_includes_environment_variables`
both pass without regenerating the snapshot, confirming the helper
produces byte-identical output to the three inlined blocks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
commit adb417ede817273d51ac5736924f7dc2bb41f5ff
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Thu Jul 9 12:23:20 2026 -0700
fix(config): keep `wt config show` from suspending on the zsh compinit probe (#3327)
## Problem
After upgrading 0.56.0 → 0.63.0, `wt config show` suspends instead of
running:
```
$ wt config show
[1] + 3071417 suspended (tty output)
```
`suspended (tty output)` is SIGTTOU: a process in a **background**
process group touched the controlling terminal. The reporter's `wt -vv
config show` shows the suspend landing right after the log-path header
and before any config content — i.e. the pager (`less`) is being
suspended on its first terminal operation.
## Root cause
`wt config show` runs an interactive zsh probe to detect whether
`compinit` is configured (`check_zsh_compinit_missing`, mirrored by
`shell::detect_zsh_compinit`). An interactive `zsh -ic` with a
controlling terminal enables job control: it `setpgid`s itself into a
new process group and `tcsetpgrp`s to claim the terminal **foreground**.
[#3165](https://github.com/max-sixty/worktrunk/pull/3165) (shipped in
0.62.0, inside the reporter's upgrade window) added a 2s
**kill-on-timeout** to that probe. When the user's interactive rc is
slow to start or blocks on a prompt (e.g. compinit's
insecure-directories prompt), the probe is **SIGKILLed before it
restores the foreground process group**. SIGKILL can't be caught, so
zsh's terminal-restore-on-exit never runs — the terminal foreground is
left pointing at the dead probe's group, and `wt` is now a background
process group. The pager `wt config show` spawns moments later then
raises SIGTTOU on its first `tcsetattr`, suspending the command.
This is environment-specific: it only fires when the probe actually
times out (slow/prompting interactive rc), which is why it doesn't
reproduce for everyone.
## Solution
Pass `+m` (disable job control) to both interactive probes. With job
control off, the probe never grabs `wt`'s controlling terminal, so a
timeout-kill can't strand `wt` in a background process group. `+m`
doesn't affect compinit/`compdef` detection — it only disables terminal
foreground management the probe never needed.
## Testing
Verified at the OS level under a PTY, replicating the probe's exact
spawn (interactive `zsh -ic`, stdin/stdout/stderr redirected, slow rc,
SIGKILL on timeout):
- **Unpatched:** `tcgetpgrp` of the controlling terminal moves to the
probe's process group and **stays there** after the kill (foreground
stolen) — the next terminal op suspends with SIGTTOU.
- **With `+m`:** the terminal foreground is **intact** after the kill,
and `wt config show` reaches the pager normally.
Added regression guards (`test_compinit_probe_disables_job_control`,
`test_zsh_probe_disables_job_control`) asserting both probes pass `+m`
before `-ic`. Full terminal-suspend behavior depends on a real
controlling TTY + a slow zsh rc, so it isn't unit-testable in CI without
flakiness; the guards lock in the load-bearing flag.
---
Closes #3322 — automated triage
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
commit 29fa13dc9553e2b40d7856f3ca714b0b88abb8d4
Author: Worktrunk Bot <w@worktrunk.dev>
Date: Thu Jul 9 12:13:47 2026 -0700
docs(code-signing): add SignPath Foundation code-signing policy page (#3366)
Addresses the first, gating step of option #1 (SignPath Foundation) from
#3355 — the fix for the `Trojan:Win32/Wacatac.B!ml` false positive on
the unsigned Windows binary.
## What this PR does
Adds a **code signing policy** page at
[worktrunk.dev/code-signing/](https://worktrunk.dev/code-signing/). A
published policy page is a hard eligibility requirement for the
[SignPath Foundation](https://signpath.org/terms.html) free OSS
code-signing program, so it's the natural thing to land first — the
application can't proceed without it. The page documents:
- **Certificate provenance** — the required attribution ("Free code
signing provided by SignPath.io, certificate by SignPath Foundation")
and the HSM-held key.
- **What's signed** — `git-wt.exe` in the Windows release archive /
winget; nothing else.
- **The build+sign pipeline** — tag-triggered, cargo-dist on GitHub
runners, SignPath action for signing, all reproducible from public
source.
- **Project roles** (Author / Reviewer / Approver), **per-release manual
approval**, and the **no-telemetry** privacy stance SignPath asks
projects to state.
The generated skill mirror
(`skills/worktrunk/reference/code-signing.md`) and
`docs/static/llms.txt` entry are produced by `test_docs_are_in_sync` —
not hand-edited.
> **Roster check:** the roles table lists @max-sixty as
Author/Reviewer/Approver. Adjust if anyone else should hold signing
authority before this is submitted to SignPath.
## What this PR deliberately leaves out
The `release.yaml` signing job and the SignPath account are a
**follow-up**, for two reasons:
1. **The concrete slugs don't exist yet.** The SignPath action needs
`organization-id`, `project-slug`, and `signing-policy-slug` — all
issued *after* the project is registered and approved.
2. **cargo-dist doesn't sign nativ…
#3374 scrubbed inherited
GIT_*discovery vars from user-hook spawns, but two more spawn sites relocate a user command into a wt-chosen worktree and had the same bug:wt step for-eachand the--executepayload when wt executes it directly (no shell integration — including every!wt-alias invocation, which bypasses the wrapper). Git resolvesGIT_DIR/GIT_WORK_TREEbefore walking up from the cwd, so the inherited value silently overrode the worktree wt placed the command in:git w step for-each -- git rev-parse --show-toplevelfrom a linked worktree printed the invoking worktree's path for every iteration while the headers named each worktree. The linked-worktree alias case is the common trigger — git exports an absoluteGIT_DIRpinned to the invoking worktree's private gitdir there (verified on git 2.54; from a main-worktree root it exports none).The rule is now stated once, on
scrub_git_discovery_env_vars: scrub exactly when wt chose the child's cwd (hooks, for-each, the--executefallback); keep when the command runs in the user's own context (aliases,commit.generation, and wt's internal plumbing, which keeps the absolutize-and-forward behavior from #1914). A CLAUDE.md rule points there, mirroring theCommandTraceany-new-spawn-site convention.Testing: regression tests for both new scrub sites (each validated by reverting the fix and watching it fail) plus an alias pass-through test pinning the keep side. The
--executetest is cross-platform, so Windows CI drives the non-unixspawnvariant and unix theexecvariant; the for-each test follows the file's unixsh -cprecedent.Ref #3373 (already closed by #3374; this completes the sweep).
🤖 Generated with Claude Code