Commit Graph

39 Commits

Author SHA1 Message Date
Worktrunk Bot d44d68797a docs(extending): add workz to custom-subcommand examples (#3513) 2026-07-18 16:40:53 -07:00
Maximilian Roos 568b6de85f docs: consolidate duplicated explanations and trim slop (#3494)
Sweep of the docs for repetition and filler, from an audit of the
hand-authored pages, the command pages' source in `src/cli/mod.rs`, and
the plugin skill. Net −574 lines; every cut either had a surviving
canonical home or restated an adjacent sentence.

**One home per mechanism** (other mentions now link to it):

- `template-append` — the LLM commits guide owns the rendering
mechanism; the user- and project-config sections keep the key, an
example, and what is unique to them (the project approval gate and the
only-`template-append`-from-project scoping). Was explained in full in
three places.
- `-vv` diagnostic files — `wt config state logs` owns the four file
descriptions; the FAQ summarizes in one sentence and links.
- fsmonitor/trash cleanup — the FAQ's two overlapping `wt remove`
bullets merge into one that distinguishes own-daemon teardown from the
orphan sweep; troubleshooting.md's restatement compresses to a pointer,
keeping its unique wedged-daemon-on-live-worktree guidance.
- copy-ignored built-in excludes — the `wt step copy-ignored` page owns
the directory list (previously enumerated 4×); the config sections state
the rule and link.
- hook-types table — `wt hook` owns it; the extending guide replaces its
verbatim copy with a sentence.
- LLM tool commands — unchanged, deliberately: the apparent hand-synced
duplication between llm-commits.md and the config example is already
machine-pinned by `test_llm_docs_commands_match_config_example`, so it
cannot drift.

**Cut-over debt**: the deprecated `wt config state ci-status` section
shrinks to a deprecation pointer — its status table, fetch order, and
caching notes all duplicated the `wt list` CI-status section.

**Structure**: `wt step copy-ignored`'s "Features" list dissolves into
the sections that owned its facts (excludes → "What gets copied",
reflink → "Performance"); the four trailing "Note: This command is
experimental…" lines go (the `[experimental]` badge already appears
twice per section); shell-integration.md described the directive-file
mechanism twice and now describes it once.

**SKILL.md** drops from 339 to 181 lines: the permission-model section
duplicated the config-types bullets, the hook-type mapping appeared
twice, and the "Determining Which Config to Use" / "Validation Before
Adding Commands" scaffolding enumerated judgments an agent makes on its
own. The approvals-escalation and agent-handoff sections are untouched.

**Deliberately not addressed**: `wt list`'s schema 1 JSON tables (still
the default schema; the wholesale deletion lands when the default flips)
and corpus-wide em-dash density (a house-style decision, not a per-page
fix).

Generated mirrors (docs command pages, skill references, plugins mirror,
`dev/*.example.toml`, help snapshots) regenerated via
`test_docs_are_in_sync` and `cargo insta`.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 13:44:44 -07:00
Zexin Yuan 22484860e8 feat(completion): mirror wrapped built-in for alias arg completion (#3172)
An alias that forwards `{{ args }}` to a wt command — e.g. `co = "wt
switch {{ args }}"` or `cm = "wt step commit {{ args }}"` — now inherits
that command's argument and flag completion instead of the generic
`--dry-run`/`--yes`/`--var` stub. Alias name completion already worked;
this adds argument and flag completion.

Detection: the single command that forwards `{{ args }}` (via
`template_references_var` — minijinja, not a substring, and scoped per
command rather than the cross-command union), a leading `wt`, then a
tree-walk `find_subcommand` that lands on a leaf subcommand. The leaf
`Command` is cloned and renamed to the alias (clap `Command`/`Arg`/
`Extensions` and `ArgValueCompleter` are all `Clone`; the completer is
`Arc`-backed, so the live git query survives the clone). Bare
dispatchers (`wt step {{ args }}`), multiple `{{ args }}` forwarders,
and aliases that do not forward args fall back to the generic stub.

Mirroring runs at the top level only; `wt step <alias>` keeps the
generic stub.
2026-06-23 09:38:50 -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 1efbcdd718 docs(extending): writing-prose cleanup (#2912)
Two-pass writing-prose cleanup of `docs/content/extending.md`.

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:08:21 -07:00
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
Worktrunk Bot 4cfc8e6acd docs(extending): note alias template timing and how to defer to nested wt (#2754)
Addresses [#2753](https://github.com/max-sixty/worktrunk/issues/2753) —
adds a short subsection under Aliases documenting that templates render
at alias dispatch (using the alias-invocation worktree's context), so a
nested `wt` command's own template variables resolve against the outer
worktree unless wrapped in `{% raw %}…{% endraw %}`.

The example uses the reporter's pattern (`wt switch ... --execute 'echo
{{ worktree_path }}'`) verified against the release binary in a scratch
repo:

- Direct: `wt switch other --no-cd --execute 'echo {{ worktree_path }}'`
-> target path
- Aliased without `{% raw %}`: -> outer worktree path (the bug)
- Aliased with `{% raw %}{{ worktree_path }}{% endraw %}`: -> target
path

`test_docs_are_in_sync` propagates the change into
`skills/worktrunk/reference/extending.md`.

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 09:56:58 -07:00
Maximilian Roos b12ac078b3 docs(extending): add stdin row to hooks-vs-aliases comparison (#2529)
The comparison table in `extending.md` covered invocation, positional
handling, approval flags, source filters, and template-context extras —
but never said anything about stdin. Hooks have always received the
template context as JSON on stdin (documented in `hook.md`), and since
#2380 aliases inherit the parent's stdin so pipes pass through and
interactive TUIs (`wt switch`) keep the tty. Adding a row makes the
contract discoverable from the aliases page rather than only from the
CHANGELOG.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-02 10:55:33 -07:00
Worktrunk Bot 6eaf349711 refactor(for-each): exec argv directly, drop implicit shell (#2465)
## Problem

`wt step for-each` rebuilt the post-`--` command with `args.join(" ")`
before passing it to `sh -c`. That dropped the user's quoting and argv
boundaries, so anything with spaces, `;`, or shell metacharacters inside
an argv element broke. The reproduction from the issue:

```console
$ wt step for-each -- python3 -c 'import sys; print(sys.argv[1:])' 'a b'
sh: 1: Syntax error: word unexpected (expecting ")")
```

## Solution

Drop the implicit shell. The post-`--` argv is the program and its
arguments, exec'd directly via `Cmd::new(program).args(rest)`. Templates
expand into argv elements with `shell_escape=false`. Users wanting shell
features (pipes, redirects, `$VAR`, globs) write `sh -c '<snippet>'`
explicitly — the same pattern as `xargs`, `find -exec`, `kubectl exec
--`, and `docker run`.

This started as the smaller fix that landed on the previous commit
(shell-escape each argv element before joining, with an arity branch —
one arg meant "shell snippet," many args meant "argv to escape and
re-join"). On review the arity asymmetry felt wrong; the design
discussion concluded that direct exec is one rule with no quoting
recovery pipeline, and the #2461 bug becomes structurally impossible.
`for-each` is still tagged `[experimental]`, so the small breakage of
the documented snippet form is acceptable.

After the fix:

```console
$ wt step for-each -- python3 -c 'import sys; print(sys.argv[1:])' 'a b'
['a b']

$ wt step for-each -- sh -c 'git status | wc -l'
[per-worktree counts...]
```

### What changes for users

| Before | After |
|---|---|
| `wt step for-each -- 'git status \| wc -l'` | `wt step for-each -- sh
-c 'git status \| wc -l'` |
| `wt step for-each -- 'echo Branch: {{ branch }}'` | `wt step for-each
-- echo 'Branch: {{ branch }}'` (or wrap in `sh -c`) |
| `wt step for-each -- python3 -c '...' 'a b'` ✗ broken | `wt step
for-each -- python3 -c '...' 'a b'` ✓ works |

The `for-each` help/docs are rewritten around the simpler model (one
rule, two example blocks).

## Testing

- `test_for_each_preserves_argv_quoting` covers the exact reproduction
from the issue and is now exercising a load-bearing property of the
design rather than a join bug.
- `test_for_each_aborts_on_signal_exit` rewritten to use `sh -c
'<snippet>'` for the shell features it needs.
- `test_for_each_json_spawn_failure` simplified — direct exec means a
missing program is enough; the PATH/symlink dance for forcing sh-spawn
failure is no longer needed.
- `cargo test --test integration for_each` — 16 passed.
- `cargo run -- hook pre-merge --yes` — 3403 passed, 0 skipped, all
lints green.

---
Closes #2461 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-04-30 12:37:34 -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 1a0a3e2f83 Widen Zola link regex to tolerate code spans in link text (#2327)
`ZOLA_LINK_PATTERN`'s link-text class `[^\]]+` rejected any `]`, so Zola
links whose text contained a code span with `]]` (e.g. `` `[[block]]`
``) never matched and shipped as dead `@/…md` references in skill files.
#2321 worked around two such links by rewording them — this fixes the
root cause and restores the original wording.

## The regex change

Alternate "a balanced `` `…` `` code span" with "any
non-`]`-non-backtick char" inside the link text:

```
\[((?:`[^`]*`|[^\]`])+)\]\(@/([^)#]+)\.md(#[^)]*)?\)
```

Forbidding bare backticks in the single-char branch is load-bearing:
without it, the regex can bridge across two unrelated code spans on the
same line. `docs/content/faq.md:63` is a live example — ``
[`worktrunk-sync`](https://github.com/…) `` followed much later by ``
[custom subcommands](@/extending.md#custom-subcommands) ``. With a
permissive single-char branch, the engine pairs backtick 2 with backtick
3 (spanning the `]` after `worktrunk-sync`) and matches the whole chunk
as one "link."

## Guardrail

Added an `UNTRANSFORMED_ZOLA_LINK_PATTERN` check in
`finalize_skill_content`. After the transform, any leftover `](@/…md)`
panics the sync test with the offending line — so a future regex miss
fails loudly instead of silently shipping a dead link.

## Doc restoration

Reverts the two rewordings from #2321 in `docs/content/extending.md`
(`[[block]]` now sits inside the link text again).

## Testing

`cargo test --test integration readme_sync` (12 tests) passes. Full
`cargo run -- hook pre-merge --yes` green locally (3291 tests, all
lints).

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 23:57:48 -07:00
Maximilian Roos d70e2f2e98 Clarify template engine scope in extending docs (#2321)
Tighten the extending docs around the template engine and pipeline
syntax shared by hooks and aliases. Link to `wt hook`'s
template-variables, filters, functions, pipeline-ordering, and
passing-values sections so binding rules and `[[block]]` semantics are
documented in one place rather than duplicated. Remove the redundant
`--KEY=VALUE` / variable-override / hyphen-to-underscore explanations
from the aliases section.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 23:34:43 -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 566f399331 docs: explain why aliases and hooks render --help differently (#2318)
Hooks inject stub subcommands into the Command tree so `wt hook --help`
and `wt hook <type> --help` render real clap output. Aliases don't —
their argument parsing is template-driven (smart routing based on which
vars the template references), `--dry-run` is rejected, and post-alias
`--yes` forwards to `{{ args }}`, so a clap stub would misrepresent
them. The help path text-splices an `Aliases:` block into `wt --help` /
`wt step --help` and redirects `wt <alias> --help` to `wt config alias
show` / `dry-run` instead.

Nothing actually changes. This PR surfaces the reason in the two doc
comments next to the relevant functions (`inject_alias_subcommands`,
`augment_help`), and rewrites the `--help` row of the `extending.md`
"hooks vs. aliases" reference table in user-facing terms — the previous
wording leaked "clap-rendered" as user-visible jargon.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 22:20:19 -07:00
Maximilian Roos 86731e8cc2 docs(extending): refine alias recipes and template prose (#2317)
## Summary

- Tighten the **move-changes** and **hook-log** alias recipes; cut the
"actual branch doesn't move" aside on template variable overrides.
- `move-changes` now forwards tokens after `--` into the new worktree
via `--execute`, so `wt move-changes --to=feature-xyz -- claude` pops
the stash and opens Claude there.
- `hook-log` takes `--kind` so it isn't locked to `post-start`; prose
clarifies that `--name` is the TOML hook key and branch is pulled from
the current worktree.
- Reword the arg-escaping paragraph to explain *why* shell-escaping
matters (spaces don't split, `;` doesn't terminate), and clarify that
`--` forwards tokens literally into `{{ args }}`.

## Test plan

- [x] \`cargo run -- hook pre-merge --yes\` (3283 passed, lints clean)
- [x] \`cargo test --test integration
test_command_pages_and_skill_files_are_in_sync\`
- [x] Verified \`wt move-changes --to=foo -- claude\` renders
\`--execute="git stash pop --index; claude"\` via \`wt config alias
dry-run\`

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 22:20:17 -07:00
Maximilian Roos a77edaf0a8 docs(extending): consolidate overlap with hook.md and within aliases (#2315)
The Hooks section of \`extending.md\` largely restated \`hook.md\` —
event table, pre-/post- rule, user/project split, three TOML forms,
variable teaser, and common patterns all duplicated. The Hooks and
Aliases sections within the page also repeated each other on the
\`[[block]]\` pipeline syntax, approval model, and the core variable
list.

Cuts:

- Added a one-paragraph shared-concepts note after the comparison table
covering TOML config, the template engine link, \`[[block]]\` semantics,
and the approval model.
- Trimmed \`## Hooks\` to the event table, the pre-/post- rule, one
flavor example, and a pointer to \`hook.md\`.
- Trimmed \`### Templates\`, \`### Multi-step pipelines\`, and the
former \`### Sources and approval\` in \`## Aliases\` to what's
alias-specific.

Renames / rewordings:

- \`### Sources and approval\` → \`### Changing directory\`. Expanded to
note that \`wt switch\`, \`wt merge\` (leaving the removed source), and
\`wt remove\` of the current worktree all propagate cd via shell
integration — but other shell state (\`cd\`, \`export\`) doesn't persist
because the alias runs in a subshell.
- Link text \"hook template engine\" → \"variable and filter reference\"
so the template system reads as shared rather than hook-owned.

Net: \`docs/content/extending.md\` 246 → 189 lines (−57).
\`skills/worktrunk/reference/extending.md\` auto-synced.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 21:51:26 -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 31b9b35103 docs(alias): clarify which template variables are available in aliases (#2314)
The aliases section of `extending.md` claimed "same template variables
as hooks" — that's wrong. Aliases get `BASE_VARS` + `args` +
`vars.<key>`, but not hook operation-context vars (`target`, `base`,
`pr_number`, `pr_url`, `target_worktree_path`, `base_worktree_path`) or
infrastructure vars (`hook_type`, `hook_name`). A user writing `{{
target }}` in an alias would hit an undefined-value error with no
pointer to what actually works.

Replaces the false equivalence with an explicit enumeration under a new
`### Templates` subsection — listing the variables that do work and
noting that operation-context vars aren't populated since there's no
operation in progress. Also renames the adjacent `### Forwarding
positional arguments` → `### Positional arguments` so the two headings
pair cleanly as siblings.

Docs-only. The synced skill mirror
(`skills/worktrunk/reference/extending.md`) picks up the same edits via
`test_command_pages_and_skill_files_are_in_sync`.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 21:10:21 -07:00
Maximilian Roos 68229ce571 Smart routing for alias arguments + dry-run routing display (#2304)
Routes `--KEY=VALUE` tokens based on what the template references.
Tokens whose `KEY` appears as `{{ KEY }}` in the alias bind to that
variable; everything else joins `{{ args }}`. Space form `--KEY VALUE`
is equivalent.

Key changes:
- Parser uses template reference introspection (via
`referenced_vars_for_config`) to decide binding at parse time.
- Post-alias `--yes` retired — use `wt -y <alias>` (the global form)
instead. `--dry-run` on an alias invocation is also retired; `wt config
alias show/dry-run <name>` replaces it (landed separately in #2291).
- Hyphens in keys canonicalize to underscores, so `--my-var=x` binds `{{
my_var }}`.
- `--` is a literal-forward escape — everything after it forwards to `{{
args }}` regardless of bindings.
- `wt <alias> --help` / `-h` prints a hint pointing at `wt config alias
show/dry-run` rather than silently forwarding the flag into `{{ args
}}`. `wt <alias> -- --help` still forwards.

Advisory warnings (printed on stderr, don't affect execution):
- `--KEY VALUE` with `--`-prefixed VALUE: almost always a typo where
`--KEY=VALUE` was meant.
- `wt config alias show`/`dry-run` on a name that shadows a top-level
built-in (e.g. `list`): the alias is unreachable via `wt <name>`.

`wt config alias dry-run` now prints `# bound:` and `# args:` routing
comments above the rendered command so users can see how each token was
interpreted.

Docs rewrite of the aliases section: concrete fly preview-env example,
"Passing values" section (renamed from the opaque "How arguments are
routed"), routing mechanism moved above the introspection tools, `up`
rebase recipe restored, `since-main` alias added.

## Test plan
- [x] `cargo test --lib --bins commands::alias::tests` — unit tests
(new: duplicate-key precedence, footgun warning, multi-`=` value)
- [x] `cargo test --test integration step_alias` — integration tests
(new: shadow warning on `show` and `dry-run`, `--help` intercept,
retired `--dry-run` via `wt step`)
- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — skill file sync
- [x] `cargo run -- hook pre-merge --yes` — full test suite + lints
green

🤖 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 <w@worktrunk.dev>
2026-04-19 14:39:07 -07:00
Maximilian Roos c7b94a7276 feat(config): add wt config alias show and dry-run subcommands (#2291)
## Summary

Third PR in the series simplifying alias-control flags. Introduces `wt
config alias` as the home for alias introspection and removes the
`--dry-run` flag that lived on alias dispatch.

- **`wt config alias show <name>`** — prints the configured template,
source-labeled (user/project). Each step in a pipeline is rendered in a
single gutter block with `# <step-name>` comment lines above named
steps.
- **`wt config alias dry-run <name> [-- args...]`** — parses the
arguments with the same parser `wt <alias>` uses, renders templates
against the execution-time context, and prints without running. Args
after `--` are forwarded verbatim, so `wt config alias dry-run s --
target-branch` previews exactly what `wt s target-branch` would run.
Same layout as `show`; only the header verb differs (`:` vs ` would
run:`).
- **`--dry-run` removed** from `AliasOptions` — both `wt <alias>
--dry-run` and `wt step <alias> --dry-run` now return an actionable
error pointing at the new subcommand. No deprecation period: top-level
alias dispatch is recent, so cutover is acceptable.
- **Tab-completion** completes alias names under `show` / `dry-run`.

## Output format

```
○ Alias deploy (project) would run:
 ┃ # install
 ┃ npm install
 ┃ # build
 ┃ npm run build
```

The `○ Alias <name> (<source>)` header matches the existing
`info_message` style. Bolding the name (what the user typed) and tagging
the source in parens keeps the invocation identifier visually primary.
When both user and project define the same alias, both entries print
back-to-back (user first, matching runtime execution order).

## Design notes

- The new subcommand reuses `AliasOptions::parse` as the source of truth
for invocation parsing, so preview stays aligned with runtime. When user
and project configs both define the alias, `referenced_vars` is unioned
across entries so a flag binds if any template references it.
- Templates referencing `vars.*` are shown unexpanded, mirroring the
lazy execution path — those values are read from git config just before
each step runs, potentially after earlier steps have set them. Syntax
errors still surface up front.
- `wt step <alias>` also breaks since both paths share
`AliasOptions::parse`. That's fine — `wt step <alias>` has no
forward-compat promise beyond the basic call.
- `AliasSource` is promoted to `pub(crate)` with a `label()` helper so
`wt config alias` and the alias runtime code read from one enum.

## Navigating the diff

- `src/cli/config.rs` — adds `ConfigAliasCommand` + the `Alias` variant
on `ConfigCommand`.
- `src/commands/config/alias.rs` — new module. `handle_alias_show` and
`handle_alias_dry_run` share a `format_entry` helper; the `verb:
Option<&str>` parameter is the only difference between show and dry-run
output. `render_preview` replaces the old `render_for_dry_run` in alias
dispatch.
- `src/commands/alias.rs` — the `--dry-run` flag and its rendering
branch are gone. The parser bails on `--dry-run` with a migration
message pointing at the new subcommand; the bail sits after the `--`
literal-mode check, so `wt alias -- --dry-run` still forwards as
positional.
- `src/completion.rs` — `alias_name_completer()` mirrors the existing
`hook_command_name_completer` pattern.
- `tests/integration_tests/step_alias.rs` — existing `--dry-run` tests
migrate to `wt config alias dry-run`, plus new coverage for `show`,
unknown-alias suggestions, and the retired-flag error.
- `docs/content/extending.md` — adds an "Inspecting and previewing"
subsection.

## Test plan

- [x] Full suite via `wt hook pre-merge --yes` (3271 tests, all passing)
- [x] `cargo insta test --accept` — all snapshots up to date
- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — docs synced
- [x] Manual verification: `wt config alias show <name>` and `wt config
alias dry-run <name> [-- args...]` in a repo with mixed user/project
aliases

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:00:01 -07:00
Maximilian Roos 8e3abf46db feat(alias): route --key=value via template refs, add -- escape, drop --var (#2287)
## Summary

Replace the alias-arg parser's "every `--KEY=VALUE` binds; bare `--KEY`
errors" grammar with template-driven routing. A token only binds when
the alias's pipeline references `{{ KEY }}`; otherwise it forwards into
`{{ args }}`. Adds `--` as a literal-forward escape and removes the
`--var KEY=VALUE` / `--var=KEY=VALUE` special cases.

The template already declares which variables it consumes — using that
as the routing signal removes both surprise binding (silent
`env=staging` for templates that never reference `env`) and the
unknown-flag error class. Every post-alias token now has a deterministic
destination.

## Grammar

Tokens after `wt <alias>` are walked left-to-right:

| Token shape | Routes to |
|---|---|
| `--KEY=VALUE` or `--KEY VALUE` where the template references `{{ KEY
}}` | Bound — `KEY` becomes the template value |
| `--KEY=VALUE` where the template doesn't reference `KEY` | Forwarded
literally to `{{ args }}` |
| `--KEY` followed by another `--…` (or end of args) | Forwarded
literally to `{{ args }}` |
| Bare positional | Forwarded to `{{ args }}` |
| Anything after `--` | Forwarded to `{{ args }}` regardless of shape |

`--dry-run` is the only post-alias built-in. `--yes`/`-y` is global only
(`wt -y <alias>`) per #2290 — post-alias `--yes` falls through the shape
rule and forwards as positional. Hyphens in the key canonicalize to
underscores before lookup, so `--my-var=value` binds to `{{ my_var }}`.

## Behavior changes

| Input | Before | After |
|---|---|---|
| `wt rm --force` (no `{{ force }}` ref) | Errored "Unknown flag" |
Forwards `--force` to `{{ args }}` |
| `wt deploy --env staging` | Errored "Unknown flag" | Binds
`env=staging` if referenced; else forwards both to `{{ args }}` |
| `wt deploy --env=staging` (no `{{ env }}` ref) | Silently bound
`env=staging` | Forwards `--env=staging` to `{{ args }}` |
| `wt foo --var x=1` | Bound `x=1` via the special `--var` arm | Same as
`--var=x=1`: binds `var="x=1"` if referenced, else forwards |
| `wt run -- --env=staging` | `--env=staging` consumed | `--env=staging`
forwarded literally |
| `wt show --branch=override` (with `{{ branch }}` ref) | Bound (worked,
undocumented) | Bound, now documented and tested |

Users who relied on `--var KEY=VALUE` should switch to `--KEY=VALUE`
directly.

## Navigating the diff

- `src/commands/alias.rs` — `AliasOptions::parse` rewritten as a
left-to-right walk against `referenced_vars: &BTreeSet<String>`.
`try_alias` and `step_alias` resolve the alias's `CommandConfig` first,
compute `referenced_vars`, then parse. `step_alias` now takes
`Vec<String>` (parses internally) because routing needs the resolved
alias.
- `src/config/expansion.rs` — new `referenced_vars_for_config` helper
unions `template.undeclared_variables(false)` across every command in a
pipeline.
- `src/main.rs` — call site updated for new `step_alias` signature.
- `tests/integration_tests/step_alias.rs` — six new integration tests
cover the grammar end-to-end (referenced bind, unreferenced forward,
`--` escape, multi-step binding, built-in overshadow, space-separated
bind). The `--var` tests are gone.
- `tests/integration_tests/approval_ui.rs` — updated
`test_post_alias_yes_does_not_skip_approval` (renamed from main's
`…_no_longer_supported`): under the new grammar `--yes` doesn't error,
it forwards as positional, but still doesn't skip approval.
- `docs/content/extending.md` (and auto-synced skill reference) — new
"How arguments are routed" section, `--` escape documented, built-in
overshadow caveat added.

## Test plan

- [x] `cargo run -- hook pre-merge --yes` (3255 tests, all pass)
- [x] Unit tests for parse grammar: `test_parse_built_in_flags`,
`test_parse_key_value_routing`, `test_parse_space_separated_routing`,
`test_parse_hyphen_canonicalization`,
`test_parse_literal_forward_escape`, `test_parse_mixed_pipeline`,
`test_parse_positionals` (covers post-alias `--yes`/`-y` forwarding),
`test_referenced_vars_for_config_unions_steps`
- [x] Integration tests for end-to-end routing
- [x] Doc sync test passes

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-18 14:15:24 -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 4ec7023ee5 feat(alias): forward positional args to templates via {{ args }} (#2280)
## Summary

Non-flag tokens after an alias name are forwarded to the template as `{{
args }}` — a space-joined, shell-escaped sequence. `wt s some-branch`
with `s = "wt switch {{ args }}"` expands to `wt switch some-branch`.

Before: `wt s some-branch` errored with `Unexpected argument
'some-branch' for alias 's'`.

## Why

Users want to pass arguments through to aliased commands — `wt s foo` →
`wt switch foo` — without configuring `--var foo=…` for every possible
parameter. Positional args are the natural fit.

## Behavior

`{{ args }}` is a minijinja sequence. All four access patterns work:

| Template | Render |
|---|---|
| `{{ args }}` | space-joined, per-element shell-escaped |
| `{{ args[0] }}` | first arg (escaped) |
| `{{ args \| length }}` | count |
| `{% for a in args %}` | iteration |

Each element is individually shell-escaped, so `wt run 'a b' 'c;d'`
splices in as `'a b' 'c;d'` and can't inject shell syntax. Positionals
interleave freely with flags — `wt deploy foo --dry-run bar` collects
`["foo", "bar"]` into `args`.

`--dry-run`, `--yes`, `-y`, `--var KEY=VALUE`, and `--KEY=VALUE` parsing
are unchanged.

## Navigating the diff

- `src/config/expansion.rs` — new `ShellArgs` struct wraps `Vec<String>`
and implements minijinja's `Object` trait with `ObjectRepr::Seq`. Its
`render()` writes `shell_escape::unix::escape` of each element,
space-joined. The shell-escape formatter in `setup_template_env` detects
`ShellArgs` via `downcast_object_ref` and passes its `Display` through
unmodified — so bare `{{ args }}` isn't double-escaped by the generic
per-value escape. Iteration and indexing yield plain
`Value::from(String)` that still flow through the generic formatter. New
`ALIAS_ARGS_KEY` constant (`"args"`) is the reserved context key
carrying the JSON-encoded list.
- `src/commands/alias.rs` — `AliasOptions` gains a `positional_args:
Vec<String>` field. `AliasOptions::parse` collects non-flag tokens into
it instead of erroring. `run_alias` inserts the JSON-encoded list into
`context_map` under `ALIAS_ARGS_KEY` right after `build_hook_context`.
The sole special-case lives in `expand_template`, which rehydrates the
key as a `ShellArgs` object; all other call sites (hooks, for-each,
eval) are untouched and never see the key.
- `validate_template` — injects an empty `ShellArgs` so templates
referencing `{{ args }}` pass pre-flight validation. `args` is added to
`TEMPLATE_VARS`.
- `docs/content/extending.md` — new "Forwarding positional arguments"
subsection under Aliases with a shell-safety guarantee and
access-pattern examples.

## Tests

3233 tests pass, lints clean. New:

- `src/commands/alias.rs` — `test_parse` snapshots extended with
positional cases including interleaved flags and metacharacter-laden
args. `test_parse_errors` no longer expects "Unexpected argument".
- `src/config/expansion.rs` — `test_expand_template_args_sequence`
covers indexing, iteration, and length.
`test_expand_template_args_empty` confirms empty renders to empty
string. `test_expand_template_args_shell_metachar_safety` asserts the
exact output for `['; rm -rf /', '$(whoami)', "a'b"]`.
`test_validate_template_valid` now covers `{{ args }}`, `{{ args |
length }}`, and iteration.
- `tests/integration_tests/step_alias.rs` — end-to-end snapshots for `wt
step <alias> positionals`, `wt <alias> some-branch --dry-run`, empty
positionals, and sequence-style access.

## Do-nots

- No changes to `--dry-run`, `--yes`, `--var`, or `--KEY=VALUE` parsing
— the broader flag-handling question (whether alias-level flags should
move pre-name) is being designed separately.
- No `KEY=value` bare-token parsing for vars — deferred.
- No change to `wt step <alias>` semantics beyond inheriting positional
support via shared `AliasOptions::parse`.

> _This was written by Claude Code on behalf of Maximilian_

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:52:31 -07:00
Maximilian Roos 07db111340 docs: trim filler sentences from prose docs (#2271)
Remove sentences that restate obvious fallback/error behavior, duplicate
nearby prose, or add visual weight without information.

- `extending.md` — drop the "not a wt command" error note, the
experimental badge on Aliases, and a duplicate recap of the
`cd`-to-parent-shell behavior at the end of a recipe.
- `faq.md` — drop "The result is cached for fast subsequent lookups"
padding after the default-branch detection explanation.
- `tips-patterns.md` — drop "Creates a worktree that builds on the
current branch's changes" (restated by the section heading) and
"Sessions are named after the branch for easy identification" (visible
in the code).

Skill reference files auto-synced via
`test_command_pages_and_skill_files_are_in_sync`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:34:48 -07:00
Maximilian Roos 016eb030e7 docs(extending): rename "external subcommand" to "custom subcommand" (#2270)
## Summary

Renames the git-style `wt-<name>` dispatch feature to "custom
subcommand" across user-facing docs and internal code. Two motivations:

- **Avoid overloading "external."** The codebase already uses "external
command" for the `shell_exec` concept (subprocesses like `git` and
`gh`). Using the same word for the `wt-foo` dispatch feature is
ambiguous.
- **Cargo uses "custom command" / "external subcommand"
interchangeably.** kubectl calls theirs "plugins," gh calls theirs
"extensions"; git doesn't have a settled term. "Custom subcommand" reads
naturally in prose and matches cargo's user-facing phrasing.

## Changes

**User-facing docs** — `docs/content/extending.md` (section heading,
description, comparison table) and `docs/content/faq.md` (link +
anchor). Skill references auto-sync.

**Internal code** — `src/commands/external.rs` → `custom.rs`,
`Commands::External` → `Commands::Custom`, `handle_external_command` →
`handle_custom_command`, plus matching renames in `src/completion.rs`
(inject/discover/forward functions) and corresponding tests. Also
renames `tests/integration_tests/external.rs` → `custom.rs`.

**Kept intact** — clap's `#[command(external_subcommand)]` attribute and
`.allow_external_subcommands(true)` are clap's own vocabulary, not ours.

CHANGELOG is historical and left unchanged.

## Test plan

- [x] `cargo build` clean
- [x] `cargo clippy --all-targets --all-features` clean
- [x] `cargo test --lib --bins` — all pass
- [x] `cargo test --test integration` — all pass (1476)
- [x] `pre-commit run --all-files` clean

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-16 22:06:12 -07:00
Maximilian Roos a1be5645c4 feat(alias): dispatch aliases from top-level wt <name> (#2266)
`wt deploy` now resolves `deploy` against configured aliases before
falling through to a `wt-deploy` PATH binary. Built-ins still win (clap
matches before alias dispatch ever runs), and `wt step <name>` keeps
working at runtime — only the docs cut over to the new form.

## Why

`wt deploy` reads better than `wt step deploy`, and aliases as
first-class commands lower friction for using them as everyday
shortcuts.

## Precedence

built-in (clap) → alias (user/project config, merged) → `wt-<name>` PATH
binary → "unrecognized subcommand" error.

User config wins over PATH binaries because aliases are how users
customize wt — same model as git, where `[alias]` entries shadow
`git-foo` externals.

## Navigating the diff

- `src/commands/alias.rs` — refactored `step_alias` to share `run_alias`
with the new `try_alias(name, rest) -> Result<Option<()>>`. Returns
`Ok(None)` when the name isn't a configured alias or when not in a git
repo; propagates config-load errors so a broken `wt.toml` fails loudly
instead of silently turning into "unrecognized subcommand". Argument
parsing is gated on alias-membership, so unrelated args meant for an
external binary don't surface as alias parse errors. New
`alias_names_for_suggestions()` mixes alias names into "did you mean"
hints. `HelpContext` enum lets the help splice annotate "(shadowed by
built-in)" against the right level (top-level builtins for `wt --help`,
step builtins for `wt step --help`). The user-facing "shadow warning"
was removed entirely — under the new model an alias named `commit` runs
fine via `wt commit`, only `wt step commit` is shadowed.
- `src/commands/external.rs` — `handle_external_command` calls
`try_alias` first, then PATH lookup, then unrecognized-subcommand error.
Suggestions include alias names. Non-UTF-8 args bypass alias dispatch
(alias parser requires UTF-8; binary subcommands get raw `OsStr`).
- `src/help.rs` + `src/main.rs` — early-parse pass returns
`Option<HelpContext>`; help splice fires for both `wt --help` and `wt
step --help`.
- `src/completion.rs` — aliases injected at the top level in addition to
`step`.
- `src/cli/mod.rs` — long Aliases section moved out of
`Step::after_long_help` into hand-authored `docs/content/extending.md`.
New sync test `test_top_level_builtins_match_clap` keeps the
`TOP_LEVEL_BUILTINS` constant aligned with the `Cli` enum.

## Tests

3221 tests pass, lints clean. New integration tests:
`test_top_level_alias_dispatch`,
`test_top_level_alias_with_step_builtin_name`,
`test_top_level_alias_did_you_mean`. Removed
`test_step_alias_shadows_builtin_plural` (warning gone). Reframed
`test_step_alias_shadows_builtin` to verify shadow filtering of typo
suggestions instead. Completion tests now isolate user config via
`WORKTRUNK_CONFIG_PATH=/dev/null` — project config isolation is a noted
gap (commented inline).

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 14:56:03 -07:00
Worktrunk Bot 9884cdcf86 docs: link worktrunk-sync in Extending and FAQ (#2225)
## Summary

Requested in max-sixty/worktrunk#2053 — link to
[`worktrunk-sync`](https://github.com/pablospe/worktrunk-sync) from the
docs.

- **Extending Worktrunk** — adds an `### Examples` subsection under
*External subcommands*, pointing to `worktrunk-sync` as a concrete
real-world use of the `wt-<name>` dispatch mechanism.
- **FAQ** — adds a "Does Worktrunk support stacked branches?" Q&A
positioned after the tool-comparison section, explaining that stacked
workflows are deliberately kept out of core and pointing readers at
`worktrunk-sync`.

Skill reference files under `skills/worktrunk/reference/` regenerated
via `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync`.

## Test plan

- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` passes
- [ ] Prose/links render as expected on the dev server

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 15:38:00 -07:00
Worktrunk Bot af1e0f5152 docs(extending): lift hook-format descriptions out of code comments (#2191) 2026-04-13 07:36:22 -07:00
Worktrunk Bot 5a02c1f916 docs(extending): trim move/copy aliases recipe (#2190) 2026-04-13 06:56:48 -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 cec8d934ed docs(extending): simplify hook-log alias body; drop brittle hash match (#2169)
Two small polish changes to the `hook-log` alias recipe added in #2161:

- **Multi-line the alias body** so the 217-char pipeline becomes
readable in the rendered docs. The jq expression lives inside single
quotes, so literal newlines are safe; the outer shell pipeline reads
top-to-bottom without wrapping.
- **Drop the `startswith($name + "-")` branch.** It was meant to also
match the sanitized-with-hash form, but only covers the specific 3-char
suffix shape and would misfire on hook names that naturally end in
`-<3alnum>`. The limitation is documented instead: users with hook names
that required sanitization should pass the on-disk sanitized form.

A follow-up PR (dispatched separately) adds a `sanitize_hash` template
filter so the alias can pre-sanitize its input and match reliably across
all hook names.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 21:23:49 -07:00
Maximilian Roos 50f9b82d2c refactor(config-state): structured JSON for logs; drop --hook/--branch filters (#2161)
Builds on [#2156](https://github.com/max-sixty/worktrunk/pull/2156)
which hoisted `--format` and added absolute `path`. Two follow-on
changes:

### 1. Structured fields on `hook_output` entries

Hook-output log entries carried a slash-packed relative path in `file` —
consumers had to split on `/` to filter by branch, source, or hook type.
Expose first-class structured fields:

```json
{
  "file": "main/user/post-start/server.log",
  "path": "/.../.git/wt/logs/main/user/post-start/server.log",
  "branch": "main",
  "source": "user",
  "hook_type": "post-start",
  "name": "server",
  "size": 1024,
  "modified_at": 1700000000
}
```

Internal-op entries (e.g., `feature/internal/remove.log`) normalize onto
the same shape with `source: "internal"`, `hook_type: null`, and the op
name in `name`.

### 2. Drop `--hook` / `--branch` filters; use jq

With the structured fields, the old `--hook=SPEC` / `--branch=X`
interface is a jq one-liner:

```bash
wt config state logs --format=json \
  | jq -r '.hook_output[]
           | select(.source == "user"
                    and .hook_type == "post-start"
                    and (.name | startswith("server")))
           | .path'
```

That subsumes the filter flags without the CLI plumbing, the
`HookLog::parse`/`to_spec` roundtrip, or the error-shape tests that
locked in a fragile `source:hook-type:name` grammar. `logs` and `logs
get` now always list — one code path, one shape.

**Breaking change**: `wt config state logs get --hook=...` and
`--branch=...` are removed. The jq pattern is documented in `wt config
state logs --help`.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 20:58:37 -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
Maximilian Roos 420cace354 docs(extending): use --key=value shorthand in move/copy alias examples (#2094)
PR #2091 added `--key=value` shorthand for alias variables. Update the
three invocation comments in the move/copy-changes recipe to use the
shorter form — `wt step move-changes --to=feature-xyz` instead of `--var
to=feature-xyz`.

Template bodies still reference `{{ to }}` internally, which remains the
canonical variable name.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 15:56:44 -07:00
Maximilian Roos 5b89ef3b99 feat(alias): support --key=value shorthand for alias variables (#2091)
Aliases previously required `--var key=value` to pass template
variables. Unknown `--key=value` flags are now treated as variable
assignments, so `wt step deploy --env=staging` is equivalent to `--var
env=staging`.

The `=` is required to disambiguate from boolean flags — `--env value`
would be ambiguous (is `value` a positional?), but `--env=value` is
unambiguous. `--var` remains as the escape hatch for variable names that
collide with built-in flags (`--dry-run`, `--yes`).

Implementation is a single fallthrough match arm in
`AliasOptions::parse()` (the alias parser is hand-rolled because aliases
are clap external subcommands). Unit tests cover the shorthand alongside
`--var`, equals-in-value, mixed forms, empty values, and bare `--key`
errors. An integration test confirms end-to-end equivalence with
`--var`.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 14:51:48 -07:00
Worktrunk Bot 61b0e5ae11 docs(extending): add recipe for carrying changes to a new worktree (#2083)
## Summary

Adds a tested recipe to the Aliases section of
`docs/content/extending.md` showing three `[aliases]` entries that
compose `wt switch --create` with git's stash/diff plumbing to carry
in-progress changes into a new worktree:

- `move-changes` — stashes staged + unstaged + untracked, creates the
new worktree, and pops the stash with `--execute`. Source becomes clean.
- `copy-changes` — same push, plus `git stash apply --index --quiet` to
restore the source before the new worktree pops. Both sides end up with
identical state.
- `copy-staged` — writes `git diff --cached` to a tempfile and applies
it with `git apply --index` in the new worktree, so the source is
untouched and staged/unstaged overlap is handled.

Each alias takes the target branch via `--var to=feature-xyz`. Because
an inner `wt switch --create` inside an alias body propagates its `cd`
to the parent shell (#2077), they drop you in the new worktree directly.

The content was worked out and tested end-to-end in the issue thread
(`git status --porcelain` verified on both sides for clean /
staged+unstaged+untracked / untracked-only / empty-staged scenarios).

Closes #938.

## Test plan

- [ ] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — keep
`skills/worktrunk/reference/extending.md` in sync
- [ ] `pre-commit run --all-files` — lints
- [ ] Visual check of the rendered Aliases section on the docs dev
server

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 14:08:17 -07:00
Maximilian Roos 7e14f811e1 docs: cross-link Extending Worktrunk page (#2088)
Follow-up to #2079. Adds cross-links from the landing page and adds the
`up` alias example to the extending page.

- worktrunk.md "Next steps": link to extending.md
- extending.md: add `wt step up` multi-line alias example, remove
redundant TOML file-path comments

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 13:39:47 -07:00
Maximilian Roos e2fff694a5 docs: add Extending Worktrunk page (#2079)
New Reference page covering the three extension mechanisms — hooks,
aliases, and external subcommands — with a comparison table and enough
detail to orient readers before linking to the full references
(`hook.md`, `step.md#aliases`).

Moves the external subcommands section out of tips-patterns.md into the
new page as its canonical location. The aliases recipe in tips-patterns
stays (it's a specific recipe, not a duplicate).

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 12:35:40 -07:00