mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
codex/remove-codex-cloud-specific-tests
40 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d4538f2047 |
fix(alias): carry the shell's cwd into alias and hook bodies (#3724)
## Problem Since #939 / #3344, `wt switch` and `wt remove` preserve the user's subdirectory position — from `monorepo.feature/subproject/` you land in `monorepo/subproject/`. #3723 reports that this is lost one layer down: with `[aliases] finish = "wt remove -y"`, `wt finish` drops you at the primary worktree root. The resolution reads the user's position from the wt process's own cwd (`resolve_subdir_in_target`, called with `std::env::current_dir()`). That answers "where is the user standing?" only for a top-level invocation. Alias and hook bodies run with the worktree root as their working directory, so the nested `wt` strips the source root off a cwd that *is* the source root, gets an empty relative path, and falls back to the destination root. ## Solution The CD directive file already travels to exactly the children that are allowed to move the user's shell. The shell's directory now travels with it: `apply_cd_directive_env` sets `WORKTRUNK_SHELL_CWD` wherever the CD file is re-added (`Cmd::stream` and the concurrent runner), and `scrub_directive_env_vars` strips it alongside the other directive vars, so an untrusted child neither keeps nor receives it. `shell_exec::shell_cwd()` reads it back, preferring the inherited value over the process cwd — which is what makes nesting compose, since each layer forwards the shell's directory rather than its own. Three sites ask that question and now go through it: `wt switch`, `wt remove` (`prepare_remove_directory_change`), and `wt step relocate`, whose existing comment already asks to behave identically to the other two. Nothing about the working directory of alias or hook *bodies* changes — `{{ cwd }}` is still the worktree root, per the documented contract. The only change is what a nested `wt` believes about the user's position. ## Testing Two integration tests in `tests/integration_tests/step_alias.rs`, both failing before the change with the exact symptom reported: ``` CD file should preserve the subdirectory (…/repo/apps/gateway), got: "…/repo\n" ``` - `test_alias_wrapping_remove_preserves_subdir` — the reported case (`[aliases] finish = "wt remove -y"` run from `feature/apps/gateway`). - `test_alias_wrapping_switch_preserves_subdir` — the same for `wt switch` inside an alias. The existing subdirectory-preservation tests in `directives.rs` (including the fall-back-to-root cases) still pass, as does the full integration suite — apart from `test_copy_ignored_preserves_file_executable_permissions`, which fails identically on `main` in this sandbox (umask `0002`, expects `0644` gets `0664`) and is unrelated. The open question from the first revision is answered: the new remove test leaves two processes with a cwd inside the worktree being removed (the alias parent in the subdirectory, the nested `wt` at the root) where the existing test has one, and `test (windows)` passes on it. Review follow-ups are in |
||
|
|
958b3082de |
fix(env): scrub GIT_* discovery vars from for-each and --execute fallback (#3400)
#3374 scrubbed inherited `GIT_*` discovery vars from user-hook spawns, but two more spawn sites relocate a user command into a wt-chosen worktree and had the same bug: `wt step for-each` and the `--execute` payload when wt executes it directly (no shell integration — including every `!wt`-alias invocation, which bypasses the wrapper). Git resolves `GIT_DIR`/`GIT_WORK_TREE` before walking up from the cwd, so the inherited value silently overrode the worktree wt placed the command in: `git w step for-each -- git rev-parse --show-toplevel` from a linked worktree printed the invoking worktree's path for every iteration while the headers named each worktree. The linked-worktree alias case is the common trigger — git exports an absolute `GIT_DIR` pinned to the invoking worktree's private gitdir there (verified on git 2.54; from a main-worktree root it exports none). The rule is now stated once, on `scrub_git_discovery_env_vars`: **scrub exactly when wt chose the child's cwd** (hooks, for-each, the `--execute` fallback); keep when the command runs in the user's own context (aliases, `commit.generation`, and wt's internal plumbing, which keeps the absolutize-and-forward behavior from #1914). A CLAUDE.md rule points there, mirroring the `CommandTrace` any-new-spawn-site convention. Testing: regression tests for both new scrub sites (each validated by reverting the fix and watching it fail) plus an alias pass-through test pinning the keep side. The `--execute` test is cross-platform, so Windows CI drives the non-unix `spawn` variant and unix the `exec` variant; the for-each test follows the file's unix `sh -c` precedent. Ref #3373 (already closed by #3374; this completes the sweep). > _This was written by Claude Code on behalf of max-sixty_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9be99ee21b |
test: consolidate absence-window sleeps behind a shared SLEEP_FOR_ABSENCE_CHECK (#3206)
Absence assertions (verifying something did NOT happen) need a fixed window rather than polling, since there's no event to wait for. That window was expressed three inconsistent ways across the test suite: a per-file `const SLEEP_FOR_ABSENCE_CHECK` duplicated in `user_hooks.rs` and `post_start_commands.rs`, and bare `Duration::from_millis(500)` literals scattered through `switch.rs`, `step_alias.rs`, `remove.rs`, `bare_repository.rs`, and `merge.rs` — none greppable as a group, and `merge.rs` sitting below the documented 500ms floor at 200ms. This promotes `SLEEP_FOR_ABSENCE_CHECK` to `src/testing/mod.rs`, next to the presence helper `wait_for_file`, re-exported via `tests/common`, and routes all 15 absence sleeps through it. The constant's doc comment states the discriminator that decides the tool: a *presence* assertion polls (and returns the instant the event lands); an *absence* assertion has no event to wait for, so it holds a bounded window. The three sleeps left as literals are genuinely different roles — bash startup, signal sequencing between two SIGINTs, and PTY output sequencing — not absence checks. It also rewrites the `tests/CLAUDE.md` timing section around that polarity discriminator (split into Presence / Absence subsections), with the absence example using the constant, plus two traps: pairing one sleep with both a presence and an absence assertion (the presence half goes flaky), and structural absence — when the event is gated on a condition the test never sets up, drop the window and poll the positive precondition instead. This is the consistency-and-guidance follow-up to the watchdog de-flaking in #3187: the marker convention makes legitimate absence sleeps self-labeling, which is what lets a reviewer (or the nightly sweep) tell them apart from a flaky fixed sleep before a presence assertion. ## Testing No behavior change — test-infrastructure and docs only. Each of the 15 conversions was verified to be followed by a negative assertion (adversarial review, 20/20 confirmed); the full `pre-merge` gate passes (4170 tests). `merge.rs`'s window widened 200ms → 500ms, which only makes its absence check more conservative. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3334ffa9d8 |
test(step_alias): reap orphaned sleep in external-signal alias tests (#3136)
The two `#[cfg(unix)]` external-signal alias tests (`test_alias_external_sigterm_reaches_child`, `test_alias_external_sigint_reaches_child`) run an alias body of `... sleep 30 & wait $!` and deliver a PID-targeted signal to wt. wt forwards it to the wrapper `sh` by PID — the contract these tests pin — so the trap's `exit` tears down the shell and leaves the backgrounded `sleep 30` orphaned in wt's process group, lingering ~30s. Two tests, one orphan each, accounted for the two stray `sleep 30` processes a full suite run left behind (invisible to nextest's leak detector because their stdio is redirected to null). This is a test artifact, not a product bug: wt faithfully reproduces plain-shell semantics, where a trap that `exit`s without `kill $!` orphans its background job. The fix reaps the orphan after `child.wait()` with a negative-pgid SIGKILL — wt ran in its own process group (`process_group(0)`) and is reaped before the kill, so the SIGKILL reaches only the leftover sleep. Same pattern as the fsmonitor SIGKILL-escalation test fix in #3135. Verified: targeted runs 5/5 leave 0 strays (was 2/2); full suite (`--features shell-integration-tests`) 4002 passed with 0 strays at +0s and +5s; clippy and pre-commit clean. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
751e419615 | fix(concurrent): drop per-child SIGINT escalation race (#3075) | ||
|
|
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> |
||
|
|
1fa70d3e08 |
fix(concurrent): report originating signal as wt's exit code after escalation (#2724)
## Summary `wt step <concurrent-alias>` interrupted by Ctrl-C could exit with code 143 (SIGTERM) instead of 130 (SIGINT) under load. The race lives in `src/output/concurrent.rs`'s signal forwarder: it sends SIGINT to each child's process group, then waits 200ms before escalating to SIGTERM. Under CI load the path `signal_hook polls → killpg → child receives → forwards to sleep → sleep dies → sh dies → pgroup empty` exceeds 200ms, so wt escalates and the child dies from SIGTERM. Wt's `interrupt_exit_code()` reads the child's actual death signal and reports `128 + 15 = 143` even though the user only ever sent SIGINT. This was the cause of the `affected tests (linux, advisory)` failure on #2709 (selected `test_alias_concurrent_receives_sigint` because that PR touched `src/main.rs`, dragging in coverage of nearly every wt invocation path) — diagnosis recorded there, not a regression from the approval refactor. ## Fix Record the user's first signal in a shared `Arc<AtomicI32>` set by the signal forwarder thread. After every child has been waited on, override any per-child `ChildProcessExited::signal` whose value differs from the originating one. wt's exit code now reflects what the user pressed (130 for SIGINT) regardless of which signal the escalation chain ended up using to actually kill the child. The fix is contained to `run_concurrent_commands`. The single-step `shell_exec.rs::stream` path has the same escalation logic but isn't exercised by the failing test; if the equivalent flake shows up there, the same pattern can be applied. ## Test plan - New `test_alias_concurrent_sigint_reports_origin_after_escalation` (`tests/integration_tests/step_alias.rs:1406`) — children `trap "" INT` so SIGINT is ignored and only the SIGTERM escalation kills them. Verified to reproduce the exact CI failure mode (`unix_wait_status(36608)` = exit 143) without the fix and pass with it. - Existing `test_alias_concurrent_receives_sigint` and `test_alias_concurrent_second_sigint_kills` still pass. - `cargo run -- hook pre-merge --yes`: 3667 tests pass, lints clean. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
014765252f |
feat(config): bare wt config alias show lists full definitions (#2691)
## Summary `wt config alias show` with no name showed a compact one-line summary listing (the `name command-summary (source)` table added in #2684). This swaps it for the full per-alias view: the same `○ Alias <name> (<source>):` header + gutter block that `wt config alias show <name>` prints, emitted for every configured alias in name order (user entry before project when a name is defined in both), with the shadowed-by-built-in stderr warning preserved (once per name). `--help` / `wt step --help` keep the compact names-only `Aliases:` section and point here. ``` ○ Alias landed (user): wt up && wt hook post-merge; git merge {{ default_branch }} --no-edit && { git diff {{ default_branch }} --quiet && git reset {{ default_branch }}; :; } ○ Alias sw (user): wt switch {{ args }} ○ Alias up (user): git fetch --all --prune && wt step for-each -- sh -c ' ... ' ``` Also drops the blank line between consecutive `format_entry` blocks — applied consistently to `wt config alias show` (the new listing), `wt config alias show <name>` (had a blank line between the user and project entries when a name is in both configs), and `wt config alias dry-run <name>` (same). The `○ Alias …` header is the delimiter everywhere. The compact-summary renderer (`render_aliases_section` + `format_alias_summary`) and its unit tests are removed — only #2684's no-arg listing used them; `--help`'s names-only section is the separate `render_aliases_help_section`, which is untouched. ## Changes - `src/commands/config/alias.rs` — `list_aliases()` rewritten to emit `format_entry` per alias; `handle_alias_show` / `handle_alias_dry_run` drop the inter-entry blank line; docstrings + module doc updated. - `src/commands/alias.rs` — delete `render_aliases_section`, `format_alias_summary`, and their tests; tidy the `render_aliases_help_section` doc. - `src/cli/config.rs` — `wt config alias show` help text reflects the new behavior. - Docs (`docs/content/config.md`, skill mirror) regenerated; three snapshots updated (one content change + cosmetic env drift on two). ## Test plan - [x] `cargo run -- hook pre-merge --yes` — 3581 tests pass, all lints green - [x] Manual: `wt config alias show`, `wt config alias show <name>`, `wt config alias dry-run`, `wt config alias show --help` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a3733dcce8 |
feat(config): list all aliases when wt config alias show has no name (#2684)
`wt config alias show <name>` shows one alias's configured template. With no name it now lists every configured alias — the same `Aliases:` block `wt --help` renders (name, source, one-line summary). This is the receiving end of a separate change that replaces the inline aliases block in `wt --help` with a pointer to `wt config alias show`. Implementation: `name` becomes optional with a single internal branch — no `--list` or `--all` flag. The listing reuses `render_aliases_section` / `load_aliases_for_listing` from `commands/alias.rs` (promoted to `pub(crate)`) rather than duplicating. It tolerates running outside a repo (user-config aliases still list, project-config ones are skipped) or outside a config, prints `No aliases configured` when empty, and pages like `wt hook show`. Top-level shadowing is annotated (`list` → `(shadowed by built-in)`). Audit of other `show`/`view`/`info`/`print` subcommands found nothing else to change: `wt hook show [TYPE]` already does this, `wt config show` / `wt config shell show-theme` / `wt config state get` take no positional and already show everything, and no `view`/`info`/`print` subcommands exist. Help text and the auto-synced docs/skill reference are updated; new snapshot tests cover the populated and empty listings, and the four `config_alias_show_unknown_*` snapshots pick up the `Usage: ... [NAME]` change. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
af3be9e655 |
refactor(alias)!: source-tag steps and decide EXEC per-step (#2474)
## Summary
Two phases on the same theme — making per-source policy first-class
instead of working around a name-keyed merge.
**Phase 1 — source-tag steps, decide EXEC per-step:**
- `SourcedStep` (`hook_type` now `Option<HookType>`) is the shared
sourced-step type; `sourced_steps_to_foreground(steps, &PipelineKind)`
is the single resolver. User-source alias steps get
`DirectivePassthrough::inherit_from_env_with_exec()`; everything else
(project-source alias steps, hooks of either source) gets
`inherit_from_env()`.
- `ForegroundStep` carries its own `directives` field, so
`execute_pipeline_foreground` no longer takes a per-pipeline
`&DirectivePassthrough`.
- Removed the name-keyed `project_aliases_contain` EXEC switch —
replaced by the per-step decision the source tag carries.
**Phase 2 — key aliases by name + source instead of merging:**
- `AliasMap = BTreeMap<String, AliasEntry { user, project }>` replaces
the `BTreeMap<String, CommandConfig>` that was previously built via
`append_aliases`. Same-name collisions are no longer a special case —
the data shape carries both sources directly.
- `load_aliases(repo, user_config, project_config)` is the single
resolver. `load_merged_aliases`, `alias_needs_approval`, and
`prepare_alias_sourced_steps` are gone.
- `run_alias` takes `&AliasEntry`, drops the duplicate
`user_config.aliases(...)` lookup, drops the `project_config` and
merged-map parameters; approval reads `entry.project.as_ref()`.
- `format_alias_announcement` walks both sources in runtime order.
- `unknown_alias_error` (config/alias.rs) routes through `load_aliases`
for canonical name iteration.
- `load_aliases_for_completion` returns `AliasMap`; stub builders use
`entry.representative()` for help text (user wins, matching the
announcement order).
- `append_aliases` is no longer used by alias dispatch — it survives
inside `UserConfig::aliases` for the global vs. per-project user-config
merge, which is a different axis.
Addresses
[@vandamm](https://github.com/max-sixty/worktrunk/issues/2101#issuecomment-4343744458)'s
use case directly:
```toml
[aliases] # in the user's config — ~/.config/worktrunk/config.toml
issue = "wt switch --create {{ args | join('-') | lower }} -x claude -- 'Read Linear issue {{ args[0] | upper }} via the linear MCP and propose an approach.'"
```
`wt issue XX-1200 add-button` now lands the user in the new worktree and
execs `claude` in their interactive shell.
## Behavior change vs prior release
Same-name user+project alias collisions used to scrub EXEC for the whole
merged pipeline — a conservative side effect of name-keyed merging in
`append_aliases`. With per-step decisions, the user's own steps keep the
EXEC relaxation; only the project-authored steps scrub. Strictly more
permissive than the prior approach.
## Test plan
- [x] `test_user_alias_passes_exec_directive` — user alias body running
`wt switch --execute echo from-user-alias` writes the payload to the
parent's EXEC file and emits no scrub warning.
- [x] `test_project_alias_scrubs_exec_directive` — project alias body
running the same command leaves the EXEC file empty, emits the warning
with the issue link, and exits 0.
- [x] `test_user_and_project_alias_collision_scrubs_only_project_step` —
same alias name in both configs: user-step EXEC payload lands in the
EXEC file, project-step EXEC payload is scrubbed, project warning fires
once.
- [x] `test_format_alias_announcement_concatenates_user_and_project` —
collision banner walks user steps first, then project steps.
- [x] Existing `test_switch_exec_scrubbed_warns` snapshot updated for
the new wording.
- [x] `cargo test --test integration` — 1584 passed, 0 failed.
- [x] `cargo clippy --all-targets` clean.
- [x] `cargo run -- hook pre-merge --yes` — 3404/3404 tests, all lints
pass end-to-end.
---------
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>
|
||
|
|
f3eb8fe93d |
fix(alias): pass alias stdout through so 'wt <alias> | ...' works (#2479)
## Problem `wt <alias>` writes the alias body's stdout onto wt's stderr instead of stdout, so `wt my-alias | tr …` (or any other downstream pipe) silently produces no output. Reported in #2478: ```sh # config.toml [aliases] test-output = "echo 'hello'" $ wt test-output | tr '[:lower:]' '[:upper:]' hello # ← should be HELLO ``` Root cause is in `src/output/handlers.rs::execute_shell_command`, which `Cmd::shell(...).stdout(Stdio::from(io::stderr()))` for *every* foreground command. The redirect was originally a hook-only behavior added for deterministic ordering with wt's own stderr "Running …" messages; PR #2089 unified hooks, aliases, and `for-each` onto this shared executor and inherited the redirect uniformly without distinguishing aliases. ## Solution Thread a `redirect_stdout_to_stderr` bool through `ForegroundStep` and `execute_shell_command`. Hooks (`src/commands/hooks.rs`) and `wt step for-each` (`src/commands/for_each.rs`) keep the merge (`true`) — their output is decoration around wt's stderr messages, and merging keeps ordering deterministic. Aliases (`src/commands/alias.rs`) pass through (`false`) — `wt <alias>` is a user-defined command and its stdout must remain pipeable. Concurrent alias steps still write through `output/concurrent.rs`'s prefixed-line stderr consumer — the gutter rendering (`build │ BUILD`) inherently belongs on stderr, so that path is unchanged. ## Testing - Added `test_alias_body_writes_to_stdout` in `tests/integration_tests/step_alias.rs`. It runs an alias whose body is `echo hello` and asserts the output appears on stdout, not stderr. Failed before the fix; passes after. - Existing snapshot tests for `step_alias` pick up the routing change — diffs are limited to lines moving from `----- stderr -----` to `----- stdout -----`, which is the user-visible behavior we want. Updated via `cargo insta test --accept`. - Manual end-to-end check: `wt -y test-output | tr '[:lower:]' '[:upper:]'` now prints `HELLO`. - Full `cargo test --test integration` (1569 tests) and `cargo test --lib --bins` (606 tests) pass. --- Closes #2478 — automated triage Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6f1974cdba |
fix(alias): share parent pgroup so interactive TUIs don't freeze (#2444)
## Summary
- Aliases with interactive children (`wt switch`'s skim picker) froze
with a blank screen because the alias child was put in a new process
group; the kernel then raised SIGTTOU when the picker called `tcsetattr`
on `/dev/tty`, stopping the child mid-render.
- New `Cmd::inherit_stdin()` builder both inherits stdin *and* keeps the
child in the parent's process group. `execute_shell_command` uses it for
the interactive (no-stdin-payload) path, which covers aliases.
- In shared-pgroup mode the signal listener forwards by PID via
`kill(child.id(), sig)` so externally-delivered signals (e.g. `kill
-TERM <wt-pid>`) reach the child — they otherwise stop at wt because the
kernel only multicasts tty-initiated signals to the foreground pgroup.
Single-shot, no SIGINT→SIGTERM→SIGKILL escalation: the caller chose the
signal, and a premature SIGKILL would skip the child's tty restore
(raw-mode reset, cursor-show) and leave the terminal wedged.
`WorktrunkError::ChildProcessExited { signal: Some(_) }` accounting is
preserved.
- Module-level docstring at the top of `shell_exec.rs` summarizes the
two `Cmd::stream` shapes (isolated vs shared-tty), how each interacts
with the structured `ChildProcessExited` channel used by loop callers,
and why detached/concurrent spawn paths isolate.
Hooks (which still get JSON-on-stdin via `stdin_bytes`) are unaffected —
they go through the `stdin_data`/piped branch and never call
`inherit_stdin()`.
## Test plan
- [x] New regression test `test_alias_child_shares_parent_pgroup`:
spawns wt as its own pgroup leader, runs an alias that records its shell
pgid via `ps -o pgid= -p \$\$`, asserts the recorded pgid equals wt's
pid (i.e. the alias child shares wt's pgroup).
- [x] New regression tests `test_alias_external_sigterm_reaches_child`
and `test_alias_external_sigint_reaches_child`: target wt with PID-only
`kill -<sig>` (not the pgroup) and assert the alias child's signal trap
fires. Without the PID-forwarding fix the test would block until the
child's `sleep 30` completes; with it, each settles in ~1.3s. Together
they exercise both arms of `forward_signal_to_pid`.
- [x] Existing `test_alias_concurrent_receives_sigint` and
`test_alias_concurrent_second_sigint_kills` still pass — concurrent
aliases use a separate executor (`output/concurrent.rs`) that's
unaffected by this change.
- [x] Full `step_alias` (70 tests) and `user_hooks` (108 tests) suites
green locally.
- [ ] Manual verification that `wt sw` (aliased to `wt switch {{ args
}}`) now renders the picker instead of hanging.
---------
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>
|
||
|
|
e1f2fbd76a |
fix(alias): inherit stdin so interactive children keep the tty (#2380)
## Summary Alias execution piped the template context JSON into each child's stdin, displacing the controlling terminal. Interactive children like `wt switch` then saw a pipe and bailed with `Interactive picker requires an interactive terminal`. See [#406 follow-up](https://github.com/max-sixty/worktrunk/issues/406#issuecomment-4290869876) for the reproduction. The fix is scoped to the single call site in `run_one_command` (`src/commands/command_executor.rs`) where `CommandOrigin` already distinguishes hooks from aliases: - `CommandOrigin::Hook { .. }` → keep piping `context_json` on stdin (documented contract, covered by `test_post_create_json_stdin` / `test_post_start_json_stdin`). - `CommandOrigin::Alias { .. }` → pass `None`; `execute_shell_command` now treats `None` as `Stdio::inherit()` so the child keeps the parent's tty. No subcommand sniffing, no new configs, no change to `for_each` (which still opts into JSON-on-stdin). ## Test plan - [x] New regression test `test_alias_inherits_stdin` in `tests/integration_tests/step_alias.rs` — pipes a sentinel into `wt`, runs an alias of `cat`, asserts the sentinel (not the context JSON) reaches the alias body. - [x] `cargo test --test integration -- step_alias` — all 67 tests pass, including the new one. - [x] `cargo test --test integration -- test_post_create_json_stdin test_post_start_json_stdin` — hook JSON-on-stdin still works. - [x] `cargo test --test integration -- post_start post_create for_each` — 70 tests pass. - [x] `cargo clippy --all-targets` — clean. 🤖 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> |
||
|
|
38e538232c |
Render alias args in -v table as shell-escaped {{ args }} (#2328)
Follow-up to #2324. The alias `-v` table previously showed `args` as its raw JSON wire format (`["foo","bar baz"]`) — the internal `HashMap<String, String>` storage. Templates rehydrate that JSON into a `ShellArgs` object whose `render` impl produces a space-joined, shell-escaped string; the table now does the same so its value matches what `{{ args }}` substitutes one line below. Factored the space-join-and-escape into a `shell_join` helper used by both `ShellArgs::render` and the alias table — one canonical rendering. Also moved `args` into the **exec** group next to `cwd`, matching the `## Template variables` help table (previously it sat between active and repo, implying "operation context"). ## Before/after | Input | Before | After | |---|---|---| | `wt -v greet world` | `args = ["world"]` | `args = world` | | `wt -v greet foo "bar baz"` | `args = ["foo","bar baz"]` | `args = foo 'bar baz'` | > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
24181f251d |
fix(alias): unify typo error format across config alias show/dry-run (#2306)
## Summary Four alias-typo surfaces rendered in two formats. After #2304's round-2 review, this aligns `config alias show/dry-run` with `wt <typo>` and `wt step <typo>`. Before: - `wt deplyo` / `wt step deplyo` → clap-native `error: unrecognized subcommand 'X'` + `tip: …`, exit 2 - `wt config alias show deplyo` / `dry-run deplyo` → custom anyhow gutter `✗ unknown alias 'X'` + gutter bars, exit 1 After (all four surfaces): - Same clap-native `error:` / `tip:` / `Usage:` layout, exit 2 - `config alias show/dry-run` say "alias" / "aliases" instead of "subcommand" / "subcommands" — the positional is an alias name, not a subcommand ## How `unknown_alias_error` builds a real `clap::Error` with `ErrorKind::InvalidSubcommand`, renders it through clap, then substitutes clap's fixed phrases (`unrecognized subcommand` → `unrecognized alias`, `similar subcommands` → `similar aliases`, `similar subcommand` → `similar alias`). Writing through `anstream::AutoStream::auto(stderr)` gets NO_COLOR / TTY detection and clap's singular-vs-plural phrasing automatically. Returns `WorktrunkError::AlreadyDisplayed { exit_code: 2 }` so `main`'s `finish_command` still runs `terminate_output` + `write_if_verbose`. Substitutions are deliberately narrow — an alias name or typo containing the literal substring `subcommand` (e.g. `my-subcommand`) is echoed verbatim, not mangled. ## Changes - `src/commands/config/alias.rs`: replace the anyhow-returning `unknown_alias_error` (exit 1, custom gutter) with a clap-rendering version (exit 2, matches clap-native surfaces). Parameterized by `sub` so the Usage line reads `wt config alias <sub> <NAME>`. - New tests: `test_config_alias_dry_run_unknown_suggests`, `test_config_alias_show_unknown_singular_suggestion`, `test_config_alias_show_unknown_preserves_user_input`. - Refreshed `test_config_alias_show_unknown_suggests` and `test_config_alias_show_unknown_no_suggestions` snapshots. ## Test plan - [x] `cargo test --test integration step_alias` (58 pass) - [x] `cargo test --lib --bins` (598 pass) - [x] `pre-commit run --all-files` clean - [x] Manual smoke on all four paths (exit 2; ANSI stripped under `NO_COLOR=1 | cat`; preserved under `CLICOLOR_FORCE=1`) - [x] Aliases containing `subcommand` echoed verbatim under typo --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
f10ea23df7 |
refactor(cli): drop post-alias --yes from hand-rolled parser (#2290)
PR #2279 promoted `-y`/`--yes` to a top-level global clap flag. The hand-rolled `AliasOptions::parse` kept consuming a post-alias `--yes` for back-compat — that's the deferred cleanup landing here. `run_alias` now reads only the global flag (`skip_approval = global_yes`). This is a real cutover, not a no-op. Clap's `global = true` does not propagate flags across an `external_subcommand` boundary (verified against clap 4.6 — `wt deploy --yes` results in `Custom(["deploy", "--yes"])` with `global_yes = false`). So the post-alias form was never reaching the global parser; it only worked because `AliasOptions::parse` consumed it. Removing the field removes user-visible behavior: | Form | Before | After | |---|---|---| | `wt -y deploy` | skips approval | skips approval | | `wt --yes deploy` | skips approval | skips approval | | `wt deploy --yes` | skips approval | errors: "Unknown flag '--yes'" | | `wt deploy -y` | skips approval | silently forwards `-y` as `{{ args }}` positional | The `--yes` / `-y` asymmetry on the post-alias side is documented in the `AliasOptions::parse` docstring and locked in by parser tests. ## Test changes - `test_post_alias_yes_still_works` → `test_post_alias_yes_no_longer_supported` (asserts the new error) - 14 call sites in `tests/integration_tests/step_alias.rs` migrated from `&[..., "--yes"]` to `make_snapshot_cmd_with_global_flags(..., &["-y"])` - 4 unnecessary `--yes` flags removed from `tests/integration_tests/bare_repository.rs` (the `print-repo-path` user-config alias never required approval) > _This was written by Claude Code on behalf of Maximilian_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
9d6c5b71d4 |
fix(step): honor -C and --config in wt step --help (#2176)
`augment_step_help` resolves aliases via `Repository::current()` and `UserConfig::load()`. Because help ran before `apply_global_options`, `wt -C other step --help` listed aliases from the process cwd instead of `other`, and `wt --config custom.toml step --help` ignored `--config`. Parse globals in a single early pass against the real `Cli` definition (`cli::build_command().ignore_errors(true)`), apply them, then run help. The same matches also tell us whether this is `wt step` help (vs a nested `step promote --help`), so the splice path has no separate arg scanner and no second declaration of which global flags take values. Net: −20 lines, one arg-scanning pass, global-flag definitions live only on `Cli`. Integration test covers the `-C` path; the shared early parse means `--config` rides the same code. > _This was written by Claude Code on behalf of Maximilian_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
56b00c8496 |
fix(help): silence user-config side effects on wt step --help (#2179)
`wt step --help` called `UserConfig::load()` to render the alias
listing, which triggered `check_and_migrate` side effects: deprecation
warnings to stderr and a `.new` migration file written next to the user
config. Help is an informational surface that should stay quiet.
Extend the existing `suppress_warnings()` latch — already used by shell
completion, picker, and statusline — to also gate `.new` file writes and
the `approved-commands` → `approvals.toml` copy. Call it from
`augment_step_help` before loading config. One latch, one mechanism; no
second loader.
Integration test writes a user config containing the deprecated `{{
main_worktree }}` template variable, runs `wt step --help`, and asserts
stderr is empty and no `.new` file was created.
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
5da43d1994 |
fix(alias): make wt step <alias> --dry-run match lazy-expansion runtime (#2170)
## Summary
Recent changes made alias template expansion lazy so that a later step
in a pipeline can read `{{ vars.foo }}` set by an earlier step via git
config. The dry-run path still eagerly expanded every command against
the initial context, so `wt step deploy --dry-run` failed with an
undefined `vars.*` even when `wt step deploy` would succeed.
Mirror the hook dry-run pattern: for templates that reference `vars.*`,
syntax-validate (catching typos like `{{ vars..foo }}`) and show the raw
template; other templates expand eagerly as before. Factor the
parse-only check into a shared `validate_template_syntax` helper used by
both alias and hook lazy paths.
## Test plan
- [x] New test `test_step_alias_dry_run_vars_across_steps` — pipeline
where step 2 reads `{{ vars.target }}` set by step 1 now succeeds on
`--dry-run --yes` and shows the raw template for step 2.
- [x] New test `test_step_alias_dry_run_catches_syntax_error` — `{{
vars..target }}` still fails dry-run with `syntax error` in stderr.
- [x] `cargo run -- hook pre-merge --yes` — all 3158 tests pass, all
lints clean.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
101c341673 |
Prefixed-line output for concurrent foreground commands (#2145)
Concurrent foreground commands (alias tables, and eventually the
pipeline-form concurrent step for pre-* hooks once its deprecation
completes) now stream each child's output prefixed by a colored command
label, via a shared `output::concurrent::run_concurrent_commands`
executor.
## Why
Main's `execute_pipeline_foreground` ran concurrent children via
`std::thread::scope` + `execute_shell_command` with inherited
stdout/stderr. Under load that path is strictly worse than serial:
concurrent writes to the same fd aren't atomic past `PIPE_BUF`, so lines
drop (measured: 12 of 100 000 lost in a two-child 50 000-line test), and
the output is unlabelled, so you can't tell which child said what.
## What
- **New executor** — `src/output/concurrent.rs`. Spawns every child with
piped stdout+stderr, one reader thread per stream, a single-writer drain
on the main thread that prefixes every line with `{label} │ ` (color
rotated through an 8-entry palette, label padded to the widest in the
group). One signal-forwarder thread watches SIGINT/SIGTERM and delivers
graceful-escalation to every child's process group.
- **Rewire** — `execute_pipeline_foreground`'s concurrent branch
(`command_executor.rs`) now dispatches to the new executor when
`concurrent=true` (aliases). Serial callers (hooks — `concurrent=false`
pending the pre-hook table-form deprecation) unchanged.
- **Hardening** — the executor was stress-tested via `/popper` and four
actionable issues were fixed inline:
- Non-UTF-8 child output no longer hangs the reader (switched from
`BufRead::lines()` to byte-oriented `read_until(b'\n')` +
`from_utf8_lossy`). `git diff` on binaries, non-UTF-8 locales, and
raw-byte tools now drain correctly.
- Spawn failure mid-loop kills+reaps every already-spawned child before
propagating (no orphans on e.g. `RLIMIT_NPROC`).
- `signal_hook::Signals` latch is installed before the spawn loop, so a
SIGINT arriving mid-spawn is queued rather than default-killing wt.
- Second SIGINT immediately SIGKILLs every pgid — "user is impatient"
path, skips the graceful escalation that was silently discarding repeat
presses.
## Tests
Alias concurrent path now has five targeted tests covering the hardened
properties end-to-end:
- `test_alias_concurrent_prefixes_output` — per-command `{label} │ …` in
stderr
- `test_alias_concurrent_receives_sigint` — SIGINT forwards to every
child, no orphans after wt exits
- `test_alias_concurrent_large_output` — two 50 000-line children,
asserts 100 000 prefixed occurrences (regression test for the line-loss
that main's path exhibited)
- `test_alias_concurrent_handles_non_utf8` — mid-stream `\xff` followed
by 50 000 lines, asserts every line drains
- Plus the existing `test_alias_concurrent_steps` and
`test_alias_concurrent_step_failure`
Deterministic-ordering tests (e.g. `test_alias_pipeline_announcement`)
still work via the `WORKTRUNK_TEST_SERIAL_CONCURRENT` env var, which the
executor honors internally by spawning sequentially while keeping
prefixed output.
## Scope
- Aliases get the new behavior today.
- Foreground hooks are unchanged (`concurrent=false`) until the pre-hook
table-form deprecation window closes — that flip is queued on a separate
branch.
- Background pipeline runner is unchanged (different output target:
per-command log files).
## Deferred low-severity findings
Documented in `.tmp/iterate-concurrent-tests.md`; none affect
correctness for typical workflows. Covers serial per-child escalation
latency, zombie-in-input-order waiting, `stderr().lock()` held for whole
drain, PID-recycling race window, and `context_json` stdin-write
blocking if payloads ever exceed 64KB.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
b6ae21d6e1 |
refactor: prefer raw strings over escaped literals (#2150)
Converts ~29 string literals across 16 files from escaped form
(`"\\d+"`, `"{\"name\": 1}"`) to raw-string form (`r"\d+"`, `r#"{"name":
1}"#`) where possible. Strings containing control escapes (`\n`, `\t`,
`\u{1b}`, etc.) are left alone — raw strings can't represent those.
No behavior change; `cargo check/clippy -D warnings/fmt/test` all clean
locally.
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
faab53f1f8 |
feat(step): show aliases in wt step --help; list user + project separately (#2141)
Follow-ups from #2131, addressing two gaps raised in that review. ## 1. `wt step --help` / `-h` now shows configured aliases In the original PR, the Aliases section appeared only when running bare `wt step`, because clap's `DisplayHelp` error (triggered by `--help` / `-h`) was intercepted in `help.rs` before `step_list()` ran. Made `StepCommand` required (`arg_required_else_help = true`) so bare `wt step` triggers the same `DisplayHelp` path. The splice now lives in `help.rs::maybe_handle_help_with_pager`, scoped to the step subcommand via a small `is_help_for_step` positional-args scan. `step_list()` and its help-rendering helper are deleted — one entry point, one splice. ## 2. User + project aliases show as two rows, not a merged summary Previously, when user and project both defined the same alias name, `load_aliases_for_listing` called `append_aliases` which merged them via `merge_append`. The summary for that merged config lost the original per-source command text (often reduced to `<2 steps>` for unnamed singles). Now each source contributes its own row, user first (matching runtime order — both still run). Rows get `(user)` / `(project)` markers only when the name appears in both; unique names stay unannotated. ## Testing Unit test covers the new render behavior (unique names, collision with two rows). Integration test covers `wt step -h` showing aliases. Existing `test_step_list_with_aliases` / `test_step_list_no_aliases` snapshots picked up the new rendering path (clap help → `md_help::render_markdown_in_help_with_width` instead of direct `eprint!`); `help_step_long` / `help_step_short` and the generated docs picked up `[COMMAND]` → `<COMMAND>` from the required-subcommand change. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
371528e4cd |
feat(step): list configured aliases in wt step (#2131)
Previously `wt step` with no subcommand printed clap's auto-help, which
only covered built-in steps (`commit`, `squash`, `push`, …). Aliases
defined in `[aliases]` were invisible there — users had to remember what
they'd configured, or grep their config files.
Now `wt step` renders the same help plus an `Aliases:` section listing
merged user+project aliases. Each entry shows a one-line template
summary (first line with `…` suffix for multi-line bodies; `name; a, b`
form for named pipelines) and a `(shadowed by built-in)` marker in
yellow for names that collide with built-in steps. The section is
spliced in right after Commands — next to the built-ins it extends — by
finding the styled `Options:` heading in clap's rendered output and
inserting before it.
`wt step -h` is unchanged (still pure clap) so reference help stays
stable; the aliases listing is a discovery surface for the no-args case.
### Example
```console
$ wt step
wt step - Run individual operations
Usage: wt step [OPTIONS] [COMMAND]
Commands:
commit Stage and commit with LLM-generated message
squash Squash commits since branching
…
relocate [experimental] Move worktrees to expected paths
Aliases:
deploy make deploy BRANCH={{ branch }}
port echo http://localhost:{{ branch | hash_port }}
squash this shadows built-in (shadowed by built-in)
Options:
-h, --help Print help (see more with '--help')
Global Options:
-C <path> Working directory for this command
…
```
### Notes
- `Commands::Step { action }` moves from `StepCommand` to
`Option<StepCommand>`; the usage line changes from `<COMMAND>` to
`[COMMAND]` (reflected in the updated help snapshots).
- Splice search string is derived from `cli::help_styles().get_header()`
— same source clap uses — so if the header style changes, the search
string moves with it. Snapshot tests catch any behavior drift.
- Outside a repo or with a malformed project config, the aliases section
falls back silently to just listing user-config aliases (or nothing).
This is a discovery surface, not an execution surface, so breaking help
rendering for a bad config seemed wrong.
### Testing
- Two new integration snapshot tests: `test_step_list_with_aliases`,
`test_step_list_no_aliases`.
- Four unit tests for `format_alias_summary` covering single-command,
multi-line, named pipeline, and all-unnamed pipeline cases.
- Updated `help_step_short` / `help_step_long` snapshots for the
`<COMMAND>` → `[COMMAND]` change.
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
b174658e29 |
Split directive file into CD (raw path) and EXEC (shell) files (#2118)
The shell wrapper previously used a single `WORKTRUNK_DIRECTIVE_FILE` where wt wrote shell commands (`cd '/path'`, arbitrary `--execute` payloads). This meant the cd path went through shell parsing — any content wt wrote was sourced as shell. This splits the protocol into two files with different trust levels: - **`WORKTRUNK_DIRECTIVE_CD_FILE`** — raw path, read with `cd -- "$(< file)"`. No shell parsing, no escaping, no injection surface. Safe to pass through to alias/hook child processes. - **`WORKTRUNK_DIRECTIVE_EXEC_FILE`** — arbitrary shell (from `--execute`), sourced by the wrapper. Scrubbed from alias/hook child environments so hook bodies cannot inject shell into the parent session. When a nested `wt` inside an alias body tries `--execute` without the EXEC file, the command is dropped with a warning linking to #2101 for user feedback. The old `WORKTRUNK_DIRECTIVE_FILE` is silently honored for one release (users who upgrade wt without restarting their shell). Bash, zsh, fish, and PowerShell self-update on restart; nushell requires `wt config shell install`. Closes #2101 > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
66458e87e4 |
refactor(alias): remove lazy/eager template expansion branching (#2110)
The lazy/eager distinction in alias template expansion was unnecessary — alias expansion always happens at execution time (in `AliasExecCtx::run`), so `vars.*` references naturally read fresh values from git config without special handling. The "lazy" path was round-tripping `context_json` back to the same `HashMap` that was serialized from `context_map`. Removes the intermediate `HashMap<&str, &str>`, the `is_pipeline` field, and the `template_references_var` branching. Both dry-run and execution paths now route through `expand_shell_template`, making it the canonical expansion path for all three consumers (hooks, aliases, pipelines). Follow-up to #2095 and #2103. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7b91a71c37 |
Show pipeline structure in alias announcements (#2092)
## Summary Two follow-ups to the recently-merged unify-hook-alias work (#2089): **Pipeline announcement.** Aliases now print a structured summary of their pipeline (e.g., `Running alias deploy: install; build, lint`) instead of the bare `Running alias deploy`. Hook and alias formatters share a new `format_pipeline_summary_from_names` helper that takes per-step names plus label closures — hooks pass `source:` prefixes with unnamed-count fallbacks, aliases pass plain names and skip unnamed (no natural fallback label like `user`/`project`). **Concurrent execution stays separate.** I considered unifying `AliasExecCtx::run` (uses `thread::scope` because `execute_shell_command` is a blocking streaming call) with `run_concurrent_group` in `run_pipeline.rs` (uses raw `Child` handles + log-file redirection). The leaf primitives diverge — one needs threads because the streaming executor blocks, the other uses processes directly — and bridging them requires either making the streaming executor non-blocking (entangled with signal forwarding, ANSI reset, `Cmd` tracing) or threading background commands that don't need it. A module-level doc comment in `alias.rs` explains the divergence so the next person doesn't repeat the analysis. **Test escape hatch.** Adds `WORKTRUNK_TEST_SERIAL_CONCURRENT=1` honored by both concurrent paths, mirroring the `RAYON_NUM_THREADS=1` pattern in `step_prune` tests. Lets snapshot tests use meaningful command output (e.g., `echo INSTALL/BUILD/LINT`) and still produce a deterministic ordering. ## Test plan - [x] 5 unit tests for `format_alias_announcement` (single unnamed, all-unnamed pipeline, concurrent named, pipeline named, mixed named/unnamed) - [x] Integration snapshot test `test_alias_pipeline_announcement` exercising the alias path - [x] Two integration tests in `post_start_commands.rs` (`test_post_start_concurrent_serial_force`, `test_post_start_concurrent_serial_bails_on_failure`) exercising the background pipeline runner's serial branch — happy path (deterministic append ordering) and failure path (bail-on-first, second never runs) - [x] Existing hook formatter tests still cover the source-prefixed path via the shared helper - [x] Full `wt hook pre-merge --yes` (3030 tests, all green) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
73b4f8305d |
Unify hook and alias shell execution (#2089)
Hooks, aliases, and `for-each` all run shell commands in a worktree but used two separate execution functions with different capabilities. This collapses them into one (`execute_shell_command`), then builds on the shared foundation. **Phase 1** — `run_command_streaming` and `CommandError` deleted. All three consumers call `execute_shell_command` (renamed from `execute_command_in_worktree`) with a new `directive_file` parameter. `Cmd` gains a `.directive_file()` builder method that re-adds the env var after the security scrub. Aliases and `for-each` gain signal forwarding and ANSI reset. **Phase 2** — Aliases iterate `cmd_config.steps()` instead of flattening via `commands()`. `HookStep::Concurrent` steps spawn threads via `thread::scope`. Lazy `vars.*` expansion supported in pipelines. **Phase 3** — Foreground hooks (pre-\*, post-\* with `--foreground`) pass the directive file through to child processes. `wt switch --create` inside a pre-start hook body now lands the shell in the new worktree. Background hooks continue to scrub. Follow-up: alias announcements could show pipeline summary (e.g., `Running alias deploy: install; build, lint`). > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
d889738258 |
fix: pass directive file through wt step aliases (#2077)
|
||
|
|
79dc6a229a |
refactor: extract append_aliases helper, add e2e alias test (#1731)
Follow-up to #1727. Two improvements: 1. **DRY**: Extract the repeated `entry().and_modify(merge_append).or_insert()` pattern into `append_aliases()` in `config::commands`. Used by all 4 alias merge sites (`merge_alias_maps`, `aliases()` accessor, `step_alias()`, `load_aliases_for_completion()`). 2. **Integration test**: `test_alias_append_executes_both` verifies both user and project aliases actually execute in order — output shows `USER` then `PROJECT`. Also adds doc comments noting the named-table format works for aliases (for consistency with hooks, not documented for users). > _This was written by Claude Code on behalf of @max-sixty_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a1fc38f3e5 |
feat: use append semantics for alias merging (#1727)
Alias merging now uses append semantics across all layers, matching how hooks merge: **Within user config** (global + per-project): both run on name collision (global first). Uses `CommandConfig::merge_append()`. **Across configs** (user + project): both run (user first, then project). Project commands always need approval — the old bypass that skipped approval when user had the same alias name is removed. The alias value type changes from `String` to `CommandConfig` — the same type hooks use. This stores each command separately and enables the named-table TOML format for multi-command aliases. Key changes: - `merge_alias_maps()` and `UserConfig::aliases()` use `CommandConfig::merge_append()` on collision - `step_alias()` iterates over `CommandConfig.commands()`, running each command separately (fail-fast) - `approve_alias_commands()` approves each project-config command individually - `alias_needs_approval()` simplified — always returns project commands if they exist > _This was written by Claude Code on behalf of @max-sixty_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
bf6030b940 |
fix(step): warn when alias shadows a built-in step command (#1389)
When aliases are configured with names that match built-in `wt step` subcommands (e.g., `commit`, `rebase`), they're silently shadowed — clap intercepts the command before the alias handler runs. This adds a warning when any alias invocation detects shadowed names in the merged config. Handles singular/plural grammar and bolds alias names for consistency with adjacent error messages: - "Alias **commit** shadows a built-in step command and will never run" - "Aliases **commit**, **rebase** shadow built-in step commands and will never run" > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
64c70ef869 |
feat(alias): add typo suggestions for unknown step commands (#1363)
When the user types an unknown step command or alias name, suggest the
closest match using Jaro similarity (same algorithm and 0.7 threshold as
clap). Checks both built-in step commands and configured aliases.
Also fixes error message styling per output guidelines: bold for names
instead of quotes, no second-person pronouns ("perhaps" instead of "did
you mean").
Examples:
- `wt step comit` → `Unknown step command comit — perhaps commit?`
- `wt step deplyo` (with alias `deploy`) → `Unknown step command deplyo
— perhaps deploy?`
- `wt step zzz` (no close match) → `Unknown alias zzz (available:
deploy, hello)`
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
55bc9188b4 |
feat(step): add alias command for user-defined command templates (#1348)
## Summary
- Adds `wt step <alias-name>` for running user-defined command templates
configured in `[aliases]` sections of user or project config
- Aliases support the same template variables as hooks (`{{ branch }}`,
`{{ worktree }}`, etc.) plus custom `--var KEY=VALUE` variables
- Project-config aliases require the same command approval flow as
project hooks; user-config aliases are trusted. `--dry-run` skips
approval since it's a read-only preview
- Alias names matching built-in step commands are filtered from the
"available" list in error messages (shadowed by the built-in)
- Introduces `Phase` enum (`Hook(HookType)` | `Alias`) replacing the old
`hook_type` + `phase_override` pattern in the approval system
- Renames `HookCommand` to `ApprovableCommand` since it now covers both
hooks and aliases
## Test plan
- [x] Unit tests for `AliasOptions::parse` (name-only, --dry-run, --yes,
--var, empty key rejection, positional arg rejection)
- [x] Integration tests for alias execution, dry-run (without --yes),
exit code propagation
- [x] Integration tests for shadowing (filtered from available list)
- [x] Integration tests for user/project config merging
- [x] Integration tests for approval flow (project prompts, user skips,
override skips, already-approved, --yes bypass, decline)
- [x] Unit tests for `merge_alias_maps` coverage
- [x] Sync test: `BUILTIN_STEP_COMMANDS` matches actual `StepCommand`
clap variants
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|