Commit Graph

276 Commits

Author SHA1 Message Date
Maximilian Roos 92dfb686bb feat(approvals): let wt config approvals add --yes record approvals without a TTY (#3819)
`wt config approvals add` refused every non-interactive run — even with
`--yes`, whose hint then suggested the flag already passed — so there
was no way to pre-approve a project's commands unattended. An
orchestrator (tend's Codex Cloud container was the motivating case) had
to hand-write `approvals.toml` from `wt config approvals list
--format=json` output, a third-party reimplementation of `add` that
breaks whenever the schema changes. The `wt config approvals` docs
already promised "`--yes` to bypass prompts in CI" and described `stale`
entries as "what `--yes` would silently re-approve"; behavior now
matches them.

The two `--yes` meanings stay distinct: on a command that runs project
commands it grants consent for that run alone and records nothing
(unchanged), while on `add` — whose product is the record — it lists
what it trusts and writes it. `add` no longer routes through
`approve_command_batch` (the execution gate) for this: it prompts or
announces, then saves itself, which also makes a failed `approvals.toml`
write fail the command instead of warning behind a `✓ saved` line and
exit 0 — an orchestrator reading only the exit code would otherwise walk
into the prompt it just paid to avoid.

The non-interactive hint's pre-approval suggestion now carries `--yes`
(`run wt config approvals add --yes`), since a hint reached in CI must
name a command that runs there. Per the existing `list --format=json`
docs, `add --yes` re-approves templates edited since an earlier approval
without comment; the `add` help now says so and points at the `stale`
field for reading them first, and the worktrunk skill's escalation rule
tells agents not to reach for it on a user's behalf.

> _This was written by Claude Code on behalf of max-sixty_
2026-08-14 02:39:29 -07:00
Maximilian Roos f3ea2fd598 fix(errors): report a directory holding no worktree as a directory (#3773)
Passing a path that holds no worktree used to be reported as a missing
branch:

```
$ wt remove /repo/.claude/worktrees/ghost
✗ No branch named /repo/.claude/worktrees/ghost
↳ To list branches, run wt list --branches --remotes
```

The argument is plainly a path; the error calls it a branch and sends
the user to a listing it could never appear in. Now:

```
$ wt remove /repo/.claude/worktrees/ghost
✗ No worktree @ /repo/.claude/worktrees/ghost
↳ The directory exists but is not a worktree; to list worktrees, run wt list
```

#3767 fixed the hook that produced the leftover directory in #3753; this
fixes the message a person gets when they type such a path themselves,
which judewang named there as why it was hard to diagnose. Ref #3753.

Two neighbouring commands had the same cause and are also fixed. `wt
switch <path>` and `wt merge <path>` used to offer `wt switch --create
<path>`, which fails with `fatal: … is not a valid branch name`; and `wt
config state marker set --branch <path> foo` **succeeded silently**,
storing state keyed by a path string.

## How the decision is made

`Repository::path_selector_error` reports a directory only when all four
conditions hold. Each is here because dropping it made `wt` assert
something false, reproduced against a build:

| Condition | Without it |
|---|---|
| Git could never accept the selector as a branch name | `wt step rebase
HEAD~9` reports revision syntax as a path |
| A directory is there | `wt remove docs` stops meaning the branch when
`docs/` sits beside it |
| No worktree of this repo is registered at it | a *detached* worktree —
reachable by path alone — is reported as absent, contradicting the `wt
list` in the hint |
| It holds no git data | a sibling repo's live checkout, or a bare
repository, is called a leftover only the user can delete |

That last one is the data-safety case: the new variant's hint says only
the user can delete the directory, so a false positive reads as an
invitation to `rm -rf` a checkout with uncommitted work. Absence of git
data is *established* rather than assumed — `symlink_metadata` with only
`NotFound` counting, so an unreadable clone or a dangling `.git` symlink
withholds the claim instead of earning it.

The first condition is git's own ref-format rules, implemented
in-process and pinned against `git check-ref-format` by a test.

## Navigating the diff

- `src/git/repository/worktrees.rs` — `Repository::path_selector_error`
and `holds_git_data`, with the rationale for each condition recorded
where it's enforced.
- `src/git/repository/branch.rs` — `is_valid_branch_name`, the
ref-format predicate.
- `src/git/error.rs` — the new `GitError::WorktreeNotFoundAtPath`
variant.
- `src/git/repository/config.rs` — `target_branch_at_path` →
`target_worktree_at_path`, returning the worktree whole rather than
collapsing "no worktree here" with "a detached worktree here".
- Call sites: `commands/remove.rs`, `commands/worktree/switch.rs`,
`commands/config/state.rs`.

Reporting a detached *target* made `DetachedHead`'s fixed hint wrong:
`git switch` acts on the tree it runs in, so `wt merge ../B` was telling
the user to switch the tree they were standing in. The variant now
carries the detached worktree when that isn't the current one, and the
hint becomes `git -C ../B switch <branch>` — the same reason
`OperationInProgress` carries `branch`. Set at the three sites where the
detached worktree isn't the current one (`require_target_branch`,
`require_selected_branch`, and `wt step promote`, which raises this
about the main worktree); every other site passes `None` and renders as
before.

140 of the 257 added production lines are comments and docstrings —
three of the four conditions look like obvious things to simplify away,
so the reason each exists is written down next to it.

## Testing

Unit tests at the resolver boundary cover each condition and its
complement, both `directory_exists` arms, the shortcut-expansion route
(`wt merge -`), detached targets, revision syntax, unresolvable `.git`
entries, and bare repositories. Two integration tests drive the real
binary end-to-end against `../repo.leftover` — `wt remove` and `wt
switch` — covering the relative-path spelling and each command's wiring,
and the `switch` snapshot pins the thing that would regress silently:
that no `--create` hint is offered for a name git would reject.

The ref-format predicate is verified beyond its committed table: 2,791
generated names were run against real `git check-ref-format` with zero
disagreements, and two review passes did the same over their own inputs.
That sweep isn't committed — it needs Python and a live `git` — so the
committed table is what guards against drift.

Not exercised locally: the Windows-specific behaviours are reasoned, not
run — `Path::new("foo.").is_dir()` succeeding against `foo`, and
drive-relative `C:foo`. CI exercises the platform but not those inputs.

<details>
<summary>Known gaps this would ship with</summary>

- `wt switch docs/` (trailing slash, branch `docs` has a worktree,
`./docs` is a source directory) answers `No worktree @ docs/`. True,
unhelpful. The real fix is stripping trailing separators before
resolution — a resolution change, not a message change. On Windows it
reads `No worktree @ …/docs`, because `path_slash`'s `to_slash_lossy`
rebuilds the string from `Path::components()` there and drops the
trailing separator — still true of the path, minus the character that
explains it. The doc comment on `path_selector_error` now says so rather
than promising a spelling guarantee that holds on one platform.
- A worktree whose registration was pruned out from under it gets a
safe-but-vague message. The useful answer is a distinct `git worktree
repair`-or-delete diagnosis; that's new detection, and nobody has
reported hitting it.
- Under `-C`, a `./`-prefixed selector renders a visible `/./` (`wt -C
repo remove ./ghost` → `repo/./ghost`). Cosmetic; tidying it would
discard the trailing separator the `docs/` case needs.
- `require_target_branch` and `require_target_ref` remain near-copies of
one ladder, and this adds a line to each. Pre-existing; extracting it is
a separate refactor.
- `CommandEnv::require_branch` passes `worktree: None` unconditionally,
and `CommandEnv::for_selector` builds an env whose `worktree_path` is
the worktree the *argument* named — so a future caller combining the two
would get the unqualified hint. Nothing reaches that combination today:
`merge` and `squash` are the only `require_branch` callers and both
build with `for_action`, and the one `for_selector` caller (`wt step
commit --branch`) never calls it.

Two pre-existing bugs surfaced while reviewing this, unrelated to the
diff and reproducible by branch name: `wt remove` will trash a clone
that now sits at a registered worktree's path, and a recreated worktree
directory leaks a raw `git rev-parse --git-dir failed (exit 128)`.

</details>

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 13:06:02 -07:00
Maximilian Roos f57fac365b Release v0.72.0 (#3759)
Release v0.72.0 — 55 commits since v0.71.0.

Minor bump: `cargo semver-checks` reports 5 breaking library changes, so
patch is disallowed pre-1.0.

## Headline changes

- **`wt merge` / `wt step push` no longer autostash the target
worktree** (#3703). Both strategies now advance the target through one
`advance_target` — a compare-and-swap `update-ref`, then `read-tree -m
-u` in the target worktree — so `refs/stash` is never entered and staged
changes stay staged.
- **Forge classification returns to brand-in-hostname** (#3673),
reverting the exact-DNS-label rule 0.71.0 shipped.
`github-enterprise.acme.com` and friends resolve again with no config.
- **`[projects."…"]` keys match by `*` pattern and carry forge
settings** (#3701), so one user-config entry covers every repository on
a self-hosted host.
- **A published JSON Schema for `wt list --format=json` schema 2**
(#3747), plus machine-readable approval state and `branch_outcome`
(#3710).

Full detail in `CHANGELOG.md`.

## One fix made during the release cut

The release audit surfaced a gap this release's own `advance_target`
rewrite introduced, fixed here rather than deferred:

**`wt merge` / `wt step push` now refuse a target worktree parked
mid-operation.** The target sync is a two-tree merge, which refuses an
unmerged index but *not* a stopped cherry-pick or rebase whose conflict
has already been staged. A target paused between steps could therefore
have the push range written into it, and the user's `--continue` would
commit the synced tree as the step's result. The old fast-forward path
got this check for free from `receive.denyCurrentBranch=updateInstead`,
which refused any unclean target outright; both strategies now ask
directly, and the refusal names the worktree holding the operation.

`test_push_refuses_target_mid_operation` covers it in both shapes a
stopped operation can take, and both are mutation-verified. With the
gate disabled, the push succeeds and writes `feature.txt` into the
mid-cherry-pick worktree.

The rebase case was added in response to review feedback on this PR, and
pins a second dependency. A rebase detaches HEAD, so `git worktree list
--porcelain` reports the target with no branch and `worktree_for_branch`
finds it only because `finalize_worktree` backfills from
`rebase-merge/head-name`. That makes the rebase arm the one place this
guarantee rests on a helper of ours rather than on git — the
fast-forward path it replaced got the refusal from `find_shared_symref`.
With the backfill disabled, `wt step push` succeeds against a worktree
parked mid-rebase while the cherry-pick case still passes, so the gap
was real.

## Validation

- Local gate green: `cargo run -- hook pre-merge --yes` — 4570 tests,
clippy, fmt, doc sync.
- Cross-platform nightly green on the cut-from tip `3817df079` (run
31133551751): full nextest matrix on linux/macOS/Windows,
feature-powerset, all three release triples, nix-flake,
minimal-versions, unused-deps, crate-build, link-check.
- Changelog verified entry-by-entry against the diffs by an independent
pass; every one of the 55 commits either maps to an entry or is a
documented skip.
- `main` advanced during the CI wait. #3762 ships in this release and
now has a changelog entry; the other two commits that landed (#3758,
#3749) touch only `.github/`.
- Data-loss surface reviewed by four independent finders over the
cumulative diff. One further finding — a pre-0.72 `approvals.toml` key
containing `*` being reinterpreted as a wildcard on upgrade — was
reviewed and accepted as out of scope for this release.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 02:22:28 -07:00
Maximilian Roos 1b35950a9c test: simplify the integration suite (#3657)
This reduces duplicated and false-confidence integration coverage while
preserving the suite's semantic and user-facing contracts.

## What changed

- Replaces three overlapping list-layout suites with two representative
CLI integrations, leaving exhaustive geometry at the direct layout
layer.
- Groups Git error render variants into labeled family snapshots and
removes command-by-shell wrapper cross-products while retaining
shell-specific conformance and regression cases.
- Updates test-authoring guidance around boundary choice, minimal
contrasts, and PTY use, and runs local and CI coverage through Nextest
isolation.

## Results

- Test catalog: 4,642 to 4,562
- Snapshots: 1,193 to 1,131
- Warm all-feature runtime: 84.70s to 78.17-80.35s
- Full coverage: 97.32% of lines

## Testing

- `cargo run -- hook pre-merge --yes`
- `cargo llvm-cov nextest --features shell-integration-tests
--summary-only`

> _This was written by Claude Code on behalf of max_.
2026-07-29 18:54:19 -07:00
Maximilian Roos 84cbb0489e fix(step): refuse to rebase or merge from a worktree mid-operation (#3558)
## The bug

With a rebase in progress (a conflicted stop, or a killed `git`), `wt
step rebase main` reported success over a conflicted tree:

```console
$ git rebase main          # stops on a conflict; HEAD detached mid-replay
$ wt step rebase main
○ Already up to date with main
$ echo $?
0
```

`handle_rebase` asked `is_rebased_onto` first. Mid-rebase, HEAD sits on
a linear extension of the target, so that returns true and the function
returns `UpToDate` before anything consults `worktree_state`.

`wt merge` from the same state failed differently but no better:

```console
✗ Cannot merge: not on a branch (detached HEAD)
↳ To switch to a branch, run git switch <branch>
```

The hint names the one command that throws the rebase away instead of
finishing it.

## The fix

Both commit-replaying commands now check for an open operation before
doing anything else. `wt merge` runs the check ahead of its branch
check, so the open operation is what gets named rather than the detached
HEAD it causes.

```console
$ wt step rebase main
✗ Cannot rebase: a git operation is already in progress
```

The gate covers every state detection reports, not just rebase. Measured
on the others beforehand: a rebase started mid-bisect ran and stacked a
second operation on the first, and one started mid-merge or
mid-cherry-pick died inside git's autostash (`Cannot save the current
index state`, `f: needs merge`). None told the user what to do next.

## Why it prescribes no remedy

An earlier draft of this branch carried a per-operation table of
`--continue`/`--abort` hints. It was wrong on first contact with the
long tail: `git am` borrows the `rebase-apply` directory, so an open am
session was reported as a rebase and pointed at `git rebase --continue`,
which git refuses outright:

```console
$ git rebase --continue
fatal: It looks like 'git am' is in progress. Cannot rebase.
```

Adding an `Am` arm would have treated the enumeration as the fixable
part. `git status` already answers this better than the table did, in
git's own words and translations, for states worktrunk has no variant
for:

```console
$ git status
interactive rebase in progress; onto b162044
You are currently rebasing branch 'feat' on 'b162044'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)
```

It names the operation, the branch, the progress, and every way out
including `--skip`, which the table never offered. So the table is gone
rather than extended, and the refusal is a single line: it says what is
blocked and why, and leaves the rest to the command every git user
reaches for next.

Detection still returns a structured `InProgressOperation` rather than a
display string, but now purely as detection: the only caller that reads
a variant is the one classifying a rebase worktrunk itself just started.

The neighbouring `RebaseConflict` error took the same treatment. It
forwards git's stderr in the gutter when it has it, and otherwise
substituted two hint lines of its own:

```console
↳ To continue after resolving conflicts, run git rebase --continue
↳ To abort, run git rebase --abort
```

That is the operation table again in miniature — one operation's
remedies, minus `--skip` — and the one path that reaches it is the
safety check for a rebase git *reported as finished*, where worktrunk
knows least about what is actually open. It now prints its title, plus
git's own words when git said any.

## One detector

Two places probed the same files for the same answer:
`Repository::worktree_state` for the gate, and
`WorkingTree::is_rebasing`/`is_merging` for the `wt list` gutter
symbols. They are now one method, `WorkingTree::operation_in_progress`,
which `Repository::operation_in_progress` delegates to — the rename also
retires a name that collided with `wt list`'s unrelated `WorktreeState`.

Detection now runs in the order `git status` consults the state files,
which is a change from the old `wt list` path: it checked rebase before
merge. Nothing can reach the difference — no rebase backend writes
`MERGE_HEAD`, checked against all three — but matching git's order is
one less thing to reason about. `wt list` maps the operations without a
gutter symbol (cherry-pick, revert, bisect) to `None`, so its output is
unchanged.

## The queued sequencer, now closed

Having one detector is what made the last gap worth closing. `git
cherry-pick A B` that stops on A writes `CHERRY_PICK_HEAD`; resolving
that stop with `git commit` instead of `git cherry-pick --continue`
removes the file and leaves B queued:

```console
$ git status
On branch main
Cherry-pick currently in progress.
  (run "git cherry-pick --continue" to continue)
$ ls .git/CHERRY_PICK_HEAD
ls: .git/CHERRY_PICK_HEAD: No such file or directory
$ cat .git/sequencer/todo
pick 1f07533 side1
pick 6b32540 side2
```

Git reads that same file to keep calling the sequence in progress, so
detection reads it too: the first instruction word, `pick` or `revert`,
exactly as git's own `sequencer_get_last_command` does.

The reason this waited for its own change is the risk of refusing a `wt
merge` the user can't un-refuse, so the lifecycle is measured rather
than assumed. The directory is gone once the sequence finishes, and gone
after `--quit` or `--abort`; a single-commit cherry-pick never creates
one; a rebase keeps its todo elsewhere, under `rebase-merge/`. An empty
or unrecognized todo reports nothing, so the failure direction is the
old behavior rather than a refusal with no way out.

## Verification

The repro above, plus the merge, am, cherry-pick, revert, and bisect
states, checked by hand against real repos: all five refuse, including
am, which is now correct precisely because nothing here claims to know
what am needs.

Three integration tests drive the real binary from a genuinely stopped
operation (`test_step_rebase_refuses_mid_rebase`,
`test_merge_refuses_mid_rebase`, `test_merge_refuses_mid_merge`).
`test_merge_refuses_mid_merge` is what holds the broadened scope: a
conflicted merge keeps HEAD on the branch, so it is the case the
detached-HEAD check waves through. Narrowing the gate back to
rebase-only turns it red, which I verified by making that edit and
re-running.

The sequencer case is driven end to end from a real stopped pick —
`test_operation_in_progress_reads_the_queued_sequencer` commits the
resolution by hand, asserts `CHERRY_PICK_HEAD` is gone, and still
expects a cherry-pick, so it can only pass through the sequencer read.
It then runs `git cherry-pick --quit` and expects the state to clear,
which is the false-refusal half. Running the same sequence against the
built binary refuses where it previously reported `○ Already up to date
with main`.

Confirmed unchanged: already-up-to-date, fast-forward, true-rebase, and
rebase-conflict output; a plain detached HEAD still gets the `git
switch` hint, which is the right remedy when no operation is open; a
sibling worktree mid-rebase does not block a clean one; `wt list`'s
gutter symbols.

Full gate green (4494/4494 tests, lints, fmt), plus `cargo clippy
--all-targets --features shell-integration-tests`, which the gate
doesn't compile.

## Merged with main

`main` gained #3539, which extracted the failed-rebase classification
into `classify_rebase_failure`, and #3549, which adds `span_upstream` —
both inside `handle_rebase`. The conflict was semantic as well as
textual: #3539's `is_rebasing` check still read the old string API
(`worktree_state()?.is_some_and(|s| s.starts_with("REBASING"))`), which
this branch removes, so resolving the markers alone left a type error.
The resolution keeps #3539's classification and reads the structured
enum instead:

```rust
let state = repo.operation_in_progress()?;
let is_rebasing = matches!(state, Some(InProgressOperation::Rebase));
return Err(classify_rebase_failure(e, is_rebasing, &integration_target));
```

All five of #3539's classification tests pass alongside the gate.

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 07:39:28 -07:00
Maximilian Roos aebe278a2c refactor(tests): drop empty-valued env entries from snapshot headers (#3063) 2026-06-13 08:08:53 -07:00
Maximilian Roos f2a8cb696d refactor(styling): end bash-gutter lines at their content, not a reopened dim (#3056)
Dispatched to fix a reported ANSI dim-bleed after bash-gutter blocks: in
committed snapshots, multi-command hook announcements appear to inherit
an unclosed `[2m` from the preceding gutter line (e.g.
`post_start_named_commands.snap`, where the gutter line ends
`'Installing deps'[0m[2m`).

Diagnosis: the bleed doesn't exist in real output. Fresh `cat -v`
captures of `wt hook pre-merge --yes` and `wt switch --create` with
two-key hook tables show every gutter line closing with a full `[0m`,
and later `◎ Running …` lines rendering un-dimmed. The snapshots are
misleading by construction: the cross-platform filter in
`tests/common/mod.rs` deletes every line-final `[0m` before
snapshotting, and the formatter reopens dim after the last highlight
token before its per-line reset, so filtered snapshots end `[0m[2m` and
read exactly like a dangling dim.

The change removes that misleading byte pattern at the source: phase 2
of `format_bash_with_gutter_impl` strips the no-op reopened dim (and the
lone dim on blank lines) before appending each line's closing reset.
Rendering is identical; lines now end at their content plus one reset.

Also bundled:

- A CAUTION comment on the reset-stripping snapshot filter, so future
readers don't diagnose SGR bleed from `.snap` bytes.
- `config_show_theme` now binds the standard env redactions;
regenerating its snapshot leaked a host `LLVM_PROFILE_FILE` path and
tripped `test_no_host_specific_paths_in_snapshots` (pre-existing gap,
invisible until regeneration since insta never compares `info:` blocks).
- 108 regenerated snapshots. Verified mechanically: ANSI-stripped bodies
are byte-identical; raw diffs are confined to line-end SGR sequences
plus stale `env:` header refreshes (`RUST_LOG: warn` from an older
harness).

Possible follow-up, not done here: the line-final-reset filter itself
may be vestigial (anstream pass-through suggests piped output is
identical across platforms now); removing it would make snapshots
byte-truthful but churns nearly every snapshot and needs Windows CI to
confirm.

The first Windows CI run caught a real latent bug the strip exposed:
askama strips a template's final newline, so on a CRLF checkout (Windows
autocrlf) the fish wrapper render ends with a lone `\r` that the
formatter's pair-wise CRLF normalization missed. The `\r` reached
tree-sitter and came back as a trailing token after the highlight
closed, defeating the end-of-line cleanup (and historically invisible
because insta trims trailing whitespace when comparing). Fixed by
trimming trailing `\r` in the formatter's normalization, plus
`templates/* text eol=lf` in `.gitattributes` since CRLF templates
embedded at compile time would leak `\r` into the shell code
Windows-built binaries emit at runtime.

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

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:38:55 -07:00
Maximilian Roos b68e10b816 refactor(error): render UncommittedChanges dirty files in the gutter (#2887)
The `UncommittedChanges` error variant printed the offending `git status
--porcelain` lines with a manual two-space dim indent. Visually similar
to the canonical gutter, but inconsistent with `format_with_gutter()` —
the helper the `writing-user-outputs` skill points at for quoting
external content. `ConflictingChanges` in the same file already does the
right thing; this aligns `UncommittedChanges` with it.

Before:
```
✗ Cannot remove: feature has uncommitted changes
   M auth.rs
  ?? .DS_Store
↳ Commit or stash changes first
```

After:
```
✗ Cannot remove: feature has uncommitted changes
▌  M auth.rs
▌ ?? .DS_Store
↳ Commit or stash changes first
```

Swept the codebase for similar manual-indent-quoting patterns; this was
the only site.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:27:52 -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 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
Maximilian Roos 302575894a fix(output): bold identifiers in skipped/squash/path messages (#2661)
## Summary

Several user-facing messages interpolated branch, worktree, file, and
path names as plain text. Per the project's `writing-user-outputs`
skill, branches and status values should be bolded; in hints, paths use
underline.

## Changes

- `wt step prune` "Skipped … (younger than X)" lines now bold each
branch name. The joined-list variant bolds names individually so commas
stay unbold.
- `wt step squash`'s "Nothing to squash; no commits ahead of `<branch>`"
bolds the branch.
- `wt config update`'s "Copied approved commands to `<file>`" bolds the
filename.
- `ConflictingChanges` hint underlines the path (hint convention).
- Config-load warning: `at PATH` → `@ <bold>PATH</>` (matches the
codebase `@`-not-`at` convention and bolds the identifier).
- Explicit-shell uninstall warnings bold the path so they match the hint
counterpart.

## Test plan

- [x] `cargo test --test integration` (1634 passing)
- [x] `pre-commit run --all-files` (lints + fmt clean)
- [x] Snapshots updated via `cargo insta accept` — all diffs are exactly
the formatting changes plus cosmetic env-var drift

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 00:10:14 -07:00
Maximilian Roos 4dd84ca189 feat(error): list dirty files in UncommittedChanges (#2653)
## Summary

The "X has uncommitted changes" error now lists the offending files via
`git status --porcelain` between the title and the hint, instead of
leaving
the user to re-run `git status` to find out *what's* dirty.

Before:
```
✗ Cannot remove worktree after merge: feature-auth has uncommitted changes
↳ Commit or stash changes first
```

After:
```
✗ Cannot remove worktree after merge: feature-auth has uncommitted changes
   M auth.rs
  ?? .DS_Store
↳ Commit or stash changes first
```

## What changed

- `GitError::UncommittedChanges` carries a new `dirty_files:
Vec<String>` field (raw porcelain lines).
- `WorkingTree::ensure_clean` runs through a new `dirty_files()` helper
that returns the porcelain lines instead of discarding them; the four
call sites (`wt merge` cleanup, `wt remove`, `wt step promote` × 2) now
thread the file list into the error.
- `wt merge --no-commit` validator in `commands/merge.rs:155` does the
same.
- No new git subprocesses: `is_dirty()` was already running `git status
--porcelain` and dropping the output. We just stop dropping it on the
failure path.
- 5 existing snapshots updated; 1 new test case added.

## Why

A user hitting "feature has uncommitted changes" today has to re-run
`git status` to see what's blocking. The list is information we already
have.

There's a side benefit: this surfaces dirty state in test snapshots when
something *unexpected* dirties a worktree mid-merge — useful for
diagnosing the local-only flake on `test_readme_example_complex` etc.,
where the failing snapshot will now carry the exact file list (likely an
OS metadata file like `.DS_Store` or an fsmonitor artifact) rather than
a worktrunk bug.

## Test plan

- [x] `cargo test --lib` (1098 passed)
- [x] `cargo test --test integration` (1633 passed; 1 new test case)
- [x] `cargo run -- hook pre-merge --yes` — full lint + test suite (3531
passed, lints clean)
- [x] Snapshots reviewed; new format renders correctly across merge /
remove / promote / git-error-display tests.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-09 12:59:23 -07:00
Maximilian Roos a9b92cff89 refactor(error): split short Display from styled Diagnostic (#2611)
## Summary
- Split each typed leaf error's textual surface in two: `Display` is a
single-line label suitable for embedding (`format!(\"…: {e}\")`, JSON
output, log files); `Diagnostic::render` produces the styled multi-line
block (emoji, color, gutter, follow-up hints).
- Introduces a `Diagnostic` trait implemented by `GitError`,
`WorktrunkError`, `HookErrorWithHint`, `TemplateExpandError`, and
`CommandError`. The renderer in `format_command_error` walks the anyhow
chain via `try_render_diagnostic` once instead of per-type downcast
branches.
- Replaces six anyhow-error free helpers with an `ErrorExt` extension
trait so call sites use method syntax (`err.exit_code()`,
`err.render_diagnostic()`). Domain-specific helpers
(`add_hook_skip_hint`) and leaf lookups (`CommandError::find_in`) stay
as their natural form.
- `HookErrorWithHint` now stores the typed `WorktrunkError` directly;
the runtime downcast in `render` is gone, and `Error::source` exposes
the inner so `exit_code` is a single chain walk.
- `GitError::title()` is the single source of truth for variant titles.
Render uses it for the styled header; Display strips ANSI codes via
`ansi_str::AnsiStr::ansi_strip`. ~150 lines of inline title duplication
removed.

Designed in two iterations with Codex review. The trait+registry shape
was preferred over `miette` because worktrunk's variants carry bespoke
rendering (gutter blocks of git stderr, copy-pasteable shell
suggestions, fetch-time annotations, switch-suggestion ctx) that don't
decompose into miette's `(code, severity, message, help, labels)` shape.

## Test plan
- [x] \`cargo run -- hook pre-merge --yes\` (3492 tests + clippy + doc +
doctest)
- [x] Unit tests in \`src/git/error.rs\` updated for the new contract;
new snapshot tests assert the short \`Display\` per variant
- [x] Integration tests in
\`tests/integration_tests/git_error_display.rs\` switched to
\`err.render()\`
- [x] Inline \`<bold>\` highlighting on entity names preserved in styled
rendering
- [x] JSON outputs (\`for-each\`) confirmed plain (no ANSI codes leaking
via embedded Display strings)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-04 18:06:39 -07:00
Maximilian Roos c710147c8c fix(bench): unbreak wt-perf invalidate; consolidate env-isolation helpers (#2540)
## Summary

Two bugs in `wt_perf::invalidate_caches_auto`, plus consolidation of the
env-isolation helpers shared between tests and benches.

## Bugs fixed

**(1) Silent unset under inherited `GIT_DIR`.** `invalidate_caches_auto`
shelled to `git config --unset worktrunk.default-branch` via
`worktrunk::shell_exec::Cmd`. `Cmd::apply_common_settings` re-injects
normalized inherited `GIT_*` path vars resolved against the wt-perf
process's startup cwd (issue #1914 — correct for `wt`-from-git-alias
where parent and child target the same repo). For wt-perf, which
operates on an explicit, possibly-unrelated repo path, the override
redirected the unset to the wrong git dir; the resulting non-zero exit
was swallowed by `let _ =`, and the cache stayed put.

Reproducer:
```bash
cd /tmp && GIT_DIR=.git target/release/wt-perf invalidate /tmp/wt-prof-test
# previously: silently leaves worktrunk.default-branch in place
# now: actually unsets it
```

**(2) Linked worktrees never cleared.** `invalidate_caches_auto`
resolved `<repo_path>/.git` naively. When `repo_path` is a *linked*
worktree, `.git` is a file (gitdir-pointer), not a directory, so every
`fs::remove_*` call no-op'd silently — `.git/wt/cache/`, `.git/index`,
`packed-refs`, commit-graph all survived. Existing bench callers all
pass the main worktree, so no current bench was affected, but the
function's contract was broken for linked-worktree inputs.

## Fixes

- wt-perf no longer routes through `worktrunk::shell_exec::Cmd`. All
`git` subprocess work uses `std::process::Command` directly via a small
`git_command()` helper that delegates to
`worktrunk::testing::configure_git_cmd`. The inherited-`GIT_*`
normalization (which was the root of bug 1) doesn't apply to plain
`Command`.
- `invalidate_caches_auto` resolves the git common dir from the
filesystem — reads `.git` as dir-or-pointer, strips `worktrees/<name>`
for linked worktrees — so the cache root is correct regardless of which
worktree of a repo is named.
- The `let _ =` on the unset is gone; non-success/non-5 exits print a
one-line `eprintln!` so a future regression can't hide the same way.

## Consolidation

Touched four near-duplicate env-isolation helpers
(`configure_cli_command`, `configure_git_cmd`, `configure_git_env`,
`wt_perf::isolate_cmd`). The shared core is now in `worktrunk::testing`:

- `pub fn isolate_subprocess_env(cmd, user_config: Option<&Path>)` —
strips host context (`GIT_*`, `WORKTRUNK_*`, `NO_COLOR`, `SHELL`,
`PSModulePath`) and sets baseline `WORKTRUNK_*_PATH`. Used by
`configure_cli_command` (test-side, layers on test determinism) and
every bench (no extra layer — benches want realism).
- `pub fn scrub_git_path_vars(cmd)` — env_remove the five `GIT_*`
repo-discovery vars. Called defensively from
`configure_git_cmd`/`configure_git_env` so test code that spawns `git`
outside a `wt`-parent isn't vulnerable to the same redirect.
- `wt_perf::isolate_cmd` deleted. Five bench files now import
`worktrunk::testing::isolate_subprocess_env` directly.
- Path strings unified: `/nonexistent/wt/{config,approvals}.toml` and
`/etc/xdg/worktrunk/config.toml` (preserved as the real XDG path). 47
snapshots regenerated.

## Test plan

- [x] `cargo clippy --all-targets --workspace` clean
- [x] 1072 + 607 lib unit tests pass
- [x] 1589 integration tests pass (vs. baseline's 3 pre-existing
failures, which the snapshot regen also resolved)
- [x] All 5 benches compile (`cargo bench --no-run`)
- [x] Bug 1 (GIT_DIR redirect): unset succeeds
- [x] Bug 2 (linked worktree): cache and config both cleared from any
worktree path
- [x] Vanilla path: still works

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:24:21 -07:00
Maximilian Roos cdb17cbd7c feat(switch): suggest pr:N or mr:N for numeric branch names (#2516)
## Summary

When `wt switch <number>` fails because no branch by that name exists,
the hint now leads with how to switch to the matching PR/MR before
mentioning `--create`.

Platform is detected by hostname-matching the primary remote's effective
URL (so `url.insteadOf` rewrites are respected):
- **GitHub** → `wt switch pr:N`
- **GitLab** → `wt switch mr:N`
- **Unknown / opaque host** → both suggestions

Non-numeric branch names keep the original hint unchanged.

### Before

```
✗ No branch named 2474
↳ To create a new branch, run wt switch --create 2474; to list branches, run wt list --branches --remotes
```

### After (GitHub)

```
✗ No branch named 2474
↳ To switch to PR #2474, run wt switch pr:2474; to create a new branch, run wt switch --create 2474; to list branches, run wt list --branches --remotes
```

## Test plan

- [x] Snapshot tests cover GitHub, GitLab, and unknown-platform variants
- [x] Direct test for `Repository::detect_ref_type` covers all URL
branches
- [x] Existing `branch_not_found` snapshots unchanged (non-numeric
names)
- [x] `cargo run -- hook pre-merge --yes` passes (3412 tests, all lints)
- [x] Manual smoke test in temp repo: GitHub remote → `pr:N`, GitLab
remote → `mr:N`, no remote → both

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-30 21:42:26 -07:00
Maximilian Roos 64e1b4efb9 feat(cli): move approvals subcommand from hook to config (#2282) 2026-04-18 00:46:27 -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 623a2f9221 Show last fetch time in branch-not-found hint (#1877)
When `wt switch` fails because a branch doesn't exist, the hint now
shows when the remote was last fetched — helping users realize stale
local refs may be the cause.

```
✗ No branch named fix/bump-mypy-1.19.1
↳ To create a new branch, run wt switch --create fix/bump-mypy-1.19.1; to list branches, run wt list --branches --remotes (last fetched 3h ago)
```

If the repo has never been fetched (no `FETCH_HEAD`), no parenthetical
is shown — the hint reads the same as before. Only appears in `switch`
context, not `remove` or `merge`.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-01 11:49:31 -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
Maximilian Roos e0067f064f feat(help): syntax-highlight code blocks in --help (#1365)
Code blocks in `--help` were rendered as plain dimmed text — the
markdown
language identifier (` ```toml `, ` ```console `) was discarded. Now
they use
the existing gutter formatting with language-aware syntax highlighting:

- **TOML** blocks get synoptic highlighting (table headers cyan, strings
green,
  comments dimmed)
- **console/bash/sh** blocks get tree-sitter-bash highlighting (commands
blue,
  flags cyan, strings green)
- **Other** languages get dimmed text with gutter

Also makes TOML highlighting consistently dimmed (matching bash style)
and
removes the 2-space indent from help tables so block elements are
flush-left.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-08 14:07:21 -07:00
Maximilian Roos dc7f5bca5c Replace bright-black with underline for inline references in hints (#1285) 2026-03-05 02:14:38 -08:00
Maximilian Roos cbd5e4da50 feat: add nushell tab completions (#1220)
## Summary

- Add `nu-complete wt` completer function to the nushell init template,
wiring it to the wrapper's rest parameter via `@"nu-complete wt"`
- The completer calls the binary with `COMPLETE=nu` and parses
tab-separated output into nushell's `{value, description}` record format
- Add `"nu"` to multi-shell completion test loops (help, version,
single-dash, deprecated flags) and a dedicated subcommand snapshot test

Thanks to @omerxx for reporting in #1215.

Closes #1215

## Limitations

Nushell's completion engine bypasses custom completers when the current
token starts with `-`, so flag completions (e.g. `wt switch --<TAB>`)
don't appear. Subcommand and value completions work correctly. This is a
nushell engine limitation (nushell/nushell#14504), not something we can
fix in our template.

## Test plan

- [x] Unit tests pass (`cargo test --lib --bins`) — snapshot updated
- [x] Integration tests pass (`cargo test --test integration`) — 1121
tests
- [x] Shell integration test (`cargo test --test integration --features
shell-integration-tests -- test_nushell_completion_subcommands`)
- [x] Lints pass (`pre-commit run --all-files`)
- [x] Manual verification in nushell 0.110.0: `wt <TAB>` shows
subcommands, `wt switch <TAB>` shows branches with descriptions

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

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 19:12:15 -08:00
Maximilian Roos 7b7c0b12d2 feat: add color palette to show-theme (#1185)
## Summary

- Add a "Color palette" section to `wt config shell show-theme` that
prints each color/style rendered in itself
- Covers base colors (red, green, yellow, blue, cyan), modifiers (bold,
dim), bold+color variants (emphasis in messages), dim bright-black
(inline code in hints), and dim+color variants (bash syntax
highlighting)
- Helps users diagnose legibility issues on their terminal theme

Thanks to @jhigh2000 for reporting in #1184.

Closes #1184

## Test plan

- [x] Snapshot test updated and passing
- [x] All 1120 integration tests pass
- [x] All lints pass

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-23 11:54:01 -08:00
Maximilian Roos 9d6f8c523a fix: show remaining entries when foreground removal fails (#1150)
## Summary

- When `wt remove --foreground` fails with "Directory not empty", list
top-level entries remaining in the directory (capped at 10 with "and N
more")
- Suggest trying `wt remove` (background) as a workaround
- For non-"Directory not empty" errors (e.g. permission denied), show
remaining entries without the background process hint

Before:
```
✗ Failed to remove worktree for test @ ~/workspace/worktrunk.test
   ┃ error: failed to delete '/path': Directory not empty
```

After:
```
✗ Failed to remove worktree for test @ ~/workspace/worktrunk.test
   ┃ error: failed to delete '/path': Directory not empty
↳ Remaining in directory: .vite/, node_modules/, target/
↳ A background process may be writing files; try wt remove (without --foreground)
```

## Test plan

- [x] Snapshot tests for: directory not empty with entries, permission
error with entries, many entries (cap at 10)
- [x] Existing `worktree_removal_failed` snapshot unchanged (no entries
case)
- [x] Integration tests triggering actual `git worktree remove` failure
(both normal-branch and detached-HEAD paths)
- [x] All tests pass on Linux, macOS, and Windows
- [x] codecov/patch passes

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 19:38:46 -08:00
Maximilian Roos bba3a209b8 fix: align user-facing output with formatting guidelines (#1138)
## Summary
- Replace `<bold>` with `<bright-black>` inside `hint_message()` calls —
bold's close tag (`[22m`) resets both bold and dim, breaking hint
styling
- Use `format_path_for_display()` instead of raw `.display()` in
user-facing messages (config warning, relocate errors, step_commands
output)
- Remove "you/your" pronouns from help text and rephrase typo
suggestions
- Reorder hint phrasing to canonical "To X, run/use Y" pattern

## Test plan
- [x] All 1084 integration tests pass
- [x] All lints pass (`pre-commit run --all-files`)
- [x] Snapshot tests updated to reflect new wording

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 16:10:58 -08:00
Maximilian Roos 339ced434e fix: remove stray blank lines before hints in error output (#1072)
Remove blank line between detail lines and hint in ConflictingChanges and
NotFastForward error display. The `"\n{detail}\n"` + `"\n{hint}"` pattern
produced `\n\n` (blank line) violating "hints attach to their subject."

Also fix trailing double blanks in --help-md output (print! instead of
println! since clap output already ends with \n) and diagnostic template
(Jinja {%- whitespace control).

Add snapshot_formatting_guard tests that scan all 726 snapshots for:
- Blank line before hint marker (↳)
- Double blank lines in output sections

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 18:34:54 -08:00
Maximilian Roos 51b5f6ef5f feat(switch): enrich error hints with --execute context (#1064)
* feat(switch): enrich error hints with --execute and trailing args context

When `wt switch --execute=<cmd> -- <args>` fails (branch not found, already
exists, path exists), error hints now include the full command context so
users can copy-paste the suggested fix directly.

Also fixes `suggest_command()` to place flags before positional args (and
before any `--` separator), preventing flags from being treated as positional
args when dash-prefixed branches are involved.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(coverage): remove unreachable -- separator branch in apply()

Clap's `#[arg(last = true)]` on execute_args means `--` always routes
to execute_args, so a dash-prefixed branch can't coexist with --execute
via the CLI. The suggested command never has a pre-existing `--`
separator when SwitchSuggestionCtx is applied, making this branch
dead code.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 14:37:35 -08:00
Maximilian Roos 3b5b609a04 refactor(env): rename WT_TEST_* env vars to WORKTRUNK_TEST_* (#1016)
Standardize the two remaining `WT_`-prefixed env vars to use the
`WORKTRUNK_` prefix, completing the migration noted in the changelog.

- WT_TEST_EPOCH → WORKTRUNK_TEST_EPOCH
- WT_TEST_DELAYED_STREAM_MS → WORKTRUNK_TEST_DELAYED_STREAM_MS

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-14 00:48:41 -08:00
Maximilian Roos 9282805b36 refactor(error): display failed commands in bash-highlighted gutter (#1001)
* refactor(error): display failed commands in separate bash-highlighted gutter

Extract the failed git command from inline parenthesized text into a
separate hint line + bash syntax-highlighted gutter block. This makes
long commands much more readable and visually distinct from git output.

Before:
  ✗ Failed to create worktree for fix from base main
     ┃ fatal: cannot lock ref 'refs/heads/fix'
     ┃ (git worktree add /path -b fix main failed, exit code 128)

After:
  ✗ Failed to create worktree for fix from base main
     ┃ fatal: cannot lock ref 'refs/heads/fix'
  ↳ Failed command, exit code 128:
     ┃ git worktree add /path -b fix main

- Add FailedCommand struct to separate command identity from git output
- Add StreamCommandError for structured error returns from run_command_delayed_stream
- Add Repository::extract_failed_command() bridge for crate boundary
- Update WorktreeCreationFailed Display impl and both callers in switch.rs
- Update writing-user-outputs skill: hints can provide non-essential context

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: simplify StreamCommandError Display to just output

Callers use Repository::extract_failed_command() to access fields
directly, so the Display impl only needs to satisfy the Error trait
bound.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add coverage for extract_failed_command branches

Tests both the StreamCommandError path (exercises Display impl) and
the fallback path for non-StreamCommandError errors.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: fully qualify doc link for run_command_delayed_stream

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: extract worktree_creation_error helper to reduce duplication

Both call sites in switch.rs had identical 6-line closures constructing
WorktreeCreationFailed from extract_failed_command. Shared helper
reduces each to a one-liner and improves test coverage.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-12 19:10:44 -08:00
Maximilian Roos 0c9e0205a3 feat(switch): integrate interactive picker into wt switch (#894)
* feat(switch): integrate interactive picker into `wt switch` (#890)

`wt switch` without arguments now opens the interactive picker (previously
`wt select`). This simplifies the mental model: one command for all switching.

- `wt switch` → opens interactive picker (Unix) or shows error (Windows)
- `wt switch --branches/--remotes` → customizes picker
- `wt select` → deprecated hidden alias with warning

Removes the separate `wt select` docs page since the functionality is now
documented under `wt switch`. The `[select]` config section is preserved
for backward compatibility with a TODO to rename once migration is confirmed.

Closes #890

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): make switch TTY test Unix-only

The test expects the Unix error message ("Interactive picker requires
an interactive terminal") but Windows shows a different message
("Interactive picker is not available on Windows").

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: trigger CI

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(switch): require branch when using --create, --base, --execute, or --clobber

These flags only make sense when switching to a specific branch, not when
opening the interactive picker. Previously, running `wt switch --create`
would silently ignore the flag and open the picker instead.

Now clap properly enforces that these flags require a branch argument:

    $ wt switch --create
    error: the following required arguments were not provided:
      <BRANCH>

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): update snapshot for interactive picker error message

The snapshot was stale after integrating `wt select` into `wt switch`.
Now `wt switch` without args shows the picker error, not clap's missing
argument message.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(switch): move interactive picker section after shortcuts

The picker is a navigation method (like shortcuts), not an afterthought.
New order: Shortcuts → Interactive picker → GitHub/GitLab PRs → Troubleshooting

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): update help snapshot after doc reorg

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 06:05:39 -08:00
Maximilian Roos ee54aa6102 chore(deps): upgrade tree-sitter to 0.26, add show-theme wrapping examples (#906)
* chore(deps): upgrade tree-sitter to 0.26, add show-theme wrapping examples

Upgrade tree-sitter ecosystem to enable unified multi-line highlighting:
- tree-sitter: 0.25 → 0.26
- tree-sitter-bash: 0.25 → 0.25.1
- tree-sitter-highlight: 0.25 → 0.26

With tree-sitter 0.26, unified highlighting correctly identifies operators
like `&&` across line breaks, enabling future switch from line-by-line to
unified highlighting for better wrapped line handling.

Add regression tests verifying unified highlighting works for:
- Multi-line commands with `&&` at line ends
- Commands containing template syntax (`{{ }}`)

Update `wt config shell show-theme` to demonstrate the wrapping issue:
- Short command
- Long command that wraps
- Multi-line string literal
- Multi-line command

All line types currently display identically, making it hard to distinguish
wrapped continuations from new commands.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(styling): preserve syntax highlighting across multiline strings

Switch from line-by-line to unified highlighting in format_bash_with_gutter().
This preserves tree-sitter context across newlines, so strings spanning
multiple lines retain their string styling.

Also adds 3 extra spaces of indentation for wrapped continuation lines,
distinguishing terminal-forced wraps from actual newlines in the source.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(styling): normalize CRLF to LF in bash highlighting

Windows templates have CRLF line endings, which caused the style
restoration code to create extra lines. Normalize to LF at input.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(styling): preserve syntax highlighting for template-heavy content

Tree-sitter's bash grammar fails to parse content when Jinja template
delimiters `{{` and `}}` are split across lines, causing all highlight
events to be suppressed. This manifested as template-heavy hook commands
losing all syntax highlighting.

Fix by replacing template delimiters with bash-valid placeholders
(`_WT_OPEN_`/`_WT_CLOSE_`) before parsing, then restoring them after
highlighting and wrapping. Also skip function styling for placeholders
since tree-sitter misidentifies them as commands.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: remove unnecessary mut from result variable

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(styling): restore template delimiters before wrapping

Move placeholder restoration before wrapping so line width calculations
use the actual 2-char `{{`/`}}` instead of the longer placeholders.
This preserves line wrapping behavior matching main.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(styling): filter out ANSI-only lines from gutter output

When input ends with a newline, style restoration after the final
newline creates a line containing only ANSI codes. Filter these out
to avoid spurious blank gutter lines in output.

Also use static LazyLock for the ANSI regex pattern.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(styling): simplify template placeholder handling

- Use plain identifier placeholders (no `$`) to avoid ANSI code insertion
- Trim trailing newlines from input to avoid spurious blank gutter lines
- Remove ANSI regex filtering (no longer needed)

Template delimiters `{{`/`}}` at wrapped line start may be styled as
commands, but this is acceptable given the code simplification.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(styling): use quoted string placeholders for consistent template styling

Use `"WTO"`/`"WTC"` as placeholders instead of plain identifiers.
Tree-sitter parses quoted strings as single tokens with consistent
"string" styling (green), so `{{` and `}}` now appear green regardless
of their position in the line.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(show-theme): add template syntax example to bash gutter demo

Shows how `{{ repo_root }}` and `{{ worktree }}` are styled (green).

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-30 22:54:10 -08:00
Maximilian Roos f1c7555dee test: update snapshots for new test environment variables (#907)
Regenerate 599 snapshot files to reflect the updated test environment:

- Add WORKTRUNK_TEST_POWERSHELL_ENV=0 (PowerShell detection control)
- Add WORKTRUNK_TEST_CLAUDE_INSTALLED=0 (Claude Code detection control)
- Add PSModulePath="" (explicit empty value for deterministic snapshots)
- Remove GIT_EDITOR="" (no longer set in test environment)

These changes follow from the test isolation improvements in dc753d0f8
which added environment variables for controlling PowerShell and Claude
Code detection in tests.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 19:49:39 -08:00
Maximilian Roos 907afad0a5 feat(styling): add TOML syntax highlighting for gutter output (#905)
* feat(styling): add TOML syntax highlighting for gutter output

Add format_toml() with syntax highlighting for config previews:
- Table headers [section] in cyan bold
- String values in green
- Comments dimmed

Update show-theme to demonstrate all three gutter styles:
- Error details (plain text)
- Config (TOML highlighted)
- Shell code (bash highlighted)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(styling): add trailing newline after format_toml in config show

The format_toml change to remove trailing newlines broke config show
output. Use writeln! instead of write! to restore the blank line
between sections.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(styling): simplify format_toml token handling

Merge TokOpt::Some and TokOpt::None branches to share the unstyled
path. Synoptic's TOML highlighter returns TokOpt::None for unstyled
content (keys, operators, whitespace), not TokOpt::Some with unrecognized
types.

Add test verifying both styled and unstyled paths are exercised.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 19:43:54 -08:00
Maximilian Roos bf3461aa4c Add prompt_message() formatting function (#858)
* Add prompt_message() formatting function and use it consistently

- Add prompt_message() to styling::constants following the pattern of
  error_message(), hint_message(), etc.
- Update all interactive prompt sites to use the new function:
  - command_approval.rs: batch approval prompt
  - configure_shell.rs: install/uninstall prompts, show-theme demo
- Prompts now have consistent cyan styling on both symbol and text

Co-Authored-By: Claude <noreply@anthropic.com>

* Update approval prompt snapshots for new prompt styling

Co-Authored-By: Claude <noreply@anthropic.com>

* Update shell integration prompt snapshots for new prompt styling

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-25 20:22:36 -08:00
Maximilian Roos 483f806e83 fix: improve BranchAlreadyExists hint wording (#797)
Change hint from "remove --create and run" to "run without --create:"
for clearer, more natural phrasing.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 00:13:48 -08:00
Maximilian Roos 85c4fc1846 fix(errors): improve "not found" error messages (#774)
- Rename NoWorktreeFound → BranchNotFound (consolidate with InvalidReference)
- Add ReferenceNotFound for commit-ish validation (--base accepts tags/commits)
- Improve message phrasing:
  - BranchNotFound: "No branch named X"
  - ReferenceNotFound: "No branch, tag, or commit named X"
  - WorktreeNotFound: "Branch X has no worktree"
- Add show_create_hint field to BranchNotFound to suppress "create" hint
  for remove operations where it doesn't make sense

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 19:40:19 -08:00
Maximilian Roos ec64dbebce fix: use WT_TEST_EPOCH instead of SOURCE_DATE_EPOCH for test time control (#767)
* fix: use WT_TEST_EPOCH instead of SOURCE_DATE_EPOCH for test time control

SOURCE_DATE_EPOCH is a reproducible builds standard commonly set by NixOS/direnv
in development shells. When set to a past timestamp (typical for builds), the
Age column in `wt list` incorrectly shows "future" for all commits.

Switch to WT_TEST_EPOCH which is only set by our test harness, leaving
production users unaffected.

Fixes #763

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: restore PTY test snapshots with WT_TEST_EPOCH

The previous commit deleted PTY-based test snapshots that can only be
regenerated in CI (they require Linux/macOS PTY support). This commit
restores those 46 snapshots from main and updates them to use
WT_TEST_EPOCH instead of SOURCE_DATE_EPOCH.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: wrap bare URL in angle brackets for rustdoc

RUSTDOCFLAGS='-Dwarnings' fails on bare URLs in doc comments. Wrap the
GitHub issue link in angle brackets to make it a proper hyperlink.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 11:38:22 -08:00
Maximilian Roos 2e2f1f5676 refactor: use -1 sentinel for delayed stream disable (#751)
Change WT_TEST_DELAYED_STREAM_MS from magic number 999999 to -1 as
a cleaner sentinel value meaning "never stream". The type changes
from u64 to i64 to allow negative values.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 19:51:36 -08:00
Maximilian Roos dc20cc63ea fix: handle CRLF line endings in multiline error display (#707)
The multiline error detection in main.rs only checked for `\n`, missing
errors with `\r\n` (Windows) or `\r` (old Mac) line endings. These would
fall through to the single-line handler and display incorrectly.

Now checks for both `\n` and `\r`, normalizes all line endings to `\n`
before displaying in the gutter.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 00:25:30 -08:00
Maximilian Roos ad8ef2d74a test: update snapshots for refactored module paths and improved test isolation (#705)
- Update source paths in shell module snapshots (shell.rs → shell/mod.rs)
- Add MOCK_CONFIG_DIR and SHELL env vars to test environment
- Remove GIT_EDITOR env var (no longer needed)
- Fix indentation in inline snapshots

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-17 21:43:07 -08:00
Maximilian Roos f584cf7833 test: add cross-platform PTY infrastructure for Windows shell tests (#670)
* test: add cross-platform PTY infrastructure for Windows shell tests

Make shell integration tests cross-platform using portable_pty's ConPTY
support on Windows.

Changes:
- tests/common/mod.rs: Remove Unix-only gate from shell module
- tests/common/shell.rs: Add Windows env vars, PowerShell -Command flag,
  fix PATH separator for PowerShell
- tests/integration_tests/shell_wrapper.rs: Add PowerShell support to
  build_shell_script and exec_in_pty_interactive, add Windows-only
  PowerShell test cases

The Windows tests are gated with #[cfg(windows)] and will run when CI
has Windows runners with PowerShell available.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: gate remaining Unix-only shell tests with #[cfg(unix)]

Three more tests use `shell_command` which is Unix-only:
- test_zsh_completion_produces_correct_output
- test_wrapper_help_redirect_captures_all_output
- test_wrapper_help_interactive_uses_pager

These tests require zsh/bash/fish which aren't available on Windows.
The Windows PowerShell tests at the end of the file remain ungated.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: gate all Unix shell tests with #[cfg(unix)]

Comprehensive update to gate all tests that use bash/zsh/fish shells
with #[cfg(unix)] to prevent them from running on Windows CI.

Tests gated:
- All parameterized shell tests (bash/zsh/fish cases)
- Bash-specific tests (completions, job control, shell integration)
- Zsh-specific tests (wrapper function, job control)
- Fish-specific tests (completions, multiline commands)
- README example tests that use Unix shells

Windows PowerShell tests at the end of the file remain active on
Windows via their existing #[cfg(windows)] attributes.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): gate Unix-only imports and statics for Windows CI

When all Unix shell tests are gated with #[cfg(unix)], the imports and
static variables they use become unused on Windows, causing clippy errors.

Add #[cfg(unix)] to:
- Imports: canonicalize, wait_for_file_content, assert_snapshot, fs,
  PathBuf, LazyLock, shell
- Statics: TMPDIR_REGEX, TMPDIR_PLACEHOLDER_COLLAPSE_REGEX, WORKSPACE_REGEX,
  COMMIT_HASH_REGEX, JOB_CONTROL_REGEX
- Methods: assert_no_job_control_messages, normalized
- Function: generate_completions

Co-Authored-By: Claude <noreply@anthropic.com>

* test(windows): temporarily ignore PowerShell PTY tests

PowerShell PTY tests timeout in CI (60+ seconds). The issue is likely
that Get-Command finds Windows Terminal's wt.exe instead of the test
binary, causing the completion setup to hang.

Mark tests as ignored pending investigation. The test infrastructure
and cross-platform PTY support remain in place.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(tests): gate assert_success method for Windows CI

The assert_success() method (added in #668) is only used by Unix shell
tests. Gate it with #[cfg(unix)] to fix Windows clippy dead_code error.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(tests): organize shell tests by platform

- Group Unix-only imports in single #[cfg(unix)] block
- Move Windows tests into `mod windows_tests` with single #[cfg(windows)]
- Add documentation explaining test organization by platform
- Remove individual #[cfg(windows)] from each Windows test

This makes it clearer which tests run on which platform and reduces
the number of scattered cfg attributes.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(tests): organize shell_wrapper tests by platform with module-level gating

- Replace 46 individual #[cfg(unix)] annotations with single module-level gate
- Restructure from nested `mod tests { mod windows_tests }` to sibling modules:
  - `#[cfg(unix)] mod unix_tests` - 47 bash/zsh/fish tests
  - `#[cfg(windows)] mod windows_tests` - 4 PowerShell tests
- Rename 30 snapshot files to match new module path (__tests__ → __unix_tests__)

This makes platform organization explicit at the module level rather than
scattered across individual test functions.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 16:43:19 -08:00
Maximilian Roos 63def67f0d feat: add --force hint when removing dirty worktrees (#671)
When `wt remove` fails due to uncommitted changes, the hint now shows
the full command to bypass the check:

  ✗ Cannot remove worktree: feature has uncommitted changes
  ↳ Commit or stash changes first, or to lose uncommitted changes,
    run wt remove feature --force

The hint is context-aware: it only appears for `wt remove` where
`--force` is available, not for `wt merge` contexts where there's no
force option.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 15:30:50 -08:00
Maximilian Roos f039f4a6ab Align user messages with output formatting guidelines (#631)
* Align user messages with output formatting guidelines

- Use "To X, run Y" pattern for hints with runnable commands
- Keep direct imperatives for action guidance (no "To proceed" filler)
- Remove quoted commands, use styling instead
- Add styling to error messages (bright-black for commands, bold for values)
- Clarify git-wt hint: "invoke directly (with the -)"
- Update guidelines to distinguish command hints from action guidance

Co-Authored-By: Claude <noreply@anthropic.com>

* Update PTY-based snapshot tests

These tests require the shell-integration-tests feature flag to run
locally, so they weren't caught in the initial commit.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 15:30:23 -08:00
Maximilian Roos d4be5ee197 refactor: reorder CLI commands by usage frequency (#434)
* refactor: reorder CLI commands by usage frequency

Reorder Commands enum to surface most-used commands first:
switch, list, remove, merge, select, step, hook, config

This matches the natural workflow (switch → work → merge) rather
than the previous arbitrary ordering.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove trailing blank line

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

Co-Authored-By: Claude <noreply@anthropic.com>

* test: update snapshots for new command ordering

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 13:42:57 -08:00
Maximilian Roos 2937aa5830 Fix snapshot raw string literals
Change snapshot assertions from standard string literals to raw string
literals to properly preserve backslash escape sequences in expected
output.
2026-01-04 12:56:51 -08:00
Maximilian Roos 49c15097ff refactor(tests): simplify snapshot testing with auto-bound settings (#396)
* refactor(tests): simplify snapshot testing with auto-bound settings

- Auto-bind snapshot settings in TestRepo::new() via _snapshot_guard field
- Remove manual setup_snapshot_settings().bind() wrappers from tests
- Inline make_snapshot_cmd calls directly in assert_cmd_snapshot! macros
- Remove unused helper functions (json_settings, bind, bind_json)
- Use auto-naming from test function names (explicit names only for multi-snapshot tests)
- Fix directive file guard lifetime in test_remove_internal_mode
- Clean up orphaned snapshot files and let git detect renames

Net reduction: -183 lines in test files

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: update statusline tests for fish-abbreviated paths

The auto-bound snapshot settings from TestRepo use `_REPO_` filters
for full paths, but the statusline command uses fish-style path
abbreviation (e.g., `/p/v/f/.../repo`). These abbreviated paths
weren't matched by the existing filters.

Updated `claude_code_snapshot_settings()` to:
- Filter fish-abbreviated paths ending in `/repo` to `[PATH]`
- Strip leading ANSI reset codes from output
- Remove unused `repo` parameter

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: normalize statusline paths across platforms

The statusline output varies by platform:
- Linux: Raw path filtered by auto-bound settings to `_REPO_`
- macOS: Fish-style abbreviation bypasses auto-bound filters

Updated claude_code_snapshot_settings() to normalize both cases
to a consistent `[PATH]` placeholder for cross-platform tests.

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-03 01:00:10 -08:00
Maximilian Roos 90f24c0ac6 Remove obsolete hook_show_outside_git_repo snapshot (#393)
This snapshot test was removed as part of the hook show refactoring.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 19:53:29 -08:00
Maximilian Roos 3af0d98619 fix: improve shell integration warning for explicit path invocations (#382)
When running wt via an explicit path (cargo run, ./wt, /usr/bin/wt),
shell integration can't intercept the command. Previously this showed
a misleading "restart shell" message.

Now shows: "ran target/debug/wt; shell integration wraps wt" - clearly
explaining what was run vs what the shell wrapper intercepts.

Detection uses argv[0]: PATH lookup sets it to just "wt", while explicit
paths include the path separator. This catches cargo run, symlinks to
dev builds, and any direct binary invocation.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 14:59:06 -08:00
Maximilian Roos 8a9db5f8f0 refactor: unify shell restart messages (#381)
* Add shell integration prompt after wt switch

When shell integration is not active after `wt switch`, show a one-time
interactive prompt asking users if they want to install it. This helps
new users discover shell integration without requiring them to read docs.

## Features

- Interactive prompt with preview option (?) to see what will be installed
- Remembers user choice via `skip-shell-integration-prompt` config flag
- Falls back to hint message for non-TTY or after declining

## Decision flow

1. Unsupported shell → hint with supported shells
2. $SHELL not set → hint to run install command
3. Already installed → hint to restart shell
4. Already prompted → hint to run install command
5. TTY available → show interactive prompt
6. Non-TTY → hint to run install command

## Code improvements

- Consolidated shell install output into `print_shell_install_result()`
- Extracted `print_skipped_shells()` helper to eliminate duplication
- Improved hint styling: commands/paths use `<bright-black>` per guidelines

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: install for all shells on prompt accept, skip flag only on decline

- Prompt now installs for ALL shells with config files, matching
  `wt config shell install` behavior
- Skip flag only set on explicit decline, not before prompt (Ctrl+C
  doesn't permanently disable prompts)
- Shell detection uses starts_with for versioned binaries (zsh-5.9)
- If no config files exist anywhere, show install hint

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: update prompt_accept snapshot for all-shells install

The prompt now installs for all shells (same as `wt config shell install`),
so the snapshot includes skip messages for shells without config files.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: show shell integration warnings for all switch operations

Previously, `wt switch` only warned about missing shell integration when
switching to existing worktrees without `--execute`. Now warnings are shown
consistently:

- `SwitchResult::Existing`: warns when shell integration not active
- `SwitchResult::Created`: shows success, then warns if shell won't cd
- `--execute`: custom message noting command runs but shell stays put

Warning reasons are now proper reasons, not suggestions:
- "shell requires restart" (configured but not active)
- "shell integration not installed" (not configured)

For created worktrees, the warning is simplified to avoid repetition:
  ✓ Created new worktree for feature from main @ ~/repo.feature
  ▲ Cannot change directory — shell integration not installed

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

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: unify shell restart messages

- All restart hints now use "Restart shell to activate" (removed
  fish/bash/zsh branching that showed source command for bash/zsh)
- Warning reasons use statement form: "shell requires restart" and
  "shell integration not installed" (per em-dash guidelines)
- Combined three conditions that showed same hint into single if
- Removed "run:" colon per CLI output formatting guidelines

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

Co-Authored-By: Claude <noreply@anthropic.com>

* Replace repo_root with repo_path in wt.toml

Update template variable references from repo_root to repo_path
to match the current configuration convention.

* fix: restore full "shell integration" text in restart hint

The abbreviated "Restart shell to activate" was unclear about what was
being activated. Restored to "Restart shell to activate shell integration".

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 14:50:27 -08:00
Maximilian Roos 058bfab195 feat: show shell integration warnings for all switch operations (#377)
Previously, `wt switch` only warned about missing shell integration when
switching to existing worktrees without `--execute`. Now warnings are shown
consistently:

- `SwitchResult::Existing`: warns when shell integration not active
- `SwitchResult::Created`: shows success, then warns if shell won't cd
- `--execute`: custom message noting command runs but shell stays put

Warning reasons are now proper reasons, not suggestions:
- "shell requires restart" (configured but not active)
- "shell integration not installed" (not configured)

For created worktrees, the warning is simplified to avoid repetition:
  ✓ Created new worktree for feature from main @ ~/repo.feature
  ▲ Cannot change directory — shell integration not installed

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 14:08:54 -08:00