## Summary
Non-flag tokens after an alias name are forwarded to the template as `{{
args }}` — a space-joined, shell-escaped sequence. `wt s some-branch`
with `s = "wt switch {{ args }}"` expands to `wt switch some-branch`.
Before: `wt s some-branch` errored with `Unexpected argument
'some-branch' for alias 's'`.
## Why
Users want to pass arguments through to aliased commands — `wt s foo` →
`wt switch foo` — without configuring `--var foo=…` for every possible
parameter. Positional args are the natural fit.
## Behavior
`{{ args }}` is a minijinja sequence. All four access patterns work:
| Template | Render |
|---|---|
| `{{ args }}` | space-joined, per-element shell-escaped |
| `{{ args[0] }}` | first arg (escaped) |
| `{{ args \| length }}` | count |
| `{% for a in args %}` | iteration |
Each element is individually shell-escaped, so `wt run 'a b' 'c;d'`
splices in as `'a b' 'c;d'` and can't inject shell syntax. Positionals
interleave freely with flags — `wt deploy foo --dry-run bar` collects
`["foo", "bar"]` into `args`.
`--dry-run`, `--yes`, `-y`, `--var KEY=VALUE`, and `--KEY=VALUE` parsing
are unchanged.
## Navigating the diff
- `src/config/expansion.rs` — new `ShellArgs` struct wraps `Vec<String>`
and implements minijinja's `Object` trait with `ObjectRepr::Seq`. Its
`render()` writes `shell_escape::unix::escape` of each element,
space-joined. The shell-escape formatter in `setup_template_env` detects
`ShellArgs` via `downcast_object_ref` and passes its `Display` through
unmodified — so bare `{{ args }}` isn't double-escaped by the generic
per-value escape. Iteration and indexing yield plain
`Value::from(String)` that still flow through the generic formatter. New
`ALIAS_ARGS_KEY` constant (`"args"`) is the reserved context key
carrying the JSON-encoded list.
- `src/commands/alias.rs` — `AliasOptions` gains a `positional_args:
Vec<String>` field. `AliasOptions::parse` collects non-flag tokens into
it instead of erroring. `run_alias` inserts the JSON-encoded list into
`context_map` under `ALIAS_ARGS_KEY` right after `build_hook_context`.
The sole special-case lives in `expand_template`, which rehydrates the
key as a `ShellArgs` object; all other call sites (hooks, for-each,
eval) are untouched and never see the key.
- `validate_template` — injects an empty `ShellArgs` so templates
referencing `{{ args }}` pass pre-flight validation. `args` is added to
`TEMPLATE_VARS`.
- `docs/content/extending.md` — new "Forwarding positional arguments"
subsection under Aliases with a shell-safety guarantee and
access-pattern examples.
## Tests
3233 tests pass, lints clean. New:
- `src/commands/alias.rs` — `test_parse` snapshots extended with
positional cases including interleaved flags and metacharacter-laden
args. `test_parse_errors` no longer expects "Unexpected argument".
- `src/config/expansion.rs` — `test_expand_template_args_sequence`
covers indexing, iteration, and length.
`test_expand_template_args_empty` confirms empty renders to empty
string. `test_expand_template_args_shell_metachar_safety` asserts the
exact output for `['; rm -rf /', '$(whoami)', "a'b"]`.
`test_validate_template_valid` now covers `{{ args }}`, `{{ args |
length }}`, and iteration.
- `tests/integration_tests/step_alias.rs` — end-to-end snapshots for `wt
step <alias> positionals`, `wt <alias> some-branch --dry-run`, empty
positionals, and sequence-style access.
## Do-nots
- No changes to `--dry-run`, `--yes`, `--var`, or `--KEY=VALUE` parsing
— the broader flag-handling question (whether alias-level flags should
move pre-name) is being designed separately.
- No `KEY=value` bare-token parsing for vars — deferred.
- No change to `wt step <alias>` semantics beyond inheriting positional
support via shared `AliasOptions::parse`.
> _This was written by Claude Code on behalf of Maximilian_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third pass over the docs, this round in `src/cli/mod.rs` (which
generates the command pages).
- **`list`** — drop "The table displays instantly and columns fill in as
results arrive." from the `--full` paragraph. The opening paragraph of
`list`'s `after_long_help` already says: "The table renders
progressively: branch names, paths, and commit hashes appear
immediately, then status, divergence, and other columns fill in as
background git operations complete." Full mode inherits that behavior.
- **`step`** — trim the `<alias>` bullet to match the two other alias
references in the file (lines 1843, 2020, both just "Command templates
that run as `wt <name>`."). Drop the now-stale `[experimental]` tag —
the Aliases section in `extending.md` stopped being marked experimental
in #2271, leaving this the only remaining reference — and drop the `(see
[Aliases](...))` parenthetical that linked to the same anchor as the
bullet name.
Snapshots updated via `cargo insta test --accept`.
Follows #2271 and #2272.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more filler sentences caught on a second pass:
- Drop "Summaries are cached and regenerated only when the diff
changes." — duplicates the authoritative sentence in `llm-commits.md`,
which the adjacent "See LLM Commits for details" link already points to.
- Drop "`pre-remove` stops all services when the worktree is removed." —
visible from the `[pre-remove]` block above, and "all services"
overstates what the one-line `kill-session` command does.
Follows #2271.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove sentences that restate obvious fallback/error behavior, duplicate
nearby prose, or add visual weight without information.
- `extending.md` — drop the "not a wt command" error note, the
experimental badge on Aliases, and a duplicate recap of the
`cd`-to-parent-shell behavior at the end of a recipe.
- `faq.md` — drop "The result is cached for fast subsequent lookups"
padding after the default-branch detection explanation.
- `tips-patterns.md` — drop "Creates a worktree that builds on the
current branch's changes" (restated by the section heading) and
"Sessions are named after the branch for easy identification" (visible
in the code).
Skill reference files auto-synced via
`test_command_pages_and_skill_files_are_in_sync`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Renames the git-style `wt-<name>` dispatch feature to "custom
subcommand" across user-facing docs and internal code. Two motivations:
- **Avoid overloading "external."** The codebase already uses "external
command" for the `shell_exec` concept (subprocesses like `git` and
`gh`). Using the same word for the `wt-foo` dispatch feature is
ambiguous.
- **Cargo uses "custom command" / "external subcommand"
interchangeably.** kubectl calls theirs "plugins," gh calls theirs
"extensions"; git doesn't have a settled term. "Custom subcommand" reads
naturally in prose and matches cargo's user-facing phrasing.
## Changes
**User-facing docs** — `docs/content/extending.md` (section heading,
description, comparison table) and `docs/content/faq.md` (link +
anchor). Skill references auto-sync.
**Internal code** — `src/commands/external.rs` → `custom.rs`,
`Commands::External` → `Commands::Custom`, `handle_external_command` →
`handle_custom_command`, plus matching renames in `src/completion.rs`
(inject/discover/forward functions) and corresponding tests. Also
renames `tests/integration_tests/external.rs` → `custom.rs`.
**Kept intact** — clap's `#[command(external_subcommand)]` attribute and
`.allow_external_subcommands(true)` are clap's own vocabulary, not ours.
CHANGELOG is historical and left unchanged.
## Test plan
- [x] `cargo build` clean
- [x] `cargo clippy --all-targets --all-features` clean
- [x] `cargo test --lib --bins` — all pass
- [x] `cargo test --test integration` — all pass (1476)
- [x] `pre-commit run --all-files` clean
> _This was written by Claude Code on behalf of Maximilian_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Route `--base` through the same `pr:N`/`mr:N` resolution used for the
positional branch argument, so these two commands now work
symmetrically:
```
wt switch -c feat-x pr:42 # already worked
wt switch -c feat-x --base pr:42 # new
```
Same-repo PRs/MRs fetch the source branch and use the branch name as the
base, so the resulting worktree tracks it naturally. Fork PRs/MRs fetch
`refs/pull/N/head` (GitHub) or `refs/merge-requests/N/head` (GitLab) and
use the resolved commit SHA as the base — avoiding polluting the local
branch namespace with a tracking branch for a fork contributor's branch.
Closes#2261
## Test plan
- [x] `test_switch_base_pr_same_repo` — same-repo PR resolves to source
branch name
- [x] `test_switch_base_pr_fork` — fork PR resolves to commit SHA, no
tracking branch created
- [x] `test_switch_base_mr_same_repo` — same-repo GitLab MR
- [x] `test_switch_base_pr_without_create` — warning (no fetch) when
`--create` absent
- [x] `cargo test --test integration` (1478 tests pass)
- [x] `cargo clippy --all-targets --all-features -- -D warnings`
- [x] `cargo fmt --all -- --check`
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
Adds a \`WORKTRUNK_PROJECT_CONFIG_PATH\` environment variable that
overrides the project config path, mirroring the existing
\`WORKTRUNK_CONFIG_PATH\` (user) and \`WORKTRUNK_SYSTEM_CONFIG_PATH\`
(system) overrides. Missing files at the overridden path resolve to no
project config — same as a missing \`.config/wt.toml\` today.
Uses it to fully isolate completion tests in
\`tests/integration_tests/shell_wrapper.rs\`, which previously only
isolated user and system config. After #2266 made aliases appear as
top-level completion candidates at \`wt <Tab>\`, any \`[aliases]\` added
to this repo's own \`.config/wt.toml\` would silently pollute completion
snapshots. The helper \`set_empty_user_config\` is renamed
\`set_empty_configs\` and now points all three env vars at
\`/dev/null\`.
Also cleans up \`wt config show --format=json\` to use
\`project_config_path()\` for the \`project.path\` field (was hardcoded
\`.config/wt.toml\`), so the override is reflected in both path and
config.
\`configure_cli_command\` intentionally leaves
\`WORKTRUNK_PROJECT_CONFIG_PATH\` unset — the \`WORKTRUNK_*\` env_remove
loop prevents host leakage, and setting it would break tests that rely
on the default \`.config/wt.toml\` lookup in their own test repo.
Completion tests opt in explicitly via \`set_empty_configs\`.
## Test plan
- [x] \`cargo run --quiet --bin wt -- hook pre-merge --yes\` — 3222/3222
green
- [x] New integration test \`test_project_config_path_env_var_override\`
covers override-to-different-file and override-to-missing-file
Co-authored-by: Claude <noreply@anthropic.com>
`wt deploy` now resolves `deploy` against configured aliases before
falling through to a `wt-deploy` PATH binary. Built-ins still win (clap
matches before alias dispatch ever runs), and `wt step <name>` keeps
working at runtime — only the docs cut over to the new form.
## Why
`wt deploy` reads better than `wt step deploy`, and aliases as
first-class commands lower friction for using them as everyday
shortcuts.
## Precedence
built-in (clap) → alias (user/project config, merged) → `wt-<name>` PATH
binary → "unrecognized subcommand" error.
User config wins over PATH binaries because aliases are how users
customize wt — same model as git, where `[alias]` entries shadow
`git-foo` externals.
## Navigating the diff
- `src/commands/alias.rs` — refactored `step_alias` to share `run_alias`
with the new `try_alias(name, rest) -> Result<Option<()>>`. Returns
`Ok(None)` when the name isn't a configured alias or when not in a git
repo; propagates config-load errors so a broken `wt.toml` fails loudly
instead of silently turning into "unrecognized subcommand". Argument
parsing is gated on alias-membership, so unrelated args meant for an
external binary don't surface as alias parse errors. New
`alias_names_for_suggestions()` mixes alias names into "did you mean"
hints. `HelpContext` enum lets the help splice annotate "(shadowed by
built-in)" against the right level (top-level builtins for `wt --help`,
step builtins for `wt step --help`). The user-facing "shadow warning"
was removed entirely — under the new model an alias named `commit` runs
fine via `wt commit`, only `wt step commit` is shadowed.
- `src/commands/external.rs` — `handle_external_command` calls
`try_alias` first, then PATH lookup, then unrecognized-subcommand error.
Suggestions include alias names. Non-UTF-8 args bypass alias dispatch
(alias parser requires UTF-8; binary subcommands get raw `OsStr`).
- `src/help.rs` + `src/main.rs` — early-parse pass returns
`Option<HelpContext>`; help splice fires for both `wt --help` and `wt
step --help`.
- `src/completion.rs` — aliases injected at the top level in addition to
`step`.
- `src/cli/mod.rs` — long Aliases section moved out of
`Step::after_long_help` into hand-authored `docs/content/extending.md`.
New sync test `test_top_level_builtins_match_clap` keeps the
`TOP_LEVEL_BUILTINS` constant aligned with the `Cli` enum.
## Tests
3221 tests pass, lints clean. New integration tests:
`test_top_level_alias_dispatch`,
`test_top_level_alias_with_step_builtin_name`,
`test_top_level_alias_did_you_mean`. Removed
`test_step_alias_shadows_builtin_plural` (warning gone). Reframed
`test_step_alias_shadows_builtin` to verify shadow filtering of typo
suggestions instead. Completion tests now isolate user config via
`WORKTRUNK_CONFIG_PATH=/dev/null` — project config isolation is a noted
gap (commented inline).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipeline blocks (`[[pre-start]]`, `[[pre-merge]]`, etc.) now run their
concurrent commands in parallel for foreground (pre-*) hooks, matching
the existing behavior in post-* hooks and aliases. The deprecated
single-table form (`[pre-start]`) remains serial.
Moves the `concurrent` flag from a function parameter on
`execute_pipeline_foreground` to a per-step field on `ForegroundStep`,
so mixed configs (e.g., user config is deprecated table, project config
is pipeline) get correct per-step behavior. `CommandConfig` now tracks
whether it was deserialized from a pipeline form (seq visitor) vs a
table form (map visitor), so even a single `[[hook]]` block is correctly
identified as a pipeline.
Also cleans up the "How it works" subsection from the hook docs — the
content was either redundant with surrounding text or now incorrect (the
pre-* serial restriction).
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
The `wt hook` page and tips/merge examples paired two `[[pre-*]]` blocks
with one command each, which runs them serially. For independent
commands like `cargo fmt --check` + `cargo clippy`, that taught the
wrong lesson.
Collapsed each pair into a single `[[pre-*]]` block with both keys (runs
concurrently). The multi-entry `[pre-*]` table form is deprecated
(`src/config/deprecation.rs`), so the single-element array form is the
non-deprecated way to express concurrent commands.
Affected: `src/cli/mod.rs` (merge local-CI, hook progressive-validation,
hook-type-examples), `docs/content/tips-patterns.md` (local CI gate).
Generated docs, skill mirrors, and help snapshots synced.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
`switch.picker.timeout-ms` bounded how long the picker blocked before
rendering. After #2231 landed progressive rendering, the field was
parsed but silently ignored — violating the CLAUDE.md rule against
silently dropping old config keys.
Wired through the standard deprecation path in
`src/config/deprecation.rs`: detection flags the field at top-level and
under `[projects."X".switch.picker]`, migration strips it, and the
warning points users at `wt config update`. Handles both
`[switch.picker]` section form and inline `picker = { ... }` form,
matching existing helpers.
Also removed the `SwitchPickerConfig::timeout_ms` field and `timeout()`
accessor — migration strips the key before serde sees it, so the struct
field is no longer needed. Tests referencing the old field updated or
dropped. Example TOMLs and the picker comment cleaned up accordingly.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude <noreply@anthropic.com>
Mirror wt list's skeleton-first model in the skim picker. Branch/path
and header render immediately; status, diff stats, counts, summaries
fill in in place as they resolve. Replaces the pre-switch 500ms blocking
freeze.
## How it works
Skim 0.20's 100ms heartbeat redraws while its item channel is open
(`!processed`). Keeping the `SkimItemSender` alive holds heartbeat open;
`SkimItem::display()` reads the current rendered string via interior
mutability, so each tick picks up in-place state updates without any
explicit poke.
- `PickerProgressHandler` trait in `src/commands/list/collect/mod.rs` —
`collect` fires `on_skeleton` once the layout is ready, `on_update` per
task result, `on_reveal` at the 200ms blank→`·` transition.
`LayoutConfig` stays inside `collect` (it's `!Sync` via a `Cell`), so
rendered strings are handed out.
- `src/commands/picker/progressive_handler.rs` — builds skim items from
the skeleton, sends through `tx`, overwrites each row's shared
`Arc<Mutex<String>>` on later events. `tx` lives inside the handler so
dropping it (when the bg thread's collect returns) stops the heartbeat.
Strips OSC 8 hyperlinks — skim's rendering pipeline mangles them into
garbage like `^[8;;…`.
- `WorktreeSkimItem` now holds the rendered line behind
`Arc<Mutex<String>>`; `text()` (matcher input) stays stable (`branch +
path`) so skim's rank cache survives in-place updates.
- `handle_picker` spawns collect on a bg thread and launches skim on the
main thread. Quick selection returns immediately — `bg_handle` isn't
joined on interactive exit (would block up to `DRAIN_TIMEOUT` on network
tasks; git subprocesses are read-only so process exit is safe).
## Simplifications enabled
- Dropped the 500ms `switch_picker.timeout` wall-clock budget — it was
the UI-freeze budget, obsolete now. Config field kept for schema compat
but ignored; users on slow repos get more data, not a truncated view.
- Shared `RowCache` consolidates what used to be duplicated render-dedup
state in two places. Fixes a partial-row reveal bug where rows whose
first result landed pre-reveal kept blank placeholders on their
still-pending cells until another result arrived (caught during
simplify).
## Base branch note
Based on `skim-cut` (#2226), now merged to main. The vendored
skim-tuikit's `write_all` fix is the reliability floor — without it,
heartbeat redraws silently drop rows past the first ~1024-byte
short-write boundary, and progressive updates look broken even though
the mechanism works.
## Test coverage
Well-covered: handler state transitions (skeleton → update → reveal),
shared cache dedup, existing picker integration/dry-run tests.
Progressive rendering in a real PTY isn't unit-tested here — there's no
skim-in-a-test harness — but the dry-run path
(`WORKTRUNK_PICKER_DRY_RUN`) exercises collect + handler end-to-end
without a TTY and continues to pass.
> _This was written by Claude Code on behalf of Maximilian._
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Requested in max-sixty/worktrunk#2053 — link to
[`worktrunk-sync`](https://github.com/pablospe/worktrunk-sync) from the
docs.
- **Extending Worktrunk** — adds an `### Examples` subsection under
*External subcommands*, pointing to `worktrunk-sync` as a concrete
real-world use of the `wt-<name>` dispatch mechanism.
- **FAQ** — adds a "Does Worktrunk support stacked branches?" Q&A
positioned after the tool-comparison section, explaining that stacked
workflows are deliberately kept out of core and pointing readers at
`worktrunk-sync`.
Skill reference files under `skills/worktrunk/reference/` regenerated
via `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync`.
## Test plan
- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` passes
- [ ] Prose/links render as expected on the dev server
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Without shell integration, post_hook_display_path behaves like pre_hook_display_path.
Tests now use an explicit-arg variant (post_hook_display_path_with) so they're
independent of process-wide OUTPUT_STATE, which may be pre-initialized to
shell-integration-active when tests run under `wt` (inheriting WORKTRUNK_DIRECTIVE_*
env vars). Added test for shell-integration-active case to complete coverage.
## Motivation
At `-vv`, raw subprocess bodies were bounded (200 lines / 64 KB) on both
stderr and `verbose.log`, with full output only available by rerunning
at `-vvv`. Large captures (notably `git log -p` piped into `patch-id`
during `wt list`) would routinely flood stderr with elision markers and
force a second run just to see what was elided.
## Change
The verbosity map is now `-v` (info) and `-vv` (debug); any `-v` count
above 2 collapses to `-vv`. Captured subprocess stdout/stderr fan out
through two log targets in `src/shell_exec.rs`:
- `SUBPROCESS_TERMINAL_TARGET` → bounded preview on stderr, mirrored to
`.git/wt/logs/trace.log` (new, replaces `verbose.log`)
- `SUBPROCESS_FULL_TARGET` → uncapped body to `.git/wt/logs/output.log`
(new), never stderr
`src/log_files.rs` (renamed from `src/verbose_log.rs`) owns both file
sinks behind a `LogSink` type and a `route(target)` helper that is the
single source of truth for sink selection. `src/main.rs`'s env_logger
format closure matches on the `Route` enum and emits once per sink.
Diagnostic reports embed `trace.log` and reference `output.log` by path
— multi-MB raw bodies would swamp a bug report.
## Fallback path
When `RUST_LOG=debug` is set without `-vv`, neither sink is active.
`FULL` records drop and the `TERMINAL` preview reaches stderr as before
— preserving the bounded-stderr guarantee. The elision marker phrases
its hint based on whether `output.log` was opened, so users in the
fallback path see `rerun with -vv for full output` rather than a pointer
to a file that doesn't exist.
## Key files
- `src/log_files.rs` — new module; `LogSink`, `TRACE`, `OUTPUT`,
`route`.
- `src/shell_exec.rs` — two `pub const` targets, `log_output` emits on
both, elision hint switches on `OUTPUT_LOG_AVAILABLE`.
- `src/main.rs` — verbosity map + format closure.
- `src/diagnostic.rs` — template splits inlined `trace.log` from
referenced `output.log`.
- `src/commands/config/state.rs` — diagnostic file recognition for the
new names.
## Testing
- `test_vv_splits_full_and_bounded_output` — `[wt-trace]` in
`trace.log`, raw stdout in `output.log`, no trace records in
`output.log`.
- `test_vv_bounded_on_stderr_full_in_output_log` — 250-ref packed-refs
trip the elision cap; asserts the marker on stderr + `trace.log`, full
content in `output.log` without elision.
- `test_rust_log_debug_fallback_without_vv` — no log files created at
`-v 0 + RUST_LOG=debug`; bounded preview reaches stderr.
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The Status column's loading glyph `⋯` (horizontal ellipsis) was too
visually prominent in the tight per-position slots. Loading and the
post-deadline timeout state don't really need to be distinguished via
glyph — the surrounding context (progressive fill in `wt list`, picker
appearing after deadline in `wt switch`) already tells the user which is
which — so both states now render as a dim `·`.
The change is centralized in a new `PLACEHOLDER` constant in
`src/commands/list/render.rs`. Its TODO documents the future re-split:
we'd like a subtle second glyph once we can evaluate candidates
side-by-side in real tables.
`render_list_item_stale` is kept as a separate entry point (passing the
same `PLACEHOLDER` today) so the picker-side call site doesn't need
re-auditing when the re-split lands.
Most of the diff is mechanical: doc-comment references to the `⋯` glyph,
test helpers counting `·` instead of `⋯`, regenerated insta snapshots,
and the user-facing help table in `src/cli/mod.rs` collapsing two rows
into one.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Adds a `sanitize_hash` minijinja filter — a pass-through wrapper for
`worktrunk::path::sanitize_for_filename`. Produces a filesystem-safe
name with a 3-char hash suffix so distinct originals never collide;
already-safe names pass through unchanged.
Uses it in a `hook-log` alias recipe in `docs/content/extending.md` so
`wt config state logs --format=json | jq ... select(.name == $name)`
matches the exact on-disk hook log filename even when the configured
branch or hook name contains characters like `/`.
## Test plan
- [x] New unit test `test_expand_template_sanitize_hash_filter` covers
safe passthrough, unsafe-char replacement + hash suffix, and empty input
- [x] `validate_template` test extended with `{{ branch | sanitize_hash
}}`
- [x] `test_command_pages_and_skill_files_are_in_sync` passes
(auto-syncs `docs/content/hook.md` and `skills/worktrunk/reference/*.md`
mirrors)
- [x] `pre-commit run --all-files` clean
- [x] Help snapshots regenerated
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two small polish changes to the `hook-log` alias recipe added in #2161:
- **Multi-line the alias body** so the 217-char pipeline becomes
readable in the rendered docs. The jq expression lives inside single
quotes, so literal newlines are safe; the outer shell pipeline reads
top-to-bottom without wrapping.
- **Drop the `startswith($name + "-")` branch.** It was meant to also
match the sanitized-with-hash form, but only covers the specific 3-char
suffix shape and would misfire on hook names that naturally end in
`-<3alnum>`. The limitation is documented instead: users with hook names
that required sanitization should pass the on-disk sanitized form.
A follow-up PR (dispatched separately) adds a `sanitize_hash` template
filter so the alias can pre-sanitize its input and match reliably across
all hook names.
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
Builds on [#2156](https://github.com/max-sixty/worktrunk/pull/2156)
which hoisted `--format` and added absolute `path`. Two follow-on
changes:
### 1. Structured fields on `hook_output` entries
Hook-output log entries carried a slash-packed relative path in `file` —
consumers had to split on `/` to filter by branch, source, or hook type.
Expose first-class structured fields:
```json
{
"file": "main/user/post-start/server.log",
"path": "/.../.git/wt/logs/main/user/post-start/server.log",
"branch": "main",
"source": "user",
"hook_type": "post-start",
"name": "server",
"size": 1024,
"modified_at": 1700000000
}
```
Internal-op entries (e.g., `feature/internal/remove.log`) normalize onto
the same shape with `source: "internal"`, `hook_type: null`, and the op
name in `name`.
### 2. Drop `--hook` / `--branch` filters; use jq
With the structured fields, the old `--hook=SPEC` / `--branch=X`
interface is a jq one-liner:
```bash
wt config state logs --format=json \
| jq -r '.hook_output[]
| select(.source == "user"
and .hook_type == "post-start"
and (.name | startswith("server")))
| .path'
```
That subsumes the filter flags without the CLI plumbing, the
`HookLog::parse`/`to_spec` roundtrip, or the error-shape tests that
locked in a fragile `source:hook-type:name` grammar. `logs` and `logs
get` now always list — one code path, one shape.
**Breaking change**: `wt config state logs get --hook=...` and
`--branch=...` are removed. The jq pattern is documented in `wt config
state logs --help`.
> _This was written by Claude Code on behalf of Maximilian Roos_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`sanitize_for_filename` previously always appended a 3-char hash, giving
logs like `main-vfz/project/post-merge/clippy-vif.log`. Clean names now
pass through unchanged, so the common case reads
`main/project/post-merge/clippy.log`. Inputs that actually require
sanitization (path separators, invalid chars, empty input) still get the
hash suffix so they can't collide with an already-safe name.
Also adds a TODO noting that the trash-sweep log shouldn't be
branch-scoped — it piggybacks on `HookLog` with a fake `"wt"`
pseudo-branch, so its actual on-disk path is the awkward
`.git/wt/logs/wt/internal/trash-sweep.log`. Cleaner would be a top-level
`internal/trash-sweep.log` alongside the other shared logs
(`commands.jsonl`, `verbose.log`, `diagnostic.md`).
Snapshot tests updated to reflect the new pass-through behavior.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously `wt config state logs --format=json` errored with "unexpected
argument '--format'" — the flag only existed on the `get` action, even
though the bare form dispatches to the same list view. Same shape on
`hints`, `ci-status`, `marker`.
Consolidates `--format` to a single `global = true` flag on each parent;
drops the now-duplicate field from the `Get` actions. `--format=json`
now works regardless of ordering: `logs --format=json`, `logs
--format=json get`, `logs get --format=json`.
Logs JSON entries now include `path` (absolute, forward-slashed)
alongside `file` (relative), so consumers don't need to join with
`wt_logs_dir()` to open the file:
```json
{
"file": "main-vfz/user/post-switch/zellij-tab-xtr.log",
"path": "/Users/.../.git/wt/logs/main-vfz/user/post-switch/zellij-tab-xtr.log",
"size": 124,
"modified_at": 1776041854
}
```
`logs get --hook=X --format=json` also emits `{"path": "..."}` instead
of a bare path line, so JSON consumers get a uniform shape whether
they're listing all logs or resolving one hook.
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
Collapses the three-sentence deprecation note above Project vs user
hooks into a single sentence. Now that the preceding section teaches
`[[hook]]` blocks as the canonical pipeline form (PR #2149), the note
can name them directly and drop the serial-vs-concurrent explanation —
that mechanics detail is already covered in Pipeline Ordering further
down.
Before:
> For pre-* hooks, prefer pipeline form over table form. Table form for
pre-* hooks currently runs serially rather than concurrently — this
inconsistency is deprecated and will change in a future version. Using
pipeline form avoids the upcoming behavior change.
After:
> Table form for pre-* hooks is deprecated and its behavior will change
in a future version — use `[[hook]]` blocks instead.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Three changes to worktrunk's logging conventions, all motivated by `wt
list -vv` dumping raw `git diff-tree -p` bodies into the log stream at
debug level:
**1. `src/trace/emit.rs` owns the `[wt-trace]` grammar.** Previously the
grammar was emitted via ad-hoc `log::debug!("[wt-trace] ...")` format
strings in `shell_exec.rs`, with `src/trace/parse.rs` silently defining
it by how it parsed. `trace_instant`, `log_command_result`, the
`TRACE_EPOCH` static, and `thread_id_number` moved into the new emitter
module so `parse.rs` and the producer share one source. Wire format
byte-identical; `wt-perf` parsing unchanged.
**2. Level discipline.** `-v` → Info, `-vv` → Debug, `-vvv` → Trace.
Previously `-v` didn't touch the `log` crate at all and `-vvv` didn't
exist. The LLM prompt dump (`src/llm.rs`) and captured subprocess
stdout/stderr (`log_output` in `shell_exec.rs`) moved to `log::trace!`,
so `-vv` stops spilling thousand-line diff bodies and full LLM prompts.
**3. Bounded `log_output`.** At Debug, each stream caps at 200 lines /
64 KB with `… (N more lines, M bytes elided — use -vvv for full
output)`. At Trace, uncapped.
## Reviewer navigation
- **New file**: `src/trace/emit.rs` — the single-source emitter.
`command_completed`, `command_errored`, `instant` plus `trace_epoch` /
`now_us` / `thread_id` helpers.
- **Delete/delegate**: `src/shell_exec.rs` loses its duplicate
`TRACE_EPOCH` + `trace_epoch` + `thread_id_number`, and the four
`log::debug!("[wt-trace] …")` branches collapse into two calls into
`trace::emit`. `log_output` gains `log_stream_full` (Trace) and
`log_stream_bounded` (Debug) helpers.
- **Level map**: `src/main.rs:init_logging` has the new match on
`verbose_level`.
- **Help + test snapshots**: the `--verbose` help text change in
`src/cli/mod.rs:249` drives the large auto-synced snapshot / docs /
skill-reference diff.
- **Test rename**: `tests/integration_tests/diagnostic.rs` —
`test_v_does_not_enable_logging` → `test_v_does_not_write_log_files`
(the old name became inaccurate now that `-v` enables Info logging on
stderr).
## Testing
- 969 library unit tests pass.
- Integration tests for `diagnostic`, `step_alias`, `test_help` pass (82
tests).
- Lints clean.
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Cut over hook pipeline docs from inline-list form (`hook = [{name =
"..."}, ...]`) to `[[hook]]` array-of-tables blocks. The deserializer
already accepts this shape — no code changes were needed; the change is
purely teaching. This mirrors what landed for aliases in #2144.
The summary in `Pipeline Ordering` now foregrounds the TOML shape
progression instead of a labeled taxonomy:
- `post-start = "npm install"` — one command
- `[post-start]` — one section of concurrent commands
- `[[post-start]]` — one of multiple sections, run in order
The bracket count itself tracks the structural escalation (value →
section → array-of-sections).
Also adds a module-level doccomment to `src/config/commands.rs`
describing the three primitive TOML shapes the hook deserializer accepts
(string / dict / list) and the asymmetry between dict-at-top (always
`Concurrent`) vs dict-in-list (1-entry → `Single(named)`). This is the
analysis that motivated the cutover and is retained so future readers
can orient on the deserialization rules without re-deriving them.
The Databases example in `hook.md` and `tips-patterns.md` got its first
pipeline step named (`set-vars`) since `[[post-start]]` blocks can't
hold anonymous bare strings the way inline lists could.
> _This was written by Claude Code on behalf of Maximilian_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Follow-ups from #2131, addressing two gaps raised in that review.
## 1. `wt step --help` / `-h` now shows configured aliases
In the original PR, the Aliases section appeared only when running bare
`wt step`, because clap's `DisplayHelp` error (triggered by `--help` /
`-h`) was intercepted in `help.rs` before `step_list()` ran.
Made `StepCommand` required (`arg_required_else_help = true`) so bare
`wt step` triggers the same `DisplayHelp` path. The splice now lives in
`help.rs::maybe_handle_help_with_pager`, scoped to the step subcommand
via a small `is_help_for_step` positional-args scan. `step_list()` and
its help-rendering helper are deleted — one entry point, one splice.
## 2. User + project aliases show as two rows, not a merged summary
Previously, when user and project both defined the same alias name,
`load_aliases_for_listing` called `append_aliases` which merged them via
`merge_append`. The summary for that merged config lost the original
per-source command text (often reduced to `<2 steps>` for unnamed
singles).
Now each source contributes its own row, user first (matching runtime
order — both still run). Rows get `(user)` / `(project)` markers only
when the name appears in both; unique names stay unannotated.
## Testing
Unit test covers the new render behavior (unique names, collision with
two rows). Integration test covers `wt step -h` showing aliases.
Existing `test_step_list_with_aliases` / `test_step_list_no_aliases`
snapshots picked up the new rendering path (clap help →
`md_help::render_markdown_in_help_with_width` instead of direct
`eprint!`); `help_step_long` / `help_step_short` and the generated docs
picked up `[COMMAND]` → `<COMMAND>` from the required-subcommand change.
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aliases share the `CommandConfig` deserializer with hooks, so
`[[aliases.NAME]]` blocks already deserialize into pipeline steps with
zero code changes — analogous to the recent `[[hook]]` cutover in
32bc61787. Teach the block form as the canonical shape for multi-step
aliases.
Changes:
- `src/cli/mod.rs` — new paragraph + `[[aliases.release]]` example in
the `wt step` Aliases section, explaining sequential blocks with
concurrent keys and fail-fast on step failure
- `src/config/project.rs` — rewrite the `aliases` field rustdoc to treat
string / named-table / `[[aliases.NAME]]` as three first-class forms
(drops the "single-string is the expected usage" framing)
- `docs/content/step.md` + `skills/worktrunk/reference/step.md` —
auto-synced
- help snapshot re-accepted
No deserializer work — covered by existing
`test_deserialize_pipeline_named_single` in `src/config/commands.rs`.
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Multi-entry table form for pre-* hooks currently runs serially, while
the same form for post-* hooks runs concurrently. The parser produces
`HookStep::Concurrent` either way, but the foreground executor flattens
steps and runs them serially. To unify the semantics — table form will
run concurrently for all hook types in a future version — this
deprecates the current form for pre-* hooks and auto-migrates it to
pipeline form, which is explicitly serial.
## Implementation
Follows the existing deprecation recipe in `src/config/deprecation.rs`:
- **Detection** and **migration** for top-level hooks (user/project
config) and per-project overrides (`[projects."id".pre-*]`).
- **Warning**: matches the terse `{old} → {new}` pattern of existing
deprecations (`[merge] no-ff → ff`, `post-create → pre-start`, etc.).
- **Auto-migration** at load time: table form rewrites to pipeline of
inline tables so current behavior (serial) is preserved until users run
`wt config update`.
## Docs
Replaces the transitional "concurrent for post-*, sequential for pre-*"
framing with a neutral three-form description (string / table /
pipeline), plus a note recommending pipeline form for pre-* hooks to
avoid the upcoming behavior change.
## Tests
- `snapshot_migrate_pre_hook_table_form` — TOML migration diff
- `test_config_show_displays_pre_hook_table_form_deprecation` — full
user-facing `wt config show` output, covering the "Project config" label
and multi-hook list form
- Unit tests for detection/migration of top-level and per-project
variants
- Existing integration test fixtures migrated to canonical pipeline form
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Previously `wt step` with no subcommand printed clap's auto-help, which
only covered built-in steps (`commit`, `squash`, `push`, …). Aliases
defined in `[aliases]` were invisible there — users had to remember what
they'd configured, or grep their config files.
Now `wt step` renders the same help plus an `Aliases:` section listing
merged user+project aliases. Each entry shows a one-line template
summary (first line with `…` suffix for multi-line bodies; `name; a, b`
form for named pipelines) and a `(shadowed by built-in)` marker in
yellow for names that collide with built-in steps. The section is
spliced in right after Commands — next to the built-ins it extends — by
finding the styled `Options:` heading in clap's rendered output and
inserting before it.
`wt step -h` is unchanged (still pure clap) so reference help stays
stable; the aliases listing is a discovery surface for the no-args case.
### Example
```console
$ wt step
wt step - Run individual operations
Usage: wt step [OPTIONS] [COMMAND]
Commands:
commit Stage and commit with LLM-generated message
squash Squash commits since branching
…
relocate [experimental] Move worktrees to expected paths
Aliases:
deploy make deploy BRANCH={{ branch }}
port echo http://localhost:{{ branch | hash_port }}
squash this shadows built-in (shadowed by built-in)
Options:
-h, --help Print help (see more with '--help')
Global Options:
-C <path> Working directory for this command
…
```
### Notes
- `Commands::Step { action }` moves from `StepCommand` to
`Option<StepCommand>`; the usage line changes from `<COMMAND>` to
`[COMMAND]` (reflected in the updated help snapshots).
- Splice search string is derived from `cli::help_styles().get_header()`
— same source clap uses — so if the header style changes, the search
string moves with it. Snapshot tests catch any behavior drift.
- Outside a repo or with a malformed project config, the aliases section
falls back silently to just listing user-config aliases (or nothing).
This is a discovery surface, not an execution surface, so breaking help
rendering for a bad config seemed wrong.
### Testing
- Two new integration snapshot tests: `test_step_list_with_aliases`,
`test_step_list_no_aliases`.
- Four unit tests for `format_alias_summary` covering single-command,
multi-line, named pipeline, and all-unnamed pipeline cases.
- Updated `help_step_short` / `help_step_long` snapshots for the
`<COMMAND>` → `[COMMAND]` change.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Wraps the first `post-start` step in the Databases example in `{
set-vars = ... }` so both steps are structurally parallel. Named steps
produce meaningful log file names (`set-vars.log`), are selectable via
`wt hook post-start set-vars`, and pair visually with the second step —
worth modeling in a teaching example.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude <noreply@anthropic.com>
The shell wrapper previously used a single `WORKTRUNK_DIRECTIVE_FILE`
where wt wrote shell commands (`cd '/path'`, arbitrary `--execute`
payloads). This meant the cd path went through shell parsing — any
content wt wrote was sourced as shell.
This splits the protocol into two files with different trust levels:
- **`WORKTRUNK_DIRECTIVE_CD_FILE`** — raw path, read with `cd -- "$(<
file)"`. No shell parsing, no escaping, no injection surface. Safe to
pass through to alias/hook child processes.
- **`WORKTRUNK_DIRECTIVE_EXEC_FILE`** — arbitrary shell (from
`--execute`), sourced by the wrapper. Scrubbed from alias/hook child
environments so hook bodies cannot inject shell into the parent session.
When a nested `wt` inside an alias body tries `--execute` without the
EXEC file, the command is dropped with a warning linking to #2101 for
user feedback.
The old `WORKTRUNK_DIRECTIVE_FILE` is silently honored for one release
(users who upgrade wt without restarting their shell). Bash, zsh, fish,
and PowerShell self-update on restart; nushell requires `wt config shell
install`.
Closes#2101
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`--KEY=VALUE` shorthand for `wt hook --var KEY=VALUE`, custom template
variables in hooks (matching alias behavior), and a warning when a
`--var` isn't referenced by any hook template.
**Shorthand**: `wt hook pre-start --branch=feature/test` rewrites to
`--var branch=feature/test` before clap sees it. Known flags (`--yes`,
`--dry-run`, etc.) are preserved. `--` stops rewriting.
**Custom vars**: Hooks previously restricted `--var` to known
TEMPLATE_VARS while aliases accepted arbitrary names. Now both use
`parse_key_val` with hyphen→underscore canonicalization, so hooks can
inject custom variables like `{{ my_env }}` into templates.
**Unreferenced-var warning**: When a `--var` key isn't referenced by any
template in the hooks being run, a warning is emitted (catches typos
like `--brnach=feature`). Respects name filters — if only "test" runs
and "build" uses the var, we still warn.
**Sync tests**: `HOOK_SUBCOMMANDS_WITH_VARS` and `KNOWN_HOOK_LONG_FLAGS`
are both validated against clap's command tree to catch drift.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds persistent SHA-keyed caching for `is_ancestor`,
`has_added_changes`, and `branch_diff_stats` — the last three operations
still running unconditionally in both `wt list` and the `wt switch`
picker. With all five merge-base-dependent tasks now cached (joining
`merge-tree-conflicts` and `merge-add-probe` from PR #2085), the
`stale_branches` skip mechanism becomes redundant: the picker's "skip
tasks on branches > 50 behind main" optimization was papering over slow
first-run cost that the cache now handles eternally.
## What changed
**New caches** (`src/git/repository/probe_cache.rs`): three new
asymmetric SHA-keyed kinds — `is-ancestor`, `has-added-changes`,
`diff-stats` — all using the same LRU-swept persistent file layout as
the existing kinds. `branch_diff_stats` skips its cache when sparse
checkout is active (path filters make the result environment-dependent).
**Dead plumbing removed**: `EXPENSIVE_TASKS` constant,
`CollectOptions::stale_branches` field, `skip_expensive_for_stale`
parameter on `collect()`, the env-var gate in `list/mod.rs`, and two
obsolete tests + snapshots. Both `wt list` and the picker now run the
same task set.
**`batch_ahead_behind` unconditional**: previously only the picker
called it. Making it unconditional replaces N `rev-list --count` calls
with one `for-each-ref`, a pure win for `wt list` too.
**Branch-ref semantics unified across all tasks**: all task
implementations now prefer the branch name over
`ctx.branch_ref.commit_sha` when one is present. Previously,
`IsAncestorTask`, `BranchDiffTask`, `MergeTreeConflictsTask`, and
`CommittedTreesMatchTask` used the worktree's current HEAD sha, which
during a rebase-in-progress is transiently at the replayed commit rather
than the branch tip. This produced contradictory rows:
`is_ancestor=true` + `1 ahead / 1 behind`. Now all tasks consistently
report the branch's state, matching `git status` semantics. Addressed
worktrunk-bot review feedback.
**Caching docs updated**: the `## Caching` section in `collect/mod.rs`
(from PR #2097) now lists the three tasks as cached rather than
"cacheable but uncached". Also addressed worktrunk-bot PR overlap
observation.
**Benchmarks**: `invalidate_caches_auto()` now clears `.git/wt/cache/`
so cold-cache benchmarks exercise cold state. The `warm_optimized`
variant in `real_repo_many_branches` collapsed since it no longer
measures anything different from `warm`.
## Testing
New probe_cache unit tests cover roundtrip + tamper-based cache
consultation for each new kind, plus a `clear_all_covers_all_kinds`
regression test. Snapshot updates for
`test_list_maximum_status_with_git_operation` and
`test_list_json_with_git_operation` reflect the branch-ref fix —
mid-rebase worktrees now show `✗` (real conflicts) and actual diff stats
instead of the transient HEAD's empty state.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up to #2091. Users naturally reach for kebab-case flag names like
`--my-var=value`, but minijinja parses `{{ my-var }}` as subtraction —
so the value would silently never appear when the template expands.
Both parser paths (`--var KEY=VALUE` and the `--KEY=VALUE` shorthand)
now call `key.replace('-', "_")` before storing the variable.
`--my-var=value`, `--var my-var=value`, and `--my_var=value` all resolve
to `{{ my_var }}`. No validation, no warning — friendlier than
rejection, and values are left untouched so things like
`--region=us-east-1` still work.
Doc updates in `src/cli/mod.rs` surface the behavior in `wt step --help`
and the generated web/skill docs.
Co-authored-by: Claude <noreply@anthropic.com>
PR #2091 added `--key=value` shorthand for alias variables. Update the
three invocation comments in the move/copy-changes recipe to use the
shorter form — `wt step move-changes --to=feature-xyz` instead of `--var
to=feature-xyz`.
Template bodies still reference `{{ to }}` internally, which remains the
canonical variable name.
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Aliases previously required `--var key=value` to pass template
variables. Unknown `--key=value` flags are now treated as variable
assignments, so `wt step deploy --env=staging` is equivalent to `--var
env=staging`.
The `=` is required to disambiguate from boolean flags — `--env value`
would be ambiguous (is `value` a positional?), but `--env=value` is
unambiguous. `--var` remains as the escape hatch for variable names that
collide with built-in flags (`--dry-run`, `--yes`).
Implementation is a single fallthrough match arm in
`AliasOptions::parse()` (the alias parser is hand-rolled because aliases
are clap external subcommands). Unit tests cover the shorthand alongside
`--var`, equals-in-value, mixed forms, empty values, and bare `--key`
errors. An integration test confirms end-to-end equivalence with
`--var`.
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Adds a tested recipe to the Aliases section of
`docs/content/extending.md` showing three `[aliases]` entries that
compose `wt switch --create` with git's stash/diff plumbing to carry
in-progress changes into a new worktree:
- `move-changes` — stashes staged + unstaged + untracked, creates the
new worktree, and pops the stash with `--execute`. Source becomes clean.
- `copy-changes` — same push, plus `git stash apply --index --quiet` to
restore the source before the new worktree pops. Both sides end up with
identical state.
- `copy-staged` — writes `git diff --cached` to a tempfile and applies
it with `git apply --index` in the new worktree, so the source is
untouched and staged/unstaged overlap is handled.
Each alias takes the target branch via `--var to=feature-xyz`. Because
an inner `wt switch --create` inside an alias body propagates its `cd`
to the parent shell (#2077), they drop you in the new worktree directly.
The content was worked out and tested end-to-end in the issue thread
(`git status --porcelain` verified on both sides for clean /
staged+unstaged+untracked / untracked-only / empty-staged scenarios).
Closes#938.
## Test plan
- [ ] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — keep
`skills/worktrunk/reference/extending.md` in sync
- [ ] `pre-commit run --all-files` — lints
- [ ] Visual check of the rendered Aliases section on the docs dev
server
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Follow-up to #2079. Adds cross-links from the landing page and adds the
`up` alias example to the extending page.
- worktrunk.md "Next steps": link to extending.md
- extending.md: add `wt step up` multi-line alias example, remove
redundant TOML file-path comments
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Caches expensive SHA-keyed git operations (merge-tree conflicts,
integration probe) to disk under `.git/wt/cache/{kind}/`. Commit SHAs
are content-addressed, so cached entries never go stale — only an LRU
size bound (5000 per kind) prevents unbounded growth.
The cache is transparent: `has_merge_conflicts` and
`merge_integration_probe` check/populate it internally, so list, picker,
merge, and remove all benefit without code changes. Refs are resolved to
SHAs before all git commands, ensuring the cache key matches the actual
computation.
Layout: one JSON file per entry at
`.git/wt/cache/{kind}/{sha1}-{sha2}.json`, with symmetric keys for
conflict detection and asymmetric keys for integration probe. Corrupt
entries degrade to cache miss (no tmp-and-rename needed). Cleared by `wt
config state clear`.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The previous wording (`.git/config` keys under `worktrunk.*`) mixed
plain text between two code spans in the Location column. The
`td:first-child` CSS rule sets `font-weight: 550`, so "keys under"
rendered semi-bold while the flanking `<code>` elements stayed at 400 —
visually inconsistent with every other row.
`git config worktrunk.*` is a single code span that also avoids tying
the location to a specific config file (`.git/config` vs
`~/.gitconfig`).
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Use `git rev-parse --verify -q` to suppress the error message natively,
plus `>/dev/null` to hide the SHA printed on success. Replaces the
`>/dev/null 2>&1` redirect.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Hooks and worktree removal spawn short-lived detached processes, so the
blanket "no background processes" was inaccurate. Adds "long-running"
qualifier.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Tera has no backslash-escape for string literals, so `\"` inside a `cmd`
parameter closes the string prematurely. The `git worktree lock` example
in the FAQ was rendering as literal `{{ terminal(...) }}` text instead
of a styled terminal block.
Switched to the `__WT_QUOT__` placeholder that the shortcode template
already handles (replacing back to `"` before Syntect highlighting).
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
New Reference page covering the three extension mechanisms — hooks,
aliases, and external subcommands — with a comparison table and enough
detail to orient readers before linking to the full references
(`hook.md`, `step.md#aliases`).
Moves the external subcommands section out of tips-patterns.md into the
new page as its canonical location. The aliases recipe in tips-patterns
stays (it's a specific recipe, not a duplicate).
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Adds a real-world alias example to the step aliases documentation,
showing multi-line syntax and composition with `wt step for-each`. The
`up` alias fetches all remotes and rebases each worktree onto its
upstream.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>