fix: restore Claude plugin hooks to conventional hooks/hooks.json - #3418
Conversation
Claude Code discovers plugin hooks by convention at hooks/hooks.json and does not honor plugin.json's string-path `hooks` override for plugin loads. The #3382 rename to claude-hooks.json moved the file off the only path Claude's loader reads, so the activity (🤖/💬) and WorktreeCreate/WorktreeRemove hooks silently stopped loading. Restore the file to hooks/hooks.json. This does not resurface the #3362 Codex collision: the Codex manifest defines its hooks inline, taking Codex's Some(Inline) branch which overrides convention discovery independently of the filename (and the inline definition predates the rename, so the rename was always redundant). Closes #3417
worktrunk-bot
left a comment
There was a problem hiding this comment.
Reviewed as a self-authored PR (COMMENT only — GitHub blocks self-approval).
The fix is well-grounded and I could verify everything that's verifiable from CI:
- Root cause holds. The reporter confirmed on Claude Code 2.1.207 that symlinking
hooks/hooks.json→claude-hooks.jsonimmediately restores the handlers, so the "loader discovers by convention, ignores the string-path override" diagnosis is grounded in an observed symptom rather than inference. - No #3362 regression. Verified the Codex manifest (
plugins/worktrunk/.codex-plugin/plugin.json) still carrieshooksas an inline object, so Codex takes itsSome(Inline)branch and never falls through to convention discovery of the sharedhooks/hooks.json— the coexistence claim checks out. - Rename is content-identical (100% similarity); the hooks payload is byte-for-byte unchanged.
- Tests pass locally:
test_plugin_layout_is_consolidated,test_codex_plugin_metadata_is_valid_json, andtest_docs_are_in_syncall green after the assertion inversion. Grepped the whole tree for leftoverclaude-hooks.jsonreferences — the only remaining ones are intentional (historical explanation in CLAUDE.md/CHANGELOG and the!…claude-hooks.json.exists()absence assertion). - CHANGELOG net-nil: 0.67.0 is unreleased and the removed
### Internal#3382 entry sat in that same unreleased block, so swapping it for the### Fixedentry is correct.
Data-loss surface — flagging for @max-sixty per the deletion-surface review policy. This PR restores plugins/worktrunk/hooks/hooks.json, shipped automation whose WorktreeRemove hook runs wt remove --foreground "$p" on users' machines. The command is the safe non-force variant (retains unmerged branches, no -D/-f) and its content is unchanged, but the effect of this PR is to re-activate that automation path — which has been dead since #3382 — so it warrants a human sign-off before merge rather than an autonomous bot merge.
Not verified end-to-end (as the PR body already notes): the bot can't drive a live Claude Code or Codex session to watch the hooks fire.
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…
…n-only discovery (#3431) ## What this does Moves the Claude plugin manifest to the canonical `plugins/worktrunk/.claude-plugin/plugin.json` and strips it to metadata only, dropping the `hooks` and `skills` keys so that both component types load purely by Claude Code's convention discovery. Rewrites the layout docs and inverts `test_plugin_layout_is_consolidated` to pin the new invariants. ## Background The plugin layout has been in flux: [#3382](#3382) Claude-scoped the hooks filename, which silently broke hook loading ([#3417](#3417)), and [#3418](#3418) reverted it. That revert restored working hooks but left two claims baked into the layout, the docs, and the test that this PR found to be stale. ## Research method Everything below was verified empirically against claude-cli 2.1.207 (the current release), not from docs alone: - **Scratch marketplace probe**: a throwaway marketplace mirroring worktrunk's exact shape (repo-root `.claude-plugin/marketplace.json`, plugin in a `./plugins/<name>` subdir source, `skills` as a symlink pointing outside the plugin dir, hooks at the conventional path) with two plugin variants — manifest in `.claude-plugin/plugin.json` vs. bare `plugin.json` at the plugin root — installed through the real `claude plugin marketplace add` + `claude plugin install` flow. - **Component inventory**: `claude plugin details` and `claude plugin validate` on both variants. - **Live sessions**: headless `claude -p` runs confirming skills appear in-session and hook commands actually execute (marker files, and reading the 🤖 activity marker from inside a live session). - **Loader inspection**: the manifest resolution order read directly out of the claude-code binary's bundled loader code. ## Findings 1. **The "no `.claude-plugin/` wrapper" claim no longer holds.** The repo documented — and the test pinned — that Claude wants `plugin.json` bare at the plugin root, and that a `.claude-plugin/` wrapper in a subdir-source plugin fails with "Plugin not found". On 2.1.207 the wrapper form installs, loads skills, and fires hooks end-to-end. The loader tries `.claude-plugin/plugin.json` first with root `plugin.json` only as a runtime fallback, several marketplace-resolution paths read *only* the wrapper, and `claude plugin validate` rejects the root-level form outright ("No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json"). 2. **"Claude has no skill auto-discovery" is wrong.** Skills are auto-discovered by scanning `skills/*/SKILL.md`; a `skills` manifest array only *adds* directories to that scan (entries equal to the default dir are filtered). An unlisted skill behind the out-of-plugin symlink loaded fine — the installer dereferences the symlink into a real directory in the install cache. Dropping the array also makes the old test guard ("every skill must be listed or it's silently invisible") structurally unnecessary: a new repo-root skill now ships to Claude, Codex, and Gemini with no manifest change anywhere. 3. **The `hooks` manifest key was dead config.** #3417 established the string-path override isn't honored for plugin loads; convention discovery of `hooks/hooks.json` is the mechanism that works. A key pointing at the conventional path adds nothing and could mask a future mislocated file, so it's gone (one mechanism per guarantee). ## Changes - `plugins/worktrunk/plugin.json` → `plugins/worktrunk/.claude-plugin/plugin.json`; content reduced to `name`/`description`/`author`. - `plugins/worktrunk/CLAUDE.md`: layout diagram and Claude path-resolution bullet rewritten to the convention-only model; the refuted wrapper claim and the stale no-auto-discovery claim removed. - Root `CLAUDE.md`: the Plugin Layout pointer updated (dropped the stale "Codex no-hooks re-enablement" phrase — Codex hooks shipped inline in #3364). - `test_plugin_layout_is_consolidated`: now asserts the wrapper manifest exists, a root-level `plugin.json` does not, the manifest carries no `hooks`/`skills` keys, the plugin `skills` symlink resolves to the repo-root `skills/` (unix only — Windows checkouts without `core.symlinks` materialize it as a file), and every repo-root skill dir carries a `SKILL.md` (the scan silently ignores a dir without one). Codex and Gemini are untouched: Codex reads only its `.codex-plugin/` wrapper, and its inline `hooks` object still overrides Codex's own convention discovery, so the #3362 collision stays closed independent of any of this. ## Verification - `claude plugin validate plugins/worktrunk` passes (it failed on the old layout). Remaining warnings are deliberate: no `version` field (installs pin the git SHA as the version; a hand-maintained semver would go stale) and the plugin-root `CLAUDE.md` (developer docs, not plugin context). - A live `claude --plugin-dir` session on this exact tree loads both skills and the `UserPromptSubmit` hook sets the 🤖 marker, observed from inside the session. - The scratch-marketplace probe covered the same shape through the full marketplace install flow, including the symlinked skills dir. - Full pre-merge gate: 4389 tests passed plus lints, doctests, and docs-sync. ## Risk The one scenario this could regress is a Claude Code version old enough to read *only* the root-level manifest. That couldn't be tested directly; the mitigations are that the wrapper is the documented convention, the loader prefers it today, and Claude Code self-updates aggressively. The old layout's "verified end-to-end" claim most likely dates from an earlier 2.1.x behavior or a misdiagnosed failure. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Problem
Since #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
/hooksshows 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 honorplugin.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). But the Codex manifest already defined its hooks inline (
Some(Inline)branch ofresolve_manifest_hooks), which overrides Codex's convention discovery — and that inline definition predates the rename (verified against2677f05^). 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.jsonand pointplugin.json'shooksback at it.plugin.jsonreverts byte-for-byte to its pre-#3382 state.This does not resurface the #3362 collision: the Codex manifest's inline
hooksobject keeps Codex off the sharedhooks/hooks.jsonregardless of the filename. The two toolchains now coexist on one file — Claude discovers it, Codex ignores it via the inline override. Thetest_plugin_layout_is_consolidatedassertions 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 conventionalhooks/hooks.jsonthat 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)integration_tests::config_showmodule: 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.jsonimmediately restores the hooks) and on Claude Code's documented convention path (hooks/hooks.jsonin 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