Visitar URL original
Comparing v0.60.0...v0.61.0 · max-sixty/worktrunk · GitHub
Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: max-sixty/worktrunk
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.60.0
Choose a base ref
...
head repository: max-sixty/worktrunk
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.61.0
Choose a head ref
  • 13 commits
  • 178 files changed
  • 5 contributors

Commits on Jun 19, 2026

  1. test(package): guard compile-time-embedded assets ship in packaged bu…

    …ilds (#3130)
    
    Follow-up to #3124, requested in [this
    thread](#3124 (comment)):
    turn the "build/package environment differs from the dev checkout" class
    inner-loop.
    
    #3123 (`env!("VERGEN_GIT_DESCRIBE")` failing on the crates.io archive)
    was one instance of a general pattern — *something read at compile time
    that exists in our git checkout but may be absent in a packaged build*.
    There are three mechanisms in the crate that do this, and a missing file
    in any of them is a hard compile error, exactly the #3123 failure mode:
    
    | Mechanism | Embeds |
    |-----------|--------|
    | `include_str!` / `include_bytes!` | `dev/*.example.toml`,
    `dev/opencode-plugin.ts`, `gemini-extension.json` |
    | askama `#[template(path = …)]` (reads files at compile time via
    proc-macro) |
    `templates/{bash.sh,zsh.zsh,fish.fish,fish_wrapper.fish,nushell.nu,powershell.ps1}`
    |
    
    The detection gap: a plain `cargo build`/`cargo test` always sees these
    files (they're in the working tree), so it can never catch a packaging
    drop. Today that's only caught by the nightly Nix build (slow) or by
    crates.io itself after publish (the worst loop). The realistic future
    regression is someone adding a new `include_str!("../../assets/foo")` or
    `#[template]` outside the currently-packaged dirs — it passes every PR
    check, then breaks Nix that night and `cargo install` at the next
    release.
    
    This adds a fast guard (`embedded_assets_ship_in_package`, no build,
    runs on every PR) that scans `src/` for every embedded path and asserts
    each is:
    
    1. **present in `cargo package --list`** — so it ships in the crates.io
    archive (the #3123 surface), and
    2. **covered by the `flake.nix` source filter** — so it survives the Nix
    build.
    
    Both currently pass. I verified the test isn't vacuous: pointing an
    `include_str!` at a tracked-but-unpackaged file
    (`tests/helpers/wt-perf/src/lib.rs`) fails it with both violations
    reported, distinct from the compiler's own "file not found" for a path
    that doesn't exist at all.
    
    This sits alongside the existing `version_build.rs` guards (the fast
    `env!`-vs-`option_env!` mechanism check on every PR, and the
    `#[ignore]`d full-archive build in nightly), extending the same
    philosophy to the file-presence half of the class.
    
    Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com>
    Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
    3 people authored Jun 19, 2026
    Configuration menu
    Copy the full SHA
    d67e6ab View commit details
    Browse the repository at this point in the history
  2. Configuration menu
    Copy the full SHA
    992e6be View commit details
    Browse the repository at this point in the history
  3. feat(list): custom template columns and cached PR numbers in the pick…

    …er (#3073)
    
    Two display features for `wt list` and the interactive picker, developed
    together because they share the column-layout and progressive-rendering
    machinery.
    
    ## Custom columns (`[list.custom-columns]`)
    
    Each `[list.custom-columns.<Header>]` entry in user config adds a `wt
    list` column: a minijinja template rendered per row over `branch`,
    `worktree_path`, `worktree_name`, and `vars.*`, with optional `width`
    and drop priority. Values expand before the skeleton renders, from
    in-memory data only — `vars` come from the bulk git-config snapshot, so
    no subprocess runs per cell. Widths are measured from content like the
    Branch and Path columns; a column that is empty on every row is dropped.
    
    Unknown variables and misspelled filters abort `wt list` with the
    available-variables hint; undefined values render as empty cells (the
    intended sparse-column shape). `wt list --format json` gains a `columns`
    map per item, and its `vars` field now reads from the snapshot too (the
    previous `--get-regexp` line-parse truncated multiline values). The
    picker shares the row renderer, so the columns appear there as well; a
    broken definition degrades to no columns plus a stashed warning, since
    collect runs while skim owns the terminal.
    
    The key is `[list.custom-columns]`, not `[list.columns]`, to avoid
    colliding with the column-visibility toggles in #3065 (which claims
    `[list.columns]` as a flat map of built-in-column bools — a mutually
    exclusive serde shape for the same protected key). Namespacing here lets
    both land independently.
    
    Ref #1982 — the custom-columns proposal lives in that thread. The
    issue's own title is a separate directory-naming request, so this
    doesn't close it.
    
    ## Cached PR/MR numbers in the picker
    
    The picker skips the networked CiStatus task, so until now it had no CI
    column at all. Cached statuses are local data, though: collect now fills
    rows from `.git/wt/cache/ci-status/` when the task is skipped under a
    progressive handler, so PR/MR numbers fetched by earlier `wt list
    --full` or statusline runs render in the picker — aligned with the same
    `MaxPrNumber` ratchet width `wt list` uses, and with zero network
    access.
    
    A valid cache entry renders as-is. An entry whose TTL passed or whose
    branch head moved keeps its PR/MR number dimmed: the number still
    identifies the PR when the pipeline color may be outdated. Expired
    entries without a number are dropped. The CI column is allocated only
    when some row had a usable entry, and rows the cache can't fill resolve
    to blank rather than a pending placeholder, since no task repaints them.
    
    ## Key files
    
    - `src/config/expansion.rs`, `src/config/user/sections.rs`,
    `src/git/repository/config.rs` — column resolution, the template
    environment, and the bulk git-config snapshot.
    - `src/commands/list/layout.rs`, `src/commands/list/render.rs` — column
    width allocation and cell rendering.
    - `src/commands/list/ci_status/mod.rs` — `populate_from_cache`, the
    cache-only fill.
    - `src/commands/picker/mod.rs` — the dry-run dump
    (`WORKTRUNK_PICKER_DRY_RUN`) that makes picker row content assertable in
    tests.
    
    ## Testing
    
    Integration tests cover both features: custom columns (table render,
    JSON output, empty-column drop, invalid-template error) and the picker
    (cached PR numbers appear in the dry-run dump, uncached branches stay
    blank). Unit tests cover the cache-population logic (valid,
    expired-with-number, head-moved, dropped). Verified against the full
    `cargo run -- hook pre-merge --yes` gate locally.
    
    > _This was written by Claude Code on behalf of max_
    
    ---------
    
    Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
    max-sixty and claude authored Jun 19, 2026
    Configuration menu
    Copy the full SHA
    a7e29f7 View commit details
    Browse the repository at this point in the history

Commits on Jun 20, 2026

  1. test(fsmonitor): reap orphaned sleep grandchild in SIGKILL escalation…

    … test (#3135)
    
    `nix_signaller_escalates_to_sigkill_when_sigterm_ignored` is the one
    test nextest flagged as leaky in the suite (`4002 passed (1 leaky)`).
    
    It spawns `sh -c "trap '' TERM; ...; sleep 30"` to model a process that
    ignores SIGTERM. The `TERM` trap stops the shell from exec-replacing
    itself with the final command, so it forks a separate `sleep` child. The
    test SIGKILL'd only the `sh` pid, orphaning that `sleep` grandchild
    (reparented to init), which kept the test's inherited stdout/stderr open
    for ~30s, past nextest's 100ms `leak-timeout`. It's a test-only
    artifact: production `escalate_terminate` targets a real `git
    fsmonitor--daemon`, which has no forked grandchild.
    
    The fix spawns `sh` as its own process-group leader (`process_group(0)`)
    and SIGKILLs the whole group after killing it, reaping the grandchild
    before the test returns.
    
    After the change the suite reports `4002 passed, 0 leaky`, the test
    leaves no stray `sleep` across repeated runs, and it runs faster (0.23s
    vs 0.44s) since it no longer waits out the leak window.
    
    > _This was written by Claude Code on behalf of max_
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    max-sixty and claude authored Jun 20, 2026
    Configuration menu
    Copy the full SHA
    79f33cc View commit details
    Browse the repository at this point in the history
  2. fix(trace): route every command spawn through one CommandTrace chokep…

    …oint (#3134)
    
    ## What
    
    `git worktree add` was invisible in `wt-perf timeline` — it showed up as
    an unattributed multi-second gap rather than a labeled slice. The root
    cause: it ran via `Repository::run_command_delayed_stream`, which built
    a raw `std::process::Command` and spawned it directly, bypassing
    `shell_exec::Cmd`. Only `Cmd`'s methods emitted `[wt-trace]` records
    (via a private `WtTraceLog`). An audit found this was one of several
    in-process spawn paths silently skipping tracing — so this fixes the
    reporting *and* restructures emission so it can't be skipped by
    omission.
    
    ## The chokepoint
    
    `CommandTrace` (in `src/trace/emit.rs`, the module that owns the
    `[wt-trace]` grammar) is now the **sole emitter** of command records —
    `command_completed`/`command_errored` are private to that module, so the
    only way to produce a `cmd=…` record is to construct a `CommandTrace`
    and resolve it with `complete`/`fail`. It's `#[must_use]` and trips a
    debug-build drop assertion if constructed but never resolved, so a
    future spawn site that forgets to wire up tracing fails tests rather
    than leaving a gap. Duration is snapshotted at resolution, so streamed
    commands (whose duration isn't known until the stream finishes) still
    record the correct spawn → wait span.
    
    ## Where to look
    
    - `src/trace/emit.rs` — the `CommandTrace` guard + module docstring
    describing the chokepoint.
    - `src/shell_exec.rs` — `WtTraceLog` replaced by `CommandTrace`; new
    `Cmd::delayed_stream` mode (the delayed-stream buffer-then-stream logic
    + `StreamCommandError` moved here from `Repository`);
    `run`/`stream`/`pipe_into` migrated.
    - `src/git/repository/mod.rs` — `run_command_delayed_stream` is now a
    thin `Cmd::delayed_stream` wrapper (~160 lines removed);
    `start_fsmonitor_daemon_at` traced.
    - `src/output/concurrent.rs`, `src/commands/run_pipeline.rs`,
    `src/commands/step/tether.rs`, `src/commands/custom.rs` — spawn paths
    that can't be `Cmd` methods construct a `CommandTrace` directly.
    
    Detached background children, pagers, and shell probes stay
    intentionally untraced (they outlive the invocation or aren't part of
    its timeline) — noted in `CLAUDE.md`.
    
    ## Testing
    
    `pre-merge` is green (3981 tests, clippy, all hooks including
    `no-direct-cmd-output`). New coverage: `CommandTrace` drop-tripwire unit
    tests and `test_switch_create_traces_worktree_add`, which asserts the
    streamed `git worktree add` emits a record. The tripwire earned its keep
    during development — it surfaced a few unresolved-drop paths in the
    partial-spawn-failure cleanups, and the snapshot suite caught a
    stdin-pipe deadlock the refactor briefly introduced in `Cmd::run` (the
    child's `stdin` handle was held open across `wait_with_output()`,
    hanging `git … --stdin` children).
    
    > _This was written by Claude Code on behalf of max_
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    max-sixty and claude authored Jun 20, 2026
    Configuration menu
    Copy the full SHA
    9dcf41f View commit details
    Browse the repository at this point in the history
  3. test(step_alias): reap orphaned sleep in external-signal alias tests (#…

    …3136)
    
    The two `#[cfg(unix)]` external-signal alias tests
    (`test_alias_external_sigterm_reaches_child`,
    `test_alias_external_sigint_reaches_child`) run an alias body of `...
    sleep 30 & wait $!` and deliver a PID-targeted signal to wt. wt forwards
    it to the wrapper `sh` by PID — the contract these tests pin — so the
    trap's `exit` tears down the shell and leaves the backgrounded `sleep
    30` orphaned in wt's process group, lingering ~30s. Two tests, one
    orphan each, accounted for the two stray `sleep 30` processes a full
    suite run left behind (invisible to nextest's leak detector because
    their stdio is redirected to null).
    
    This is a test artifact, not a product bug: wt faithfully reproduces
    plain-shell semantics, where a trap that `exit`s without `kill $!`
    orphans its background job. The fix reaps the orphan after
    `child.wait()` with a negative-pgid SIGKILL — wt ran in its own process
    group (`process_group(0)`) and is reaped before the kill, so the SIGKILL
    reaches only the leftover sleep. Same pattern as the fsmonitor
    SIGKILL-escalation test fix in #3135.
    
    Verified: targeted runs 5/5 leave 0 strays (was 2/2); full suite
    (`--features shell-integration-tests`) 4002 passed with 0 strays at +0s
    and +5s; clippy and pre-commit clean.
    
    > _This was written by Claude Code on behalf of max_
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    max-sixty and claude authored Jun 20, 2026
    Configuration menu
    Copy the full SHA
    3334ffa View commit details
    Browse the repository at this point in the history
  4. Add --config-set for inline TOML config overrides (#3138)

    A global, repeatable `--config-set <toml>` flag that overrides any
    user-config key for a single invocation, layered above config files and
    `WORKTRUNK_*` env vars. The value is a real TOML fragment, so arrays and
    tables work natively — no bespoke `key=value` grammar.
    
    ## Behavior
    
    - **Precedence**: config files → `WORKTRUNK_*` env vars → `--config-set`
    (highest).
    - **Merge semantics**: a later override replaces an earlier one for the
    same key; scalars and arrays replace the lower-layer value; nested
    tables deep-merge, so `--config-set list.full=true` leaves sibling
    `list.*` keys untouched.
    - **Global**: works in any position (`wt --config-set … list` or `wt
    list --config-set …`), like `-v` / `-y` / `--config`.
    - **Graceful degradation**: a malformed, ill-typed, or invalid override
    drops the whole `--config-set` layer with an attributed warning (`▲
    Ignoring --config-set overrides: …`) and preserves the lower layers —
    consistent with how the existing `WORKTRUNK_*` env overlay degrades.
    
    ## Why
    
    This is the foundation for a follow-up that parameterizes `wt list`'s
    column set without baking it into config: e.g. an alias `list-fast = "wt
    --config-set list.columns=[...] list"` renders a smaller view while
    plain `wt list` stays full. Doing it as a generic config-override lever
    (rather than a `list`-specific flag) keeps one canonical path and
    composes with aliases and any future config key.
    
    ## Implementation
    
    `Cli.config_override` (`--config-set`, global, `Vec<String>`) is stashed
    in a process-global `OnceLock` (mirroring `set_config_path`) and applied
    in `UserConfig::load_with_warnings` via `apply_cli_overrides` as the top
    layer. New `LoadError::CliOverride` variant carries the raw values for
    attribution; the warning is rendered in `emit_user_config_warnings`.
    
    ## Testing
    
    8 unit tests on `apply_cli_overrides` (sets,
    deep-merge-preserves-siblings, last-wins, array-replace, malformed,
    type-mismatch, validation-failure, empty) and 3 integration tests
    (overrides-the-file, malformed-warns-attributed,
    works-after-subcommand). Full suite green: lib + 1828 integration,
    clippy clean, docs in sync.
    
    > _This was written by Claude Code on behalf of max_
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    max-sixty and claude authored Jun 20, 2026
    Configuration menu
    Copy the full SHA
    a17fc22 View commit details
    Browse the repository at this point in the history
  5. Configuration menu
    Copy the full SHA
    964fbe0 View commit details
    Browse the repository at this point in the history
  6. refactor(picker): migrate to skim 4.8 (ratatui), drop vendored skim-t…

    …uikit (#3137)
    
    Migrates the `wt switch` picker from skim 0.20.5 (tuikit backend) to
    skim 4.8.0 (ratatui/crossterm), removes the `vendor/skim-tuikit/` patch
    tree we carried against the old line, and makes the full test suite
    green under the new backend.
    
    ## Why the vendor tree goes away
    
    We vendored skim-tuikit for two patches: `alt-<digit>` key parsing and
    an `Output::flush` `write_all` fix for dropped bytes under PTY pressure.
    Both are moot in 4.8. Key parsing is native — `binds.rs` routes every
    single character (digits included) through `KeyCode::Char` rather than a
    hardcoded `alt-a..alt-z` arm list. The flush fix is subsumed by stdlib
    `BufWriter::flush` in the ratatui backend, which loops over partial
    writes correctly. So the migration carries zero vendor patches against
    skim, and the build machinery that kept the vendored source alive
    (Taskfile `vendor-diff`, flake `vendorSrc`/`extraDummyScript`) is
    deleted too.
    
    ## The picker rewrite
    
    skim 4.x is a different API. `WorktreeSkimItem::display()` now returns
    `ratatui::text::Line` instead of an `AnsiString`. alt-r removal rides
    skim's `reload(remove {})` token (the selected row's `output()`,
    expanded into the reload command) instead of the old
    `as_any().downcast_ref` path (which never worked across compilation
    units) plus a signal file. Preview-tab switching and the progressive
    list redraw are driven by `Skim::event_sender()` +
    `Event::Render`/`Event::RunPreview`.
    
    ## The regression that motivated the bulk of this
    
    skim 4.x renders on demand. There is no 100ms tuikit heartbeat
    re-rendering the frame, so the progressive `wt switch` list rendered
    blank while collection ran in the background — the original report. The
    fix pokes `Event::Render` from the list-collect callbacks (throttled to
    16ms). The same on-demand model surfaced four more regressions, all
    fixed here and verified head-to-head against the 0.20 picker in a
    terminal: preview-tab lag, current-row highlight, Shift-Tab back-cycle
    (crossterm reports Shift-Tab under three distinct `KeyEvent` shapes, so
    all three are bound), and alt-r removal (4.x `execute-silent` is
    fire-and-forget and raced its reader).
    
    ## Help text and the test harness
    
    Two things the integration suite caught that the unit tests did not (the
    prior pass ran `--lib --bins` only):
    
    - **Styled help.** skim 0.20 transitively pulled in
    `clap/unstable-markdown`, which renders worktrunk's `///` doc-comments
    as styled help. skim 4 with `default-features = false` drops it, so help
    reverted to raw text (`[experimental]` → `\[experimental\]`). Restored
    by depending on `unstable-markdown` explicitly, keeping help output
    identical and the `test_help` / `test_step_alias` /
    `test_docs_are_in_sync` snapshots passing unchanged.
    - **PTY harness.** skim 4.x queries cursor position (`ESC[6n`) at
    startup in partial-height mode and blocks in `select()` for the reply;
    `portable_pty` is a bare PTY and never answered, so every
    `switch_picker` test failed init with "Cursor position detection timed
    out." The Unix harness now answers the query, mirroring the existing
    ConPTY responder. skim 4.x also draws the list/preview separator one
    column left, so the snapshot panel-split columns shifted to match. The
    regenerated picker snapshots show two cosmetic changes: the match
    counter no longer overlaps the preview tab header, and the HEAD column
    shows the full short-SHA instead of a truncated one.
    
    ## Minimal-versions CI
    
    The skim 4.8 dep tree pins tighter floors than our manifests declared,
    so the nightly `minimal-versions` job needed updating. It now runs `-Z
    direct-minimal-versions` — minimize only our own direct deps and let
    transitive crates resolve normally — and the manifests raise each
    under-specified floor to the minimum the workspace builds against
    (largely mirroring skim 4.8's own requirements). Full `-Z
    minimal-versions` would instead drag in skim's transitive TUI/image
    stack (ansi-to-tui, ratatui's `instability` macro, color-eyre,
    ratatui-image → image/avif), whose crates under-declare their floors and
    don't compile at the picked versions; direct minimization confines the
    check to floors we own, so no transitive pins are needed. Normal
    resolution (the committed `Cargo.lock`) is unchanged. Full floor list
    and the `signal-hook` libc-pin detail are in the `ci(min-versions)`
    commit message.
    
    ## Reviewing
    
    Start at `src/commands/picker/mod.rs` (the `run_skim` entry point, the
    action keybinds, and `parse_reload_remove_token`), then
    `progressive_handler.rs` (the render pokes) and `items.rs` (the
    `Line`-based `display()`). Test-harness changes are in
    `tests/common/pty.rs` (the `ESC[6n` responder) and
    `tests/integration_tests/switch_picker.rs` (the panel-split columns).
    The rest is the dependency swap, the vendor and build-machinery
    deletions, and regenerated snapshots.
    
    All 3980 tests pass (`cargo run -- hook pre-merge --yes`), including the
    `shell-integration-tests` PTY suite that exercises the picker end-to-end
    across the list, previews, scroll, create/remove, and accept flows. No
    automated test drives a real terminal; the interactive surface was also
    checked by hand against the 0.20 picker.
    
    > _This was written by Claude Code on behalf of max_
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    max-sixty and claude authored Jun 20, 2026
    Configuration menu
    Copy the full SHA
    958b9c1 View commit details
    Browse the repository at this point in the history
  7. Configuration menu
    Copy the full SHA
    9147c0e View commit details
    Browse the repository at this point in the history

Commits on Jun 21, 2026

  1. Add no-color.org to lychee exclusions

    The site frequently blocks CI checks; exclude it from link validation to improve reliability.
    max-sixty committed Jun 21, 2026
    Configuration menu
    Copy the full SHA
    389c596 View commit details
    Browse the repository at this point in the history
  2. perf(copy): skip redundant chmod after reflink on macOS (#3149)

    On macOS/APFS, clonefile() already preserves the source's mode bits, so the post-reflink set_permissions call in copy_leaf is redundant; gate it to non-macOS unix. Linux btrfs/XFS still need it because FICLONE clones data extents only and drops the execute bit, while std::fs::copy's fallback preserves permissions on every platform. Saves one chmod syscall per file on macOS for the copy-ignored step and every other copy.rs caller.
    max-sixty authored Jun 21, 2026
    Configuration menu
    Copy the full SHA
    ae3752c View commit details
    Browse the repository at this point in the history
  3. Release v0.61.0 (#3151)

    max-sixty authored Jun 21, 2026
    Configuration menu
    Copy the full SHA
    e9b822c View commit details
    Browse the repository at this point in the history
Loading