Commit Graph

154 Commits

Author SHA1 Message Date
Caleb Cox aa9d8c43df feat: add remote_repo variable (#3745)
Add a `remote_repo` variable that returns the repo name from the remote
URL. Unlike `repo`, it stays consistent even if the clone was renamed.

Feel free to reject, or suggest other names for the variable. But this
change would improve my workflow. I hope you don't mind my submitting a
PR before opening an issue. Thanks for an amazing developer tool!

AI Disclosure 🤖: I used Claude Code to generate the changes, but
reviewed every line and made adjustments.

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-14 00:48:54 -07:00
Maximilian Roos 9e7ad0144d fix(hooks): expand everything but vars.* in a preview (#3638)
Follow-up to #3635, from two reviews that landed after it merged.

## The `vars.*` preview was worse than #3635 claimed

`render_template_preview` short-circuits on
`template_references_var(template, "vars")`, returning the raw template.
So one `vars.` token disabled expansion for the entire command:

```
pre-commit = "deploy --branch={{ branch }} --repo={{ repo }} --env={{ vars.env }}"

before:  deploy --branch={{ branch }} --repo={{ repo }} --env={{ vars.env }}
after:   deploy --branch=main --repo=repo --env={{ vars.env }}
```

#3635 described this as "a `vars.*` template renders raw", which is true
but understates it: `{{ branch }}` and `{{ repo }}` stopped expanding
too, in a command whose whole job is to show the expansion. That
short-circuit predates #3635 and has been degrading `wt hook <type>
--dry-run` the same way; #3635 only extended it to `wt hook show
--expanded`.

The fix is at the source rather than at either caller. A preview now
injects a stand-in object for `vars` that renders each reference back as
itself, nested access included (`{{ vars.config.port }}` round-trips),
while every other variable expands normally. `VarsMode::Resolve` keeps
execution reading real values from git config; only previews pass
`VarsMode::Literal`. A preview also no longer spawns the git read that
resolving `vars` required.

`vars.*` stays literal on purpose: those values are read when the step
runs, after an earlier step in the pipeline may have written them, so a
value resolved at preview time can differ from the one the run uses.

Nothing covered this, which is why the suite stayed green through the
regression. `test_hook_show_expanded_matches_dry_run` now sets a var and
asserts the listing and the dry-run both leave it alone while expanding
`{{ branch }}` beside it.

## The syntax gate is a type error now

#3635 moved the template syntax check out of `prepare_steps` into a free
`validate_pipeline_syntax` that both execution funnels had to remember
to call. `prepare_steps` now returns a `PreparedPipeline` the caller
must resolve: `.validated()` for the paths that run hooks,
`.into_unvalidated()` for the listing, which annotates a broken template
in place rather than blanking itself. Forgetting is a compile error, the
same property `ApprovedHookPlan` gives hook approval.

## Smaller items

Four cross-references went stale when the syntax check moved:
`PreparedCommand.template`, `validate_template_syntax`, the `switch.rs`
skip comment, and `HOOK_INFRASTRUCTURE_VARS` (which still named two
deleted functions). The `--expanded` behavior is now documented in the
sentence that already owns `{{ vars.<key> }}` semantics, with its three
generated mirrors regenerated.

`PreparedStep::commands()` replaces two hand-rolled matches in
`hooks.rs`. `default_branch` moves inside
`build_manual_hook_template_vars` — only the commit-hook arm reads it,
and resolving it can cost a `git ls-remote` on a fresh clone, so the
other eight hook types no longer pay for it. The listing carries its
expansion state in an `Option<String>` instead of re-deriving "was this
expanded?" from whether a context exists.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:44:29 -07:00
Worktrunk Bot c6eb148e62 docs(hook): correct pre-switch {{ branch }} resolution note (#3516) 2026-07-19 18:13:17 -07:00
Worktrunk Bot 67d2177029 docs(hook): split cwd exception bullet to match 'three cases' count (#3367) 2026-07-05 06:51:22 -07:00
Maximilian Roos 3d0b99e072 Add WORKTRUNK_VERBOSE env var equivalent to -v/-vv (#3166)
Shell tab-completion runs the `wt` binary as its own subprocess (the
shell sets `COMPLETE=<shell>`), and that path returns from `parse_cli`
before `main` ever reaches `logging::init`. So when a tab-completion is
slow, there's no flag that turns on logging for it — `-v`/`-vv` never
run, and `RUST_LOG` only sets a level, not the `-vv` file sinks. There
was no way to profile a slow completion.

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

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

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

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

### Testing

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

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:46:06 -07:00
Maximilian Roos a17fc22904 Add --config-set for inline TOML config overrides (#3138)
A global, repeatable `--config-set <toml>` flag that overrides any
user-config key for a single invocation, layered above config files and
`WORKTRUNK_*` env vars. The value is a real TOML fragment, so arrays and
tables work natively — no bespoke `key=value` grammar.

## Behavior

- **Precedence**: config files → `WORKTRUNK_*` env vars → `--config-set`
(highest).
- **Merge semantics**: a later override replaces an earlier one for the
same key; scalars and arrays replace the lower-layer value; nested
tables deep-merge, so `--config-set list.full=true` leaves sibling
`list.*` keys untouched.
- **Global**: works in any position (`wt --config-set … list` or `wt
list --config-set …`), like `-v` / `-y` / `--config`.
- **Graceful degradation**: a malformed, ill-typed, or invalid override
drops the whole `--config-set` layer with an attributed warning (`▲
Ignoring --config-set overrides: …`) and preserves the lower layers —
consistent with how the existing `WORKTRUNK_*` env overlay degrades.

## Why

This is the foundation for a follow-up that parameterizes `wt list`'s
column set without baking it into config: e.g. an alias `list-fast = "wt
--config-set list.columns=[...] list"` renders a smaller view while
plain `wt list` stays full. Doing it as a generic config-override lever
(rather than a `list`-specific flag) keeps one canonical path and
composes with aliases and any future config key.

## Implementation

`Cli.config_override` (`--config-set`, global, `Vec<String>`) is stashed
in a process-global `OnceLock` (mirroring `set_config_path`) and applied
in `UserConfig::load_with_warnings` via `apply_cli_overrides` as the top
layer. New `LoadError::CliOverride` variant carries the raw values for
attribution; the warning is rendered in `emit_user_config_warnings`.

## Testing

8 unit tests on `apply_cli_overrides` (sets,
deep-merge-preserves-siblings, last-wins, array-replace, malformed,
type-mismatch, validation-failure, empty) and 3 integration tests
(overrides-the-file, malformed-warns-attributed,
works-after-subcommand). Full suite green: lib + 1828 integration,
clippy clean, docs in sync.

> _This was written by Claude Code on behalf of max_

---------

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

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

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

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

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

## Smaller bits

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

## Testing

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

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

## Follow-up

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

Re #2838.

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

## What changes

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

## Semantic flip

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

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

## Reviewing this diff

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

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

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

## Testing

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

Part of #2838.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-20 19:31:50 -07:00
Maximilian Roos 408f4f5bee Add wt step tether: kill a command's process group when its worktree is removed
`wt step tether -- CMD…` runs CMD in its own process group and tears the whole
group down when CMD exits or its worktree is removed (a 250ms portable poll;
killpg on Unix, taskkill /T /F on Windows). Replaces the leaked-dev-server /
fseventsd-saturation failure mode with a fire-and-forget supervisor needing
only a single post-start hook. No unsafe, no new deps. Shell handling matches
`wt step for-each`. Windows taskkill has a documented self-exit-detach edge.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 11:57:16 -07:00
Maximilian Roos 7d09c5da58 fix(merge): gate commit-append separately so declining it keeps hooks (#2802)
## Summary

`wt merge` bundled the project commit-append into the hook-approval
batch and shadowed `verify = false` on *any* decline. When the project
hooks were already approved (and thus filtered out of the prompt),
declining the lone commit-append prompt skipped every
pre-merge/post-merge hook for that run — even though the user only meant
to skip the append. This was flagged as a follow-up during review of
#2774.

The append is now resolved through the shared
`approve_commit_template_append` gate — the same path `wt step commit` /
`wt step squash` already use — so its decline drops only the append and
never touches hook approval. Hooks are approved on their own; only a
hook decline skips hooks.

Trade-off: when both project hooks *and* the append are unapproved on a
fresh repo, the user now sees two prompts instead of one bundled prompt.
This matches the standalone commit/squash flow and is the canonical
behavior; the old single-prompt bundling was what introduced the
conflation. Already-approved appends still don't re-prompt.

## Also in this PR

- **Message canonicalization** — `merge`, `removal`, `prune`, and
`switch` now print `Commands declined, … without hooks`, matching the
wording `step squash` / `step commit` already used. `wt hook` and `wt
config approvals add` stay bare (they end on decline rather than
continuing an operation).
- **Docs** — the hook Security section now states that declining skips
every project command for that operation (already-approved ones
included) and that saved approvals are unaffected. Synced to the skill
reference.
- **Doc-comment fixes** — `HookGate` / `PreApprovedGuidance` comments no
longer describe the removed bundled-prompt flow.

## Testing

- New regression test `test_merge_decline_append_keeps_approved_hooks`:
with the hook pre-approved, declining the append still runs the
pre-commit hook and the merge succeeds (fails on the old code, passes
now).
- `test_merge_bundles_append_into_hook_approval` renamed to
`test_merge_prompts_hooks_and_append_separately`, updated for the
two-prompt flow.
- Four decline snapshots regenerated (message text only).
- Full pre-merge gate green: 3736 tests, clippy, fmt, doc-sync.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 11:42:04 -07:00
Worktrunk Bot a77c92e25b docs(help): point banner at the actual cli source path (#2665) 2026-05-10 08:14:37 +00:00
Jesse ecdb7bfc35 feat(config): add codename template filter (#2641) 2026-05-09 07:46:56 -07:00
Maximilian Roos c82236e034 refactor(config): rename path-traversal filters to dirname/basename (#2605)
\`name\` was too generic — likely to collide with future filters
operating on domain objects (a Branch, a worktree, a remote). Switch to
the POSIX/jinja-convention pair so both filters travel together: ansible
ships \`dirname\`/\`basename\`, shell users already know them, and
\`dirname\` is no more ambiguous than \`parent\` was.

Follow-up to #2592, which landed ~30 minutes ago — no users to break
yet.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-04 13:23:39 -07:00
Maximilian Roos 670cb2a7b0 feat(config): add parent and name path-traversal template filters (#2592)
Two new filters expose `Path::parent` and `Path::file_name` to
templates, enabling path traversal that previous filters couldn't
express. They unblock the bare-repo-in-hidden-directory layout
(`myproject/.git`), where `{{ repo }}` resolves to `.git`: users who
want a richer worktree-path naming scheme can now write `{{ repo_path |
parent | name }}` to recover `myproject`.

The interactive bare-repo prompt still suggests the simpler `{{
repo_path }}/../{{ branch | sanitize }}` template, since the bare layout
already nests worktrees inside a repo-specific wrapper directory — no
prefix needed for disambiguation. The filters are there for users with
different layout preferences (#1281 discussion).

Thanks to @seakayone for reporting #1279 and @Xilis for raising the
`parent_dir` question that prompted this approach.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-04 11:55:04 -07:00
Maximilian Roos 3593606283 refactor: route every short-SHA display through git's abbreviation logic (#2576)
Every site that abbreviated a commit SHA was either slicing `&sha[..7]`
or running its own ad-hoc `git rev-parse --short` call. 7-char prefixes
regularly collide in repos with many commits, and none of the slicing
sites honored `core.abbrev`. Cut over to a single canonical helper.

## Single helper, every display site

`Repository::short_sha(&str) -> Result<String>` wraps `git rev-parse
--short`. Routes display through git's own abbreviation logic so
`core.abbrev` is honored and prefixes auto-extend on collision. Used by:

- `step commit` / `step squash` success lines
- `step push --no-ff` `Merged to @ <hash>` line — the original bug.
Flagged on #2560 as a pre-existing third instance of the same pattern
fixed there in `commit.rs` and `step_commands.rs`.
- `{{ short_commit }}` template var in hook contexts
(`command_executor.rs`, `template_vars.rs`)
- post-remove hook context for the removed worktree
- safety-backup ref display (`create_safety_backup`)
- `(detached <sha>)` label in the orphan-check loop

All seven sites previously sliced `commit[..7]` or called their own
`rev-parse`. Now they route through one helper.

## Batched form for `wt list --format=json`

The JSON list path emits one `short_sha` per worktree row. Looping
`short_sha` would fork a subprocess per row, so the short SHA is folded
into the existing `commit_details_many` batch instead — `%h` is added to
the `git log --no-walk --format=...` call that already fetches timestamp
and subject. One subprocess for the whole list, same `core.abbrev`
behavior as every other site.

`CommitDetails` gains a `short_sha: String` field with the same
provenance as the timestamp and subject. The JSON schema is unchanged
(`commit.short_sha` was already a field) — only its length now varies by
`core.abbrev` instead of being hard-coded to 7.

## API change

`TemplateVars::with_active_commit(commit, short_commit)` now takes both
forms. Previously it sliced `commit.get(..7)` internally. The sole
caller (`worktree/finish.rs`) resolves the short form via
`Repository::short_sha` and passes both.

## Docs

`{{ short_commit }}` is no longer documented as "(7 chars)" —
`src/cli/mod.rs` and `src/config/project.rs` now describe `core.abbrev`
behavior. Doc-sync regenerated `docs/content/hook.md` and the skill
mirror.

## Tests

Full suite passes (3463/3463). `commit_details_many` tests updated for
the new tuple shape; `CommitDetails` fixtures in `layout.rs` get a
`short_sha` field; `template_vars` tests pass both forms explicitly. Two
integration tests previously asserting `short_commit` was exactly 7
chars (`post_start_commands.rs`, `user_hooks.rs`) still pass — fresh
test repos default to `core.abbrev = 7`.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-03 23:17:03 -07:00
Worktrunk Bot 5dae8d8e92 feat(config)!: cap sanitize_db output at 48 chars (#2467)
## Summary

- Lowers `sanitize_db`'s total output cap from 63 (PostgreSQL's
identifier limit) to 48, leaving headroom for users composing the output
into longer paths or identifiers — e.g., Unix socket paths capped at 107
bytes (#2397).
- The 3-char hash suffix is unchanged (still derived from the original
input), so collision avoidance is preserved at the new budget; only the
truncated base shrinks.
- **Breaking change** for branches whose current `sanitize_db` output
exceeds 48 chars: the truncated base shifts, so the final identifier
changes. Most branch names are well under 48 chars and pass through
unchanged. Users who relied on the previous output as a stable database
identifier should be aware.

Companion to #2453 (added the `hash` filter so users can compose their
own truncate-with-collision-avoidance recipes when 48 still isn't tight
enough).

Refs #2397.

## Test plan

- [x] `cargo test --lib --bins -- sanitize_db` — 5 unit tests pass
(including updated `test_sanitize_db_truncation`)
- [x] `cargo test --test integration -- sanitize_db` — 3 integration
tests pass (including updated `test_doc_sanitize_db_truncation`)
- [x] `cargo test --test integration test_docs_are_in_sync` — passes
(docs/skills auto-synced)
- [x] `cargo insta test --accept --test integration -- "test_help"` —
help snapshots regenerated; no surprise diffs

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-29 12:43:49 -07:00
Worktrunk Bot 719a102409 feat(config): add hash template filter (#2453) 2026-04-28 23:30:55 -07:00
Maximilian Roos 4cbc8ca5fd refactor(docs): drop mirrored help-page close; align convert_console_blocks_in_docs error channel (#2422)
## Summary

Three follow-ups from #2419 review.

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

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

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

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

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

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

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

## Test plan

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-26 02:18:39 -07:00
Maximilian Roos 66a5becc25 refactor(tests): consolidate three sync tests; drop dead inner snapshot wrappers (#2419)
## Summary

Two follow-ups from #2418 review.

- **Test brittleness fix.** Three previously-separate sync tests had
data dependencies that nextest could interleave:
`test_readme_examples_are_in_sync` reads `docs/content/*.md`, which
`test_docs_quickstart_examples_are_in_sync` generates from snapshots.
Under parallelism, README sync could see stale docs and produce content
that required a second run to converge. Collapsed both into the existing
`test_command_pages_and_skill_files_are_in_sync` pipeline, renamed to
`test_docs_are_in_sync`. Steps run sequentially in dependency order;
single pass converges from a clean working tree.

Each step's errors and updated-file list are tagged with the pipeline
stage (`[command pages]`, `[standalone docs]`, `[README]`, etc.) so a
failure tells a developer which stage broke without reading the test
source. README failure also now reports `(N of M section(s) updated)`.

- **Dead inner snapshot wrappers.** `expand_command_placeholders`
wrapped each terminal shortcode in command pages with `<!-- ⚠️
AUTO-GENERATED from <snap> --> ... <!-- END AUTO-GENERATED -->` markers.
Command pages regenerate wholesale from `--help-page` each sync, so the
inner wrapper served no in-place-refresh purpose — it was dead weight
nested inside the outer help-page region's markers. Stripped from
`expand_command_placeholders`'s HTML branch; net -32 lines across six
command-page docs files.

The outer help-page region's mirrored close stays — it's load-bearing
whenever any nested `AUTO-GENERATED` marker appears in the body. Comment
in `src/help.rs:emit_footer` updated to explain the role. (My follow-up
note that the mirrored close was redundant turned out to be a
misanalysis — non-greedy `.*?` only safely pairs the open with the right
close when there are no nested closes; once the inner snapshot wrappers
were gone, the mirror became technically unnecessary, but keeping it
survives any future reintroduction of nesting at no cost.)

Renamed the test in `CLAUDE.md` (4 sites) and `docs/CLAUDE.md` (2 sites)
so the documented `cargo test` invocations resolve.

Net diff: −54 lines.

## Test plan

- [x] `cargo test --test integration readme_sync` — 11 sync tests pass
(was 13; minus the two collapsed)
- [x] `cargo test --test integration` — full integration suite (1557
tests) pass
- [x] **Single-pass convergence verified**: `git checkout docs/
README.md && cargo test --test integration test_docs_are_in_sync && git
diff --stat` produces an empty diff after the first run
- [x] Visual check via local Zola dev server: \`/worktrunk/\`,
\`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`,
\`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal
blocks cleanly with no leaked marker strings
- [x] Adversarial review of the consolidation (subagent /popper-style):
all 4 actionable findings (stale doc references, lost README count,
missing stage tags, etc.) addressed
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 19:20:44 -07:00
Maximilian Roos 1e053d2d88 refactor(docs): fix blank-line cmd-stream corruption; share AUTO-GENERATED marker constants (#2417)
## Summary

Three small follow-ups + a placeholder rename, all from #2405 review
feedback.

- **Root-cause fix for `|||` corruption.**
`convert_dollar_console_to_terminal` was promoting blank lines inside
mixed `\$ cmd + output` blocks to extra `cmd=` entries, dropping the
blank from the rendered body and emitting a stray `\$` prompt (e.g.
`cmd=\"wt list|||\"`). Now distinguishes command-only blocks (blanks are
visual spacing in `cmd=`) from mixed blocks (blanks belong to body).
Removes the matching `trim_end_matches('|')` bandage in
`expand_command_placeholders`. New unit test in `src/docs.rs` covers the
mixed-block case.
- **Single-pass classification.** Folded the two filter chains over
`block_lines` into one loop with one match — fewer branches, no
duplicated predicates.
- **AUTO-GENERATED marker consolidation.** Within
`tests/integration_tests/readme_sync.rs`, the `<!-- ⚠️ AUTO-GENERATED
... -->` literal appeared in four producer / regex sites with subtly
different shapes (HTML vs plain, ID format). Factored into
`MARKER_OPEN_PREFIX` / `MARKER_OPEN_HTML_PREFIX` / `MARKER_CLOSE`
constants plus a `wrap_in_marker()` helper, and threaded those constants
into the regexes via `regex::escape`. Pure refactor — no docs/skill
files regenerated.
- **`__WT_OPEN2__` / `__WT_CLOSE2__` → `__WT_OPEN__` / `__WT_CLOSE__`.**
The `2` was meant to distinguish doubled-brace placeholders from
hypothetical single-brace ones, but Tera only treats `{{`/`}}` (not
single braces) as template delimiters — there's no second variant to
disambiguate from. Updated the producer (`src/docs.rs`), the test-side
decoder (`tests/integration_tests/readme_sync.rs`), the Zola template
(`docs/templates/shortcodes/terminal.html`), the docs-site `CLAUDE.md`,
and the regenerated `docs/content/{step,hook}.md`.

## Test plan

- [x] `cargo test --lib
docs::tests::test_convert_dollar_console_to_terminal` — exercises new
mixed-block case + existing command-only / multi-cmd / comment cases
- [x] `cargo test --test integration readme_sync` — 13 sync tests still
pass after the refactor and bandage removal
- [x] `cargo test --test integration` — full integration suite (1558
tests) pass
- [x] Visual check via local Zola dev server: `/merge/`, `/step/`,
`/remove/`, `/hook/`, `/llm-commits/`, and `/list/` all render terminal
blocks cleanly with no `|||` artifacts and no leaked placeholder strings
(`__WT_OPEN__` etc. don't appear in served HTML). The multi-command jq
recipes block on `/list/` continues to render comments as bash-styled
section headers.
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 16:21:24 -07:00
Worktrunk Bot 5f126d21fe docs(pages): add static command output to sections dominated by GIFs (#2405)
## Summary

Adds static command-output blocks to the docs pages dominated by GIFs
(addresses #2403). The blocks are **driven from insta snapshots** so
they stay in lockstep with what `wt` actually prints — one source flows
to all three surfaces:

- terminal `wt <cmd> --help` (plain text, gutter-formatted)
- `docs/content/*.md` (colorized `{% terminal(cmd="...") %}` shortcode)
- `skills/worktrunk/reference/*.md` (plain `$ cmd\noutput\n` block)

## What changed

- **Six scripted snapshot tests** produce realistic output (`cargo
nextest run`, `flyctl scale count 0`, LLM-generated commit messages):
  - `test_docs_merge_pre_merge_hook` — `wt merge` with pre-merge hook
  - `test_docs_step_commit_llm` — `wt step commit` with LLM
  - `test_docs_step_squash_llm` — three-commit squash with LLM
- `test_docs_merge_squash_llm` — `wt merge` (squash + LLM + merge) for
`llm-commits.md`
- `test_docs_remove_pre_remove_hook` — `wt remove` with pre-remove hook
  - `test_docs_hook_pre_merge` — `wt hook pre-merge` direct invocation

- **Sync pipeline extension** in
`tests/integration_tests/readme_sync.rs`:
- New write-back pass `sync_cli_mod_example_bodies` fills the
```console``` body in each `<!-- wt <cmd> (docs-example) -->`
placeholder in `src/cli/mod.rs` from the registered snapshot. Runs
before `--help-page` reads the file.
- `COMMAND_PLACEHOLDER_PATTERN` extended to match three forms
(```bash```, `{{ terminal() }}` self-closing, `{% terminal %} body {%
end %}`). This **fixes a pre-existing bug** in `docs/content/list.md`
where the HTML-mode expansion was silently broken.
- Stripped trailing `|||` corruption that arises when blank lines in
snapshot bodies are interpreted as empty commands by
`convert_dollar_console_to_terminal`.

- **Docs page migration**:
- `src/cli/mod.rs` — replaced four hand-written ```console``` blocks
(merge, step, remove, hook) with `<!-- wt <cmd> (docs-example) -->`
markers.
- `docs/content/llm-commits.md` — replaced three hand-crafted HTML
blocks with `<!-- ⚠️ AUTO-GENERATED-HTML from X.snap -->` markers.

- **Refactor follow-up** in a separate commit:
- `BADGE_EXPERIMENTAL_HTML`, `SUBDOC_MARKER_PREFIX`,
`DEMO_MARKER_PREFIX` hoisted to `worktrunk::docs` so producer and
consumer stay in lockstep.
- `normalize_clap_help_fences()` consolidates the `text→` +
`console→bash` replacement pair shared by `--help-md` and
`help_reference_inner`.

- **CLAUDE.md note** documenting the `.gitattributes`
`linguist-generated=false` exemption requirement when adding skill-only
files (carryover from #2409).

## Test plan

- [x] `cargo test --test integration readme_sync` — all 13 tests pass,
idempotent
- [x] `cargo test --test integration test_help` — help snapshots updated
- [x] `cargo run -- {merge,remove,step,hook} --help` — clean gutter
formatting, no visible HTML comments
- [x] `cargo run -- hook pre-merge --yes` — full project gate green
(3355 tests)
- [ ] Visual check on dev site (maintainer — terminal shortcodes now
render with colors via ANSI→HTML; dark/light variants both expected to
look like the GIFs they replace)

🤖 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.7 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-04-24 16:40:21 -07:00
Worktrunk Bot 3a9d6402cb docs(hook): clarify when pre-start/post-start fire (#2360)
## Summary

Refs #1571. Adjusts the existing `pre-start` and `post-start` rows in
the hook types table to say when the hooks fire, rather than only what
to put in them.

Before:

```
| `pre-start` | Tasks that must complete before `post-start`/`--execute`: dependency install, env file generation |
| `post-start` | Dev servers, long builds, file watchers, copying caches |
```

After:

```
| `pre-start` | Runs once when a new worktree is created, blocking `post-start`/`--execute` until complete: dependency install, env file generation |
| `post-start` | Runs once when a new worktree is created, in the background: dev servers, long builds, file watchers, copying caches |
```

Matches the phrasing of neighbouring rows (e.g. `pre-switch` has \"Runs
before...\", `post-merge` has \"Runs in the target...\"). No new section
or explanation added — the adjustment is confined to the two rows whose
definitions were silent on timing.

Per @max-sixty's ask in
https://github.com/max-sixty/worktrunk/issues/1571#issuecomment-4291227117,
marked as draft for review.

## Test plan

- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — passes after
regenerating `docs/content/hook.md` and
`skills/worktrunk/reference/hook.md`
- [x] `cargo insta test --accept -- --test integration test_help` — no
snapshot changes (the row is inside the table rendered from
`after_long_help`; existing help snapshots already absorbed the old
wording in their expected forms but don't anchor on it)

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 13:48:58 -07:00
Maximilian Roos 771918f29e docs(hook): promote Recipes to top-level H1 (#2351)
Follow-up to #2349. With the old "Designing Effective Hooks" umbrella
heading removed, `## Recipes` was landing at H2 under `# Running Hooks
Manually`, reading as a sub-topic of manual invocation — which it isn't.
Promoting to `# Recipes` makes it a peer of the other top-level sections
(`# Hook Types`, `# Security`, `# Configuration`, `# Running Hooks
Manually`).

Trailing `## See also` stays at H2 — consistent with how other command
docs handle their "See also" footers.

> _This was written by Claude Code on behalf of Maximilian_

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 22:15:14 -07:00
Maximilian Roos 970375afd4 docs(hook): restructure recipes list and callout for copy-ignored (#2349)
Three tweaks to the hook docs:

- Drop the "Designing Effective Hooks" heading and rename "More recipes"
to "Recipes".
- Make each recipe bullet lead with a specifically-named link to its
Tips & Patterns section, so the list functions as a table of contents
rather than a paragraph with a trailing URL.
- Move "Copying untracked files" up next to the JSON context section and
frame it as a specific command worth calling out, rather than leaving it
as a lone sibling of "Recipes" under the old umbrella heading.

Source is `after_long_help` in `src/cli/mod.rs`; `docs/content/hook.md`
and `skills/worktrunk/reference/hook.md` are regenerated by the sync
test. Dead post-processor entries in `src/help.rs` that used to convert
the old bare-URL bullet format are removed.

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 21:40:18 -07:00
Maximilian Roos f0152a5a0d docs(hook): consolidate pipeline forms into one section (#2333)
The \`# Configuration\` intro introduced the string/table/pipeline
forms, and a later \`# Pipeline Ordering [experimental]\` section
re-explained the same three forms with different examples.

Merge them under a single \`## Hook forms\` subsection inside \`#
Configuration\`, keep the "when to use \`[[hook]]\`" guidance, and drop
the stale \`[experimental]\` tag. Update the two \`#pipeline-ordering\`
anchor links in \`extending.md\` to \`#hook-forms\`.

Net: 6 files, -137 lines. No behavior change — help output unchanged (no
snapshot updates needed).

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 00:40:41 -07:00
Maximilian Roos 18825c3e3c docs(hook): move progressive-validation and target-specific examples to tips-patterns (#2329)
Keeps "Copying untracked files" inline (the one example that materially
uses worktree-specific mechanics) and moves the two plain-TOML patterns
— progressive validation and target-specific `post-merge` — to
`tips-patterns.md`. `hook.md` references them as `More recipes` bullets
instead.

> _This was written by Claude Code on behalf of Maximilian._

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 00:21:16 -07:00
Maximilian Roos 0be0a28eae docs(hook): drop pre-start vs post-start section (#2326)
The section mostly restated content already covered by the "Hook Types"
table and the hook purpose table above it. Removing it and folding the
load-bearing guidance ("prefer `post-start` unless a later step needs
the work completed first") into the existing "most common starting
point" sentence near the top.

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 23:50:53 -07:00
Maximilian Roos 064f8940c0 docs(hook): consolidate copy-ignored recipe and trim redundant examples (#2323)
Docs consolidation sweep, same pattern as #2319.

**Copy-ignored recipe** — collapses hook.md's "Copying untracked files"
overlap with tips-patterns.md into a "More recipes" bullet pointing at
the canonical recipe. Keeps a one-paragraph intro in hook.md (worktrees
don't share untracked files → use `wt step copy-ignored`) since it's a
fundamental worktree concept. Moves the concrete pnpm pipeline example
into tips-patterns.md's canonical recipe, and switches it from
`[[pre-start]]` to `[[post-start]]` — the pipeline form handles ordering
without blocking worktree creation. `pre-start` is now called out only
as the exception for when `--execute` needs the files immediately.

**Trims**
- Dropped hook.md's "Hook type examples" dump: most of its 10 entries
duplicate existing recipes (dev server, database, cold starts) or the
Progressive validation section.
- Dropped tips-patterns.md's "Local CI gate": four-line recipe that
duplicated hook.md's Progressive validation and merge.md's Local CI
narrative.
- Dropped hook.md's "Python virtual environments" snippet: step.md
already owns language-specific notes.

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

---------

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

## What's in here

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

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

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

## Example

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:39:29 -07:00
Maximilian Roos df448861bc docs(hook): link dev-server/database recipes out to tips-patterns (#2319)
The Dev servers and Databases sections in `hook.md` duplicated nearly
identical TOML and narration from the corresponding recipes in
`tips-patterns.md`. Replaces them with two bullets under a new "More
recipes" section that link to the canonical recipes, and redirects
inbound links (`README.md`, `worktrunk.md` feature list) from
`/hook/#dev-servers` to `/tips-patterns/#dev-server-per-worktree`.

The bullets use bare URLs in `cli.rs` so `wt hook --help` shows
terminal-auto-linkable `https://...` rather than stripped markdown link
text; `post_process_for_html` rewrites them to inline markdown links for
the web docs. Same pattern as the existing "Open an issue at ..."
transform.

Also tightens the extending.md note on operation-context variables to
"aren't auto-populated" — they can still be bound via `--KEY=VALUE` on
the CLI.

Net −156 lines.

> _This was written by Claude Code on behalf of Maximilian_

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 22:36:08 -07:00
Maximilian Roos eec3b24883 Show resolved template variables under -v for hooks (#2316)
Closes #2309.

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

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

## Key files

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

## Testing

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:28:43 -07:00
Maximilian Roos e4fab6c791 feat(hook): unify hook argument syntax with alias smart routing (#2313)
## Summary

Hooks and aliases now share a single mental model for CLI variable
binding: `--KEY=VALUE` binds `{{ KEY }}` if the template references it,
else forwards to `{{ args }}`. Tokens after `--` forward
unconditionally. `{{ args }}` is now available in hook templates.

Internally, the 11 duplicated hook clap variants collapse into a single
`#[command(external_subcommand)]` mirroring the alias pattern, with
completion and help injection grafting hook-type stubs onto the
augmented `Command` tree.

- `--var KEY=VALUE` still works but emits a deprecation warning pointing
at `--KEY=VALUE`.
- `wt hook --help` lists all hook types (via help-tree injection).
- New `## Reference: hooks vs. aliases` section in
`docs/content/extending.md` documents remaining interface differences.

## Test plan

- [x] `cargo run -- hook pre-merge --yes` (all tests + lints, 3283 tests
passing)
- [x] `wt hook pre-merge --branch=foo --yes` — smart-binds when
referenced
- [x] `wt hook pre-merge --KEY=VALUE --yes` — forwards to `{{ args }}`
when not referenced
- [x] `wt hook pre-merge -- --extra --yes` — post-`--` tokens forward
unconditionally
- [x] `wt hook pre-merge --var branch=foo --yes` — deprecation warning +
force-bind
- [x] `wt hook --help` shows all 10 hook types
- [x] Shell completion: `wt hook <TAB>` lists hook types

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 21:38:42 -07:00
Maximilian Roos 0639b6bf0e docs(hook): group template variables by kind and align ordering (#2303)
## Summary

The hook template-variable surface had drifted: `BASE_VARS`,
`hook_extras`, and the user-facing help table each used a different
order. After #2300 added `pr_number`/`pr_url`, that drift got worse —
the new vars landed at the bottom of the help table next to hook
infrastructure, even though semantically they're operation context (they
travel with `base`/`target`, populated by the same `pr:N`/`mr:N` code
path).

This PR reorganises around five semantic groups and applies the same
ordering everywhere:

| Kind | Vars |
|------|------|
| `active` | branch, worktree_path, worktree_name, commit, short_commit,
upstream |
| `operation` | base, base_worktree_path, target, target_worktree_path,
pr_number, pr_url |
| `repo` | repo, repo_path, owner, primary_worktree_path,
default_branch, remote, remote_url |
| `exec` | cwd, hook_type, hook_name |
| `user` | vars.\<key\> |

## Changes

- `BASE_VARS` reordered into active → repo/remote → exec(`cwd`), with a
doc-comment pointing at the help table as the canonical order.
- `hook_extras` doc-comment requires each arm to be a prefix-ordered
subset of the operation-context block.
- Help table gains a `Kind` column printed once per group (blank on
continuation rows). `pr_number`/`pr_url` move up to `operation`; `cwd`
moves down to `exec`; the word "Active" is dropped from the first six
descriptions since `Kind` now carries that signal.
- Auto-generated docs (`docs/content/hook.md`,
`skills/worktrunk/reference/hook.md`) re-synced by
`test_command_pages_and_skill_files_are_in_sync`.

No behaviour change — ordering only, plus the new Kind column.

## Follow-ups (not in this PR)

Add a test that enforces alignment across the three sites — the
help-table row order, `BASE_VARS`, and each `hook_extras()` arm must all
be prefix-subsets of one canonical ordered list. Would prevent the drift
that #2300 introduced.

## Test plan

- [x] `cargo run -- hook pre-merge --yes` — clippy + 3272 tests +
doctests + `RUSTDOCFLAGS=-Dwarnings cargo doc`
- [x] `test_command_pages_and_skill_files_are_in_sync` passes
- [x] `test_help` rstest suite passes (no changes — it doesn't cover `wt
hook --help`)

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:32:34 -07:00
Maximilian Roos 68ee0a327b feat(hook): expose pr_number and pr_url to PR/MR worktree hooks (#2300)
## Summary

`pr_number` and `pr_url` are now first-class template variables for
hooks running on PR/MR-created worktrees (`wt switch pr:N` / `mr:N`).
Previously the ForkRef code path injected `pr_*` (GitHub) or `mr_*`
(GitLab) extras into the pre-start template context, but those names
weren't in the validation allowlist — any user hook referencing them was
rejected before the hook could run, so the feature was unreachable.

This PR canonicalizes on a single pair (`pr_number`/`pr_url`) for both
platforms, threads it through the validation scope, plumbs it into
post-switch and post-start hooks via `SwitchResult::Created`, and
documents it in CLI help (which auto-syncs to docs and skill reference).

## Notable decisions

- **One canonical pair, not two.** GitHub and GitLab both populate
`pr_number`/`pr_url`; no `mr_*` aliases. Keeps the template surface
low-cardinality.
- **Symmetric across pre/post.** The `hook_extras` table accepts
`pr_number`/`pr_url` for `pre-switch`/`post-switch` and
`pre-start`/`post-start`. Pre-switch never actually populates them (PR
resolution hasn't happened yet at that point), but grouping pre/post
pairs matches the existing `base`/`target` convention and avoids a
one-off scope arm.
- **Threaded through `SwitchResult::Created`.** Earlier drafts only
wired pre-start; post-switch and post-start were silently dropping the
data. The Option fields on `SwitchResult::Created` carry it forward to
background hooks via `switch_extra_vars`.

## Test coverage

- `test_validate_template_scope_rejects_out_of_scope_vars` — accepts
`pr_number`/`pr_url` for pre-start, rejects for pre-merge.
- `test_switch_pr_hooks_see_pr_vars` — fork-PR scenario with mocked
`gh`; pre-start, post-start, and post-switch hooks each write a marker
file and the test asserts all three observe `pr_number=42 pr_url=...`.

## Drive-by fix: stub cargo in --source flag test

Last commit replaces `cargo run --bin wt` in
`test_source_flag_forwards_errors` with a stub-cargo shell script that
execs the existing wt binary directly. Real cargo unlinks and re-links
`target/debug/wt` on every invocation (~3.9% non-existence window
measured locally), racing against any concurrent test that
`spawn(target/debug/wt)` and producing the long-standing ENOENT flake in
`test_wrapper_switch_with_hooks` on Linux CI. Same end-to-end coverage
of the `--source` branch; runs in 2s instead of 30–60s.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 10:57:58 -07:00
Maximilian Roos 1fd6eaba20 fix(hook): inject target symmetrically on switch/create/start (#2295)
`pre-switch` already injected `target` (and conditionally
`target_worktree_path`), but `post-switch`, `pre-start`, and
`post-start` only injected `base`. A user writing `{{ target }}` in
`post-start` expecting the documented "target = bare vars" semantics
would hit an undefined-var error at runtime — runtime and docs
disagreed.

This injects `target` (skipped for detached HEAD) and
`target_worktree_path` (always present on switch/create since the
worktree just got created) in `handle_switch.rs` post-switch and
`worktree/switch.rs` pre-start (both `Regular` and `ForkRef` arms). The
commit hook injection (one-worktree, `target` only) is left untouched —
integration target may not have a worktree, and the existing behavior
already matches the design.

After merging main, the scope-aware validation refactor (#2288) had
landed with `PreStart`/`PostStart` whitelisting only
`base`/`base_worktree_path`. Adds `target`/`target_worktree_path` to
that whitelist so validation accepts the vars the runtime now injects,
and updates the stale `PreSwitch | PostSwitch` comment (post-switch now
injects target too). The
`test_validate_template_scope_rejects_out_of_scope_vars` case that
checked `{{ target }}` rejection in `PreStart` swaps to `{{ base }}` in
`PreMerge` — still proves scope rejection since `base` remains
switch/start-only.

Docs: adds a commit row to the `base`/`target` table scoped to
merge/squash context (standalone `wt step commit` doesn't inject
`target`), and rewrites the "only in two-worktree hooks" sentence to
describe each conditional var accurately.

Tests: adds one post-start case (create path) and one post-switch case
(existing-worktree switch path) that assert `{{ target }}` resolves to
the destination branch. The `post_create_upstream_template` snapshot
picks up `target` / `target_worktree_path` in its "Available variables"
error message — expected side effect of making them always-injected on
the switch path.

Out of scope: `pr_number`/`mr_number`/`pr_url`/`mr_url` (undocumented
ForkRef-only vars, separate concern).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-18 14:41:54 -07:00
Maximilian Roos 1a4eea3c03 feat(cli): promote --yes to a global flag (#2279)
## Summary

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

## Why

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

## Call-site survey

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

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

## Alias compat

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

## Navigating the diff

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

## Notes

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

## Test plan

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

> _This was written by Claude Code on behalf of Maximilian_

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-04-18 11:08:41 -07:00
Maximilian Roos 64e1b4efb9 feat(cli): move approvals subcommand from hook to config (#2282) 2026-04-18 00:46:27 -07:00
Maximilian Roos ea9a52dfdc feat(hooks): enable concurrent execution in pre-* pipeline blocks (#2249)
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>
2026-04-15 19:03:31 -07:00
Maximilian Roos d4c80ca970 docs(hook): use concurrent form in multi-key hook examples (#2248)
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>
2026-04-15 16:22:34 -07:00
Maximilian Roos 3e1b351ac1 feat(log): split -vv output into trace.log + output.log, drop -vvv (#2201)
## Motivation

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

## Change

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

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

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

## Fallback path

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

## Key files

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

## Testing

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-13 10:50:18 -07:00
Maximilian Roos d7680a27c6 feat(config): add sanitize_hash template filter (#2172)
## 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>
2026-04-12 22:18:01 -07:00
Maximilian Roos 1e51526601 docs(hook): tighten pre-* table-form deprecation note (#2154)
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>
2026-04-12 19:54:19 -07:00
Maximilian Roos 2a6389a092 Centralise [wt-trace] emitter and fix -vv log verbosity (#2146)
## Summary

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

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

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

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

## Reviewer navigation

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

## Testing

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 18:17:33 -07:00
Maximilian Roos 7e12435b33 docs(hook): teach pipelines as [[hook]] blocks; add TOML notes to config::commands (#2149)
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>
2026-04-12 18:12:33 -07:00