Commit Graph

115 Commits

Author SHA1 Message Date
Worktrunk Bot 667c6efaf0 docs(switch): note that Alt-x never forces (#3811)
Requested by @max-sixty in
[#3809](https://github.com/max-sixty/worktrunk/issues/3809#issuecomment-5273484502)
— a couple of words clarifying that `Alt-x`'s removal is safe-only.

The keybinding table read `Remove selected worktree/branch`, which
doesn't say the removal never forces; that's what sent the reporter
looking for a force-remove that isn't there. The picker hardcodes the
safe path —
[`prepare_removal`](https://github.com/max-sixty/worktrunk/blob/7a2a3e003e7eed138ff5f2dcd2296f6bbd8e86d4/src/commands/picker/mod.rs#L323-L330)
passes `BranchDeletionMode::SafeDelete` and `force_worktree: false`.

Deliberately scoped to the table cell, per the "(only)" in the request.
The bigger question — whether `Alt-x` should ever pass `-D` — is still
open on the issue and isn't touched here.

Primary source is `after_long_help` in `src/cli/mod.rs`; the three
mirrors and the `--help` snapshot are regenerated.

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-12 16:19:20 -07:00
Maximilian Roos 6d09125b7b test: converge suite on semantic boundaries (#3663)
This follows the first test-simplification tranche by converging the
remaining suite around distinct semantic and pragmatic contracts rather
than raw case count. The branch removes false-confidence tests, invalid
setup variants, repetitive snapshots, and expensive PTY overlap while
strengthening the retained route, precondition, and interaction proofs.

## What changed

- Replace obsolete CI-status integration mocks and blank snapshots with
direct provider semantics, mixed-priority cases, and strict
GitHub/GitLab route assertions.
- Remove free-riding merge, push, remove, list, security, config, and
switch cases whose setup never reached the named behavior; consolidate
repetitive direct cases into labeled tables.
- Reduce the switch picker from 42 PTYs to 19 distinct terminal
contracts, using causal release gates for asynchronous loading and
repaint behavior.
- Add a cached main-only picker fixture, eliminating 138 unnecessary Git
subprocesses across the retained PTYs, and integrate it with main's
generated hermetic standard fixture.
- Tighten test guidance around proving setup preconditions and mock
invocation routes, and correct the comments-tab help text and generated
mirrors.

## Reviewer map

- `tests/integration_tests/ci_status.rs` and
`src/commands/list/ci_status/`: provider semantics and route coverage.
- `tests/integration_tests/switch_picker.rs`, `src/commands/picker/`,
and `src/testing/`: retained PTY contracts, causal mocks, and fixture
design.
- `tests/integration_tests/config_show.rs`, `src/config/deprecation.rs`,
`src/config/expansion.rs`, and worktree type/resolve tests:
direct-boundary consolidation.
- `tests/CLAUDE.md`: the testing rules extracted from the
false-confidence cases found during the survey.

The measured loop removed 98 tests, 65 snapshots, and 23 picker PTYs.
Controlled warm Nextest execution improved from a 79.593-second mean to
72.574 seconds (8.8%), while comparable production-line coverage moved
from 97.32% to 97.23%. The tracked PR diff is a net deletion of more
than 5,700 lines.

## Validation

- `cargo run -- hook pre-merge --yes` after syncing current `main`:
4,468 passed, one configured skip; docs, doctests, clippy, formatting,
policy checks, and snapshots green.
- `task coverage` on the completed change before the base sync: 4,465
passed, one configured skip; 97.23% comparable production-line coverage.
- Three independent final audits found no remaining lost beliefs,
fixture hazards, or safe PTY consolidations.

> _This was written by Claude Code on behalf of max_.
2026-07-30 00:01:32 -07:00
Maximilian Roos 14580de79c feat(worktree): accept a worktree path wherever a branch is accepted (#3607)
Follow-up to the [`wt remove <path>` discussion on
#3480](https://github.com/max-sixty/worktrunk/pull/3480#issuecomment-5039137116),
widened from that one command to the whole surface. #3480 has since
landed and is merged in here — its duplicate-checkout warning composes
with this: the warning names the shadowed worktrees, and a path is how
you then address one.

## Audit

Verified against the built binary. wt had three answers to "what does
this token mean?":

| Route | `@` `-` `^` | worktree path | `pr:N` |
|---|---|---|---|
| `wt switch` (`resolve_switch_target`) | yes | only if absolute or ≥2
components, and not `--create` | yes |
| `wt remove` (`resolve_worktree_arg`) | yes | any token | no |
| everything else (raw `worktree_for_branch`) | **no** | **no** | no |

Same token, same cwd, two answers:

```console
$ wt remove inner     # ✓ Removed innerbranch worktree & branch
$ wt switch inner     # ✗ No branch named inner
```

And outside switch/remove the shortcuts didn't work at all — `wt step
diff --branch @` was `✗ Branch @ has no worktree`, while `wt config
state marker set --branch @` silently wrote state under the literal key
`@`. Separately, wt prints paths as `~/…` but wouldn't accept that form
back.

## Change

One canonicalizer in the lib, `Repository::resolve_worktree`, absorbing
the path fallback that lived in the bin crate's `resolve_worktree_arg`
(now deleted). Resolution order is documented once, on that function:
`@`, then `-`/`^`, then a branch with a worktree, then a path naming a
registered worktree, then the branch alone.

**Branch-first, everywhere.** A directory never shadows a branch that
shares its name; a path answers only what a branch cannot — a detached
worktree, or one of two checkouts of the same branch (#3480's case). The
`looks_like_path` shape gate is gone, so a single-component path
resolves like any other.

Two shapes cover what callers need: `require_worktree` for commands that
need a worktree to operate in, `require_selected_branch` for arguments
that key by branch. The merge/rebase target validators fall through to
the same path lookup, so a target can be named by the worktree it's
checked out in.

Routed through it: `switch` (including `--base`), `remove`, `step commit
--branch`, `step diff --branch` and its target, `step copy-ignored
--from`/`--to`, `step promote`, `step relocate`, `config state --branch`
(9 sites), and `merge` / `step rebase` / `step squash` / `step push`
targets.

`resolve_input_path` — already documented as the one resolution point
for user-supplied paths — now expands a leading `~`, so the tilde form
worktrunk prints is a form it reads back. `~user` stays literal; wt
doesn't reimplement that shell feature.

## Documentation

A path is an alias, not a second addressing scheme, so it is stated once
rather than on every argument: one paragraph in `wt switch`'s help and
one sentence on the addressing line in `worktrunk.md`. Argument
descriptions still read as branches. The two exceptions are the
arguments whose descriptions are already catalogues of accepted forms —
`wt switch`'s (`Branch, worktree path, shortcut, or PR/MR URL`) and `wt
remove`'s, which has named the path since before this branch. The
Worktree Model section of `CLAUDE.md` records which way to document it,
so the next argument doesn't grow its own copy.

## Two silent no-ops fixed along the way

- `wt step relocate <unmatched>` matched arguments against branch names
by string equality, so a typo filtered everything out and the empty
result rendered as `○ All worktrees are at expected paths` — a success
message for work that never happened. Every way an argument can fail to
land on a relocatable worktree now errors, including the detached and
prunable cases the new path route makes reachable.
- A selector matching nothing was reported as a branch without a
worktree, hinting `wt switch <token>` — which creates a worktree only
when the branch exists, so for a mistyped path it would just fail again.
`WorktreeSelectorNotFound` now says `No branch or worktree named X`; a
branch that genuinely exists without a checkout keeps the create hint.

## Testing

Full gate green: 4596 tests, lints, docs sync, `--features
shell-integration-tests` clippy. `codecov/patch` is 99.25% of diff hit
against a 97.93% target. New coverage:

- Unit: branch-and-path equivalence, branch-beats-same-named-directory,
detached-by-path (and its `require_selected_branch` refusal), shortcuts
never treated as paths, branch-only fallthrough, and the two distinct
not-found errors. Plus `expand_tilde` round-tripping
`format_path_for_display`.
- Integration: `switch` by relative/single-component/absolute/tilde
path, `--base` by path, `step diff --branch` by path and `@` (asserted
equal to the by-branch output), `config state --branch` set via `@` and
read via the worktree path, and both new relocate errors.

`wt remove`'s resolution is unchanged — it already had this rule; it now
shares the implementation. The 106-test `remove::` suite is untouched
and green.

- Integration: `wt step push <worktree-path>` (the
`require_target_branch` half of the target fallback), and `wt step
relocate` against a prunable worktree.

One diff line is unhit: `expand_tilde`'s fallback when `home_dir()`
returns `None`, which has no deterministic trigger. The `@`-resolution
backstop in `resolve_worktree` is untested for the same reason — no CLI
route reaches it — so it kept its original `match` arm rather than being
re-indented into the diff.

## Left out

`wt config state default-branch set` and `previous-branch set` take a
branch name as a *value to store* rather than a selector, so they still
take it literally.

> _This was written by Claude Code on behalf of Maximilian Roos_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:10:46 -07:00
Worktrunk Bot bd504f0998 feat(switch): run --execute against the picked worktree (#3394) 2026-07-10 04:30:14 -07:00
Maximilian Roos 8e2ab04c7d feat(switch): rework the alt-x picker removal — morph in place, decline current worktree, identity cursor (#3262)
Overhauls the `wt switch` picker's `alt-x` (remove) so a removal updates
the selected row in place instead of re-collecting the whole list,
declines removals that can't safely happen with an explanation, and
lands the cursor on the right row afterward — including under an active
filter.

## What changes for the user

`alt-x` on a row now behaves by what the removal does:

- **Unmerged worktree** → the row morphs in place to a `/ branch` row
(worktree gone, branch kept); the worktree removal runs in the
background. No flicker, cursor stays put.
- **Merged worktree** → the row drops; the cursor lands on the row that
slid up into its slot.
- **Current worktree (`@`), main, dirty, locked** → kept, with the same
diagnostic `wt remove` prints (drained to stderr on picker exit) instead
of a silent dead keypress or a disruptive cd-home.

Before, `alt-x` re-collected the entire list (a visible flicker, cursor
reset to top), and removing the current worktree forced a `cd` elsewhere
mid-render.

## The cursor fix (last commit)

The post-removal reposition scrolled to the removed row's index in the
full `shared_items` list. Under a fuzzy query, skim's `item_list` is
filtered and reordered, so that index pointed at the wrong row — the
cursor jumped +N rows down (N = filtered-out rows above it), compounding
on a second removal. Reposition is now by **row identity**: it walks
`item_list` to the target row's `output()` token. The drop's landing row
(the display-neighbor) is captured before the reload via a native
`alt-x` binding (`[capture, reload]`); keep/morph/restore land on their
own row's token.

## Reviewing

The bulk is `src/commands/picker/mod.rs`. Key pieces:
- `invoke` dispatch (`removal_targets_current_worktree` →
`worktree_removal_keeps_branch` → `removal_will_remove_target`) decides
keep / morph / drop.
- `morph_and_remove_in_background` does the in-place row morph
(optimistic, reverted if the worktree unexpectedly survives).
- `reposition_cursor_action` / `install_remove_keybinding` are the
identity-based cursor landing.

## Testing

Picker unit tests plus PTY integration tests through real skim,
including the new
`test_switch_picker_alt_x_lands_on_neighbor_under_filter` (removes a row
with filtered-out decoys above it — confirmed to fail on the old
index-based reposition and pass on identity). Multi-row cursor tests
cover both drop and keep paths.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:33:22 -07:00
Maximilian Roos a725a2d786 Unify wt switch and --prs: differ only by the extra PR rows (#3252)
Full filter parity (branch, path, PR number/title/author) across worktree and --prs rows via a shared token builder; --prs dedups already-shown branches; worktree match text folds in PR data live as the forge fetch lands. Author plumbed through both forges.
2026-06-25 17:55:54 -07:00
Maximilian Roos de97744718 feat(switch): picker shortcuts — copy, open, refresh, remap remove to alt-x (#3233)
Adds four keyboard shortcuts to the interactive `wt switch` picker: `alt-y` (copy the selected branch), `alt-o` (open the row's PR/MR), `alt-r` (refresh the list), and `alt-x` (remove, remapped from `alt-r`).

`alt-y`/`alt-o` are native skim `Action::Custom` callbacks reading the selected row off `App.item_list` (no reload, so the cursor stays put and `--prs` rows aren't dropped); the row → branch/URL lookup is extracted into `resolve_shortcut_branch`/`resolve_shortcut_url` and unit-tested. `alt-r` refresh re-enters the collect pipeline via a new `PipelineFactory`. `alt-y` no-ops on a detached worktree (no branch). New cross-platform deps: `arboard` (clipboard) and `open` (browser), gated behind the `cli` feature.

Merged over `codecov/patch` (89.76% vs 97.61% target) with explicit approval: the uncovered residual is the picker's clipboard/browser/thread-spawn and skim-`App`-bound closures, which a headless CI runner can't exercise. All required jobs green; reviewed and approved by worktrunk-bot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:21:55 -07:00
Maximilian Roos 8f17cb4a91 test(switch): run the interactive picker tests on Windows (#3221)
PR #3217 made `wt switch`'s interactive picker run on Windows, but its
tests stayed `#[cfg(unix)]` — so the Windows CI leg proved the picker
*compiled* without ever exercising its runtime. This ungates them so
Windows CI drives the real thing.

**What's ungated** (the test infra — `mock_commands`, the ConPTY
handling in `tests/common/pty.rs` — was already cross-platform, so this
is largely a gate flip):

- `switch_picker_dry_run.rs` — the headless dry-run pipeline (collect →
render → preview cache → summary spawn → warning drain).
- The `--prs` dry-run tests in `switch.rs` — forge fetch + row render
via the cross-platform mock `gh`/`glab`.
- The two requires-tty snapshots — the no-TTY bail is uniform across
platforms.
- `switch_picker.rs` — the PTY/vt100 TUI snapshot tests.

**Test-quality fix folded in:** the four `--prs` dry-run tests asserted
only `output.status.success()`, but the fetch path is fail-soft (any
error → stashed warning → exit 0). On a platform where the mock didn't
resolve they would pass vacuously. They now assert the fetched row
actually rendered (a `pr:42`/`mr:7` preview-cache entry, or the
empty-list warning).

**Also:** `/bin/cat` → `cat` in the summary test (resolves via the
platform shell on Unix and Git Bash alike); removed the stale "picker is
Unix-only" line that #3217 left in the `wt switch` docs (and regenerated
the help snapshot).

**Windows results (first full run):** of the ~50 ungated tests, 46
passed on Windows on the first run — including every PTY/vt100 snapshot
test (skim renders byte-identically under ConPTY), the accept-path drain
tests, and the `cat`-via-Git-Bash summary test. Two issues surfaced and
are handled here:

- The three interactive `--prs` picker tests failed because the test
helper `mock_forge_env` joined mock-bin onto `PATH` with a hardcoded `:`
separator — malformed on Windows (`;`), so the mock `gh.exe`/`glab.exe`
was never found. Fixed to use `std::env::join_paths` (OS-aware),
matching the dry-run path's helper.
- `test_switch_picker_pre_switch_hook_requires_approval` exits 1 instead
of 0 on Windows: declining the hook returns `Ok` on both platforms, so
the failure is in the interactive approval prompt's stdin handoff after
skim releases the ConPTY. Not reproducible locally —
`#[cfg_attr(windows, ignore)]` with a TODO until a Windows box can debug
it (still compiles on Windows, runs on Unix). This is a genuine
picker-on-Windows gap surfaced by this work, tracked for follow-up.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:45:58 -07:00
Maximilian Roos b3e1fc06b4 feat(switch): log + comments preview tabs for --prs picker rows (#3169)
Adds background-loaded `log` and `comments` preview tabs to `wt switch --prs` picker rows, with a width-aware tab bar that compacts to fit the 7th tab. The log tab uses a local `git log` when the head commit is present, else a forge-fetched list.
2026-06-23 19:10:15 -07:00
Maximilian Roos 81f9260a46 feat(switch): browse open PRs/MRs in the interactive picker (#3128)
Adds `wt switch --prs`: browse the repository's open PRs (GitHub) / MRs (GitLab) in the interactive picker, with a live CI column (same data as `wt list --full`), a `pr` preview tab showing the markdown-rendered description, and `#`-gutter-sigil filtering. Selection routes through the existing `pr:`/`mr:` fetch-and-switch path, so there's no new switch logic.
2026-06-22 16:34:04 -07:00
Maximilian Roos 3d0b99e072 Add WORKTRUNK_VERBOSE env var equivalent to -v/-vv (#3166)
Shell tab-completion runs the `wt` binary as its own subprocess (the
shell sets `COMPLETE=<shell>`), and that path returns from `parse_cli`
before `main` ever reaches `logging::init`. So when a tab-completion is
slow, there's no flag that turns on logging for it — `-v`/`-vv` never
run, and `RUST_LOG` only sets a level, not the `-vv` file sinks. There
was no way to profile a slow completion.

This adds `WORKTRUNK_VERBOSE=0|1|2` as the env-var equivalent of the
`-v`/`-vv` flag count. It's read everywhere — including the completion
path, which no flag can reach — and combined with the flag via `max`, so
the env sets a baseline the flag can raise but never lower. Completion
behaves *identically* to a flagged command at the same level: at level 2
it writes the same `trace.log`/`subprocess.log`/`diagnostic.md` under
`.git/wt/logs/`, so a slow tab-completion can be profiled with:

```console
$ WORKTRUNK_VERBOSE=2 COMPLETE=fish wt -- wt switch ''
```

then reading `trace.log`. (Set it inline like that, or as a one-off,
rather than `export`-ing it — an exported value makes *every* TAB run as
`-vv`, printing the "Writing to…" banner above your prompt and
re-truncating the shared trace files on each keystroke. That's just
normal `-vv` shared-sink behavior, but it's noisy interactively.)

The one place completion deliberately diverges: it strips
`WORKTRUNK_VERBOSE` from the environment of any forwarded `wt-*`
custom-subcommand child, so the child doesn't re-run `logging::init` and
clobber the trace files the parent completion just wrote (its stderr is
discarded anyway).

### Testing

Integration tests cover: `WORKTRUNK_VERBOSE=2` opens the trace files
like `-vv` while `=1` does not; flag `-vv` combined with env `0` still
writes (the `max`); and completion at level 2 writes `[wt-trace]`/`$
git` records to `trace.log` while candidates still go to stdout. A unit
test pins the lossy parse (empty/garbage/out-of-range → `0`, never an
error) so a stray value can't corrupt the completion candidate list.
Docs (faq, config, the env-var table) and help snapshots are synced.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:46:06 -07:00
Maximilian Roos c5c2742058 feat(switch): make the picker filter text match the gutter sigil (#3143)
Typing into the `wt switch` picker filtered only on branch name + path, so typing a gutter sigil like `+` matched nothing. Each row now folds its gutter glyph into the fuzzy-search text via a canonical `ItemKind::gutter_glyph` (the single source shared with the rendered Gutter column, replacing `BranchScope::gutter_sigil`), so `+` filters to linked worktrees and `@` to the current worktree.

The glyph is a skeleton-time fact (`is_current`/`is_main` set at construction, `BranchScope` structural), so folding it in keeps fuzzy ranks stable across the picker's progressive column updates, and the rendered Gutter column stays byte-identical.

`^` and `|` collide with skim's prefix-anchor and OR query operators (so `^` matches everything and `|` nothing), and `/` over-matches because every path contains it; the picker help documents which sigils filter cleanly, and an engine-level test pins the operator semantics against skim 4.8's default stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:36:11 -07:00
Maximilian Roos a17fc22904 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>
2026-06-19 23:04:19 -07:00
Maximilian Roos a7e29f79c7 feat(list): custom template columns and cached PR numbers in the picker (#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>
2026-06-19 11:37:22 -07:00
Maximilian Roos 41d5ef7349 docs(cli): render --format help as terse inline possible-values (#3108)
Every `--format` flag — `wt step
commit`/`squash`/`rebase`/`push`/`for-each`/`copy-ignored`, `wt remove`,
`wt switch`, `wt merge`, `wt config show` — rendered a verbose `Possible
values: - text: … - json: …` block in long help and the generated doc
pages. clap auto-generates that block from the per-variant doc comments
on the shared `SwitchFormat` enum.

Dropping those variant doc comments makes clap render the terser inline
`[possible values: text, json]` instead, slimming every `--format`
flag's long help in one place. The variant names are self-describing, so
the descriptions added nothing.

The sibling `OutputFormat` enum gets the same trim for `Table`/`Json`,
but `claude-code` keeps its description — "reads context from stdin"
conveys real, non-obvious behavior the bare name doesn't. That one
surfaces in `wt list statusline --help`.

Doc mirrors (`docs/content/`, `skills/worktrunk/reference/`) and help
snapshots are regenerated.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:49:43 -07:00
Worktrunk Bot 6f63a5bc54 docs(switch): refine wt switch --help text (#3096)
## Summary

Picks up the four `wt switch --help` refinements from #3095:

1. Use backtick emphasis for shortcut symbols (`^`, `-`, `@`, `pr:{N}`,
`mr:{N}`) instead of single quotes — matches the existing style on
`--base`.
2. Surface PR/MR URL acceptance in the `[BRANCH]` argument summary
("Branch name, shortcut, or PR/MR URL"), so the short help mirrors what
the long help already documents.
3. Rewrite the Forks paragraph as prose, opening with *"If the PR or MR
is on a fork…"* and using descriptive mood ("requires renaming") instead
of imperative ("rename it first"). Drops the `(e.g., feature-fix)`
example.
4. Consolidate the per-forge notes — the Requires line, the standalone
Gitea (experimental) paragraph, and the standalone Azure DevOps
(experimental) paragraph — into a single footer pointing at [forge
platform](https://worktrunk.dev/config/#forge-platform) for the
platform-specific setup.

## Before / after — `Pull requests and merge requests` section

Before:

> Both work anywhere a branch is accepted, including `--base`.
>
> Requires `gh` (GitHub) or `glab` (GitLab) CLI to be installed and
authenticated. The `--create` flag cannot be used with a PR/MR reference
since the branch already exists.
>
> **Forks:** The local branch uses the PR/MR's branch name directly
(e.g., `feature-fix`), so `git push` works normally. If a local branch
with that name already exists tracking something else, rename it first.
>
> **Gitea (experimental):** `pr:` is also compatible with Gitea via the
`tea` CLI. Set `[forge] platform = "gitea"` in `.config/wt.toml` to opt
in; worktrunk also auto-detects Gitea when the remote host contains
`gitea` or when `tea login add` has been run for the host.
>
> **Azure DevOps (experimental):** `pr:` is also compatible with Azure
DevOps via the `az` CLI (with the `azure-devops` extension). Set
`[forge] platform = "azure-devops"` in `.config/wt.toml` to opt in;
worktrunk also auto-detects Azure DevOps from `dev.azure.com` and
`*.visualstudio.com` remotes.

After:

> Both work anywhere a branch is accepted, including `--base`. The
`--create` flag cannot be used with a PR/MR reference since the branch
already exists.
>
> If the PR or MR is on a fork, the local branch uses its branch name
directly, so `git push` works normally. A pre-existing local branch with
that name tracking something else requires renaming first.
>
> Requires `gh` (GitHub), `glab` (GitLab), or an equivalent CLI
installed and authenticated; see [forge
platform](@/config.md#forge-platform) for Gitea, Azure DevOps, and other
supported platforms.

## Testing

- `cargo test --test integration test_docs_are_in_sync` — generated docs
mirrors regenerated and now in sync.
- `cargo test --test integration test_help` — short and long `switch
--help` snapshots updated.
- `cargo build` — clean compile.
- Inspected the rendered `wt switch --help` output to confirm the
shortcuts render with bold emphasis and the consolidated footer reads
cleanly.

## Notes for review

- I did not sweep other `--help` pages for parenthesized "e.g." examples
— the issue mentioned looking for more, but I scoped this PR to the
switch help and left the broader pass for a maintainer call. One other
in-scope candidate at `src/cli/mod.rs:569` (`Switching to a remote
branch (e.g., wt switch feature when only origin/feature exists) creates
a local tracking branch.`) felt like a useful clarification of "remote
branch" rather than a redundant example, so I left it alone.
- The forge-platform link uses the `@/config.md#forge-platform` form
that the rest of `after_long_help` uses for cross-doc references.

---
Closes #3095 — automated triage

Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-17 10:37:29 -07:00
Maximilian Roos 7d3c923ef0 feat(picker): free digit keys for typing; preview tabs to Alt + Tab/Shift-Tab (#3079) 2026-06-15 01:59:29 -07:00
Maximilian Roos 5da0d2c3e4 docs: alias template-expansion timing; fix narrow help-test env redaction (#3006)
## Docs: alias template-expansion timing

The main addition documents a gotcha that's easy to hit and hard to
diagnose: an alias body renders its `{{ … }}` once at dispatch, in the
invoking worktree, so a per-worktree variable like `{{ branch }}` is
baked to a single value *before* a nested `wt step for-each` / `wt
switch --execute` iterates — printing the same value in every worktree.
The fix is `{% raw %}…{% endraw %}` deferral (plus a quoted `sh -c '…'`
for `for-each`, since the deferred `{{ branch }}` contains spaces).

- `extending.md` — rewrote "Deferring expansion to a nested `wt`
command" around the `for-each` symptom; improved the `up` rebase recipe.
- `faq.md`, `troubleshooting.md` — symptom-first entries with `wt config
alias dry-run` as the diagnostic.
- `hook.md` / `config.md` / `step.md` — distinguish repo-level
(constant) vs per-worktree (active) variables; note `{{ default_branch
}}` needs no deferral; cross-link the `{{ default_branch }}` variable vs
the `wt config state default-branch` shell command.

## Factual corrections

- `integration_reason` JSON values are hyphenated (`trees-match`,
`no-added-changes`, `merge-adds-nothing`) — the docs had underscores.
Verified against `src/commands/list/model/state.rs`.
- `SKILL.md`: 7 → 10 hook types (5 events × pre/post), added an
aliases/multi-worktree task section, fixed stale anchor links.

## Test fix: narrow help-test env redaction

`test_help_list_narrow_terminal` built its own `insta::Settings` but
skipped `add_standard_env_redactions` (every other help snapshot routes
through `snapshot_help`, which calls it). Its snapshot env block
therefore leaked host-specific paths (`LLVM_PROFILE_FILE` = the
machine's temp dir, plus the `WORKTRUNK_*` paths), which churn whenever
the snapshot is regenerated on a different machine. Adding the one call
mirrors `snapshot_help` and makes the snapshot reproducible.

Worth noting (and a candidate follow-up): this gap was masked under
`cargo test` (libtest) because the `repo` fixture's `mem::forget`'d
`bind_to_scope()` guard leaks redaction settings across the shared
process's reused threads. Under nextest (process-per-test, what the
pre-merge hook uses) there's no leak, so a test missing its own
redactions is exposed. A few other help tests (`test_help_md`,
`test_version`, `test_nested_subcommand_suggestion`) have the same gap
and could be consolidated through one settings helper — left out of this
PR to keep it focused.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 00:00:52 -07:00
Maximilian Roos 2dd5d60902 docs(switch): give PR links equal billing with pr:N (#2970) 2026-06-02 22:47:30 -07:00
Worktrunk Bot 456c8506ea feat(switch): accept forge PR/MR URLs anywhere pr:N / mr:N works (#2898) 2026-05-27 02:11:22 -07:00
Maximilian Roos 21e0b27e49 refactor(verbose): rename output.log → subprocess.log; -vv keeps Info on stderr (#2913)
## Motivation

The `-v` / `-vv` UX had three small issues that compounded:

1. **`output.log` is misnamed.** It holds the *uncapped raw
stdout/stderr of every subprocess `wt` spawns* — multi-MB possible (`git
log -p`, patch-id pipelines, etc.). "output" reads as "stuff `wt`
printed" — the small thing — when it's actually the big thing. Easy to
misread.
2. **`-vv` went fully dark on stderr.** PR #2892 moved the noisy debug
pipeline to files at `-vv`; in the process, the stderr layer was
disabled entirely. Users running `-vv` to see hook output (info-level,
which `-v` shows on stderr) suddenly couldn't.
3. **`-v` help text was a 150-char one-liner** packed into a
parenthetical, and the surrounding docs leaned on a "stderr stays
readable / `log::*` pipeline" framing that was Rust-jargon-flavored and
implied stderr-quiet at `-vv` — which is no longer true after change #2.

## Change

- **Rename `output.log` → `subprocess.log`.** Filename now matches
content. `OUTPUT` static → `SUBPROCESS`, plus the related
`OutputMakeWriter` / `OutputFileFormat` / `build_output_layer` symbol
renames.
- **`-vv` keeps the Info baseline on stderr.** `build_stderr_layer` no
longer returns `None` at `-vv`; debug-level records still route to file
layers only, so the terminal stays readable while info-level status
(hook output, template variables, the `Tracing to ...` pointer) shows
the same as at `-v`.
- **`-v` help text rewritten** to describe both levels cleanly without a
wall of detail.
- **`docs/content/faq.md` gets a "What does -v / -vv do?" section** with
a three-level table.
- **Docs cleanup**: drop "stderr stays readable" / `log::*` jargon /
"but not subprocess.log" negative framing from user-facing prose.

## Notes for review

- The only `log::info!` site in the codebase is
`commands/picker/mod.rs:389` (a single picker error message), so making
`-vv` show info-level on stderr doesn't add meaningful noise.
- `test_vv_log_pipeline_silent_on_stderr` is renamed to
`test_vv_debug_pipeline_silent_on_stderr` — its assertions only check
debug-level records stay out of stderr (they do); the old name implied
the whole `log::*` pipeline was silent, which was never quite true
(direct `eprintln!` always showed) and is less true now (info-level
routes to stderr).
- 67 of the 69 changed files are snapshot updates (help text and one
diagnostic snapshot) and auto-synced doc/skill mirrors. `git diff --stat
-- 'tests/snapshots/*' 'docs/content/*' 'skills/worktrunk/reference/*' |
tail -1` separates them.
- CHANGELOG: not touched. The historical entry that introduced
`output.log` (`#2201`) stays accurate for its release; this rename gets
a new line in the next release.

## Tests

3870 tests pass. Re-snapshotted all `test_help_*` snapshots, three
`step_alias` snapshots that quote the global help, and the diagnostic
file format snapshot.

> _This was written by Claude Code on behalf of max-sixty_

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:55:03 -07:00
Maximilian Roos 93b441c8f0 feat(switch): deprecate shell command lines in --execute (#2852)
## What

A future release will switch `wt switch --execute` (`-x`) to an argv
input model: a single program name, with arguments after `--` passed
verbatim, run via `execvp` with no implicit shell. That is a breaking
change to a protected CLI interface, so it ships as a two-release
deprecation — **this PR is the warn phase.**

`validate_switch_templates` now emits a deprecation warning when the
`-x` value is not a single program token — it contains shell syntax,
multiple words, or `{{ }}` template markup. The hint shows the concrete,
copy-pasteable migration:

```
▲ --execute will change in a future release: it will run a single program,
  with arguments after --, not a shell command line
↳ To run this command line unchanged, pass it to a shell:
  --execute sh -- -c 'echo hi && ls'
```

`sh` is itself a single program token, so the suggested form works
today, does not warn, and survives the cutover. A single program name —
including a path — stays silent.

The `-x` help examples in `src/cli/mod.rs` move to the argv-compatible
form (`-x code -- '{{ worktree_path }}'`) so the docs no longer
recommend a form that warns.

## Why no PATH check

The classifier is purely structural — it decides whether the value is
one bare program token, nothing more. It deliberately does not check
whether a bare name resolves to a real executable. An earlier iteration
did, to also warn on `-x my-alias`, but a PATH lookup at pre-flight is
environment-sensitive, runs in the source worktree rather than where
`-x` will execute, and cannot distinguish a shell alias from a typo or
an uninstalled tool. A bare alias/function `-x` is left to fail loudly
(`execvp` → `ENOENT`) at the cutover rather than guessed at here. The
warning still catches every multi-word / shell-syntax form, which is
what users actually write.

## Testing

`cargo run -- hook pre-merge --yes` — 3799 tests pass; clippy, fmt, and
pre-commit hooks green. New: a unit test for the token classifier and an
integration test covering the warn and no-warn cases. Most of the
32-file diff is snapshot regeneration — the warning is new stderr output
on existing `-x` tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:40:51 -07:00
Worktrunk Bot ec62580c29 revert(hooks): keep docs on pre-start/post-start; code accepts both (#2857)
Per @max-sixty's [direction in
#2838](https://github.com/max-sixty/worktrunk/issues/2838#issuecomment-4509447593):
revert the docs portion of #2840 and keep the code. Docs continue to
recommend `pre-start`/`post-start`; both names work in code so anyone
who already followed the briefly-changed docs (e.g. @EcksDy) isn't
stranded once a release ships these aliases.

## User-visible — back to `pre-start`/`post-start`

- README, docs site, skill mirrors, `dev/*.example.toml`,
`plugins/worktrunk/README.md`, `flake.nix`, `.config/wt.toml`
- `src/cli/mod.rs` / `src/cli/config.rs` / `src/cli/step.rs` /
`src/help.rs` after_long_help and example snippets — and the auto-synced
`docs/content/` and `skills/worktrunk/reference/` mirrors
- `wt hook --help` canonical subcommand names; completion advertises
`-start` only
- `HookType` Display via strum, serde `rename`, and clap `ValueEnum`
name — all `pre-start`/`post-start`. The Rust variant identifiers stay
`PreCreate`/`PostCreate` (internal; we already paid for that rename in
#2840, and now the eventual flip is a Display-only change)
- `HooksConfig` serde canonical fields

## `*-create` still works (kept code)

- `wt hook pre-create` / `post-create` — CLI alias on the canonical
subcommand
- `pre-create` / `post-create` in config: top-level, `[hooks.*]`, and
per-project, in string, `[table]`, and `[[array-of-tables]]` form.
Mechanism: serde `alias = ...` on the field, plus a silent in-memory
rename in `migrate_content()` so the round-trip in `unknown_tree`
doesn't flag table forms as schema-unknown.
- The pre-0.32.0 `post-create` fatal-load-error machinery stays removed
— the name is reclaimed, and both forms load without error.

## Smaller bits

- `valid_user_config_keys()` / `valid_project_config_keys()` append
`pre-create` / `post-create` so the unknown-field round-trip skips them.
`test_valid_*_keys_all_deserialize` skips both aliases (they can't sit
alongside the canonical without a duplicate-field error).
- `DEPRECATED_SECTION_KEYS` drops the `pre-start`/`post-start` entries
#2840 added — `pre-start`/`post-start` are canonical again.
- `find_pre_start_from_doc` / `find_post_start_from_doc` /
`find_renamed_hook_key` / `is_non_empty_item` /
`migrate_start_hooks_doc` and their tests are removed; the migration
direction flips via a new `migrate_create_hooks_doc` (silent, mirrors
the prior shape).
- Test files `e2e_shell_post_create.rs` and `post_create_commands.rs`
rename back to `_post_start_` (via `git mv`, so the rename shows as a
rename).

## Testing

`cargo run -- hook pre-merge --yes` — 3806 tests pass; the 10 failures
are all `case_4` of `shell_wrapper::unix_tests::*` (nu-shell case; `nu`
isn't installed in this runner; same failures occur on `main`).

Also manually verified that a fresh `wt switch --create` against a
project config with `[post-create]` loads cleanly with no unknown-field
warning and the hook fires as `post-start`.

## Follow-up

Per @max-sixty: in a couple of weeks, once a release with
both-names-work is out and users have had a chance to upgrade, the docs
flip is straightforward (most of it is in `src/cli/mod.rs`'s
`after_long_help` and the doc-sync test propagates).

Re #2838.

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:03:03 +00:00
Maximilian Roos ff4e481a4c fix(switch): interactive picker runs the full switch pipeline (#2845)
With `[switch] cd = false` or `wt switch --no-cd`, opening the
interactive picker (`wt switch` with no branch argument) and selecting a
worktree printed the branch name and exited — no switch, no hooks, and
`Alt-c` created nothing. #1445 introduced this as an explicit print-only
mode, but it surprises anyone who sets `cd = false` as a standing
navigation preference (driving worktree switches through tmux or cmux):
the picker silently stops doing anything.

The picker now runs the same `plan_switch` → `execute_switch` pipeline
as `wt switch <branch>`, suppressing only the cd directive. Pre- and
post-switch hooks fire, `Alt-c` creates the worktree, and
`--format=json` works in the picker too (`requires = "branch"` and the
`--branches`/`--remotes` conflict are dropped from the flag). JSON
emission is extracted into a shared `emit_switch_json` so the argument
path and the picker produce identical output.

This is a behavior change: the print-only picker mode is gone. Scripts
that captured its bare-branch-name output should switch to `wt switch
--format=json`, which both performs the switch and prints a structured
`action`/`branch`/`path` result — now shown as a `pick` alias example in
tips-patterns. The cmux recipe drops its `pick` alias workaround, since
the picker fires `pre-switch` hooks directly.

Closes #2837. Thanks to @endigma for the discussion that pinned down the
intended behavior: the picker should be identical to passing the branch
as an argument.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:05:19 -07:00
Maximilian Roos d7e3f88422 feat(hooks): rename worktree-creation hooks to pre-create/post-create (#2840)
Phase 1 of the staged hook rename tracked in #2838: the worktree-creation hooks `pre-start`/`post-start` become `pre-create`/`post-create`. The old names keep working with no deprecation warning yet (Phase 2, months out, adds the warning).

## What changes

- `pre-create`/`post-create` are canonical everywhere: the `HookType` enum, the `HooksConfig` serde fields, the `wt hook` CLI, completion, and all docs.
- Old names keep working: `migrate_content()` rewrites `pre-start`/`post-start` config keys to `-create` before serde, and `parse_hook_type` accepts the old CLI names as silent aliases. `wt config update` rewrites them on disk; `wt config show` shows the migration diff. `wt hook <type>` execution and `wt hook show` both accept the old names; completion and `--help` advertise only the canonical names.
- `detect_deprecations()` flags the old keys so `update`/`show` act on them, but `format_deprecation_warnings()` stays silent (Phase 2 adds the warning). A new empty-warnings guard in `check_and_migrate` keeps a `-start`-only config from emitting a stray hint.
- The dead pre-0.32.0 `post-create` machinery is removed: the fatal `POST_CREATE_REMOVED_MSG` load error, the vestigial `HooksConfig.post_create` merge-fold, and `find_post_create_from_doc`. `post-create` is reclaimed as the canonical background creation hook.

## Semantic flip

Before v0.32.0, the key `post-create` named a *blocking* hook. It now names the *background* one.

Since v0.44.0, a pre-0.32.0 `post-create` config is a fatal load error on the `check_and_migrate` paths: `ProjectConfig::load` and user/system config loading, which fire on essentially every `wt` command. A repo carrying one has been unusable ever since. The one path that skips that check is `project_config_at_ref` (the base-ref read behind `wt switch --create`), which applies only structural migration. A pre-0.32.0 `post-create` surviving solely on a base ref, never checked out into a worktree, would now load as a background hook rather than folding into the blocking `pre-start`. That edge case is accepted: once `post-create` is valid again, reclaiming the name and detecting the dead key are mutually exclusive.

## Reviewing this diff

205 files, but the substance is ~36 files under `src/`. The rest is regenerated snapshots and auto-synced doc mirrors. Start with:

- `src/config/deprecation.rs` — detection (`find_renamed_hook_key`), migration (`rename_hook_key`), removal of the fatal block, the empty-warnings guard, and the `DEPRECATED_SECTION_KEYS` entries that stop unknown-field detection from flagging the migrated keys.
- `src/config/hooks.rs`, `src/git/mod.rs` — the serde field and enum renames.
- `src/config/project.rs` — `ProjectConfig::load` deserializes `check_and_migrate`'s migrated content, so a current-worktree config using the old keys loads into the canonical fields.
- `src/cli/hook.rs`, `src/commands/hook_commands.rs`, `src/completion.rs`, `src/main.rs` — the CLI alias layer; `wt hook show` accepts the old type names as hidden value-parser aliases.
- `src/cli/mod.rs` — the `wt hook` docs, including the soft-deprecation note linking #2838.

The ~93 modified snapshots also pick up deterministic env-block lines (`GIT_*: ""`, `LLVM_PROFILE_FILE`) that pre-existing snapshots already carry. That is stale-snapshot drift surfaced by the regeneration, not a behavior change.

## Testing

Full suite green (3799 tests). New coverage: `snapshot_migrate_start_to_create` (migration preserves value shape and position), `test_deprecated_start_hook_key_runs_silently` and `test_standalone_hook_start_alias_runs_silently` (old config and CLI names run with no warning), `test_config_show_displays_start_hook_migration` (`config show` reveals the diff without an "unknown field" warning), and `test_hook_show_accepts_deprecated_start_hooks` (a current-worktree config using the old keys loads, and `wt hook show` takes both the canonical and the deprecated type arguments).

Part of #2838.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-20 19:31:50 -07:00
dependabot[bot] 3848a9da7a chore: bump the patch group with 3 updates (#2792) 2026-05-17 23:13:15 -07:00
mikeyroush 1ca73ab52c feat: experimental Azure DevOps support (#1256)
## Summary

Adds **experimental Azure DevOps support** alongside the existing GitHub and GitLab integrations:

- `wt switch pr:<N>` resolves Azure DevOps PRs via the `az` CLI (auto-detected from `dev.azure.com` / `ssh.dev.azure.com` / `*.visualstudio.com` remotes, or pinned via `[forge] platform = "azure-devops"`).
- `wt list --full` surfaces Azure DevOps PR and pipeline CI status.
- `wt config show --full` reports `az` install/auth state when Azure DevOps is the detected platform.

GitHub still wins in mixed-remote setups; `forge.platform` is the override. Requires the `azure-devops` CLI extension (`az extension add --name azure-devops`).

## Context

Originally proposed by @mikeyroush; reimplemented against current `main` to pick up the `[forge]` config section, `url.insteadOf` fallback, the `handle_switch` consolidation, and the new Gitea provider that all landed after the original branch was opened. The dispatch path (`choose_pr_provider`) is shared with GitHub/Gitea — Azure is just a fourth provider in the same priority chain.

Implementation notes worth a reviewer's attention:

- Azure DevOps URLs don't fit the standard `host/owner/repo` shape (`dev.azure.com/{org}/{project}/_git/{repo}`), so `Repository::find_remote_for_azure` matches on `org` + `project` + `repo` instead of the owner-based path used for the other forges.
- Pipeline/PR web URLs are constructed from `org/project/build-id`, not the API's `url` field (which is a REST endpoint).
- `*.visualstudio.com` legacy hosts encode the org in the hostname; the URL helpers handle both shapes.

Fixes #1144

## Test plan

- `cargo run -- hook pre-merge --yes` — 3631 tests pass, clippy + fmt clean
- Unit tests cover the host-aware URL helpers, `find_remote_for_azure` (all URL shapes + the same-org/different-project collision case), and `choose_pr_provider` dispatch
- Integration tests bring Azure to parity with the other forges (see Coverage):
  - 13 `test_switch_pr_azure_*` tests mirroring the Gitea suite — same-repo, fork, `*.visualstudio.com` host, create/base conflicts, not-found, az-not-installed, `forge.platform` override, invalid JSON, generic server error, auth error, missing `azure-devops` extension, undeterminable org/host
  - 9 `test_list_full_with_azure_*` tests covering `detect_azure_pr` (conflicts, queued, stale, retriable error) and `detect_azure_pipeline` (passed/failed/running, stale, no runs, retriable error)
- Manual validation: `wt switch pr:<N>` and `wt list --full` against an Azure DevOps repo

## Coverage

The `az`-shelling code (`fetch_pr_info`, `detect_azure_pr`, `detect_azure_pipeline`) is now exercised by integration tests via new `setup_mock_az*` helpers (modeled on `setup_mock_gh` / `setup_mock_glab`) — covering the happy paths plus the not-found / auth / extension-missing / generic-error / retriable-error branches. The non-`az` parts (URL parsing, provider dispatch, remote matching) remain unit-tested.
2026-05-10 23:41:52 -07:00
Steve Beaulac 7fca05441e feat(switch): experimental Gitea PR support via pr: shortcut (#1320)
Add experimental Gitea PR support to `wt switch pr:<number>`.

The `pr:` syntax already resolved GitHub PRs; this teaches it to also
resolve Gitea PRs via the `tea` CLI. GitLab continues to use `mr:`.

## Dispatch

`pr:N` now goes through `choose_pr_provider`:

1. `[forge] platform` in `.config/wt.toml` if set (`github` / `gitea` /
`gitlab`)
2. Primary remote URL detection (host contains `github` / `gitea` /
`gitlab`)
3. CLI auth lookup: if `tea` is configured for this host (per
`~/.config/tea/config.yml`) but `gh` is not (per `gh auth token
--hostname <host>`), pick Gitea
4. Default to GitHub

There is no longer an "ambiguous" fallback that tries both providers and
wraps both errors — users on self-hosted Gitea instances either run `tea
login add <host>` (auto-detected) or set `[forge] platform = "gitea"`.

## New code

- `src/git/remote_ref/gitea.rs` — `GiteaProvider` implementing
`RemoteRefProvider` via `tea api repos/<owner>/<repo>/pulls/<n>`; reuses
the shared `cli_api_error` / `run_cli_api` helpers.
- `src/git/remote_ref/info.rs` — `PlatformData::Gitea { host,
head_owner, head_repo, base_owner, base_repo }`, wired into
`source_ref()`, `prefixed_local_branch_name()`, and `find_remote()`.
- `src/git/url.rs` — `GitRemoteUrl::is_gitea()`.

Shared helpers introduced in this PR:

- `mod.rs::extract_host_from_html_url()` (used by github + gitea;
identical 7-line chains collapsed).
- `github::is_authed_for()` (wraps `gh auth token --hostname`).
- `gitea::is_authed_for()` (reads tea's config.yml; never invokes `tea`
to avoid OAuth refresh on lookup).

## Docs

User-facing copy says "GitHub PR" by default; one paragraph in `wt
switch --help` mentions Gitea support, marked experimental.

## Tests

- 13 new integration tests covering Gitea same-repo, fork, error
responses (401/403/404/5xx/malformed JSON/deleted fork/no source
branch), `tea` not installed, `forge.platform` overrides,
GitLab-remote-with-`pr:` bail, self-hosted defaults-to-GitHub, and
self-hosted-with-`tea`-login routes-to-Gitea.
- Unit tests for `extract_source_branch` edge cases and the tea config
parser.

## Compatibility

No CLI flag or config file changes. The `tea` CLI is only required for
Gitea PRs; GitHub-only users see no change.

---------

Co-authored-by: worktrunk-bot <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-05-10 20:47:31 -07:00
Worktrunk Bot a77c92e25b docs(help): point banner at the actual cli source path (#2665) 2026-05-10 08:14:37 +00:00
Maximilian Roos 4cbc8ca5fd refactor(docs): drop mirrored help-page close; align convert_console_blocks_in_docs error channel (#2422)
## Summary

Three follow-ups from #2419 review.

- **Bare close everywhere.** With inner snapshot wrappers gone from
command pages (#2419), no `AUTO-GENERATED` markers nest inside the
help-page region anywhere in `docs/content/*.md`. The mirrored close
(`<!-- END AUTO-GENERATED from \`wt cmd --help-page\` -->`) was the only
remaining variant of the close form; collapsed to bare `MARKER_CLOSE`.

  - `src/help.rs::PageMode::emit_footer()` no longer takes `subcommand`.
- The help-page regex in `readme_sync.rs` matches via non-greedy `.*?`
to bare `MARKER_CLOSE`, with a comment pointing at the new invariant
test.
- `AUTO_GENERATED_MARKER_PATTERN` strip regex built from the constants.

Added **`test_no_nested_auto_generated_markers`** — walks
`docs/content/*.md` and fails if any `AUTO-GENERATED` open ever appears
inside an already-open region. This is the explicit invariant that
bare-close pairing depends on; if a future change tries to re-introduce
nesting (e.g., restore an inner snapshot wrapper around terminal
shortcodes), the test catches it before the subtle "regex chops region
at first inner close" failure mode lands.

- **Aligned error channel.** `convert_console_blocks_in_docs` now
returns `(Vec<String>, Vec<String>)` like its sibling sync steps, with
per-file error capture for `read_dir`, dir entries, and
`read_to_string`. The caller passes errors through the same `tag()`
aggregation as everything else, so a transient I/O failure on one file
no longer aborts the whole pipeline silently. Also fixes the matching
clippy warning (`is_some_and(|e| e == \"md\")` → `is_none_or(|e| e !=
\"md\")`).

- **Docs alignment.** `docs/CLAUDE.md` updated in two places where the
prose still documented the mirrored close as the canonical form.

Visual check via curl across 12 dev-server pages confirmed no leakage of
any prior marker form. Adversarial review (subagent /popper-style)
caught the stale prose; otherwise no regressions found.

Net diff: +113/-49 (the growth is the new invariant test and explicit
per-file error handling; structural simplification shows as -49).

## Test plan

- [x] `cargo test --test integration readme_sync` — 12 sync tests pass
(was 11; +1 for the new invariant test)
- [x] `cargo test --test integration` — full integration suite (1558
tests) pass
- [x] `cargo test --test integration
test_no_nested_auto_generated_markers` — new guard test passes; manually
verified it fires on synthetic nesting
- [x] Single-pass convergence verified: `git checkout docs/ && cargo
test --test integration test_docs_are_in_sync && git diff --stat`
produces an empty diff after the first run
- [x] Visual check via local Zola dev server: 12 docs pages
(\`/worktrunk/\`, \`/llm-commits/\`, \`/claude-code/\`,
\`/tips-patterns/\`, \`/merge/\`, \`/step/\`, \`/remove/\`, \`/hook/\`,
\`/list/\`, \`/switch/\`, \`/config/\`, \`/faq/\`) — only HTML comments
contain marker text, no rendered leakage
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-26 02:18:39 -07:00
Maximilian Roos 0253260503 Extend -v variable dump to aliases + help-table drift test (#2324)
Follow-ups from #2316.

## What's in here

1. **Alias `-v` variable dump.** Added `format_alias_variables(ctx)`
alongside the existing `format_hook_variables(hook_type, ctx)`, with a
private `format_variables_table` helper sharing the alignment +
`(unset)` logic. Wired into `run_alias` before the announcement,
symmetric with the foreground hook path.

2. **Help-table drift test.**
`test_template_variables_table_matches_constants` parses the `##
Template variables` table out of `src/cli/mod.rs`, extracts `(kind,
var_name)` pairs, and asserts presence + group placement against
`ACTIVE_VARS` / `REPO_VARS` / `EXEC_BASE_VARS` / `ALIAS_ARGS_KEY` /
union of `vars_available_in(Hook(*))`. Uses public API only — no leak of
the private `hook_extras` helper. Adding a var to the constants without
updating the table (or vice versa) fails the test. Descriptions stay
free-form.

3. **Shorter `-v` help text.** `Verbose output (-v: info logs +
hook/alias template variable & output; ...)`.

## Example

```
\$ wt -v greet world
○ template variables:
  branch                = feature
  worktree_path         = _REPO_.feature
  worktree_name         = repo.feature
  …
  args                  = ["world"]
  repo                  = repo
  …
  cwd                   = _REPO_.feature
◎ Running alias greet
○ Expanding greet
  echo hello {{ args }}
  →
  echo hello world
hello world
```

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:39:29 -07:00
Maximilian Roos eec3b24883 Show resolved template variables under -v for hooks (#2316)
Closes #2309.

When a hook fires under `-v`, `wt` now prints a `template variables:`
block listing every variable in scope for that hook type and the value
it resolved to for this specific invocation. Vars that are in scope but
not populated render as `(unset)` — which is exactly how
`target_worktree_path` surfaces during `wt switch -`, the thing the
issue reporter hand-rolled an echo-hook to discover.

The block prints *before* the `◎ Running …` announce line so it
describes what the hook is about to see, not what already ran. Works in
both paths: the foreground path (`announce_command`, one block per
command) and the background path (`announce_and_spawn_background_hooks`,
one block per distinct hook type in the pipeline batch).

## Key files

- `src/config/expansion.rs` — `format_hook_variables(hook_type, ctx)`
emits the aligned `name = value` block ordered per the docs table
(active → operation → repo → exec). `BASE_VARS` is split into
`ACTIVE_VARS` / `REPO_VARS` / `EXEC_BASE_VARS` with a `base_vars()`
helper, so `vars_available_in` and the printer share one source of
truth.
- `src/commands/command_executor.rs` — foreground wiring in
`announce_command`, gated on `verbosity() >= 1`.
- `src/commands/hooks.rs` — background wiring in
`announce_and_spawn_background_hooks`; prints one table per distinct
hook type (since within a pipeline only `hook_name` varies).
- `src/cli/mod.rs` — updates the global `-v` help text and adds a
pointer under `## Template variables` in the hook help.

## Testing

- Unit: `test_format_hook_variables_groups_and_unset` snapshots a
pre-switch context with `target_worktree_path` omitted so `(unset)` is
covered; `test_format_hook_variables_scope_filters_operation` confirms
pre-commit's narrower operation scope.
- Integration: `test_hook_verbose_prints_variable_table` covers the
foreground path; the existing
`test_post_start_verbose_shows_per_hook_output` now also exercises the
background path.

No new CLI surface, no new config keys — only behavior added behind the
existing `-v` flag.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:28:43 -07:00
Maximilian Roos 1a4eea3c03 feat(cli): promote --yes to a global flag (#2279)
## Summary

`-y, --yes` is now a top-level global clap flag. It moves off every
subcommand's clap args and lives once on `Cli`, so `wt -y <anything>`,
`wt <anything> --yes`, and `wt --yes <anything>` all skip any approval
or confirmation prompt for that invocation.

## Why

Follow-up to the top-level alias dispatch (#2266). The long-term plan is
to unify approval-bypass behavior — `-y` should be a true global rather
than duplicated across switch, remove, merge, commit, squash, prune, all
ten hook subcommands, shell install/uninstall, plugin install/uninstall,
and config update. A single canonical flag removes ~50 lines of
duplicated clap definitions and one source of drift.

## Call-site survey

Approval and confirmation prompt sources, as surfaced by `rg -l
'approve_|requires_approval|confirm_'`:

- `approve_hooks` / `approve_hooks_filtered` — reads `ctx.yes`, already
threaded via `CommandContext::new(..., yes)`. No change needed; the
global now feeds those call sites from `handle_*_command` in `main.rs`.
- `approve_command_batch` (merge, config `hook approvals add`) — takes
`yes: bool`. Receives the global.
- `approve_alias_commands` — takes `yes: bool`. Receives `global_yes ||
opts.yes` (see "Alias compat" below).
- `handle_configure_shell` / `handle_unconfigure_shell` /
`handle_config_update` / `handle_claude_install[_statusline]` /
`handle_claude_uninstall` / `handle_opencode_install` /
`handle_opencode_uninstall` — take `yes: bool` for confirmation-prompt
bypass. All receive the global.

## Alias compat

`AliasOptions` is hand-rolled (not clap) so it doesn't conflict with the
clap global. Its post-alias `--yes` parsing is intentionally preserved
here — `run_alias` does `let skip_approval = global_yes || opts.yes;` so
both `wt -y deploy` and `wt deploy --yes` work unchanged. Removing the
post-alias form is a separate cleanup, tracked by the user's long-term
simplification plan.

## Navigating the diff

- `src/cli/mod.rs` — new `yes: bool` on `Cli` with `global = true`,
`short = 'y'`, `help_heading = "Global Options"`, `display_order = 103`
(slots after `-v`). Removed the per-command `yes` field from
`SwitchArgs`, `RemoveArgs`, `MergeArgs`.
- `src/cli/step.rs` — removed `yes` from `CommitArgs`, `SquashArgs`, and
`StepCommand::Prune`. Also dropped `help_heading = "Automation"` from
the four step subcommands where the group was left with a single flag
(`commit`, `squash`, `for-each`, `prune`); those flags now render under
default Options instead of a single-item group.
- `src/cli/hook.rs` — removed `yes` from all ten hook subcommands
(`pre-switch`, `post-switch`, `pre-start`, `post-start`, `pre-commit`,
`post-commit`, `pre-merge`, `post-merge`, `pre-remove`, `post-remove`).
`--yes` stays in `KNOWN_HOOK_LONG_FLAGS` so the shorthand rewriter still
recognizes it as a real flag rather than a template variable.
- `src/cli/config.rs` — removed `yes` from
`ConfigShellCommand::Install/Uninstall`, `ConfigCommand::Update` (and
its now-redundant `conflicts_with = "yes"` on `--print`),
`ConfigPluginsOpencodeCommand::Install/Uninstall`,
`ConfigPluginsClaudeCommand::Install/Uninstall/InstallStatusline`.
- `src/main.rs` — `dispatch_command` takes `yes: bool`; each
`handle_*_command` accepts and threads it. `Cli` destructure adds `yes`.
- `src/commands/alias.rs` — `try_alias`, `step_alias`, `run_alias` take
`global_yes: bool`. `run_alias` OR's with `opts.yes` before calling
`approve_alias_commands`.
- `src/commands/custom.rs` — `handle_custom_command` accepts and passes
the global to `try_alias`.
- `docs/content/` + `skills/worktrunk/reference/` — auto-generated from
`--help-page`; the global appears under "Global Options" on every
subcommand.
- `tests/integration_tests/approval_ui.rs` — six new tests:
`test_global_yes_before_subcommand`, `test_global_yes_for_hook`,
`test_global_yes_for_alias`, `test_post_alias_yes_still_works`,
`test_global_yes_for_step_alias`,
`test_global_yes_on_command_without_approval`.

## Notes

- Does not remove `AliasOptions::yes` — deferred cleanup per the task
brief.
- Snapshot churn (~27 help files) is the expected fallout of adding a
global flag; each subcommand's `--help` now shows `-y, --yes` under
Global Options.

## Test plan

- [x] 3242 tests pass (`cargo run -- hook pre-merge --yes`)
- [x] Lints clean (clippy, cargo fmt, pre-commit)
- [x] Doc sync test passes after regeneration
- [x] New approval_ui tests cover before-subcommand, after-subcommand,
alias, post-alias, step-alias, and no-approval positions

> _This was written by Claude Code on behalf of Maximilian_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-04-18 11:08:41 -07:00
Worktrunk Bot 1d01a25e90 feat(switch): accept pr:N/mr:N in --base (#2263)
## Summary

Route `--base` through the same `pr:N`/`mr:N` resolution used for the
positional branch argument, so these two commands now work
symmetrically:

```
wt switch -c feat-x pr:42          # already worked
wt switch -c feat-x --base pr:42   # new
```

Same-repo PRs/MRs fetch the source branch and use the branch name as the
base, so the resulting worktree tracks it naturally. Fork PRs/MRs fetch
`refs/pull/N/head` (GitHub) or `refs/merge-requests/N/head` (GitLab) and
use the resolved commit SHA as the base — avoiding polluting the local
branch namespace with a tracking branch for a fork contributor's branch.

Closes #2261

## Test plan

- [x] `test_switch_base_pr_same_repo` — same-repo PR resolves to source
branch name
- [x] `test_switch_base_pr_fork` — fork PR resolves to commit SHA, no
tracking branch created
- [x] `test_switch_base_mr_same_repo` — same-repo GitLab MR
- [x] `test_switch_base_pr_without_create` — warning (no fetch) when
`--create` absent
- [x] `cargo test --test integration` (1478 tests pass)
- [x] `cargo clippy --all-targets --all-features -- -D warnings`
- [x] `cargo fmt --all -- --check`

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-04-16 21:46:45 -07:00
Maximilian Roos 3e1b351ac1 feat(log): split -vv output into trace.log + output.log, drop -vvv (#2201)
## Motivation

At `-vv`, raw subprocess bodies were bounded (200 lines / 64 KB) on both
stderr and `verbose.log`, with full output only available by rerunning
at `-vvv`. Large captures (notably `git log -p` piped into `patch-id`
during `wt list`) would routinely flood stderr with elision markers and
force a second run just to see what was elided.

## Change

The verbosity map is now `-v` (info) and `-vv` (debug); any `-v` count
above 2 collapses to `-vv`. Captured subprocess stdout/stderr fan out
through two log targets in `src/shell_exec.rs`:

- `SUBPROCESS_TERMINAL_TARGET` → bounded preview on stderr, mirrored to
`.git/wt/logs/trace.log` (new, replaces `verbose.log`)
- `SUBPROCESS_FULL_TARGET` → uncapped body to `.git/wt/logs/output.log`
(new), never stderr

`src/log_files.rs` (renamed from `src/verbose_log.rs`) owns both file
sinks behind a `LogSink` type and a `route(target)` helper that is the
single source of truth for sink selection. `src/main.rs`'s env_logger
format closure matches on the `Route` enum and emits once per sink.
Diagnostic reports embed `trace.log` and reference `output.log` by path
— multi-MB raw bodies would swamp a bug report.

## Fallback path

When `RUST_LOG=debug` is set without `-vv`, neither sink is active.
`FULL` records drop and the `TERMINAL` preview reaches stderr as before
— preserving the bounded-stderr guarantee. The elision marker phrases
its hint based on whether `output.log` was opened, so users in the
fallback path see `rerun with -vv for full output` rather than a pointer
to a file that doesn't exist.

## Key files

- `src/log_files.rs` — new module; `LogSink`, `TRACE`, `OUTPUT`,
`route`.
- `src/shell_exec.rs` — two `pub const` targets, `log_output` emits on
both, elision hint switches on `OUTPUT_LOG_AVAILABLE`.
- `src/main.rs` — verbosity map + format closure.
- `src/diagnostic.rs` — template splits inlined `trace.log` from
referenced `output.log`.
- `src/commands/config/state.rs` — diagnostic file recognition for the
new names.

## Testing

- `test_vv_splits_full_and_bounded_output` — `[wt-trace]` in
`trace.log`, raw stdout in `output.log`, no trace records in
`output.log`.
- `test_vv_bounded_on_stderr_full_in_output_log` — 250-ref packed-refs
trip the elision cap; asserts the marker on stderr + `trace.log`, full
content in `output.log` without elision.
- `test_rust_log_debug_fallback_without_vv` — no log files created at
`-v 0 + RUST_LOG=debug`; bounded preview reaches stderr.

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-13 10:50:18 -07:00
Maximilian Roos 2a6389a092 Centralise [wt-trace] emitter and fix -vv log verbosity (#2146)
## Summary

Three changes to worktrunk's logging conventions, all motivated by `wt
list -vv` dumping raw `git diff-tree -p` bodies into the log stream at
debug level:

**1. `src/trace/emit.rs` owns the `[wt-trace]` grammar.** Previously the
grammar was emitted via ad-hoc `log::debug!("[wt-trace] ...")` format
strings in `shell_exec.rs`, with `src/trace/parse.rs` silently defining
it by how it parsed. `trace_instant`, `log_command_result`, the
`TRACE_EPOCH` static, and `thread_id_number` moved into the new emitter
module so `parse.rs` and the producer share one source. Wire format
byte-identical; `wt-perf` parsing unchanged.

**2. Level discipline.** `-v` → Info, `-vv` → Debug, `-vvv` → Trace.
Previously `-v` didn't touch the `log` crate at all and `-vvv` didn't
exist. The LLM prompt dump (`src/llm.rs`) and captured subprocess
stdout/stderr (`log_output` in `shell_exec.rs`) moved to `log::trace!`,
so `-vv` stops spilling thousand-line diff bodies and full LLM prompts.

**3. Bounded `log_output`.** At Debug, each stream caps at 200 lines /
64 KB with `… (N more lines, M bytes elided — use -vvv for full
output)`. At Trace, uncapped.

## Reviewer navigation

- **New file**: `src/trace/emit.rs` — the single-source emitter.
`command_completed`, `command_errored`, `instant` plus `trace_epoch` /
`now_us` / `thread_id` helpers.
- **Delete/delegate**: `src/shell_exec.rs` loses its duplicate
`TRACE_EPOCH` + `trace_epoch` + `thread_id_number`, and the four
`log::debug!("[wt-trace] …")` branches collapse into two calls into
`trace::emit`. `log_output` gains `log_stream_full` (Trace) and
`log_stream_bounded` (Debug) helpers.
- **Level map**: `src/main.rs:init_logging` has the new match on
`verbose_level`.
- **Help + test snapshots**: the `--verbose` help text change in
`src/cli/mod.rs:249` drives the large auto-synced snapshot / docs /
skill-reference diff.
- **Test rename**: `tests/integration_tests/diagnostic.rs` —
`test_v_does_not_enable_logging` → `test_v_does_not_write_log_files`
(the old name became inaccurate now that `-v` enables Info logging on
stderr).

## Testing

- 969 library unit tests pass.
- Integration tests for `diagnostic`, `step_alias`, `test_help` pass (82
tests).
- Lints clean.

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 18:17:33 -07:00
worktrunk-bot 7f8b58d7a2 docs: comment explaining why Alt-r is omitted from picker keybindings (#1996)
## Summary

- Adds an HTML comment next to the picker keybinding table in
`src/cli/mod.rs` explaining why `Alt-r` is intentionally omitted — the
skim `reload` action resets the cursor (#1695), so the UX isn't polished
enough to document yet.
- Comment auto-syncs to `docs/content/switch.md` and
`skills/worktrunk/reference/switch.md`.

Closes #1881

## Test plan

- [x] `test_help` snapshots pass (HTML comment is stripped from rendered
help)
- [x] `test_command_pages_and_skill_files_are_in_sync` passes

🤖 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.6 <noreply@anthropic.com>
2026-04-07 19:27:12 -07:00
Maximilian Roos d6e84ed258 feat: add --format=json to wt switch and CC worktree hooks (#1959)
Adds `--format=json` to `wt switch` and WorktreeCreate/WorktreeRemove
hooks to the Claude Code plugin, so agent-created worktrees go through
`wt` instead of raw `git`.

**`wt switch --format=json`** prints structured JSON to stdout
(`action`, `branch`, `path`, etc.) without changing any behavior —
hooks, `--execute`, shell integration, and cd all run normally
regardless of format. A `SwitchFormat` enum (`text`/`json`) keeps the
help text clean (only valid values shown).

**Plugin hooks** are inline one-liners in `hooks.json`:
- `WorktreeCreate` — pipes CC's JSON through `jq` to extract the branch,
calls `wt switch --create --format=json`, extracts the path
- `WorktreeRemove` — extracts the worktree path from CC's JSON, passes
it directly to `wt remove -D --foreground`

Approval prompts still run — the hooks don't pass `--yes`, so `wt`'s
non-interactive detection applies normally.

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-06 19:17:03 -07:00
Maximilian Roos 679fe539fe Deprecate --no-verify in favor of --no-hooks (#1932)
`--no-hooks` describes what the flag does — skip hooks. `--no-verify`
was inherited from git's naming but doesn't match worktrunk's semantics
(there's no "verification" step being skipped).

`--no-verify` remains as a hidden alias that emits a deprecation
warning, retained for at least one release cycle per the project's
deprecation policy.

Changes across switch, remove, merge, step commit, and step squash:
- `--no-hooks` is the canonical visible flag
- `--no-verify` hidden, emits `▲ --no-verify is deprecated; use
--no-hooks instead`
- Error hints (`↳ To skip pre-merge hooks, re-run with --no-hooks`),
info messages, help text, docs, and config examples all updated
- `resolve_verify()` helper in main.rs deduplicates the deprecation
logic
- Backward-compatibility test verifies `--no-verify` still works

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-06 15:01:18 -07:00
Maximilian Roos 33b87f0b95 Widen help text wrapping from 80 to 100 chars for web docs (#1786)
The web docs content area fits ~101 monospace characters on desktop. The
previous 80-char wrap caused unnecessary line breaks in option
descriptions — e.g., enum variant descriptions wrapping mid-phrase like
"Stage everything: untracked files + unstaged tracked / changes".

Widens the two `help_reference()` call sites in `src/help.rs` from
`Some(80)` to `Some(100)`. Also fixes a pre-existing issue where clap's
line wrapping would break bold (`<b>`) spans across lines —
`ensure_line_resets` now tracks active SGR styles and re-opens them on
continuation lines.

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-29 12:38:00 -07:00
worktrunk-bot 37192edf04 docs: improve hook command help text (#1785)
## Problem

The hook command's `--help` text had several issues (#1784):

1. The intro sentence included full config paths
(`~/.config/worktrunk/config.toml`, `.config/wt.toml`) — unnecessary
detail for an overview
2. Background log output referenced a raw path
(`.git/wt/logs/{branch}-{source}-{hook}-{name}.log`) instead of linking
to the `wt config state logs` command that manages them
3. "Approvals are saved to user config
(`~/.config/worktrunk/config.toml`)" was incorrect — approvals are saved
to `approvals.toml`
4. Per-hook-type examples were verbose, each with its own code block
interleaved with one-line descriptions

## Solution

- Removed inline paths from the intro sentence
- Replaced the raw log path with a link to `wt config state logs`
- Fixed the approvals path to `~/.config/worktrunk/approvals.toml`
- Consolidated per-hook-type examples into a single TOML block at the
bottom, keeping the descriptions inline as a compact reference section

## Testing

- `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — passes, docs synced
- `cargo insta test --accept -- --test integration "test_help"` — no
snapshot changes needed
- `pre-commit run --all-files` — all checks pass (except lychee not
installed)

---
Closes #1784 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-28 13:44:02 -07:00
Maximilian Roos b6a6744380 Consistent code block convention for syntax highlighting (#1777)
Shell code blocks on the docs site had inconsistent syntax highlighting.
Blocks with `$ ` prompt prefixes rendered as a single flat color because
Syntect's bash grammar treats `$` as variable expansion. This PR adds `$
` prompts to all shell commands while preserving full Syntect
highlighting by routing through terminal shortcodes.

## Approach

All shell commands in `console` blocks use `$ ` prefix.
`convert_dollar_console_to_terminal()` (new library function in
`src/docs.rs`) detects `$ ` lines and emits Zola terminal shortcodes:

- **Single or multi-command blocks** (no `{{ }}`): Uses `cmd` parameter
with `|||` delimiter. The shortcode template splits, highlights each
line individually through Syntect, and wraps commands in `<span
class="cmd">` (CSS `::before` adds `$ `). Comment lines (`#`) are
highlighted as comments without a prompt.
- **Blocks with `{{ }}` template syntax**: Falls back to body approach
with `<span class="cmd">` (accent color only, since Tera would interpret
`{{ }}` in the `cmd` parameter).

The function runs in both the `--help-page` generator (CLI source →
docs) and the doc sync test (hand-written docs → terminal shortcodes).
Hand-written docs can use plain `console` fences with `$ ` and get
auto-converted.

## Key files

- `src/docs.rs` — New library module with
`convert_dollar_console_to_terminal()` and unit tests
- `docs/templates/shortcodes/terminal.html` — Template enhanced to loop
over `|||`-delimited commands, highlighting each through Syntect.
Supports self-closing `{{ }}` syntax for bodyless blocks.
- `src/help.rs` — Uses library function, updated pipeline docs
- `tests/integration_tests/readme_sync.rs` — Sync test runs conversion
on all docs (not just CLI-generated). Updated skill transformation to
handle both body and self-closing terminal shortcodes.
- All `src/cli/*.rs` — `$ ` added to all console blocks
- All `docs/content/*.md` — Auto-converted to terminal shortcodes (zero
`bash` blocks remain)

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <w@worktrunk.dev>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-03-28 12:18:48 -07:00
worktrunk-bot e14e80c0ce docs: clean up switch command help text (#1782) 2026-03-28 09:50:12 -07:00
Maximilian Roos acff4cd03b Replace parentheticals with prose in after_long_help docs (#1764)
Rewrites parenthetical asides in `after_long_help` text across switch,
list, merge, step, hook, and config docs. Qualifiers and conditions
become em-dashes or semicolons; examples and analogies stay in parens.

Changes: `src/cli/mod.rs` only (docs, skills, and snapshots are
auto-synced).

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-26 22:54:26 -07:00
Maximilian Roos d3b63690a1 Reorder hook types from lifecycle to paired-by-event (#1763)
The hook subcommands in `wt hook --help` were ordered by a lifecycle
narrative (pre-switch, pre-start, post-start, post-switch, ...) that
claimed to represent execution order but didn't — post-switch and
post-start actually run concurrently, and in `wt merge`, post-merge runs
after post-remove.

Reorders to paired-by-event (pre-switch, post-switch, pre-start,
post-start, ...) which is easier to scan and matches how users think
about hooks. Also fixes the switch docs to accurately describe
post-start and post-switch as concurrent rather than sequential, and
replaces parenthetical annotations with prose.

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-26 21:05:19 -07:00
Maximilian Roos 85a58fa62a fix: hide alt-r remove from picker legend and docs (#1696)
The alt-r keybinding for in-place worktree removal has poor UX — cursor
resets to the top after each removal due to a skim 0.20 limitation
(#1695). This hides the feature from the picker legend and documentation
so users don't discover it until fixed. The keybinding itself remains
functional for power users who already know about it.

Ref #1695

> _This was written by Claude Code on behalf of @max-sixty_

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 13:00:07 -07:00
Maximilian Roos efad9312db feat!: rationalize hooks — rename post-create, add post-commit, background post-merge (#1679)
Implements the hook rationalization from #1670, establishing a symmetric
`pre-` (blocking) / `post-` (background) pattern for every lifecycle
event.

## Changes

**Rename `post-create` → `pre-start`**: The old name suggested it ran
*after* creation, but it actually runs *before* `post-start` as a
blocking dependency step. Both names accepted for one release cycle —
config deprecation detection, migration file generation, `wt config
update` support, and CLI alias all in place. `merge_with` folds old-name
hooks into new-name so cross-config combinations don't silently drop
hooks.

**Add `post-commit` hook**: New background hook firing after successful
commits (including squash commits), completing the commit lifecycle
pair. Included in approval batches for `wt step commit`, `wt step
squash`, and `wt merge`.

**Change `post-merge` to background**: Was blocking with `Warn`
strategy, now runs in background like all other `post-` hooks.
`--foreground` flag available for debugging.

The hook table is now a clean symmetric grid:

| Event | `pre-` (blocking) | `post-` (background) |
|-------|-------------------|---------------------|
| start | `pre-start` | `post-start` |
| switch | `pre-switch` | `post-switch` |
| commit | `pre-commit` | `post-commit` |
| merge | `pre-merge` | `post-merge` |
| remove | `pre-remove` | `post-remove` |

## Testing

868 lib + 483 bin + 1227 integration tests pass, all 13 lint checks
pass. The deprecation has 23 dedicated unit tests covering detection at
all three config scopes, migration, empty table filtering, cross-config
merge safety, and integration with `wt config show` / `wt config
update`.

Closes #1670

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 00:33:30 -07:00
worktrunk-bot df54d28c3a docs(switch): trim upstream tracking, add missing hooks, combine PR/MR sections (#1521)
## Summary

- **Trimmed upstream tracking paragraph** — `--create` doesn't configure
upstream tracking, but that's standard `git switch -c` behavior. Removed
the prominent explanation and folded the remote-branch note into the
existing sentence.
- **Added missing hooks to creation lifecycle** — The numbered list now
includes `pre-switch` (step 1) and `post-switch` (step 6), matching the
actual execution order in `handle_switch.rs`.
- **Combined GitHub/GitLab sections** — Merged two near-identical
sections into a single "Pull requests and merge requests" section,
reducing repetition.

Closes #1518

## Test plan
- [x] `test_command_pages_and_skill_files_are_in_sync` passes (docs
auto-synced)
- [x] Unit tests pass
- [ ] CI green

🤖 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.6 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
2026-03-14 23:31:26 +00:00
Jarryd Tilbrook 00eb04a042 feat(switch): add no-cd config option (#1401) 2026-03-13 07:42:40 -07:00
worktrunk-bot 54f34bb19c feat(switch): make --no-cd print-only in picker mode (#1445)
## Summary

Draft implementation per @max-sixty's
[suggestion](https://github.com/max-sixty/worktrunk/pull/1404#issuecomment-4041170944):
reuse `--no-cd` to make the picker print-only instead of adding a
separate `--print` flag.

- When `wt switch --no-cd` opens the interactive picker (no branch
argument), selecting a branch prints its name to stdout and exits — no
switching, no cd directive, no hooks
- When `wt switch --no-cd <branch>` is used with an explicit branch,
behavior is unchanged (switches without cd)
- `alt-r` (remove) is blocked in print mode since it's a read-only
operation

This avoids adding a new CLI flag while giving the same scripting
capability that #1404 proposed.

## Test plan

- [x] Unit tests for `resolve_print_identifier` (switch, create, remove
cases)
- [x] PTY integration test: `--no-cd` picker prints branch name, emits
no cd directive
- [x] Existing `--no-cd` tests still pass (directive suppression, hooks,
execute)
- [x] Help snapshots and doc sync updated


🤖 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.6 <noreply@anthropic.com>
2026-03-11 22:12:16 -07:00