Commit Graph

1003 Commits

Author SHA1 Message Date
Maximilian Roos 68dae3a2fc docs(cli): drop redundant --format value parenthetical where clap lists them (#3112)
Follow-up to #3108. That PR made clap render `--format`'s values inline
as `[possible values: …]`, but five flags still carried a hand-written
value parenthetical in their doc comment, so the list rendered twice —
three times in `wt list statusline -h`:

```
--format <FORMAT>  Output format (table, json, claude-code) [default: table] [possible values: table, json, claude-code]
```

This drops the parenthetical (→ bare `/// Output format`) from every
`--format` flag that does **not** hide possible-values, leaving clap's
inline list as the single source: `wt list statusline`, `wt step
for-each`, `wt step prune`, `wt config show`, `wt config state vars
list`.

The deliberate hiders are left untouched — they set
`hide_possible_values = true`, so their parenthetical is the *only*
value list: `wt list`, `wt config state get` (both suppress the
`claude-code` value, which is meaningless for them), and the shared
`GlobalFormatFlag` for `config state
cache`/`logs`/`hints`/`ci-status`/`marker`. The rule: if clap prints the
list, drop the hand-written one; if clap's list is suppressed, keep it.

For `wt list statusline`, `claude-code`'s non-obvious behavior stays
visible in the long-help possible-values block (`- claude-code: Claude
Code statusline mode (reads context from stdin)`) and the dedicated
"Claude Code mode" section.

This also regenerates a stale mirror: `wt step eval --format`'s
`step.md` section was out of sync with the binary (expanded block vs.
terse) because #3106 added that flag with the old `SwitchFormat` while
#3108 trimmed it — their merge order left `test_docs_are_in_sync`
failing on `main`. This PR fixes that.

> _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 21:12:58 -07:00
Worktrunk Bot ea126f1466 docs(step): regen wt step eval --format mirror (#3109) 2026-06-17 11:53:33 -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
Maximilian Roos c9cfc1ef16 feat(eval): add --format=json for structured template output (#3106)
Adds `--format=json` to `wt step eval`, the structured counterpart to its `-v` view.

`wt step eval` printed only the bare rendered result to stdout; the template's name, source, and rendered result were reachable solely from the `-v` human lane on stderr, with no machine-readable form. `--format=json` emits `{name, template, result}` to stdout — the structured analog of the `-v` expansion view, and consistent with `wt hook show --format=json` and the other `wt step` JSON lanes.

Text mode is unchanged (bare result to stdout). The two lanes compose: `--format=json -v` keeps JSON on stdout and the human expansion view on stderr, so `wt step eval --format=json -v … 2>/dev/null` yields clean JSON.

Implementation mirrors the sibling `wt step` commands: a `SwitchFormat` flag under the `Automation` heading, an inline `serde_json::json!` payload, and snapshot tests covering the JSON lane and the JSON + `-v` composition.

> _This was written by Claude Code on behalf of max_
2026-06-17 10:42:24 -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 01242177d3 refactor(expand): label -v template expansion as source/result blocks (#3099)
Reworks the `-v` template-expansion view. It used to stack the template,
a lone `→`, and the result in one shared gutter; the arrow on its own
line read awkwardly and the shared gutter blurred input vs output.

Now each side is its own labeled block — an info header above a bash
gutter:

```
○ eval source
  {{ branch | hash_port }}
○ eval result
  12107
```

The two headers carry the input/output distinction, so both blocks keep
the same gutter with content at column 2 (no marker glyph, no in-gutter
indent). Multi-line templates and results follow the same shape, one
gutter line per source line.

`expand_template` emits no trailing blank. The standalone `wt step eval`
adds one after the view; in a command pipeline the executor already
separates the block from the next output (an unconditional trailing
blank there would trip the no-double-blank guard).

This view also surfaces in `wt switch -v`, hooks, aliases, and `wt -v
list`'s deprecation preview — all snapshots regenerated. Known rough
edge: `wt -v list` previews a deprecated `worktree-path` once per
worktree, and those previews now sit back-to-back without a separating
blank.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:52:59 -07:00
Worktrunk Bot 044ed66d56 docs: add "Per-worktree env vars" tips section (direnv/mise) (#3074) 2026-06-16 02:34:45 -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 a01a9020ca refactor(eval): inspect template variables via -v, not --dry-run (#3078)
`--dry-run` previews a mutation a command would perform. `wt step eval`
mutates nothing — it expands a template and prints the result — so its
`--dry-run` was a category error: it dumped the full variable context as
raw `key=value` text, and was the only `--dry-run` in the CLI not using
the gutter house style.

This moves variable discovery to the verbose lane. `wt step eval -v` now
lists the available template variables on stderr in the gutter style,
above the `{{ template }} → result` expansion view that `-v` already
rendered:

```console
$ wt step eval -v '{{ branch }}'
○ Available template variables
  branch        = feature/auth-oauth2
  worktree_path = /home/user/projects/myapp-feature-auth-oauth2
  …
○ Expanding eval
  {{ branch }}
  →
  feature/auth-oauth2
feature/auth-oauth2
```

The result still goes to stdout, so `$(wt step eval …)` is unchanged.
`eval` is experimental, so `--dry-run` is removed outright rather than
deprecated — it now errors loudly instead of breaking silently.

The convention is documented in `src/commands/CLAUDE.md`: `--dry-run`
previews a mutation, so a command that changes nothing carries none, and
inspection belongs to `-v`. The other nine `--dry-run` commands already
conform (`config alias dry-run` and `hook --dry-run` use `info_message`
+ the gutter), so eval was the only outlier.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:44:23 -07:00
Worktrunk Bot 0d9b9b8082 feat(commit-generation): deprecate commits squash template variable (#2985) 2026-06-13 14:37:15 -07:00
Maximilian Roos 11129d3811 fix(plugin): fail worktree hooks before side effects; document path args (#3060)
Hardens the plugin's worktree-lifecycle hooks against malformed
payloads, and documents two things they rely on.

**Hooks** (`plugins/worktrunk/hooks/hooks.json`): in the pipeline form,
`jq -r .name | xargs … wt switch --create {} …` runs `wt` with whatever
jq printed — so a payload missing `.name` minted a real branch named
`null`, and `set -o pipefail` could only report the failure after the
side effect (verified under `/bin/sh`). Both hooks now validate the
field in a command substitution before `wt` runs (`name=$(jq -er .name)
|| exit 1; …`), which fails with nothing created and stays
whitespace-safe via quoted variables. The `bash -c` wrapper remains —
hook commands must parse under fish/zsh/bash and fish rejects
`name=$(…)` — but `set -o pipefail` is gone: the only remaining pipe
ends in `jq -er .path`, whose exit is the pipeline's. Exercised under
`sh -c` against the shipped JSON: missing field → exit 1, no branch;
fresh create → path on stdout, exit 0; existing branch → wt's real
error, nonzero; remove by path → removed.

**Docs**: `wt remove`'s positional also accepts worktree paths
(`resolve_worktree_arg` tries branches first, then paths) and the
`WorktreeRemove` hook passes a path — the help line now reads "Branch
name or worktree path". The plugin README lists `jq` as a hook
dependency, and the skill's branch-naming step asks for names consistent
with the repo's existing worktrees.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:33:58 -07:00
Maximilian Roos fd3ae5bff4 feat(plugin): optional branch name and single-route flow for /wt-switch-create (#3058)
The `/wt-switch-create` branch argument becomes optional (a name is
picked from the task when omitted), and the skill's workflow is rebuilt
from a guard-heavy three-route flow into one route plus one error-driven
fallback.

**The new flow**: pick a branch name if none was given, create the
worktree with `wt -C <repo> switch --create <branch> --no-cd
--format=json` in Bash (rerunning without `--create` when the user named
an existing branch), re-root the session with `EnterWorktree({path})`,
and on a graceful rejection (different repo, pinned cwd, nested session)
work in the worktree via absolute paths instead.

**Why the rewrite**: the old flow's guards encoded hypotheses about
Claude Code that turned out wrong or untested. The load-bearing
discoveries, each verified by binary inspection, official docs, or live
tests and documented in the new `skills/wt-switch-create/rationale.md`:
`EnterWorktree({path})` accepts worktrunk's sibling layout on first
entry; every rejection it can raise is side-effect-free, so
try-then-fallback replaces prediction; the previous
`EnterWorktree({name})`/hook route silently auto-removes a clean
worktree at session exit, which is wrong for durable worktrunk worktrees
(path-entered worktrees persist); and the "pinned cwd" the old guards
defended against was a misdiagnosis of the harness resetting `cd` that
leaves the session's working directories.

**Hook fix**: both `WorktreeCreate` and `WorktreeRemove` pipelines are
wrapped in `bash -c 'set -o pipefail; ...'`. Without it the trailing
`jq` exits 0 on empty input, so a `wt` failure surfaced as a
"successful" hook with an empty path. The wrapper is explicit `bash`
because hooks spawn via `/bin/sh -c`, where dash rejects `set -o
pipefail` fatally. `xargs -I{}` also fixes worktree paths containing
spaces. Tested end-to-end in both directions (success exits 0 with the
path; failure exits 1 with empty stdout).

Docs, README, and plugin CLAUDE.md are aligned with the new mechanism.
The agent-isolation use of the `WorktreeCreate` hook is unchanged.

Review trail: the design survived an evidence audit (every rationale
claim independently re-verified), a code review (bare directory-named
tokens no longer parse as the repo; mid-session moves now stash
uncommitted work across), and a level check that considered and rejected
fixing this in `wt` itself (an idempotent `--create` cannot encode who
chose the branch name, which determines the correct recovery).

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:01:13 -07:00
Maximilian Roos 8f44e98636 feat(statusline): show used % instead of pace above 90% of the rate limit (#3057)
When the statusline rate-limit segment shows and the binding window is
above 90% used, it now displays the used percentage instead of the pace
ratio — `95%(8:30am–1:30pm)` rather than `1.1×(8:30am–1:30pm)`. Near the
cap, how much is left matters more than how fast it's going; the pace
form is unchanged below 90%.

The show condition is untouched (P(over) ≥ 0.5 via the existing Bayesian
gate), so this only changes which number renders once the segment is
already visible. Strictly greater-than 90: a window at exactly 90% still
shows pace. Unit test pins the switch and the boundary; the 95%- and
100%-used integration snapshots now render the percent form, and the
help text and claude-code docs describe it.

🤖 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 22:57:53 -07:00
Maximilian Roos 22768d425f feat(hooks)!: table-form pre-* hooks run concurrently — remove the serial path (#3052)
## Summary

Lands the cutover announced in v0.37.0 (#2135): a multi-entry table hook
now runs its commands concurrently for every hook type. `[pre-merge]`
with several keys behaves like `[post-start]` with several keys and like
a single `[[pre-merge]]` block — one execution semantic for `Concurrent`
steps.

The serial behavior was not implemented where one might expect. The
executor's serial branch (`ForegroundStep.concurrent` /
`SourcedStep.is_pipeline` / `CommandConfig::is_pipeline()`) was mostly
dead: the deprecation shipped with a load-time TOML migration that
rewrote multi-entry pre-* tables into serial pipeline form in memory on
every config load, so affected configs never reached the executor as
`Concurrent` steps. Removing that migration (now a `DEPRECATION_RULES`
row, after #3045) is the actual behavior change; the executor flags come
out with it.

## Why no replacement runtime warning

Affected configs have printed a warning on every `wt` invocation for
twenty minor releases (v0.37.0, April 12 → v0.57.0), with `wt config
update` offering a one-command migration that preserved serial behavior
explicitly ("migrate now to keep the current serial behavior once the
table form is repurposed"). A post-cutover warning has no coherent
shape: table form is now legitimate concurrent config, so it would nag
users who want exactly that behavior with no way to silence it, and the
`wt config update` rewrite would no longer be behavior-preserving.

## Parse normalization

A one-entry top-level table previously parsed as a `Concurrent` group of
one; with the serial branch gone it would have picked up `name │` output
prefixes. One-entry maps now parse as `Single(named)` everywhere,
matching one-entry maps inside pipeline lists (removing the documented
dict-at-top vs dict-in-list asymmetry), and a new `Serialize` arm keeps
one-step named configs round-tripping as named tables. Single-entry
table hooks render exactly as before.

## Behavior changes

1. **Multi-entry table-form pre-* hooks: serial → concurrent.** The
announced change.
2. **`wt hook <post-type>` foreground runs of multi-entry table hooks:
serial → concurrent.** Previously an undocumented inconsistency — the
same config already ran concurrently via the background path.
3. **Single-entry table aliases (`[aliases.x]` with one key):
prefixed-stderr → stdout passthrough.** Now matches the string and
`[[aliases.x]]` spellings, and makes `wt <alias> | …` work for this
spelling too.

(2) and (3) were never deprecation-warned; all three belong in the
release notes.

## Tests

- `test_pre_merge_deprecated_table_runs_serially` → rewritten as
`test_pre_merge_table_form_runs_concurrently`; the
single-`[[pre-merge]]`-block test is deleted (both forms now parse
identically, so it duplicated the rewritten test).
- Fixtures that genuinely need serial ordering (`>`/`>>` chains, the
signal-abort "second must not run" assertion) converted to pipeline
form.
- `test_user_hooks_preserve_toml_order` (#737) and
`test_post_start_named_commands` keep table form and pin ordering via
`WORKTRUNK_TEST_SERIAL_CONCURRENT=1`, so insertion order is still
asserted through the real concurrent input ordering.
- New: one-entry-table parse/round-trip unit test;
`test_alias_single_entry_table_writes_to_stdout` pinning change (3).
- Regenerated snapshots drop a stale `RUST_LOG: warn` env line (the
harness stopped setting it in #2901).

## Known follow-up

Concurrent-group announcements expose a pre-existing ANSI dim-bleed:
`format_bash_with_gutter` output ends without closing the dim attribute,
so back-to-back `◎ Running …` lines render dim (visible today on
pipeline-form hooks and `--execute` verbose). Now that this rendering is
the table-form default it's worth fixing, but the fix lives in the
gutter formatter and churns every bash-gutter snapshot — separate PR.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:31:07 -07:00
Maximilian Roos ce7c7c4861 fix(statusline): drop the word "pace" from the rate-limit segment (#3053)
The Claude Code statusline's rate-limit segment read `2.9×pace(Tue–Tue
5pm)`. The word "pace" made the longest segment longer without earning
the space, so the segment now renders as `2.9×(Tue–Tue 5pm)` and `wt
list statusline --help` explains the reading instead: 2.9× the pace that
would exactly fill the window, plus a one-line gesture at why the
segment doesn't show on every over-pace reading ("Likely" is a Bayesian
forecast; early-window bursts don't trigger it).

While there, the help page's structure had drifted: the `claude-code`
bullet under "Output formats" and the "Claude Code mode" section both
described the stdin context and the added segments. The segment list now
lives in the bullet (parallel with the `table` bullet) and the section
carries the input fields and the pace explanation. Ranges use en-dashes
(`0–100`), and the hard-wrapped paragraphs are unwrapped to match
`after_long_help` convention elsewhere in the CLI.

`docs/content/claude-code.md` and its skill mirror drop the word from
the example line and segment description. CHANGELOG.md keeps the old
spelling as a historical record.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:29:34 -07:00
Maximilian Roos c511d3fa2b feat(list): show PR/MR number in the CI column (#3041)
The CI column in `wt list --full` (and the statusline) previously showed
a single colored dot. It now shows the branch's open PR/MR reference —
`#3035` on GitHub/Gitea/Azure DevOps, `!3035` on GitLab — colored by CI
status, dimmed when stale, and hyperlinked to the PR. When no number is
available (branch workflows without a PR/MR, pre-number cache entries,
or a number wider than the allocated column), the cell shows a bare `#`
in the same colors. Fetch errors always render `⚠`, even when a number
is known — Error and Conflicts share yellow, so a yellow `#3035` would
read as a conflicted PR.

The branch merges main's review-state feature (#3044): review colors
(magenta/cyan) and draft dimming apply to the number cells exactly as
they did to the dot, and the `--help` legend shows colored `#` samples
for all seven states (the interim version had dropped the colored
samples from the legend entirely).

## The width problem

`wt list` renders skeleton-first: column widths are fixed before any CI
data arrives, and the table never resizes mid-render. The PR number's
width therefore has to be known up front. The solution is a repo-level
ratchet cache (`.git/wt/cache/pr-number/max.json`) holding the largest
PR number any fetch has seen — PR numbers are monotonic per repo, so the
value needs no invalidation. Pre-skeleton, `collect` reads that one file
and sizes the column exactly; on a cold cache the estimate is 5 chars
(`#9999`). A number that outgrows the estimate renders as the bare `#`
for that run and sizes correctly on the next run once the ratchet
records it. The ratchet is deliberately separate from the per-branch
`ci-status/` entries so the width hint isn't coupled to branch-entry
retention, and `detect` re-ratchets on cache hits too, so a deleted or
racily regressed `max.json` heals from locally cached numbers instead of
waiting out the TTL.

## Reviewer's map

- `src/commands/list/ci_status/mod.rs` — `PrRef` (number + forge sigil,
`PrRef::pr`/`PrRef::mr` constructors), `PrStatus.number` (serde-default
so pre-existing cache entries still deserialize, rendering `#` until
their 30–60s TTL expires), `format_cell` width-aware renderer with the
Error guard, ratchet in `detect` (both cache-hit and fetch paths)
- `src/commands/list/ci_status/cache.rs` — `MaxPrNumber` ratchet
(read/ratchet/clear)
- `src/commands/list/ci_status/{github,gitlab,gitea,azure}.rs` — each
fetcher populates the number (`gh --json number`, `iid`, Gitea `number`,
`pullRequestId`); GitLab's mr-view-failure path carries the
iid/URL/review state into the error status so the `⚠` stays clickable
- `src/commands/list/layout.rs`, `collect/mod.rs` — width estimate
threading
- `src/commands/list/render.rs`, `model/item.rs` — table cell and
statusline both go through `format_cell`
- `src/commands/list/json_output.rs` — `ci.number` field
- `src/commands/config/state.rs` — ratchet shown by `state get`/`cache
get` (table + JSON) and swept with the CI cache category, including the
deprecated `ci-status clear --all` path
- `src/md_help.rs`, `src/help.rs` — legend colorization rules rewritten
from `●` to `#` (terminal + website)

Most of the diff is snapshot churn from the column width and glyph
changes plus regenerated docs mirrors.

Known trade-offs: concurrent statusline ratchet writes can transiently
lose an update (monotonic, re-learns on the next render, documented at
the write site); one anomalously high PR number widens the column until
`wt config state cache clear`; an open Azure DevOps PR still shows gray
`NoCI` instead of its pipeline status — a pre-existing gap, now marked
`TODO(azure-pr-pipeline)`.

Testing: unit tests for `format_cell` (including the Error-with-number
and oversized-number link cases)/`pr_ref_width`/ratchet/width estimates;
integration coverage for all four forges with real numbers (the Gitea
mocks now exercise the number path too), review-state × number
composition, the GitLab mr-view-failure `⚠` and branch-pipeline success
paths, cache-TTL expiry → refetch, the statusline number view, and the
`wt config state` surfaces.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:36:44 -07:00
Maximilian Roos 214d82aeda Add PR/MR review state to wt list CI status (#3044)
Adds PR/MR review state to `wt list`'s CI status, merged into the
existing CI dot color.

## What

- New `ReviewState` (`approved` / `changes_requested` / `pending` /
`draft`) on `PrStatus`, absent when the forge reports no review signal —
branches with no review activity render exactly as before.
- **GitHub**: `reviewDecision,isDraft` join the existing single `gh pr
list --json` call (no extra API cost). Draft wins over the decision; an
empty `reviewDecision` (no required reviewers, no reviews) maps to
absent rather than `pending`, so solo repos don't show a perpetual
waiting state.
- **GitLab**: `draft` + `detailed_merge_status == "not_approved"` →
`draft`/`pending`; MR list data carries no approved/changes-requested
signal. Gitea/Azure: none.
- **Display**: review state merges into the CI dot in
`PrStatus::color()` — conflicts (yellow) > changes-requested
(**magenta**) > running (blue) > failed (red) > review-required
(**cyan**) > base CI color; draft dims like staleness. Cool colors mean
waiting (blue: on CI, cyan: on a human), warm mean act;
changes-requested outranks running because waiting can't clear it.
Magenta was the one color not already carrying a meaning in the list
row; cyan is reused from the working-tree symbols column, where the
differing glyph disambiguates.
- **JSON**: `ci.review_state` in `wt list --format=json`, vocabulary
matching Claude Code's statusline `pr.review_state` so the two surfaces
agree on names.

## Notes for review

- The merge lives in `PrStatus::color()`/`style()` — `CiStatus` stays
pure CI (cache values and JSON `status` strings unchanged). Both the
table and the statusline render through `format_indicator` → `style()`,
so there's a single chokepoint.
- Old CI cache files deserialize unchanged (the new field is an
`Option`).
- `docs/content/list.md` and `skills/worktrunk/reference/list.md` are
regenerated from `src/cli/mod.rs`. The HTML and terminal help colorizers
learned magenta/cyan (they previously mapped only the five existing
colors). Also fixed a pre-existing missing `repo_url` row in the
ci-object doc table.
- Testing: unit tables for the color merge and both forge mappings;
integration snapshots (mocked `gh`) for changes-requested /
review-required / approved / draft verifying the exact ANSI codes; the
JSON field name is pinned by an inline snapshot.

If this change is bad, it's most likely because the dot now encodes two
axes (CI health and review attention) in one color — a consumer who
reads green as "CI passed" still gets that, but red-vs-magenta now
distinguishes who rejected the branch, which is more vocabulary to
learn. The `--format=json` output keeps the axes separate for anyone who
wants their own logic.

Ref #2950

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:14:41 -07:00
Maximilian Roos 371d286628 refactor(hooks): always-lazy template expansion for hooks and aliases (#3042)
Hook templates expanded eagerly at prep time unless they referenced
`vars.*` (#1840); aliases were unconditionally lazy. The dual
representation (`expanded` + `lazy_template` on `PreparedCommand`) put a
mode choice in front of every consumer, and the seams produced real bugs
(#1855, #1910). The background runner also re-rendered eagerly-expanded
strings: a latent double-expansion whenever expansion output contained
`{{`.

This PR unifies on one rule: syntax is validated at preparation (before
step 1), templates render when each step runs, and `vars.*` always reads
fresh from git config. This is the direction #2128 recorded ("treat
hooks as aliases bound to triggers").

**Navigating the diff:**

- `src/commands/command_executor.rs`: `PreparedCommand` now carries the
raw template plus the frozen context map; JSON is produced only at the
process boundary. A `template_name` field keeps the moved errors' text
identical to the old prep-time messages. `prepare_steps` validates
syntax and freezes context; `resolve_command_str` renders at execution;
`map_config_steps` is the structural walker shared with aliases.
- `src/commands/alias.rs`: alias prep shares the walker. The residual
fork is genuinely alias-specific: context construction (filtered to
referenced vars, `{{ args }}` injection) and labels. A prep-time syntax
check turned out to be dead code (the arg-routing parse already aborts
on syntax errors), so `referenced_vars_for_config` now produces the rich
`TemplateExpandError` instead.
- `src/commands/hooks.rs`: the background spec always ships raw
templates; the runner renders each step (it already did for vars steps).
- Dry-run paths (`wt hook --dry-run`, `wt config alias dry-run`) share
`render_template_preview`: vars-referencing templates shown raw after a
syntax check, everything else rendered at display time.

**Behavior changes** (each pinned by a test or snapshot):

1. A foreground pipeline whose step N has a semantic template error
(undefined variable) runs steps 1..N-1 before failing, matching what
vars-referencing steps already did. New test:
`test_foreground_pipeline_undefined_var_runs_earlier_steps`.
2. A semantic error in a background hook template no longer fails the
foreground command; it surfaces in the runner log. Syntax errors still
abort at prep. New test:
`test_background_hook_undefined_var_fails_in_runner`;
`test_merge_drops_pending_hooks_when_post_merge_fails` switched to a
syntax-error fixture since that is what now reaches its subject (the
announcer Drop-flush).
3. `-v` no longer prints the parent-side rendered command for background
hooks (rendering happens in the runner). The variables table remains and
`wt hook <type> --dry-run` previews commands; help text updated. Three
snapshots changed by exactly this.
4. Announce gutters for vars-referencing foreground steps show the
rendered command instead of the raw template.
5. Syntax errors caught by the arg-routing parse (aliases, `wt hook`
`--KEY=VALUE` routing) render as the rich template error with the
source-line gutter, naming the alias or hook.

`ApprovedHookPlan` freezing, approval semantics (keyed and displayed by
template), config file format, and CLI flags are unchanged.

**Testing:** full local gate green (3917 tests + lints). The deferral
semantics in changes 1 and 2 previously had no coverage; both are now
pinned.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:50:28 -07:00
Maximilian Roos 19aa6f0a28 feat(config-state): add cache subcommand, gate clear behind confirmation (#3027)
`wt config state` had a sprawl of per-category subcommands, several of
which only really existed to be cleared. This consolidates the
regenerable caches under one `cache` subcommand, makes the destructive
`clear` confirm first, and deprecates the now-redundant subcommands
without breaking them.

## What changed

**New `cache` subcommand** — the single home for everything regenerable:
CI status, summaries, git-commands (the SHA-keyed disk caches), hints,
and the `wt switch -` target. `wt config state cache` shows them
(`--format=json` too); `cache clear` drops them all with **no prompt**,
since clearing only forces recomputation. It re-shows one-time hints and
forgets the switch-back target — both repopulate on their own.

**`clear` now confirms** — `wt config state clear` still removes
*everything*, but it also wipes hand-authored markers and vars, so it
now prompts before doing so. `--yes` skips the prompt. Non-interactive
without `--yes` declines safely (no data loss) — the conservative
direction. This is the one behavior change to watch: a script doing `wt
config state clear` without `-y` now cancels instead of wiping.

**`ci-status` / `hints` / `previous-branch` deprecated** — hidden from
`--help` and emitting a one-line stderr deprecation notice, but still
fully functional. Existing scripts keep working (the notice is
stderr-only, so stdout parsing is unaffected); new users only discover
`cache`.

## Navigating the diff

- `src/commands/config/state.rs` — the bulk. `handle_state_clear_all` is
refactored to compose per-category `clear_*_reported` helpers + the
confirmation gate; `handle_cache_clear` reuses the regenerable subset.
`cache get` and the aggregate `state get` share extracted
`render_*_section` / `*_json` helpers, so the two views render each
category identically. The module docstring documents the
regenerable-vs-authoritative split and the preserved `get`↔`clear`
parity invariant.
- `src/cli/config.rs` — the `Cache` variant + `CacheAction`, `hide =
true` on the three deprecated subcommands, and the doc/help text.
- `src/main.rs` — dispatch: the `cache` arm, the deprecation warnings
(now uniformly bolding the replacement command across all four `wt …`
deprecation messages), and threading `--yes` into `clear`.

## Testing

Well covered: new integration tests for `cache get`/`cache clear` (empty
and populated), the `clear` prompt (accept and decline paths),
authoritative-state preservation under `cache clear`, and the
deprecation warnings — plus the existing aggregate `state get`/`clear`
suite, which exercises the shared helpers. Docs regenerated and
`test_docs_are_in_sync` passes. Full pre-merge gate (tests + clippy +
fmt) green.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:40:08 -07:00
Maximilian Roos 6df9623e23 docs(state): distinguish reading the cache from resolving it (#3028)
Documentation follow-up to #3024 (which made `wt config state get` read
the default-branch cache read-only).

After that change the dump shows `DEFAULT BRANCH (none)` right after a
`clear`, while `wt config state default-branch get` still returns the
resolved branch. That contrast can read as inconsistent. Two small
additions make the model explicit:

- The `default-branch` `--help` gains one line: `default-branch get`
resolves and caches; the aggregate `wt config state get` only reports
the cache, so it can show `(none)` until something populates it.
- The `state` module spec gains a "Reading vs resolving" section
recording the invariant for future developers: the aggregate `get`
inspects read-only (`cached_default_branch()`,
`CachedCiStatus::list_all`), while a per-key `get` for a derived value
resolves it (`default_branch()`, `PrStatus::detect`).

No behavior change. The `docs/` and `skills/` mirrors and the `--help`
snapshot are regenerated from the source.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:08:10 -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 77f2f5a405 fix(help): render inline code in --help section headings (#3003)
Terminal `--help` applied heading styling to the raw header text without
stripping inline-code markers, so a heading authored as `` ### `--stage`
`` rendered with literal backticks. Header rendering now reduces inline
`code` and links to plain text, letting the heading's single uniform
style (color/weight) cover the whole line. A dim ANSI reset inside a
colored heading would otherwise terminate the heading color partway
through — visible on the `wt config state logs` heading `Command log
(`commands.jsonl`)`, where the trailing `)` follows the code span. That
heading is now fixed too.

Separately, renamed the `--stage` / `--dry-run` subsection headings in
`wt step commit` and `wt step squash` to sentence case ("Staging", "Dry
run"). They were the only flag-literal, inline-code headings in the
entire help system; every other heading uses sentence-case topic
wording, which the `writing-user-outputs` skill also prescribes. The
flag name is still surfaced by the clap `Options:` block and each
section's usage example.

The markdown source feeds three render contexts (terminal `--help`, web
docs, skill mirrors); only the terminal path was wrong. Doc mirrors
regenerated via `test_docs_are_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-06 20:17:01 -07:00
Maximilian Roos 293b44245c docs(faq): add entry on moving uncommitted changes to a new worktree (#3002) 2026-06-06 20:04:52 -07:00
Worktrunk Bot b41e210f82 feat(step): add --branch arg to wt step diff (#2995)
## Problem

`wt step diff` could only diff the current worktree. #2994 (part of the
effort to extend `--branch` to more commands) asks for a `--branch` flag
so the diff can target another worktree's branch without leaving the
current one.

## Solution

Add `-b/--branch` to `wt step diff`, mirroring the existing flag on `wt
step commit`. When provided, the repo is rooted at that branch's
worktree (via `worktree_for_branch` + `Repository::at`) so both the diff
and its target/merge-base resolution operate there. The branch must have
a checked-out worktree; otherwise it errors with `no worktree for branch
'<b>'`.

## Testing

Two integration tests in `tests/integration_tests/step_diff.rs`:

- `test_step_diff_branch_arg` — runs `wt step diff --branch feature`
from the main worktree and asserts the output matches
`test_step_diff_committed_changes` (the same diff run from inside the
feature worktree).
- `test_step_diff_branch_no_worktree` — asserts the error path for a
branch without a worktree.

Docs regenerated via `test_docs_are_in_sync`; `cargo clippy
--all-targets` and `cargo fmt --check` are clean. The only failing tests
locally are the `case_4` nushell shell-wrapper tests, which fail because
`nu` isn't installed in this sandbox — unrelated to this change.

---
Closes #2994 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-06 18:50:21 -07:00
Worktrunk Bot b75b3cf1a7 fix(shell): install nushell wrapper to vendor-autoload dir (#2878) (#2992)
Fixes #2878.

Implements the approach agreed in the issue: query
`$nu.vendor-autoload-dirs` directly and install to its last
(user-writable) entry, with cross-platform tests and stranded-file
cleanup on install/uninstall.

## The bug

The nushell integration wrote `wt.nu` to
`$nu.default-config-dir/vendor/autoload`, which Nushell **never
autoloads**. It happened to work on macOS/Windows because there
`$nu.default-config-dir == $nu.data-dir`, so the wrong subpath resolved
to the real vendor dir; on Linux the wrapper was written but silently
never loaded — so `wt` was never wrapped.

Verified against `nu` 0.113.1: `$nu.vendor-autoload-dirs | last` is
`<data-dir>/vendor/autoload`, and
`$nu.default-config-dir/vendor/autoload` is in neither
`$nu.vendor-autoload-dirs` nor `$nu.user-autoload-dirs`.

## The fix

- **Resolve the install target from `$nu.vendor-autoload-dirs | last`**
(one memoised `nu` spawn that also reads `$nu.default-config-dir` for
legacy cleanup).
- **Fallback when `nu` isn't on PATH** mirrors nushell's own
`nu_path::data_dir`: `XDG_DATA_HOME` (when absolute) wins on every
platform, otherwise `dirs::data_dir()` — matching nushell on
Linux/macOS/Windows.
- **Wrapper-based shells now always write to the canonical location**
(`paths.first()`), so a wrapper stranded at a legacy path can't become
the write target. (No-op for fish, which has a single canonical path.)
- **Stranded-file cleanup**: install writes the correct path and removes
any worktrunk-managed wrapper left under the legacy
`<config-dir>/vendor/autoload` locations; uninstall already iterates
every candidate, which now includes those legacy paths. Legacy files are
only removed when they carry the `# worktrunk shell integration for
nushell` header, so a user's own `wt.nu` is never touched.
- `config_line`, the `wt config shell init` manual-setup help, and the
FAQ/shell-integration docs now use `$nu.vendor-autoload-dirs | last |
path join wt.nu`.

## Tests

- New `WORKTRUNK_TEST_NU_VENDOR_AUTOLOAD_DIR` override pins the install
dir so the install/uninstall/cleanup tests are **deterministic on Linux,
macOS, and Windows** and don't depend on `nu` being installed.
- New `test_nushell_install_target_is_a_vendor_autoload_dir` (gated on
`shell-integration-tests`, where CI has `nu`) runs the **real `nu`
flow** and asserts the wrapper worktrunk wrote is a member of
`$nu.vendor-autoload-dirs`. This fails against `main` (the old
config-dir path is in neither autoload list) and passes with the fix.
- New `test_nushell_install_cleans_stranded_legacy` and a reworked
`test_uninstall_nushell_cleans_all_candidate_locations` cover the
migration cleanup.
- Existing nushell tests updated to the data-dir path; full suite green
(`cargo test --lib` + `cargo test --test integration`, and the nushell
subset with real `nu` under `--features shell-integration-tests`).

## Not in scope

"Not best practice" cleanup flagged in the issue (the overlap between
the candidate-building helpers, and the dubious Windows branch) — the
helpers here are restructured around the autoload model, but a broader
tidy can be a follow-up.

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

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:43:38 -07:00
Florian Ilch 3a4ae84715 Expose commit bodies to squash templates (#2983) 2026-06-05 18:40:09 -07:00
Maximilian Roos 3287052b5b chore(demos): drop the obsolete CLAUDECODE= env from the shared tape (#2980)
Follow-up to #2979. The shared demo tape set `Env CLAUDECODE ""` so that
a `claude` invoked while recording would not be rejected as a nested
session. Claude Code no longer blocks nested `claude -p`, and #2979
dropped the matching workaround in commit generation, so the override is
unneeded. It is not part of the recorded output, so the GIFs are
unchanged; no re-record.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:38:37 -07:00
Maximilian Roos 7d16e63fad refactor(commit-generation): drop the CLAUDECODE= nesting workaround (#2979)
worktrunk stripped `CLAUDECODE` before spawning the
`[commit.generation]` LLM command, and the recommended Claude Code
command carried a leading `CLAUDECODE=`, both to get past Claude Code's
nested-session check that rejected `claude -p` launched from inside
another Claude Code session.

That check is gone. It's absent from the `claude` binary across every
current build (2.1.157 through 2.1.162), and `CLAUDECODE=1 claude -p …`
runs cleanly (exit 0, empty stderr). So the workaround is no longer
needed.

This drops it: `src/llm.rs` no longer calls `.env_remove("CLAUDECODE")`,
and the recommended command loses the `CLAUDECODE=` prefix in the source
of truth (`dev/config.example.toml`), the `wt config --help` text, and
the Taskfile bench command. Docs, the skill reference, and the config
help snapshots are regenerated to match.

Caveat: there's no published guarantee the nested-session check stays
gone, and a Claude Code old enough to still have it (older than roughly
late May 2026) would again block nested commit generation. Since the
check is absent from the implementation across all current builds, that
risk is low.

Tests: 1236 lib + 693 integration pass; fmt and clippy clean.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:26:32 -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 d65d5028fa docs(commit-generation): bump Codex model to gpt-5.4-mini (#2949) 2026-06-02 00:42:18 -07:00
Adriano Machado ca91420e1e fix(bare-repo): UX fixes for worktree-path prompt and wt config create --project in bare repos (#2951) 2026-06-02 00:19:15 -07:00
Maximilian Roos 51e32f4425 fix(statusline): space the context gauge emoji from its percent (#2944)
The context gauge in the Claude Code statusline rendered the moon-phase
emoji flush against the percent (`🌕42%`). Most terminals draw emoji as
double-width and bleed the glyph into the cell to its right, so the moon
visually collided with the digits. This adds a trailing space — `🌕 42%`.

The flush-packing convention still holds for ASCII and narrow markers
(`@+1`, `↓1`, `●`); emoji are the exception. That distinction is now
documented in the `writing-user-outputs` skill so future output keeps it
consistent.

Tests, the three inline statusline snapshots, nine rate-limit `.snap`
files, and the hand-authored docs example (auto-synced to the skill
reference) are all updated.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:40:18 -07:00
Maximilian Roos 2617348b22 docs: writing-prose cleanup (faq, config, list, remove) (#2925)
Continues the writing-prose pass on the remaining doc-site pages — the
items deferred at the end of #2922.

**`faq.md`** — dropped "Worktrunk creates files in four categories."
scaffolding; the four H3s below (Worktree directories, Config files,
Shell integration, Metadata in .git) make the count self-evident.

**`config.md`** (edits in the `Config` command's `after_long_help` in
`src/cli/mod.rs`) — four small fixes:
- Replaced the "For context:" three-bullet preamble in *User
project-specific settings* with a direct lead sentence. Side benefit:
fixes the "User configs _also_ has" grammar bug. The new sentence avoids
second-person ("for you") and third-person addressing ("for the user")
per the writing-prose indicative-mood rule.
- Dropped "also" from the system-config sentence — it was the only
signal the sentence was an orphan footnote relative to the table above.
- Dropped "Similarly," before the first-commit-prompt sentence; the
parallel "On first run … On first commit …" structure carries the
relation.
- Dropped "Note the single underscore after `WORKTRUNK` and double
underscores between nested keys." that restated what the env-var table
already showed.

**`list.md`** (edits in the `List` command's `after_long_help`) — folded
the three-dot diff detail into the `main…±` column description;
tightened the remaining footnote to just the label-stays-main point.

**`remove.md`** (edits in the `Remove` command's `after_long_help`) —
extracted the cap-detail appendix from the "Patch-id match" bullet,
which had four sentences while the surrounding five bullets averaged one
or two; it now sits as its own paragraph. Also dropped the two sentences
in *Force flags* that inverted the force-flags table just above; only
the new `--no-delete-branch` note remains.

**Skipped** (re-reviewed and judged not worth changing):
- `switch.md` fork material — mechanism + naming-rule, not duplication.
- `step.md` mixed-shape operations list — the asymmetry signals which
subcommands have subdoc sections.
- `step.md` "How it works" subsections — useful reference content, not
internal commentary.
- `step.md` `wt step promote` opener — opinionated voice framing.
- `step.md` `wt step tether` "Why" — now the canonical place for the
leakage rationale (the tips-patterns dup was already removed in #2922).

Auto-synced skill mirrors (`skills/worktrunk/reference/`) and
regenerated help snapshots carry the same edits.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:18:37 -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 3be40ad36f docs: writing-prose cleanup (claude-code, hook, llm-commits, tips-patterns) (#2922)
Targeted writing-prose cleanup across four docs pages. Per-page summary:

**`claude-code.md`** — dropped the duplicate skill definition (the lead
paragraph already covered it) and rewrote the `## Configuration skill`
opener as "With the `/worktrunk` skill, the agent can help with:" so the
section stands without depending on its heading.

**`hook.md`** (edits in the `Hook` command's `after_long_help` in
`src/cli/mod.rs`) — four small fixes:
- Dropped "As usual, post-* hooks run in the background" (already
established earlier on the page).
- Split the perspective+cwd run-on paragraph; the three `cwd ≠
worktree_path` cases (`pre-switch`, `post-remove`, `post-merge` with
removal) are now a bulleted list.
- Folded the semicolon-spliced conditional-variables enumeration into
the existing template-variables table descriptions (added "switch/create
only" to `base`, "when target has a worktree" to `target_worktree_path`,
hook-type qualifiers to `pr_number`/`pr_url`). The follow-on guidance
about undefined variables and conditionals/defaults stays.
- Trimmed the redundant `sanitize` sentence from the filters paragraph
(the table above already says it).

The two hook-types tables (event×pre/post matrix + per-hook purpose) are
intentionally kept — they're complementary, not duplicate.

**`llm-commits.md`** — dropped the "How it works" stub that mostly
restated the lead, and the "There are sensible defaults, but templates
are fully customizable" hedge.

**`tips-patterns.md`** — three trims:
- The `wt step tether` recipe's middle "This matters because…" sentence
(covered by tether's own docs).
- The ports-deterministic line tightened to lean on the concrete example
rather than restate the abstract claim.
- "in real-time" filler dropped from the Monitor hook logs section.

Auto-synced skill mirrors (`skills/worktrunk/reference/*.md`) carry the
same edits.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:49:54 -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 1efbcdd718 docs(extending): writing-prose cleanup (#2912)
Two-pass writing-prose cleanup of `docs/content/extending.md`.

**First pass** (em-dashes, mostly mechanical): replaced 16 em-dashes
with colons, parens, or sentence breaks; flattened a few nested asides;
reworked the interface-differences table cells that relied on inner
em-dashes.

**Second pass** (concision and consistency):
- Made the three intro paragraphs parallel — each says what the thing is
and how it's defined. Hooks and aliases both end with "Defined in TOML";
custom subcommands describe the `wt-foo`-on-`PATH` mechanism. The
previous text said TOML for hooks, omitted it for aliases, and called
out "No configuration needed" for subcommands.
- Dropped the `[[block]]` pipeline syntax mention from the
shared-features paragraph. It's a sub-feature, not orientation-level
content, and the reader hasn't seen a hook yet. Block pipelines are
still covered in the Multi-step pipelines section where they're actually
relevant.
- Trimmed the Hooks section opener so it doesn't restate the intro's
"Hooks are shell commands…".
- Cut "TOML forms, template variables and filters" from the `wt hook`
link blurb (covered elsewhere on this page and on the linked page).
- Cut the four `same / same / same / same` repetitions from the
Templates list.
- Dropped the duplicate resolution-order rule and the template-variables
sentence from the Custom subcommands section (both already in the
comparison table).
- Collapsed the Reference intro paragraph to "Aside from the differences
below, hooks and aliases behave the same."

The skill mirror (`skills/worktrunk/reference/extending.md`) regenerates
from this via the existing sync test.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:08:21 -07:00
Maximilian Roos a2e95dbf6b refactor(prune): stream removals, skip unapproved hooks instead of prompting (#2908)
## Summary

Three changes to \`wt step prune\`, all pointing the same way — let the
scan run end-to-end and react to each result as it arrives.

- **Removability uses the same gate as \`wt remove\`.** Dirty / locked /
primary worktrees flunk \`prepare_worktree_removal\` and drop out
silently, before the age check. A young + dirty + integrated worktree
(HEAD == default branch, working tree modified) no longer surfaces as
\`Skipped (younger than 1d)\` — the dirty tree would have blocked
removal anyway. The \`(younger than X)\` message now fires only when the
worktree would actually have been pruned.
- **Scanning streams instead of batching.** One parallel pass per check
item bundles integration + removability + age in a single rayon worker.
Results flow through a channel; the main thread calls \`try_remove\`
immediately for positives instead of waiting for the full scan to
finish. Removals overlap with continued scanning (brief check-lock pause
per removal for the Windows \`.git/config\` race).
- **Hook approval moves out of the way.** Prune never prompts inline —
that would deadlock against the streaming structure. With \`--yes\`,
every project command is auto-approved. Without \`--yes\`,
already-approved commands run; a candidate whose hooks include any
unapproved project command is SKIPPED with \`(approval required)\` plus
a hint pointing at \`wt config approvals add\` (or \`wt -C <wt>
remove\`). Unapproved hooks never run silently.

New \`HookPlan::unapproved_project_commands\` exposes the templates that
gate the approval decision.

## Test plan

- [x] \`cargo run -- hook pre-merge --yes\` (3867 tests, lints, all
green)
- [x] \`test_prune_pre_remove_needs_approval\` updated to assert
skip-with-hint (not abort)
- [x] \`test_prune_unmerged_pre_remove_is_not_approved\` still passes —
unmerged worktrees are silently filtered before the approval check, no
spurious prompts
- [x] \`test_prune_non_current_removal_does_not_approve_post_switch\`
still passes — only Current candidates check post-switch
- [x] \`test_prune_runs_pre_remove_hook\` (\`--yes\`) still passes —
auto-approval path runs hooks in full
- [x] Removed \`test_approval_prompt_prune_decline\` (PTY-based,
asserted the old prompt-and-decline path that no longer exists)

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:09:07 -07:00
Maximilian Roos e22ed71282 feat(statusline): add Claude Code rate-limit pace segment (#2899)
## Motivation

Claude Code exposes
`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}` on its
statusLine JSON. Without UI for it, users only learn they're over-pacing
once Claude throttles them mid-task. We want a single-glance warning
that surfaces *before* throttling, and only when continuing at the
current pace would actually blow the cap — not whenever raw `u/t > 1`,
which fires constantly on early-window bursts.

## Change

A new yellow segment `1.3×pace(10am–3pm)` appears in the
`--format=claude-code` statusline when either rate-limit window is
meaningfully likely to be hit before reset. For the weekly window it
formats as `1.9×pace(Mon–Mon 3pm)`. Only the worse-projected of the two
windows is shown; the segment is hidden entirely when both are safe.

The trigger is a Bayesian forecast on `P(final ≥ 100%)`: cumulative
consumption modelled as Brownian-with-drift, prior on the long-run rate
pulled toward "most windows finish under" (`m₀ = 0.8`), posterior
predictive Gaussian. Segment appears only when `P > 50%`. Early-window
noise (e.g., 5% used at 3% elapsed → naive `u/t = 1.67` but P(over) =
42%) stays correctly hidden.

The *displayed* number is the naive `u/t` ratio (one decimal), not the
Bayesian posterior — the user sees an honest measurement of "what you've
actually done over the elapsed window," while the smoothing stays
internal where it decides visibility. Window bounds in parentheses
locate which 5h or weekly window the pace was measured over.

Adjacent delimiter cleanup in the same file, so the new segment fits the
existing line:

- Drop the vestigial `| ` prefix on the model segment — the two-space
inter-segment separator already does the job
- Drop the space inside `🌕 42%` → `🌕42%`, matching the other prefix-char
segments (`@+1`, `↓1`, `?^|`)
- En-dash inside the parenthetical (`10am–3pm`, not `10am-3pm`) —
typographically correct for a range, visually distinct from the ASCII
`-3` used for line deletions

## Tests

Eight file snapshot tests cover hidden/well-paced, 5h binding, the `:mm`
time branch, 5h at-limit edge, 7d binding (day+time form),
both-windows-pick-worse, dirty-worktree combined state, and narrow-width
segment drop. Time and timezone are pinned via the existing
`WORKTRUNK_TEST_EPOCH` env hook plus a per-subprocess `TZ=UTC`.

23 new unit tests: erf reference-value accuracy, standard-normal CDF,
`p_over` boundaries + monotonicity + six Python-prototype calibration
points, `format_clock`, `format_window_bounds` for both window kinds,
`select_binding_window` selection logic across five scenarios,
`format_rate_limit_segment` shape, and `parse_rate_limits` JSON parsing
across four cases.
2026-05-25 18:22:04 -07:00
Maximilian Roos a50785f70a refactor(log): route -vv log pipeline to trace.log instead of stderr (#2892)
## Motivation

`wt … -vv` produced ~15K lines of stderr per invocation — unreadable in
scrollback, forced redirect to a file, at which point the parallel
`trace.log` mirror introduced by #2201 was redundant noise. The earlier
split (#2201) only file-routed the *uncapped raw subprocess bodies*
(`SUBPROCESS_FULL_TARGET`); everything else — `$ cmd` headers,
`[wt-trace]` spans, bounded subprocess previews — stayed on stderr.

## Change

At `-vv`, the `log::*` pipeline routes to `.git/wt/logs/trace.log`
instead of stderr. A one-line startup pointer on stderr tells the user
where it went:

```
○ Tracing to ~/.../trace.log (raw subprocess output @ ~/.../output.log)
```

User-facing `eprintln!` output (status messages, template expansions,
hints) is **unaffected** — it stays on stderr at every verbosity level.
This change governs only the `log::*` macro pipeline.

`-v` is unchanged (Info on stderr, no file). `RUST_LOG=debug` without
`-vv` is unchanged (Debug on stderr, no file — the documented fallback
from #2201).

Result on `wt list -vv`: stderr 15K → 3 lines. `trace.log` gets the full
~1K-line debug trace as before.

## Implementation notes for review

- `src/log_files.rs::route()` is now the only place that picks the sink.
Non-`FULL` targets go to `File(&TRACE)` when `TRACE.is_active()`, else
`Stderr`. Format closure in `src/main.rs` simplified to a `match route`
— no more "mirror to TRACE then write to stderr" branch.
- `Repository::current()` is primed *before* `env_logger.init()` so the
rev-parse fired by `log_files::init` is a memory-cache hit. Records
emitted during priming go to a not-yet-installed logger and are dropped
— robust against future `Repository::current` emissions. A `Ctrl-C`
during the ~5ms priming window can leak one `$ git rev-parse` line;
documented inline, cheaper than violating "all commands through `Cmd`".
- `announce_trace_destination()` handles the rare split-init case where
`output.log` open fails (path-type mismatch, fs quota) but `trace.log`
succeeds — partial pointer + "output.log unavailable" hint. New
regression test `test_vv_pointer_handles_split_init` reproduces the
failure with a pre-existing directory at the `output.log` path.
- `SUBPROCESS_TERMINAL_TARGET` → `SUBPROCESS_BOUNDED_TARGET`. The
"terminal-safe" framing no longer fits now that the bounded preview
lives in `trace.log` rather than stderr.
- `diagnostic.md` doc cutover: three surfaces (`src/diagnostic.rs`,
`src/cli/config.rs`, `docs/content/faq.md`) still claimed "written when
warnings occur" — the code has no warning gate and writes on every
`-vv`. Docs cut over to match reality.

## Deferred follow-ups

Surfaced during review but out of scope here:

- **Unify `trace.log` + `output.log`** — the bounded preview at `-vv`
duplicates a subset of `output.log`'s uncapped bytes. The dual-file
design was explicitly approved for this PR; a follow-up could collapse
them with `diagnostic.md` doing the bounding at extraction time.
- **`RUST_LOG` precedence at `-v`** — pre-existing: `-v 0` honors
`RUST_LOG`, `-v` and `-vv` hardcode the level. Needs a policy decision
(always-honor / always-ignore / merge) more than a cleanup.
- **`tracing` crate migration** — deserves its own dedicated PR with
`[wt-trace]` migration as the headline.

## Tests

3822 tests pass (+1 regression). Notable changes:

- `test_vv_bounded_on_stderr_full_in_output_log` →
`test_vv_log_pipeline_silent_on_stderr` — inverted assertions (marker
must NOT appear on stderr; must appear in `trace.log`). Added a
stderr-pointer presence check.
- `test_vv_pointer_handles_split_init` — new; reproduces the split-init
asymmetry by creating a directory at `output.log`'s path, asserts the
partial pointer fires and `trace.log` still works.

> _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-24 23:26:01 -07:00
Maximilian Roos 818aef5c5c feat(prune): bump default --min-age from 1h to 1d (#2886)
## Summary

- Raise the default `--min-age` guard in `wt step prune` from `1h` to
`1d`.
- A worktree just created from the default branch looks "merged" because
its branch still points at the same commit; a one-day floor keeps an
unattended prune from sweeping it up before its owner has a chance to
start work.
- Explicit `--min-age=0s` or any other value is unchanged.

## Test plan

- [x] `cargo run -- hook pre-merge --yes` (3821 tests + lints green)
- [x] `cargo run -- step prune --help` shows `[default: 1d]`
- [x] Docs synced: `docs/content/step.md`,
`skills/worktrunk/reference/step.md`
2026-05-23 12:45:10 -07:00
Maximilian Roos 1716ecab37 fix(hooks): resolve all hook config from the invoking worktree (#2873)
Worktrunk resolved each hook's `.config/wt.toml` from a different
worktree depending on the hook — `post-merge` from the merge target,
`post-switch` from the destination, `pre-remove`/`post-remove` from each
removed worktree, `wt step prune` from each prunable worktree, and `wt
switch --create` from the base ref's *committed* config via `git show`.
That last one is the bug behind #2856 and #2818: an uncommitted or
branch-local `.config/wt.toml` silently failed to fire creation hooks,
and `wt config show` (which reads the working tree) disagreed with what
actually ran.

This replaces all of it with one rule: **every hook resolves its
commands from the `.config/wt.toml` of the worktree `wt` ran in** — the
invoking worktree, read from its working tree, the same file `wt config
show` displays.

## Behavior changes

- `wt switch --create` / `pr:` / `mr:` creation hooks read the invoking
worktree's config, so an uncommitted `.config/wt.toml` fires them; the
base ref's or PR's committed config is no longer consulted.
- `post-merge` runs the feature worktree's config, not the merge
target's.
- `post-switch` into an existing worktree uses the source, not the
destination.
- `wt remove <other-branch>` and `wt step prune` use the invoking
worktree's config, not each removed worktree's.

In the common case — a committed, repo-wide `.config/wt.toml` — these
are identical; they diverge only when a branch carries its own
working-tree edits.

## For reviewers

The module docstring in `src/commands/hooks.rs` is the spec — its
per-hook config-source table collapsed to one rule. The change is
concentrated in five approval gates that now call
`repo.load_project_config()` once instead of
`Repository::at(<other-worktree>)`: `merge::approve_merge_plan`,
`main.rs`'s `approve_remove`, `step::prune::approve_prune_hooks`,
`picker::approved_removal_plan`, and `worktree::switch`. The
`switch_hook_project_config` helper and the `base_ref_for_create` /
`project_config_at_ref` `git show` machinery are deleted. The *anchor* —
the worktree a hook runs in, the executor's plan-lookup key — is
unchanged; only the config *source* unifies. The frozen
`ApprovedHookPlan` still closes the approval-boundary TOCTOU.

## Testing

Hook config-resolution tests across `switch`, `merge`, `remove`, and
`step_prune` were rewritten to assert the new rule, each also checking
that the non-invoking worktree's config is ignored.
`test_post_merge_hook_from_rebased_in_config_does_not_run` is the TOCTOU
regression: a `post-merge` that enters the invoking worktree's config
only via the rebase, after the gate froze the plan, must not run.

Ref #2856, #2818.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:56:14 -07:00
Maximilian Roos 9e8c028476 fix(relocate): atomic no-overwrite rename for --clobber backup (#2865)
`wt switch --clobber` and `wt step relocate --clobber` each had their
own logic for backing up a path blocking the target, and the two had
diverged. relocate used an `exists()` check followed by
`std::fs::rename` — a time-of-check/time-of-use race, since
`std::fs::rename` silently replaces an existing file or empty directory
on Unix and could destroy a just-created backup. switch (#2849) instead
used `renamore::rename_exclusive`, an atomic no-overwrite rename, and
counted up through suffixes (`…-2`, `…-3`, …) on a name collision rather
than failing.

This PR replaces both with one shared helper in `commands::backup`.
`back_up_clobbered_path_now` computes the timestamped name and delegates
to the suffix-iterating `back_up_clobbered_path`, which moves the
blocker with `renamore::rename_exclusive` — atomic and no-overwrite, so
an existing backup is never clobbered; a name collision just lands on
the next free `-N` name. relocate gains the suffix fallback (previously
it failed closed on `AlreadyExists`), the timestamp computation is no
longer duplicated across the two call sites, and
`renamore::rename_exclusive` now has a single caller.

`renamore` is already a direct dependency and keeps the platform FFI and
its `unsafe` inside that crate, so worktrunk stays `unsafe_code =
"forbid"`.

relocate's backup name changes from `.bak-<timestamp>` to the
extension-aware `.bak.<timestamp>` form used by switch (e.g.
`repo.feature` → `repo.feature.bak.<timestamp>`). The
`test_relocate_clobber_error_backup_exists` regression test asserted the
old fail-closed behavior and is rewritten as
`test_relocate_clobber_falls_back_when_backup_taken`, mirroring the
switch test. CLI help and the generated doc mirrors are updated for the
new naming and fallback.

The shared helper carries the suffix-iteration, collision, and
missing-source unit tests (moved from `resolve.rs`); the relocate
`--clobber` path keeps its integration coverage.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:43:12 -07:00
Maximilian Roos 61f4efc1e4 refactor(cli): consolidate --no-verify deprecation into one path (#2868)
The deprecated `--no-verify` flag was declared as a clap arg five times
— across `SwitchArgs`, `RemoveArgs`, `CommitArgs`, `SquashArgs`, and
`MergeArgs` — under two field names, resolved through two helpers, with
the deprecation-warning string hand-duplicated in two places.

This collapses the four bool-resolving commands (`switch`, `remove`,
`step commit`, `step squash`) onto a single flattened `HookFlags` args
struct with one `resolve()` method, and routes every `--no-verify`
deprecation warning — merge's included — through one emitter, so the
warning text exists in exactly one place.

`wt merge` keeps its own flags: its hooks flag is genuinely tri-state
(`Option<bool>`, so config `[merge] verify` still applies) with a
positive `--verify` override, which `HookFlags`'s
`SetFalse`/default-true shape cannot express. It shares only the warning
emitter — forcing it into the struct would need a special case.

Behavior is unchanged: `--no-verify` still works as a deprecated alias
and emits the same warning under the same conditions. One visible
`--help` change, a direct consequence of the shared struct: `--no-hooks`
for `step commit`/`step squash` moves under the `Automation` heading,
matching where `switch`/`remove`/`merge` already place it. Doc mirrors
regenerated.

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

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:41:20 -07:00
Maximilian Roos 89ec4f70fa docs(remove): state that --force discards all uncommitted changes (#2869)
The FAQ and the `wt remove --force` help text said `--force` overrides
the untracked-files check "for build artifacts". `--force` actually
removes a dirty worktree including staged and modified *tracked* files,
not just untracked ones (`test_remove_force_with_modified_files`,
`test_remove_force_with_staged_files`). On a destructive command, that
wording can lead users to consent to more data loss than they expected.

Updates the `--force` arg help, the "Force flags" table and example in
`src/cli/mod.rs`, and the FAQ to say `--force` discards staged,
modified, and untracked files; the `help_remove_long` snapshot and the
`remove.md` / `faq.md` doc and skill mirrors are regenerated to match.

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

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:40:41 -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
Worktrunk Bot af8de75341 docs: re-add cmux recipe with verified config from @endigma (#2836) 2026-05-20 07:53:17 -07:00