Skip to content

fix(test): tolerate the prune race in picker alt-x branch assertions - #3412

Merged
max-sixty merged 2 commits into
mainfrom
fix/ci-29069403862
Jul 10, 2026
Merged

fix(test): tolerate the prune race in picker alt-x branch assertions#3412
max-sixty merged 2 commits into
mainfrom
fix/ci-29069403862

Conversation

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Problem

The nightly feature-powerset job failed on main when commands::picker::tests::test_apply_drops_unmorphable_kept_branch_row panicked with called Result::unwrap() on an Err value: git branch --list feature failed (exit 128) (failed run).

Root cause is a background-removal race in the test, not the picker. The picker's alt-x removal runs on a detached thread: stage_worktree_removal renames the worktree into the trash — so its directory vanishes — and then runs git worktree prune, which deletes the .git/worktrees/<id> admin dir. The three apply-based tests poll only on the worktree directory disappearing (which happens at the rename, before the prune) and then immediately query git branch --list in the main repo. git branch --list enumerates worktrees to mark checked-out branches, so a query that races the in-flight prune reads a half-deleted admin dir and fails with exit 128. The test .unwrap()ed that query and panicked.

The failure surfaced under the nightly job's serial, heavily-loaded run; the test predates this commit (from #3268) — this was a latent flake, not a regression.

Solution

Add a retry-tolerant await_branch_presence test helper that polls git branch --list <branch> until git succeeds and the branch reaches its expected steady state (a transient prune race surfaces as Err, never a successful empty read, so only a successful query is authoritative). Route all three background-removal tests through it:

  • test_apply_morphs_unmerged_worktree_to_branch_row — branch retained
  • test_apply_drops_unmorphable_kept_branch_row — branch retained (the one that failed)
  • test_apply_drops_integrated_worktree_row — branch deleted

The synchronous do_removal tests and the branch-only keep test are unaffected — they run no concurrent prune — so they're left alone. This is a test-synchronization fix; picker behavior is unchanged.

The second failing job in the same run, nix-flake (test_remove_reap_kills_process needing lsof), is already addressed by #3410 and #3406, so it's out of scope here.

Testing

  • cargo test --no-default-features --features cli,git-wt,shell-integration-tests,syntax-highlighting --bin wt for the three tests — pass.
  • Stress: ran all three under --test-threads=8 for 40 iterations — 0 failures.
  • cargo fmt --check and cargo clippy --bin wt --tests (same feature set) — clean.

Automated fix for failed run

@worktrunk-bot worktrunk-bot added the automated-fix Automated CI fix label Jul 10, 2026

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The flake diagnosis and fix are sound — I traced all three call sites and the list.is_empty() != expect_present logic is correct for both the retained (true) and deleted (false) cases, and tolerating the transient exit 128 (retry on Err, only a successful query is authoritative) is exactly right for the prune race.

One non-blocking reuse note below: the hand-rolled poll loop duplicates an existing shared test helper. Not a correctness concern — flagging per the repo's "check before hand-rolling a utility" convention.

Comment thread src/commands/picker/mod.rs
`test_apply_drops_unmorphable_kept_branch_row` flaked in the nightly
feature-powerset job with `git branch --list feature failed (exit 128)`.

The picker's alt-x removal runs on a background thread:
`stage_worktree_removal` renames the worktree into the trash (so its
path vanishes) and then runs `git worktree prune`, which deletes the
`.git/worktrees/<id>` admin dir. The affected tests poll only on the
worktree *directory* disappearing — which happens at the rename, before
the prune — then immediately query `git branch --list` in the main repo.
`git branch --list` enumerates worktrees to mark checked-out branches, so
a query that races the in-flight prune reads a half-deleted admin dir and
fails with exit 128; the test `.unwrap()`ed it and panicked.

Add a retry-tolerant `await_branch_presence` helper that polls the branch
list until git succeeds and the branch reaches its steady state, and route
all three `apply`-based background-removal tests through it. The
synchronous `do_removal` tests are unaffected (no concurrent prune).

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the hand-rolled poll loop with the shared wait_for helper per the
repo's "check before hand-rolling a utility" convention. wait_for already
implements poll-with-deadline and exponential backoff; its BG_TIMEOUT (60s)
backing-off wait is a better fit than the fixed 5s bound for tolerating the
background prune race under a loaded nightly run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@max-sixty
max-sixty merged commit 4f6f6d8 into main Jul 10, 2026
39 checks passed
@max-sixty
max-sixty deleted the fix/ci-29069403862 branch July 10, 2026 11:27
Skogix added a commit to skogai/worktrunk that referenced this pull request Jul 11, 2026
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 />

    [![Dependabot compatibility
    score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=taiki-e/install-action&package-manager=github_actions&previous-version=2.82.11&new-version=2.83.0)](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
    &quot;apply the project lens&quot; 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: &quot;&quot;, pr: &quot;&quot;}</code> in the agent's settings,
    emptying Claude Code's auto-added <code>Co-Authored-By</code> commit
    trailer and &quot;Generated with Claude Code&quot; 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
    &quot;apply the project lens&quot; 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: &quot;&quot;, pr: &quot;&quot;}</code> in the agent's settings,
    emptying Claude Code's auto-added <code>Co-Authored-By</code> commit
    trailer and &quot;Generated with Claude Code&quot; 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 />

    [![Dependabot compatibility
    score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=max-sixty/tend&package-manager=github_actions&previous-version=0.1.9&new-version=0.1.10)](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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automated-fix Automated CI fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants