## Summary
- `wt foo` now runs `wt-foo` from PATH when `foo` is not a built-in,
mirroring `git foo` → `git-foo`. Third-party tools like a hypothetical
`wt-sync` can be installed and invoked as `wt sync` without touching
this repo.
- Built-ins always take precedence (clap only dispatches `External` when
no built-in matched), so external binaries cannot shadow existing
subcommands.
- Nested subcommand hints still pre-empt the PATH lookup — `wt squash`
continues to suggest `wt step squash` rather than searching for
`wt-squash`.
- When nothing matches, wt prints a git-style `'foo' is not a wt
command` error with a Levenshtein-based typo suggestion (replacing the
old clap `InvalidSubcommand` handler).
- The global `-C <path>` flag is forwarded as the child's working
directory, matching git's semantics.
- Child exit codes (including Unix signal codes) are propagated
verbatim. wt does **not** decorate child failures with its own error
line — the child has already reported whatever it needed to.
Requested in #2053 — the original PR added `wt sync` as a built-in, but
the preferred approach is a generic extensibility mechanism so `wt sync`
can call any `wt-sync` binary on PATH.
## Implementation
- `Commands::External(Vec<OsString>)` captured via clap's
`#[command(external_subcommand)]`.
- `src/commands/external.rs` owns the dispatch: nested-suggestion check
first, then `which::which("wt-<name>")`, then run with
`Command::status()` inheriting stdio.
- `main()` dispatches `External` directly (instead of via
`dispatch_command`) so the parsed `-C <path>` can be forwarded as the
child's cwd.
- The nested-suggestion path moved from clap's built-in error renderer
to our module; help snapshots updated to the cleaner worktrunk-style
output (✗ / ↳).
## Test plan
- [x] `cargo test --test integration external_subcommand` — 7 new
integration tests cover happy path, not-found error, typo suggestion,
nested suggestion winning over PATH lookup, exit-code propagation (exit
42), `-C` flag forwarding, and `--help` passthrough.
- [x] `cargo test --bins` — 3 new unit tests for `closest_subcommand`
(typo, unrelated, hidden).
- [x] `cargo test --test integration` — full suite passes (1407 tests).
- [x] `cargo test --lib --bins` — full unit suite passes (500 tests).
- [x] `cargo fmt --check` and `cargo clippy --all-targets --all-features
-- -D warnings`.
- [x] Manual smoke tests: `wt wt-<name>`, `wt unknown`, `wt siwtch`, `wt
squash`, `wt -C /tmp wt-<name>`, `wt wt-<name> --help`, child exit code
42 propagation.
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
`docs/content/config.md` was rendering `$ wt list (markers)` as its
prompt line. The HTML branch of `expand_command_placeholders` used the
placeholder id (the snapshot-lookup key, e.g. `wt list (markers)`) for
the `cmd=` parameter of the generated terminal shortcode, so
disambiguation suffixes leaked into the rendered prompt. This swaps it
for `display_cmd` (the inner command from the source code block),
mirroring what `ExpandMode::Plain` already does and the fix that landed
for the plain pipeline in #2049.
After rebasing on top of #2057 (which unified the HTML/plain functions
behind `ExpandMode`), the fix is a one-line change:
`cmd=\"{placeholder_id}\"` → `cmd=\"{display_cmd}\"`, plus a comment
update explaining why. The `docs/content/config.md` line is the
auto-regenerated artifact.
## Test plan
- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — passes;
`docs/content/config.md:856` reads `cmd="wt list"` with no `(markers)`
suffix
- [x] `cargo run -- hook pre-merge --yes` — all tests, lints, doctests
pass
- [x] `docs/content/list.md` unchanged
- [x] `skills/worktrunk/reference/config.md` unchanged (plain pipeline
was already correct via #2049)
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
`wt config state vars` now lists subcommands as `get → list → set →
clear` instead of `get → set → list → clear`, grouping all reads ahead
of all writes. The policy comment on `VarsAction` is updated to match.
Follow-up to #2043 — was noted there as a deferred tightening.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds per-enum ordering policy comments to every clap `Subcommand` enum
so future edits have a clear rule for where new variants go. Two enums
are reordered to match their newly documented policy.
Each `Subcommand` enum now has a `// Ordering: ...` comment immediately
above its `#[derive(Subcommand)]`. The comments use one of five
principles, picked per group:
- **Pipeline / lifecycle order** — `Commands`, `StepCommand`,
`HookCommand` (steps follow execution order; hooks follow worktree
lifecycle phases with `pre-`/`post-` paired)
- **CRUD** — `*Action` enums (`get`, `set`, `clear`, with `list` where
present)
- **Action + inverse adjacency** — `install`/`uninstall`, `add`/`clear`
- **User journey / core-ness** — `ConfigCommand`, `StateCommand`,
top-level `Commands`
- **Alphabetical** — `ConfigPluginsCommand` (equal-weight siblings)
Two enums are reordered to match these policies:
- **`StepCommand`**: swap `Push` and `Rebase` so the enum matches the
actual `wt merge` pipeline order (`commit` → `squash` → `rebase` →
`push`). The bullet list and `wt merge` prose were already in this
order; only the enum was out of step.
- **`StateCommand`**: lead with the aggregate operations (`get`,
`clear`), then primary state managed by wt (`default-branch`,
`previous-branch`, `logs`, `hints`), then per-branch display annotations
(`ci-status`, `marker`), then experimental keys (`vars`). Previously the
keys came first and `get`/`clear` were buried at the end.
The auto-generated `docs/content/{config,step}.md` and
`skills/worktrunk/reference/{config,step}.md` files are regenerated to
reflect the new orderings, and `help_config_state` /
`help_step_{long,short}` snapshots are updated.
Top-level `Commands` ordering keeps `remove` before `merge` because
`remove` is a primitive worktree operation while `merge` is a composite
that wraps it — "more core" wins, not "lifecycle order".
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flattens categorization of `.git/wt/logs/` by making the filesystem
encode what the old scheme crammed into filenames.
**Layout:** top-level *files* are shared logs (`commands.jsonl*`,
`verbose.log`, `diagnostic.md`); top-level *directories* are per-branch
log trees — `{branch}/{source}/{hook-type}/{name}.log` for hook output,
`{branch}/internal/remove.log` for background removal,
`wt/internal/trash-sweep.log` for the trash sweeper. Categorization
becomes a trivial file-vs-directory check, eliminating the exclusion
rule `ends_with(".log") && !is_diagnostic_file(name)`.
**Wins:** per-branch listing/clearing is now O(that branch) instead of
O(all logs); orphan cleanup for a removed branch is a single
`remove_dir_all`; filenames drop the joined-tuple collision hashes they
only needed to disambiguate flat keys.
**Transition:** `clear_logs` keeps a self-healing sweep of legacy
top-level `.log` files so users transition without an explicit
migration. A pinning test
(`test_state_clear_logs_sweeps_legacy_flat_files`) guards that behavior.
**Observable change:** `logs get --format=json` now puts relative paths
in the `file` field (e.g. `main/user/post-start/server.log`). Log
locations are listed as "flexible" in `CLAUDE.md`, so this is in scope.
**Reviewer orientation:**
- `src/commands/process.rs` — `HookLog::path()` rewritten; `suffix()` /
`filename()` deleted.
- `src/commands/config/state.rs` — new `walk_hook_output_files` /
`walk_branch_dir` / `HookOutputEntry`; `clear_logs` handles legacy
sweep; `partition_log_files_json` + `render_*` split along the top-level
vs hook-output seam; module docstring pins the invariant.
- `src/testing/mod.rs` — `wait_for_file_count` walks recursively.
- Test fixtures in `tests/integration_tests/config_state.rs` use new
`hook_log_rel_path` / `internal_log_rel_path` / `write_log_at` helpers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
## Summary
Background `wt remove` renames worktrees into
`.git/wt/trash/<name>-<timestamp>` before firing a detached `rm -rf`. If
that cleanup is interrupted (SIGKILL, reboot, disk full), the directory
is orphaned and nothing reclaims the space.
After `wt remove` prints its primary output, it now sweeps trash entries
older than 24 hours via a single detached `rm -rf`. Age is parsed from
the filename suffix, so the sweep is deterministic under
`WORKTRUNK_TEST_EPOCH`.
## Notes
- Runs **after** `handle_remove_output` so it never delays
time-to-first-output. `.claude/skills/writing-user-outputs/SKILL.md`
documents the general rule for future handlers.
- Uses existing `spawn_detached` infrastructure and a new
`InternalOp::TrashSweep` log name (`.git/wt/logs/wt-trash-sweep.log`).
- Unparseable entries are left alone — the sweep only touches names
worktrunk produced.
- Integration test seeds stale + fresh entries, runs `wt remove`, and
polls for the stale entry to disappear while the fresh one stays.
Ref #1974 — related safety improvement (data lingers in trash briefly
before deletion), but doesn't resolve the prune confirmation prompt
discussion there.
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Both features were hard to find from the docs entry points: the homepage
workflow showcase didn't mention them, and tips-patterns used them
inside larger recipes without dedicated anchors.
**Homepage (\`worktrunk.md\`):** Add an "Aliases & per-branch variables"
bullet to the Workflow automation list, as the final item right before
"lots more".
**Tips & patterns:**
- Add two new dedicated sections near the top: \`wt\` aliases and
Per-branch variables. Both are kept brief — only the recipe-specific
content (composing with \`hash_port\`, parametrizing via \`vars\`,
sticky per-branch environment) — with pointers to the reference docs for
breadth.
- Rename the existing shell-alias section to "Shell alias for new
worktree + agent" to disambiguate from \`wt\` aliases.
- Reorder so the template-variable recipes (dev server, database per
worktree) appear before "Eliminate cold starts", grouping related
content.
No changes to reference docs or CLI source this round — purely homepage
and tips-patterns.
Co-authored-by: Claude <noreply@anthropic.com>
Two small follow-ups to the link audit in #2036:
- `post-start hooks` in the "Parallel agents" paragraph now points at
`#pre-start-vs-post-start` so readers land on the specific subsection.
- `Set up project hooks` in the Next steps list is now `Set up hooks` —
the narrower phrasing was misleading because the link covers both user
and project hooks. Target URL is unchanged.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude <noreply@anthropic.com>
The "Copy build caches" bullet in the feature list under "Workflow
automation" pointed to the whole `wt step` page (covers 10 subcommands)
instead of the specific `copy-ignored` section. Anchor it so readers
land on the relevant docs.
Audited the other anchored links in the same list (Interactive picker,
`wt list --full`, CI status, LLM summaries, PR checkout, Dev server per
worktree) — all resolve to valid anchors in their target pages.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude <noreply@anthropic.com>
Mirrors the `vars` pattern from #2034: wraps each state key's
description in a link to its subdoc section on `config.md`. Covers
`default-branch`, `ci-status`, `marker`, and `logs`.
`previous-branch` has no `<!-- subdoc: -->` marker in the parent
`after_long_help`, so there's no `#wt-config-state-previous-branch`
anchor to link to — left as plain text.
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
The vars feature is documented in three places (hook template variable
table, config state vars page, tips-patterns recipes), but these pages
didn't cross-reference each other. Readers encountering vars in one
place had no clear path to the other.
Added links so the two main vars pages link to each other
bidirectionally, and every other mention now points readers to either
the CLI reference (how to set/get) or the template reference (how to use
in hooks/aliases).
**Changes (5 locations — 4 in CLI source, auto-synced to docs/ and
skills/):**
- \`hook\` template variables table: \`{{ vars.<key> }}\` row now links
to \`wt config state vars\` docs
- \`wt list\` JSON output table: \`vars\` field now links to \`wt config
state vars\` docs
- \`wt config state vars\` "Template access" section: "hook templates"
now links to the template variables page
- \`wt config state\` keys list: the \`vars\` entry now links to its own
section
- \`tips-patterns.md\` "Database per worktree": "vars" now links to the
config page
No changes to aliases — their cross-references were already complete.
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Adds an HTML comment next to the picker keybinding table in
`src/cli/mod.rs` explaining why `Alt-r` is intentionally omitted — the
skim `reload` action resets the cursor (#1695), so the UX isn't polished
enough to document yet.
- Comment auto-syncs to `docs/content/switch.md` and
`skills/worktrunk/reference/switch.md`.
Closes#1881
## Test plan
- [x] `test_help` snapshots pass (HTML comment is stripped from rendered
help)
- [x] `test_command_pages_and_skill_files_are_in_sync` passes
🤖 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.6 <noreply@anthropic.com>
The `wt config state logs` documentation had several factual errors
discovered during an audit:
- **Wrong scope label**: "Background operation logs" was the subcommand
definition, but the directory also contains diagnostic files
(`verbose.log`, `diagnostic.md`) from `-vv` — not background operations.
Changed to "Operation and debug logs".
- **Wrong behavior for `commands.jsonl`**: The "Behavior" section said
"Overwrites — Same operation on same branch overwrites previous log" but
`commands.jsonl` appends (with rotation at 1MB). Removed the blanket
section, moved accurate behavior inline to each subsection where it
applies.
- **FAQ table overlap**: `*.log` glob matched `verbose.log`,
contradicting the explicit `verbose.log` row two lines below. Narrowed
to `{branch}-*.log`.
- **Undocumented JSONL fields**: Docs said "timestamp, command, exit
code, and duration" but the actual fields are `ts`, `wt`, `label`,
`cmd`, `exit`, `dur_ms` — the `wt` and `label` fields were missing.
Added a field table since the docs encourage `jq` queries.
- **Incomplete `clear` description**: FAQ said `wt config state clear`
"removes all worktrunk keys from `.git/config`, deletes CI cache, clears
logs, and removes stale trash" — omitting markers, hints, variables, and
previous-branch.
- **Inaccurate hook output filename pattern**: Table showed
`{branch}-{source}-post-start-{name}.log` but actual filenames include
hash suffixes from `sanitize_for_filename()` and cover all `post-*` hook
types, not just post-start.
- **Dead code cleanup**: Removed unreachable `"logs"` arm from
`handle_state_get` — `StateCommand::Logs` routes directly to
`handle_logs_get`.
Also includes the `state-logs-docs` branch changes:
`is_diagnostic_file()` filter so `logs get` and `logs clear` handle
`diagnostic.md`, `render_log_section()` generic helper, and three-way
partition in the JSON output.
> _This was written by Claude Code on behalf of Maximilian Roos_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add `--format=json` to 10 commands across two commits.
## Config state subcommands (commit 1)
- `logs get` — `{command_log: [...], hook_output: [...]}` with
file/size/modified_at per entry
- `ci-status get` — `{status, source, stale, url}` (richer than the text
mode's bare status string)
- `marker get` — `{branch, marker, set_at}` or `null`
- `vars list` — `{key: value, ...}` object
- `hints get` — `["hint-name", ...]` array
Also extracts a shared `log_entry_to_json` helper, deduplicating the
DirEntry→JSON conversion.
## Top-level commands (commit 2)
- `config show` — serialized user/project/system config with paths and
existence flags
- `step prune --dry-run` — array of candidates with branch, path, kind,
reason, target
- `remove` — array of removed worktrees with branch, path, deletion
status
- `merge` — summary with branch, target, committed, squashed, rebased,
removed
- `step for-each` — per-worktree results with branch, path, exit_code,
success
For action commands (remove, merge, for-each), progress goes to stderr
as usual; JSON summary goes to stdout at the end — same pattern as `wt
switch --format=json`.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`verbose.log` and `diagnostic.md` (created by `-vv`) were undocumented
in the `wt config state logs` help text and FAQ. `verbose.log` was
miscategorized as hook output in `logs get`, and `diagnostic.md` was
invisible to both `logs get` and `logs clear` (not matched by
`is_wt_log_file`).
Adds a DIAGNOSTIC section to all output paths (human table, JSON, `logs
get`, `state get logs`), updates help text ("two kinds" → "three
kinds"), and updates the FAQ file inventory.
Also extracts the three identical per-section render functions into a
single `render_log_section` parameterized by heading and filter
predicate — `render_all_log_sections` composes them and is called from
all three display paths.
Fixes a test filter bug where `\s*$` consumed trailing newlines,
collapsing blank line separators between populated sections. Changed to
`[ \t]*$` to match only horizontal whitespace.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- document `wt config plugins claude install` as the recommended plugin
install path
- keep the existing Claude marketplace commands as the manual equivalent
- align the docs page with the shipped CLI help and plugin installer
behavior
## Testing
- compared the docs change against `src/cli/config.rs`, which already
advertises `wt config plugins claude install`
- verified the diff stays limited to `docs/content/claude-code.md`
## Notes
- docs-only change
---------
Co-authored-by: ming <silverchris@foxmail.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The marker help text incorrectly stated markers appear at the "start" of
the Status column with examples like `🚧↑`. The rendering code
(`status_symbols.rs:300`) places markers at the end, after all git
symbols (`↑🚧`).
Replaced the hand-written example with the `<!-- wt list (markers) -->`
placeholder pattern, backed by a new `test_readme_example_list_marker`
snapshot test. In terminal `--help`, this shows `wt list` as a command
hint; in web docs, it expands to the actual colored table output.
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
Remove the "Use wt config show to view file-based configuration"
cross-reference (discoverable from `wt config --help`) and simplify the
marker example from `"🚧 WIP"` to just `🚧`.
Also fixes a pre-existing nondeterministic test failure in
`test_prune_removes_integrated_detached` by adding `RAYON_NUM_THREADS=1`
(matching the pattern used by `test_prune_removes_multiple_detached`).
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Adds `--format=json` to `wt switch` and WorktreeCreate/WorktreeRemove
hooks to the Claude Code plugin, so agent-created worktrees go through
`wt` instead of raw `git`.
**`wt switch --format=json`** prints structured JSON to stdout
(`action`, `branch`, `path`, etc.) without changing any behavior —
hooks, `--execute`, shell integration, and cd all run normally
regardless of format. A `SwitchFormat` enum (`text`/`json`) keeps the
help text clean (only valid values shown).
**Plugin hooks** are inline one-liners in `hooks.json`:
- `WorktreeCreate` — pipes CC's JSON through `jq` to extract the branch,
calls `wt switch --create --format=json`, extracts the path
- `WorktreeRemove` — extracts the worktree path from CC's JSON, passes
it directly to `wt remove -D --foreground`
Approval prompts still run — the hooks don't pass `--yes`, so `wt`'s
non-interactive detection applies normally.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Worktree removal renames directories into `.git/wt/trash/` for instant
UX, then deletes them in a background `rm -rf`. If the background
process fails or is killed, entries accumulate indefinitely — discovered
22 GB of stale trash from two entries whose background cleanup never
completed.
Adds trash cleanup to `wt config state clear`, which already handles CI
cache, logs, markers, hints, and vars. Also updates the FAQ to document
`.git/wt/trash/` in the "What files does Worktrunk create?" table and
the "Other cleanup" description.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`--no-hooks` describes what the flag does — skip hooks. `--no-verify`
was inherited from git's naming but doesn't match worktrunk's semantics
(there's no "verification" step being skipped).
`--no-verify` remains as a hidden alias that emits a deprecation
warning, retained for at least one release cycle per the project's
deprecation policy.
Changes across switch, remove, merge, step commit, and step squash:
- `--no-hooks` is the canonical visible flag
- `--no-verify` hidden, emits `▲ --no-verify is deprecated; use
--no-hooks instead`
- Error hints (`↳ To skip pre-merge hooks, re-run with --no-hooks`),
info messages, help text, docs, and config examples all updated
- `resolve_verify()` helper in main.rs deduplicates the deprecation
logic
- Backward-compatibility test verifies `--no-verify` still works
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
This PR adds OpenCode integration: activity tracking markers in `wt
list`, plugin installation via CLI, `wt config show` diagnostics, and
LLM commit generation detection.
Continues the work started in #1295 and #1533 which added OpenCode to
the docs and example config.
## What's included
- **Activity tracking plugin** (`dev/opencode-plugin.ts`): maps
`session.status`/`session.idle`/`session.deleted` to branch markers,
same pattern as Claude Code's plugin
- **`wt config plugins opencode install/uninstall`**: installs the
plugin to `~/.config/opencode/plugins/worktrunk.ts` — source embedded
via `include_str!()`, no npm needed. Sits under `wt config plugins`
alongside Claude Code's `wt config plugins claude`.
- **`wt config show` OPENCODE section**: shows plugin install status
with actionable hints (only when `opencode` is on PATH)
- **`LlmTool::OpenCode` variant**: detected via PATH for commit
generation auto-config
## Docs approach
Kept deliberately low-profile — no dedicated docs page, no README
mention. OpenCode is discoverable via `wt config show` and a mention in
tips-patterns. If it becomes popular, docs prominence can increase.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Maximilian Roos <m@maxroos.com>
Co-authored-by: Claude <noreply@anthropic.com>
`wt step copy-ignored` now calls `renice -n 19` on its own process
before starting work, so large reflink copies yield to interactive
foreground tasks. The call is in `step_copy_ignored()` rather than the
shared `COPY_POOL` init, so other callers (like `handle_promote`'s
cross-device fallback) keep normal priority. Non-fatal if `renice` is
missing or fails.
Avoids `libc::nice()` directly because of `forbid(unsafe_code)` —
`renice` is a one-time subprocess spawn during pool init.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Background pipelines no longer compile to compound shell strings with
`__WT_TPL_*` env vars and `eval "$(wt step eval --shell-escape ...)"`
wrapping. Instead, the parent `wt` process serializes a `PipelineSpec`
to JSON and spawns `wt hook run-pipeline` as a detached background
process, piping the spec to stdin. The background runner expands
templates just-in-time and spawns shell children per step.
Key changes:
- **New `spawn_detached_exec`** in `process.rs` — spawns a binary
directly (no intermediate shell), pipes data to stdin, with proper
detachment on both Unix (`process_group(0)`) and Windows
(`CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS`). Shared log setup
extracted into `create_detach_log` helper.
- **New `pipeline_spec.rs`** — serde types for the JSON spec
(`PipelineSpec`, `PipelineStepSpec`, `PipelineCommandSpec`) with
roundtrip test.
- **New `run_pipeline.rs`** — the background orchestrator. Reads spec
from stdin, walks steps in order (serial abort-on-failure, concurrent
spawn-then-wait-for-all), expands templates with `shell_escape=true`,
pipes context JSON to each shell child's stdin. Module docstring
specifies the full execution model.
- **`wt hook run-pipeline`** — hidden subcommand (not in `--help` or
autocomplete) replacing the old top-level `_run-pipeline`.
- **Deleted**: `build_pipeline_command`, `format_cmd`, `--shell-escape`
flag, `__WT_TPL_N` env vars, `extra_env` parameter on `spawn_detached`,
7 unit tests for the old shell builder, 2 integration tests + snapshots
for `--shell-escape`.
- **Updated docs** — "How it works" sections in CLI help,
`docs/content/hook.md`, and skill reference no longer show the compound
shell command example. Test comments updated to describe the new
execution model.
- **New tests** — concurrent group execution (both commands run),
concurrent partial failure (sibling completes, later steps abort), and
shell escaping of metacharacters (spaces, quotes, `$`).
> _This was written by Claude Code on behalf of maximilian_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Problem
`wt step prune` (and `wt remove`) fail to detect squash-merged branches
when the default branch has since modified the same files that the
branch touched. This is because `git merge-tree --write-tree` reports
conflicts when both sides changed the same files, and the code
conservatively treated conflicts as "not integrated."
The same issue affects `wt list` — the `WouldMergeAdd` task calls the
same function, so the integration symbol `⊂` is not shown for these
branches.
## Solution
When `git merge-tree --write-tree` conflicts, fall back to **patch-id
matching**: compute the branch's squashed patch-id (`git diff-tree -p
merge-base..branch | git patch-id --verbatim`) and check if any commit
on the target has a matching patch-id. This detects squash merges
because the squash-merge commit on the target has the exact same content
changes as the branch.
Uses `--verbatim` (not `--stable`) to avoid false positives from
whitespace normalization — `--stable` strips whitespace, so
tabs-vs-spaces would produce matching patch-ids even though file content
differs.
The fallback is only triggered when merge-tree conflicts — the happy
path (no conflicts) is unchanged.
The merge-tree → patch-id sequence is extracted into
`Repository::merge_integration_probe()`, a shared method used by both
`wt list` (parallel tasks) and `wt remove`/`wt merge` (sequential path).
This fixes a drift where the two call sites handled patch-id errors
differently.
## Known limitation
The patch-id fallback checks if the branch's diff was **ever** applied
to the target, not whether the effect **persists**. If a squash merge is
later reverted on the target and then the same files are modified
(causing merge-tree conflicts), the historical squash-merge commit's
patch-id still matches. This is documented in #1818. A follow-up could
add a revert-detection check after finding a match.
## Testing
- `test_remove_squash_merged_then_same_files_modified` — reproduces the
exact scenario from #1818 (branch modifies file, squash-merged, then
target modifies same file)
- `test_prune_squash_merged_same_files_modified` — verifies `wt step
prune --dry-run` detects the branch
- All existing squash-merge tests continue to pass (5 remove + 22 prune
tests)
Closes#1818
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
Adds a `## Hooks` section to user config docs — the config supported
hooks but didn't document them. Also fixes comment style issues and
improves cross-references.
- Added `## Hooks` section to user config with the three formats
(string, named table, pipeline) and user-vs-project differentiation
- Used plain text descriptions before code blocks instead of TOML
comments inside them, avoiding `# #` double-commenting in
`dev/config.example.toml`
- Replaced the duplicate TOML block in project config hooks with a
minimal example and cross-reference to `wt hook` docs
- Both hooks sections now link to `wt hook` docs as the canonical
reference
- Added CLAUDE.md guidance on avoiding TOML comments inside code blocks
in config docs
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Follow-up to #1856 (`no-ff` → `ff`). Renames the last negative-sense
config field so all boolean config fields use positive names defaulting
to true: `squash`, `commit`, `rebase`, `remove`, `verify`, `ff`, and now
`cd`.
The old `no-cd` field is kept for backward-compatible deserialization
with a deprecation warning guiding users to `cd = false`.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
`ff = true` (default) is more natural than `no-ff = false` — now
consistent with all other merge config fields which are positive-sense
defaulting to true.
CLI flags `--no-ff` and `--ff` are unchanged. The old `no-ff` config key
still works via serde deserialization + polarity inversion in
`normalize_deprecated_fields()`, with a deprecation warning.
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
## Problem
CI failed on main after #1845 (491108a). The
`test_project_config_docs_include_all_sections` test requires at least
one hook key (e.g. `pre-start`, `pre-merge`) to appear as a bare key in
the project config docs section of `src/cli/mod.rs`. The hooks
deduplication in #1845 replaced all hook examples with a
cross-reference, leaving no hook keys in the section.
## Solution
Added a brief TOML example showing three common hooks (`pre-start`,
`post-start`, `pre-merge`) below the cross-reference text. This
preserves the deduplication intent while satisfying the test constraint.
## Testing
- `test_project_config_docs_include_all_sections` — passes
- `test_command_pages_and_skill_files_are_in_sync` — passes (auto-synced
docs and skills)
- Help snapshots — no changes needed
---
Automated fix for [failed
run](https://github.com/max-sixty/worktrunk/actions/runs/23827619802)
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
The user config (`~/.config/worktrunk/config.toml`) supports hooks via
`OverridableConfig.hooks`, but the user config documentation didn't
mention them. The project config docs had a hooks section with the full
TOML format reference — this adds a comparable section to user config
and deduplicates the project config side.
- Added `### Hooks` section to user config docs with the three formats
(string, named table, pipeline) and user-vs-project differentiation
- Replaced the duplicate TOML block in project config hooks with a
cross-reference to the user config section
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Hyphens in var keys conflict with Jinja2 expression parsing —
`vars.db-url` is interpreted as `vars.db` minus `url`. Renamed `db-url`
to `db_url` in the database examples in hook docs and tips-patterns.
> _This was written by Claude Code on behalf of maximilian_
Co-authored-by: Claude <noreply@anthropic.com>
Same treatment as #1832: move prose descriptions out of TOML code blocks
so they don't produce `# #` lines in the generated `wt.example.toml`.
Also uncomments `hostname` field with an "Example:" note.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Pipeline steps referencing `{{ vars.* }}` are expanded at execution time
rather than upfront, so vars set by step N are available to step N+1.
This enables DRY patterns like deriving a container name once and
reusing it across pipeline steps and across hooks
(post-start/post-remove).
A pipeline that sets vars in step 1 and uses them in step 2:
```toml
post-start = [
"wt config state vars set container='{{ repo }}-{{ branch | sanitize }}-postgres'",
{ db = "docker run --name {{ vars.container }} ..." },
]
[post-remove]
db-stop = "docker stop {{ vars.container }} 2>/dev/null || true"
```
**Background pipelines** wrap lazy steps in `eval "$(wt step eval
--shell-escape "$__WT_TPL_N")"` with templates passed as env vars on the
spawned process. **Foreground mode** (`--foreground`) re-expands
templates in-process for structured error reporting.
Detection uses `minijinja::undeclared_variables` (via new shared
`template_references_var()` helper) — no string heuristics. Syntax
errors are caught at prepare time; only var resolution is deferred.
`--shell-escape` on `wt step eval` is hidden from `--help` (internal
mechanism).
Updates database examples in hook docs and tips-patterns to use the
pipeline pattern.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The "Setting overrides" subsection was the only child of "User
project-specific settings" — unnecessary nesting. Merged the override
description into the parent paragraph and dropped the `[experimental]`
tag.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The project config example file (`dev/wt.example.toml`) was
hand-maintained with no sync tests, while the user config example had a
robust auto-generation pipeline. This brings both to parity — `mod.rs`
is the single source of truth, sync tests generate both example files,
and CI catches drift.
Hooks documentation (~70 lines of formats, template variables, and
per-type examples) replaced with a pointer to `wt hook --help` plus a
quick-reference showing all three hook formats (string, named table,
pipeline). The example file now focuses on project-specific settings:
`list.url`, `forge`, `step.copy-ignored`, and aliases.
The sync test infrastructure is refactored from a user-config-specific
function into shared `extract_config_section` and
`assert_config_example_in_sync` helpers that both tests call.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The example config had all TOML values double-commented (`# #`), making
them look disabled-within-disabled. Config code blocks in the source
markdown now show actual default values uncommented, producing clean
single-commented lines in the generated `config.example.toml`.
Other fixes: `switch.no-cd` default was shown as `true` but code
defaults to `false`; `step.copy-ignored.exclude` was shown as
`[".cache/", ".turbo/"]` but defaults to `[]`.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>