Skip to content

refactor(perf): canonicalize the benchmark / wt-perf system#3403

Merged
max-sixty merged 9 commits into
mainfrom
simplify-wt-perf
Jul 10, 2026
Merged

refactor(perf): canonicalize the benchmark / wt-perf system#3403
max-sixty merged 9 commits into
mainfrom
simplify-wt-perf

Conversation

@max-sixty

@max-sixty max-sixty commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Implements the design proposal from this branch's first commit (b22aced, 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

max-sixty and others added 6 commits July 9, 2026 20:14
Design proposal (review-only, per design/ convention). Inventories the five
perf-tooling layers and proposes seven consolidations: delete the cache-check
subcommand (subsumed by logs profile --format=json), move the timeline
renderer into worktrunk::trace, add by_context to Profile and retire the
trace_processor SQL section, share one warm/cold bench runner, prune the
bench matrix (~15-20 min/day of CI), move the wt-perf CLI tests in-package,
and unify the setup config parser. Emission, chrome.rs, the separate wt-perf
package, and fixture shapes stay as is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements design/simplify-bench-perf.md (items 1-7):

1. Delete wt-perf cache-check — its output is the .cache field of
   wt config state logs profile --format=json.
2. Move the timeline renderer into worktrunk::trace, sharing
   command_label/render_table/fmt_dur with the profile so the two views
   can't drift (drops wt-perf's tabwriter and insta deps; timeline
   durations now render in the profile's fixed-point format).
3. Add by_context to Profile (BY CONTEXT table + JSON array): per-worktree
   subprocess totals, the one analysis that previously required
   trace_processor SQL. benches/CLAUDE.md's SQL section becomes a pointer
   to logs profile + Perfetto; the running-tend statusline recipe now uses
   logs profile too.
4. One warm/cold bench runner (wt_perf::bench_wt) owns the
   BatchSize::PerIteration rationale once; five inline copies deleted.
   All runs now assert child exit status (list.rs previously ignored it).
5. Prune the matrix: real_repo keeps only the 8-worktree variants,
   cow_copy is deleted (shadow serial impl validating a settled choice),
   and the remove_e2e/first_output arm (duplicate of first_output/remove)
   is deleted. Ends three gist series; saves ~15-20 min/day of bench CI.
6. Move the wt-perf CLI tests in-package (native CARGO_BIN_EXE_wt-perf),
   deleting the dummy builds.rs and dropping wt-perf from the nextest
   setup script.
7. parse_config returns a SetupConfig enum covering mixed-W-B; shared
   init_bench_repo plumbing between the two fixture builders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From two review passes over the branch:

- CacheReport shrinks to its same-context fields. The six dropped fields
  (total_commands, total_time, contexts, duplicated_commands, extra_calls,
  unique_commands) duplicated Profile fields or tallied cross-context
  repeats — expected fan-out noise the text report never showed. Their
  only external reader was wt-perf cache-check, deleted earlier on this
  branch.
- src/trace/mod.rs docstring cut over to the module map + logs profile
  pointer; the trace_processor SQL examples it carried were retired from
  benches/CLAUDE.md earlier on this branch for the same reason.
- Chrome args drop duration_ms (redundant with the native dur field).
- render_table pads by display width (unicode-width, already a direct
  dep), replacing the ASCII-only invariant that the new BY CONTEXT table
  violated; CJK alignment test added.
- Timeline instant-event row gains coverage (the one render path no test
  exercised); stale /main suffix fixed in benches/CLAUDE.md's invalidate
  example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main's diagnostic-section addition calls the per-render cache analysis 'a
separate tool', pointing at the wt-perf cache-check recipe — which this
branch already cut over to wt config state logs profile. Reword: it's the
same report against a statusline capture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per the design/ convention, proposals guide implementation PRs but don't
land on main. The doc remains in this branch's history (b22aced) and
its content is implemented by the commits above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@max-sixty max-sixty changed the title docs(design): canonicalize the benchmark / wt-perf system refactor(perf): canonicalize the benchmark / wt-perf system Jul 10, 2026
# Conflicts:
#	tests/helpers/wt-perf/src/lib.rs
#	tests/helpers/wt-perf/src/main.rs

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewing as a draft — flagging anything that looks worth a quick fix. Mark ready for a full review.

The refactor is clean and internally consistent — the by_context aggregation, the moved traced/wall timeline computation, the SetupConfig enum, and the bench_wt warm/cold strategy all preserve the prior behavior, and I found no correctness issues. Two documentation-accuracy nits:

  • benches/CLAUDE.md (expected-numbers table, the remove_e2e/first_output row) — this row (~86 ms, "single-target validation up to first output") documents the bench variant this PR deletes from benches/remove.rs. It's outside this diff's hunks so I can't attach an inline suggestion; either drop the row or repoint it at the surviving first_output/remove variant in time_to_first_output.rs.

Inline suggestion below for the other one.

Comment thread src/trace/profile.rs Outdated
max-sixty and others added 2 commits July 9, 2026 23:41
The profile module spec referenced a nonexistent `wt diagnose` command
(the consumer is the -vv diagnostic report), and the expected-numbers
table still documented the deleted remove_e2e/first_output variant —
repointed at the surviving first_output/remove.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@max-sixty
max-sixty marked this pull request as ready for review July 10, 2026 09:54
@max-sixty
max-sixty merged commit 18d2bef into main Jul 10, 2026
44 checks passed
@max-sixty
max-sixty deleted the simplify-wt-perf branch July 10, 2026 10:44
@max-sixty max-sixty mentioned this pull request Jul 10, 2026
max-sixty added a commit that referenced this pull request Jul 10, 2026
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_
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…
max-sixty added a commit that referenced this pull request Jul 12, 2026
…surface (#3433)

Follow-up to #3403, applying the deferred `--persist` decision and two
sibling subtractions found sweeping the perf tooling for the same shape
(a vestigial option or dual path guarding nothing).

- **`wt-perf setup` is persist-only.** The `--persist` flag and the
interactive "Press Enter to clean up (or Ctrl+C to keep)" path are gone
— it was the one blocking, stdin-reading path in an otherwise scriptable
tool, and it guarded little: default-path fixtures land in
`/tmp/wt-perf-<config>`, which the next `setup <config>` run already
wipes and rebuilds. `prune-real` never offered cleanup, so setup's exit
behavior is now uniform.
- **`timeline --repo` deleted.** It only restated the traced repo:
`run_timeline` resolves the repo from `-C`/cwd, `--repo` defaulted to
exactly that, and the only documented use passed the identical path to
both flags. `--cold` now invalidates the repo being traced — the only
coherent target.
- **`wt_perf` lib surface tightened.** Seven helpers plus
`PruneFixtureState` had no callers outside `lib.rs` and are now private
(`ensure_rust_repo`, `clone_rust_repo_at`, `history_spread_shas`,
`add_diverged_backdrop`, `create_mixed_repo_at`,
`create_prune_real_repo_at`, `prune_fixture_state`); the public surface
now matches what the benches and CLI actually use. Doc references to
them are plain code spans rather than intra-doc links — rustdoc's
`private-intra-doc-links` lint rejects public docs linking private items
even under `--document-private-items`.

Checked and deliberately kept: `wt-perf trace` and `wt-perf invalidate`
(distinct documented workflows), the `TraceResult`/`CacheReport`
re-exports (public field types of `TraceEntry`/`Profile`), and the
`create_repo`/`create_repo_at` two-layer fixture API (both layers used).

Gate green: 4387 tests, clippy/fmt/doctests/rustdoc (`-Dwarnings`)
clean.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants