The Commit cell sliced `&head[..8]` while `--format=json`'s `short_sha`
carried git's `%h`, so one commit read `1b9f1d96` in the table and
`1b9f1d9` in JSON, and `core.abbrev` reached only the JSON. #3675 gave a
detached row's Branch cell the same slice, so the disagreement showed up
twice on one row.
`ListItem::short_sha` becomes the only abbreviation of `head` anywhere:
the Commit cell, a detached row's Branch cell, the statusline, and JSON
all render it. `abbreviated_head()` is gone.
## Column widths
`COMMIT_HASH_WIDTH = 8` is gone. The Commit column and the Branch
column's detached budget both measure the SHAs they will render, so
`core.abbrev = 12` no longer truncates mid-hash and the default 7 stops
reserving a column nothing fills — the freed character goes to Message.
## Latency
`collect()` folds `%h` onto the rows before layout instead of after the
skeleton. The batch carrying it already gates the skeleton for `%ct`
sort order, so this is a map lookup rather than new I/O, and both cells
are identity columns with no placeholder — they still paint in the first
frame.
Measured on a 40-worktree / 400-branch fixture: git subprocess counts
are identical (5 pre-skeleton, 108 for the full run). Pre-skeleton wall
time is unchanged; running both binaries in each order, the sign of the
difference follows run order rather than the binary (+1.5 ms with this
branch second, −0.5 ms with it first), so the residual sits inside
drift.
## Behavior change
Where the commit-details batch fails, the Commit cell is now empty
rather than a slice of a SHA git refused. Age and Message already report
that failure the same way, under the same warning, and two snapshots
show it. `render_text_cell` also stops styling empty text, so a blank
cell no longer emits an escape pair around nothing.
## Reading the diff
160 files, but the hand-written part is +91/−79 in `src/commands/list/`
plus a +71 test. The rest is generated. The docs mirrors and help
snapshots are symmetric. Of the snapshot lines, content is +799/−799 —
every changed line a 1-for-1 hash swap — while +1396 is insta `env:`
metadata refreshing on the 128 snapshots this happens to touch.
`test_list_abbreviated_sha_follows_git` pins the invariant: the table's
hash equals JSON's `short_sha` at git's default and at `core.abbrev =
12`, and a longer prefix is ruled out. It fails against the old fixed
slice.
> _This was written by Claude Code on behalf of max-sixty_
Sweep of the docs for repetition and filler, from an audit of the
hand-authored pages, the command pages' source in `src/cli/mod.rs`, and
the plugin skill. Net −574 lines; every cut either had a surviving
canonical home or restated an adjacent sentence.
**One home per mechanism** (other mentions now link to it):
- `template-append` — the LLM commits guide owns the rendering
mechanism; the user- and project-config sections keep the key, an
example, and what is unique to them (the project approval gate and the
only-`template-append`-from-project scoping). Was explained in full in
three places.
- `-vv` diagnostic files — `wt config state logs` owns the four file
descriptions; the FAQ summarizes in one sentence and links.
- fsmonitor/trash cleanup — the FAQ's two overlapping `wt remove`
bullets merge into one that distinguishes own-daemon teardown from the
orphan sweep; troubleshooting.md's restatement compresses to a pointer,
keeping its unique wedged-daemon-on-live-worktree guidance.
- copy-ignored built-in excludes — the `wt step copy-ignored` page owns
the directory list (previously enumerated 4×); the config sections state
the rule and link.
- hook-types table — `wt hook` owns it; the extending guide replaces its
verbatim copy with a sentence.
- LLM tool commands — unchanged, deliberately: the apparent hand-synced
duplication between llm-commits.md and the config example is already
machine-pinned by `test_llm_docs_commands_match_config_example`, so it
cannot drift.
**Cut-over debt**: the deprecated `wt config state ci-status` section
shrinks to a deprecation pointer — its status table, fetch order, and
caching notes all duplicated the `wt list` CI-status section.
**Structure**: `wt step copy-ignored`'s "Features" list dissolves into
the sections that owned its facts (excludes → "What gets copied",
reflink → "Performance"); the four trailing "Note: This command is
experimental…" lines go (the `[experimental]` badge already appears
twice per section); shell-integration.md described the directive-file
mechanism twice and now describes it once.
**SKILL.md** drops from 339 to 181 lines: the permission-model section
duplicated the config-types bullets, the hook-type mapping appeared
twice, and the "Determining Which Config to Use" / "Validation Before
Adding Commands" scaffolding enumerated judgments an agent makes on its
own. The approvals-escalation and agent-handoff sections are untouched.
**Deliberately not addressed**: `wt list`'s schema 1 JSON tables (still
the default schema; the wholesale deletion lands when the default flips)
and corpus-wide em-dash density (a house-style decision, not a per-page
fix).
Generated mirrors (docs command pages, skill references, plugins mirror,
`dev/*.example.toml`, help snapshots) regenerated via
`test_docs_are_in_sync` and `cargo insta`.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Move the `main…±` branch-diff column (line diffs since the merge-base) into the default `wt list` view — it's pure local git backed by a persistent content-addressed cache, so the original blocking-walk concern no longer applies. `--full` now gates only the two off-machine columns: CI status (network) and LLM branch summaries. The interactive picker (`wt switch`) follows suit and is effectively `wt list --full`; on narrow terminals with the preview shown, CI clips past the split and alt-p reveals it.
Also adds a `.typos.toml` ignore rule for truncated word fragments glued to the … ellipsis, so the narrower Message column's truncated quickstart embed doesn't get spell-"corrected" by pre-commit.ci.
Hook templates expanded eagerly at prep time unless they referenced
`vars.*` (#1840); aliases were unconditionally lazy. The dual
representation (`expanded` + `lazy_template` on `PreparedCommand`) put a
mode choice in front of every consumer, and the seams produced real bugs
(#1855, #1910). The background runner also re-rendered eagerly-expanded
strings: a latent double-expansion whenever expansion output contained
`{{`.
This PR unifies on one rule: syntax is validated at preparation (before
step 1), templates render when each step runs, and `vars.*` always reads
fresh from git config. This is the direction #2128 recorded ("treat
hooks as aliases bound to triggers").
**Navigating the diff:**
- `src/commands/command_executor.rs`: `PreparedCommand` now carries the
raw template plus the frozen context map; JSON is produced only at the
process boundary. A `template_name` field keeps the moved errors' text
identical to the old prep-time messages. `prepare_steps` validates
syntax and freezes context; `resolve_command_str` renders at execution;
`map_config_steps` is the structural walker shared with aliases.
- `src/commands/alias.rs`: alias prep shares the walker. The residual
fork is genuinely alias-specific: context construction (filtered to
referenced vars, `{{ args }}` injection) and labels. A prep-time syntax
check turned out to be dead code (the arg-routing parse already aborts
on syntax errors), so `referenced_vars_for_config` now produces the rich
`TemplateExpandError` instead.
- `src/commands/hooks.rs`: the background spec always ships raw
templates; the runner renders each step (it already did for vars steps).
- Dry-run paths (`wt hook --dry-run`, `wt config alias dry-run`) share
`render_template_preview`: vars-referencing templates shown raw after a
syntax check, everything else rendered at display time.
**Behavior changes** (each pinned by a test or snapshot):
1. A foreground pipeline whose step N has a semantic template error
(undefined variable) runs steps 1..N-1 before failing, matching what
vars-referencing steps already did. New test:
`test_foreground_pipeline_undefined_var_runs_earlier_steps`.
2. A semantic error in a background hook template no longer fails the
foreground command; it surfaces in the runner log. Syntax errors still
abort at prep. New test:
`test_background_hook_undefined_var_fails_in_runner`;
`test_merge_drops_pending_hooks_when_post_merge_fails` switched to a
syntax-error fixture since that is what now reaches its subject (the
announcer Drop-flush).
3. `-v` no longer prints the parent-side rendered command for background
hooks (rendering happens in the runner). The variables table remains and
`wt hook <type> --dry-run` previews commands; help text updated. Three
snapshots changed by exactly this.
4. Announce gutters for vars-referencing foreground steps show the
rendered command instead of the raw template.
5. Syntax errors caught by the arg-routing parse (aliases, `wt hook`
`--KEY=VALUE` routing) render as the rich template error with the
source-line gutter, naming the alias or hook.
`ApprovedHookPlan` freezing, approval semantics (keyed and displayed by
template), config file format, and CLI flags are unchanged.
**Testing:** full local gate green (3917 tests + lints). The deferral
semantics in changes 1 and 2 previously had no coverage; both are now
pinned.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Docs: alias template-expansion timing
The main addition documents a gotcha that's easy to hit and hard to
diagnose: an alias body renders its `{{ … }}` once at dispatch, in the
invoking worktree, so a per-worktree variable like `{{ branch }}` is
baked to a single value *before* a nested `wt step for-each` / `wt
switch --execute` iterates — printing the same value in every worktree.
The fix is `{% raw %}…{% endraw %}` deferral (plus a quoted `sh -c '…'`
for `for-each`, since the deferred `{{ branch }}` contains spaces).
- `extending.md` — rewrote "Deferring expansion to a nested `wt`
command" around the `for-each` symptom; improved the `up` rebase recipe.
- `faq.md`, `troubleshooting.md` — symptom-first entries with `wt config
alias dry-run` as the diagnostic.
- `hook.md` / `config.md` / `step.md` — distinguish repo-level
(constant) vs per-worktree (active) variables; note `{{ default_branch
}}` needs no deferral; cross-link the `{{ default_branch }}` variable vs
the `wt config state default-branch` shell command.
## Factual corrections
- `integration_reason` JSON values are hyphenated (`trees-match`,
`no-added-changes`, `merge-adds-nothing`) — the docs had underscores.
Verified against `src/commands/list/model/state.rs`.
- `SKILL.md`: 7 → 10 hook types (5 events × pre/post), added an
aliases/multi-worktree task section, fixed stale anchor links.
## Test fix: narrow help-test env redaction
`test_help_list_narrow_terminal` built its own `insta::Settings` but
skipped `add_standard_env_redactions` (every other help snapshot routes
through `snapshot_help`, which calls it). Its snapshot env block
therefore leaked host-specific paths (`LLVM_PROFILE_FILE` = the
machine's temp dir, plus the `WORKTRUNK_*` paths), which churn whenever
the snapshot is regenerated on a different machine. Adding the one call
mirrors `snapshot_help` and makes the snapshot reproducible.
Worth noting (and a candidate follow-up): this gap was masked under
`cargo test` (libtest) because the `repo` fixture's `mem::forget`'d
`bind_to_scope()` guard leaks redaction settings across the shared
process's reused threads. Under nextest (process-per-test, what the
pre-merge hook uses) there's no leak, so a test missing its own
redactions is exposed. A few other help tests (`test_help_md`,
`test_version`, `test_nested_subcommand_suggestion`) have the same gap
and could be consolidated through one settings helper — left out of this
PR to keep it focused.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Targeted writing-prose cleanup across four docs pages. Per-page summary:
**`claude-code.md`** — dropped the duplicate skill definition (the lead
paragraph already covered it) and rewrote the `## Configuration skill`
opener as "With the `/worktrunk` skill, the agent can help with:" so the
section stands without depending on its heading.
**`hook.md`** (edits in the `Hook` command's `after_long_help` in
`src/cli/mod.rs`) — four small fixes:
- Dropped "As usual, post-* hooks run in the background" (already
established earlier on the page).
- Split the perspective+cwd run-on paragraph; the three `cwd ≠
worktree_path` cases (`pre-switch`, `post-remove`, `post-merge` with
removal) are now a bulleted list.
- Folded the semicolon-spliced conditional-variables enumeration into
the existing template-variables table descriptions (added "switch/create
only" to `base`, "when target has a worktree" to `target_worktree_path`,
hook-type qualifiers to `pr_number`/`pr_url`). The follow-on guidance
about undefined variables and conditionals/defaults stays.
- Trimmed the redundant `sanitize` sentence from the filters paragraph
(the table above already says it).
The two hook-types tables (event×pre/post matrix + per-hook purpose) are
intentionally kept — they're complementary, not duplicate.
**`llm-commits.md`** — dropped the "How it works" stub that mostly
restated the lead, and the "There are sensible defaults, but templates
are fully customizable" hedge.
**`tips-patterns.md`** — three trims:
- The `wt step tether` recipe's middle "This matters because…" sentence
(covered by tether's own docs).
- The ports-deterministic line tightened to lean on the concrete example
rather than restate the abstract claim.
- "in real-time" filler dropped from the Monitor hook logs section.
Auto-synced skill mirrors (`skills/worktrunk/reference/*.md`) carry the
same edits.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per @max-sixty's [direction in
#2838](https://github.com/max-sixty/worktrunk/issues/2838#issuecomment-4509447593):
revert the docs portion of #2840 and keep the code. Docs continue to
recommend `pre-start`/`post-start`; both names work in code so anyone
who already followed the briefly-changed docs (e.g. @EcksDy) isn't
stranded once a release ships these aliases.
## User-visible — back to `pre-start`/`post-start`
- README, docs site, skill mirrors, `dev/*.example.toml`,
`plugins/worktrunk/README.md`, `flake.nix`, `.config/wt.toml`
- `src/cli/mod.rs` / `src/cli/config.rs` / `src/cli/step.rs` /
`src/help.rs` after_long_help and example snippets — and the auto-synced
`docs/content/` and `skills/worktrunk/reference/` mirrors
- `wt hook --help` canonical subcommand names; completion advertises
`-start` only
- `HookType` Display via strum, serde `rename`, and clap `ValueEnum`
name — all `pre-start`/`post-start`. The Rust variant identifiers stay
`PreCreate`/`PostCreate` (internal; we already paid for that rename in
#2840, and now the eventual flip is a Display-only change)
- `HooksConfig` serde canonical fields
## `*-create` still works (kept code)
- `wt hook pre-create` / `post-create` — CLI alias on the canonical
subcommand
- `pre-create` / `post-create` in config: top-level, `[hooks.*]`, and
per-project, in string, `[table]`, and `[[array-of-tables]]` form.
Mechanism: serde `alias = ...` on the field, plus a silent in-memory
rename in `migrate_content()` so the round-trip in `unknown_tree`
doesn't flag table forms as schema-unknown.
- The pre-0.32.0 `post-create` fatal-load-error machinery stays removed
— the name is reclaimed, and both forms load without error.
## Smaller bits
- `valid_user_config_keys()` / `valid_project_config_keys()` append
`pre-create` / `post-create` so the unknown-field round-trip skips them.
`test_valid_*_keys_all_deserialize` skips both aliases (they can't sit
alongside the canonical without a duplicate-field error).
- `DEPRECATED_SECTION_KEYS` drops the `pre-start`/`post-start` entries
#2840 added — `pre-start`/`post-start` are canonical again.
- `find_pre_start_from_doc` / `find_post_start_from_doc` /
`find_renamed_hook_key` / `is_non_empty_item` /
`migrate_start_hooks_doc` and their tests are removed; the migration
direction flips via a new `migrate_create_hooks_doc` (silent, mirrors
the prior shape).
- Test files `e2e_shell_post_create.rs` and `post_create_commands.rs`
rename back to `_post_start_` (via `git mv`, so the rename shows as a
rename).
## Testing
`cargo run -- hook pre-merge --yes` — 3806 tests pass; the 10 failures
are all `case_4` of `shell_wrapper::unix_tests::*` (nu-shell case; `nu`
isn't installed in this runner; same failures occur on `main`).
Also manually verified that a fresh `wt switch --create` against a
project config with `[post-create]` loads cleanly with no unknown-field
warning and the hook fires as `post-start`.
## Follow-up
Per @max-sixty: in a couple of weeks, once a release with
both-names-work is out and users have had a chance to upgrade, the docs
flip is straightforward (most of it is in `src/cli/mod.rs`'s
`after_long_help` and the doc-sync test propagates).
Re #2838.
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
With `[switch] cd = false` or `wt switch --no-cd`, opening the
interactive picker (`wt switch` with no branch argument) and selecting a
worktree printed the branch name and exited — no switch, no hooks, and
`Alt-c` created nothing. #1445 introduced this as an explicit print-only
mode, but it surprises anyone who sets `cd = false` as a standing
navigation preference (driving worktree switches through tmux or cmux):
the picker silently stops doing anything.
The picker now runs the same `plan_switch` → `execute_switch` pipeline
as `wt switch <branch>`, suppressing only the cd directive. Pre- and
post-switch hooks fire, `Alt-c` creates the worktree, and
`--format=json` works in the picker too (`requires = "branch"` and the
`--branches`/`--remotes` conflict are dropped from the flag). JSON
emission is extracted into a shared `emit_switch_json` so the argument
path and the picker produce identical output.
This is a behavior change: the print-only picker mode is gone. Scripts
that captured its bare-branch-name output should switch to `wt switch
--format=json`, which both performs the switch and prints a structured
`action`/`branch`/`path` result — now shown as a `pick` alias example in
tips-patterns. The cmux recipe drops its `pick` alias workaround, since
the picker fires `pre-switch` hooks directly.
Closes#2837. Thanks to @endigma for the discussion that pinned down the
intended behavior: the picker should be identical to passing the branch
as an argument.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of the staged hook rename tracked in #2838: the worktree-creation hooks `pre-start`/`post-start` become `pre-create`/`post-create`. The old names keep working with no deprecation warning yet (Phase 2, months out, adds the warning).
## What changes
- `pre-create`/`post-create` are canonical everywhere: the `HookType` enum, the `HooksConfig` serde fields, the `wt hook` CLI, completion, and all docs.
- Old names keep working: `migrate_content()` rewrites `pre-start`/`post-start` config keys to `-create` before serde, and `parse_hook_type` accepts the old CLI names as silent aliases. `wt config update` rewrites them on disk; `wt config show` shows the migration diff. `wt hook <type>` execution and `wt hook show` both accept the old names; completion and `--help` advertise only the canonical names.
- `detect_deprecations()` flags the old keys so `update`/`show` act on them, but `format_deprecation_warnings()` stays silent (Phase 2 adds the warning). A new empty-warnings guard in `check_and_migrate` keeps a `-start`-only config from emitting a stray hint.
- The dead pre-0.32.0 `post-create` machinery is removed: the fatal `POST_CREATE_REMOVED_MSG` load error, the vestigial `HooksConfig.post_create` merge-fold, and `find_post_create_from_doc`. `post-create` is reclaimed as the canonical background creation hook.
## Semantic flip
Before v0.32.0, the key `post-create` named a *blocking* hook. It now names the *background* one.
Since v0.44.0, a pre-0.32.0 `post-create` config is a fatal load error on the `check_and_migrate` paths: `ProjectConfig::load` and user/system config loading, which fire on essentially every `wt` command. A repo carrying one has been unusable ever since. The one path that skips that check is `project_config_at_ref` (the base-ref read behind `wt switch --create`), which applies only structural migration. A pre-0.32.0 `post-create` surviving solely on a base ref, never checked out into a worktree, would now load as a background hook rather than folding into the blocking `pre-start`. That edge case is accepted: once `post-create` is valid again, reclaiming the name and detecting the dead key are mutually exclusive.
## Reviewing this diff
205 files, but the substance is ~36 files under `src/`. The rest is regenerated snapshots and auto-synced doc mirrors. Start with:
- `src/config/deprecation.rs` — detection (`find_renamed_hook_key`), migration (`rename_hook_key`), removal of the fatal block, the empty-warnings guard, and the `DEPRECATED_SECTION_KEYS` entries that stop unknown-field detection from flagging the migrated keys.
- `src/config/hooks.rs`, `src/git/mod.rs` — the serde field and enum renames.
- `src/config/project.rs` — `ProjectConfig::load` deserializes `check_and_migrate`'s migrated content, so a current-worktree config using the old keys loads into the canonical fields.
- `src/cli/hook.rs`, `src/commands/hook_commands.rs`, `src/completion.rs`, `src/main.rs` — the CLI alias layer; `wt hook show` accepts the old type names as hidden value-parser aliases.
- `src/cli/mod.rs` — the `wt hook` docs, including the soft-deprecation note linking #2838.
The ~93 modified snapshots also pick up deterministic env-block lines (`GIT_*: ""`, `LLVM_PROFILE_FILE`) that pre-existing snapshots already carry. That is stale-snapshot drift surfaced by the regeneration, not a behavior change.
## Testing
Full suite green (3799 tests). New coverage: `snapshot_migrate_start_to_create` (migration preserves value shape and position), `test_deprecated_start_hook_key_runs_silently` and `test_standalone_hook_start_alias_runs_silently` (old config and CLI names run with no warning), `test_config_show_displays_start_hook_migration` (`config show` reveals the diff without an "unknown field" warning), and `test_hook_show_accepts_deprecated_start_hooks` (a current-worktree config using the old keys loads, and `wt hook show` takes both the canonical and the deprecated type arguments).
Part of #2838.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`wt step tether -- CMD…` runs CMD in its own process group and tears the whole
group down when CMD exits or its worktree is removed (a 250ms portable poll;
killpg on Unix, taskkill /T /F on Windows). Replaces the leaked-dev-server /
fseventsd-saturation failure mode with a fire-and-forget supervisor needing
only a single post-start hook. No unsafe, no new deps. Shell handling matches
`wt step for-each`. Windows taskkill has a documented self-exit-detach edge.
Co-Authored-By: Claude <noreply@anthropic.com>
## Problem
The cmux recipes in `docs/content/tips-patterns.md` don't work as
documented — see #2796. `wt switch` cd's the invoking shell *in addition
to* the `pre-switch` hook selecting the per-worktree cmux workspace, so
the original cmux workspace ends up pointing at the new worktree
alongside the freshly-opened one. The Agent-handoffs cmux entry has a
similar pollution issue.
I initially proposed adding `[switch] cd = false` (prior revision of
this PR), but I couldn't end-to-end verify the fix because cmux isn't
installable in this CI environment. Per @max-sixty's call ([#2796
(comment)](https://github.com/max-sixty/worktrunk/issues/2796#issuecomment-4478578957))
— *"let's just remove the cmux recipes unless someone can certify that
they work"* — the safer move is to drop the recipes until someone with
cmux installed can re-add a verified configuration.
## What this PR does
Removes both cmux blocks from `docs/content/tips-patterns.md` (and the
auto-synced skill reference):
- The `**cmux** (new workspace):` entry under **Agent handoffs**.
- The full `## cmux workspace per worktree` section.
Diff is the inverse of #1907 (the PR that added them): 33 + 36 = 69
deletions.
The tmux and Zellij Agent-handoffs examples are unaffected.
## Testing
- `cargo test --test integration test_docs_are_in_sync` ✓ (regenerated
`skills/worktrunk/reference/tips-patterns.md`)
- `git grep -ni cmux` returns no matches.
---
Closes#2796
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
- Adds cmux to the **Agent handoffs** section alongside tmux and Zellij
- Adds a dedicated **cmux workspace per worktree** recipe with
create/select/close lifecycle hooks
- Documents the key gotcha: cmux socket restricts access to processes
with cmux terminal ancestry, so `pre-*` hooks must be used instead of
`post-*`
## Context
[cmux](https://cmux.com) is a macOS terminal built on libghostty with
workspace management via a Unix socket API. This recipe wires up
worktrunk hooks so each worktree automatically gets its own cmux
workspace.
The `pre-*` vs `post-*` distinction was discovered through debugging:
`post-*` hooks are detached background processes that lose the cmux
process ancestry, causing the socket to reject connections with "Access
denied — only processes started inside cmux can connect."
## Test plan
- [x] Tested full lifecycle: `wt switch -c`, `wt switch`, `wt remove` —
all correctly create, select, and close cmux workspaces
- [ ] Verify docs render correctly on worktrunk.dev (Zola build)
🤖 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>
Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com>
## Summary
Follow-ups from #2417 review, plus adjacent simplifications.
- **Cross-crate share of marker constants.** `MARKER_OPEN_PREFIX` and
`MARKER_CLOSE` now live in `src/docs.rs`. `src/help.rs` (the
`--help-page` producer of help-region markers) and
`tests/integration_tests/readme_sync.rs` (the consumer + producer of
snapshot/section markers) both reference them — drift becomes a build
error rather than a silent mismatch.
- **Drop `MARKER_OPEN_HTML_PREFIX`.** The `-HTML` variant emitted
visually identical wrapping; the suffix only signalled "in-place
refreshable" but the `.snap` ID + terminal-shortcode body in
`DOCS_SNAPSHOT_MARKER_PATTERN` already discriminate that. Standardised
on a single open prefix and stripped the `-HTML` literals from
`README.md`, the four standalone docs files, and the test file.
- **Help-page marker uses the shared prefix.** The mirrored close (`<!--
END AUTO-GENERATED from \`wt <cmd> --help-page\` -->`) stays as-is
because adjacent regions need unambiguous pairing, but the open prefix
now goes through `MARKER_OPEN_PREFIX`.
- **Drop `MarkerType::output_format()` / `extract_inner()`.** Both had a
single trivial non-panic branch that existed only to enforce "no
Snapshot markers in README" via `unreachable!()`. Replaced with one
explicit assertion in `sync_readme_markers` — surfaces the invariant as
an actionable error message instead of a panic.
- **Collapse `format_replacement`.** `wrap_in_marker` is invoked once
for both output formats; only body construction varies.
- **`__WT_QUOT__` rename in `tests/integration_tests/user_hooks.rs`.**
Inlined the single quotes — the `.replace('__WT_QUOT__', \"'\")` was
unnecessary indirection (the outer raw-string literal already tolerates
`'`, and TOML / Tera don't care about embedded `'`). Removes a
name-conflation footgun with the unrelated `__WT_QUOT__` placeholder in
`src/docs.rs`.
Net diff: -7 lines.
## Test plan
- [x] `cargo test --test integration` — full integration suite (1559
tests) pass
- [x] `cargo test --test integration readme_sync` — all 13 sync tests
pass; `test_readme_examples_are_in_sync` now produces a stable README on
a clean run (verified by re-running multiple times — no further updates
after the initial regeneration)
- [x] `cargo test --test integration
test_args_indexing_and_length_in_hook_template` — confirms the
user_hooks single-quote inlining works through TOML and Tera
- [x] Visual check via local Zola dev server: \`/worktrunk/\`,
\`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`,
\`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal
blocks cleanly with no leaked marker strings (`AUTO-GENERATED-HTML`,
`__WT_OPEN2__`, trailing `|||`)
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Keeps "Copying untracked files" inline (the one example that materially
uses worktree-specific mechanics) and moves the two plain-TOML patterns
— progressive validation and target-specific `post-merge` — to
`tips-patterns.md`. `hook.md` references them as `More recipes` bullets
instead.
> _This was written by Claude Code on behalf of Maximilian._
Co-authored-by: Claude <noreply@anthropic.com>
Docs consolidation sweep, same pattern as #2319.
**Copy-ignored recipe** — collapses hook.md's "Copying untracked files"
overlap with tips-patterns.md into a "More recipes" bullet pointing at
the canonical recipe. Keeps a one-paragraph intro in hook.md (worktrees
don't share untracked files → use `wt step copy-ignored`) since it's a
fundamental worktree concept. Moves the concrete pnpm pipeline example
into tips-patterns.md's canonical recipe, and switches it from
`[[pre-start]]` to `[[post-start]]` — the pipeline form handles ordering
without blocking worktree creation. `pre-start` is now called out only
as the exception for when `--execute` needs the files immediately.
**Trims**
- Dropped hook.md's "Hook type examples" dump: most of its 10 entries
duplicate existing recipes (dev server, database, cold starts) or the
Progressive validation section.
- Dropped tips-patterns.md's "Local CI gate": four-line recipe that
duplicated hook.md's Progressive validation and merge.md's Local CI
narrative.
- Dropped hook.md's "Python virtual environments" snippet: step.md
already owns language-specific notes.
> _This was written by Claude Code on behalf of Maximilian Roos_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Two more filler sentences caught on a second pass:
- Drop "Summaries are cached and regenerated only when the diff
changes." — duplicates the authoritative sentence in `llm-commits.md`,
which the adjacent "See LLM Commits for details" link already points to.
- Drop "`pre-remove` stops all services when the worktree is removed." —
visible from the `[pre-remove]` block above, and "all services"
overstates what the one-line `kill-session` command does.
Follows #2271.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove sentences that restate obvious fallback/error behavior, duplicate
nearby prose, or add visual weight without information.
- `extending.md` — drop the "not a wt command" error note, the
experimental badge on Aliases, and a duplicate recap of the
`cd`-to-parent-shell behavior at the end of a recipe.
- `faq.md` — drop "The result is cached for fast subsequent lookups"
padding after the default-branch detection explanation.
- `tips-patterns.md` — drop "Creates a worktree that builds on the
current branch's changes" (restated by the section heading) and
"Sessions are named after the branch for easy identification" (visible
in the code).
Skill reference files auto-synced via
`test_command_pages_and_skill_files_are_in_sync`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`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>
The `wt hook` page and tips/merge examples paired two `[[pre-*]]` blocks
with one command each, which runs them serially. For independent
commands like `cargo fmt --check` + `cargo clippy`, that taught the
wrong lesson.
Collapsed each pair into a single `[[pre-*]]` block with both keys (runs
concurrently). The multi-entry `[pre-*]` table form is deprecated
(`src/config/deprecation.rs`), so the single-element array form is the
non-deprecated way to express concurrent commands.
Affected: `src/cli/mod.rs` (merge local-CI, hook progressive-validation,
hook-type-examples), `docs/content/tips-patterns.md` (local CI gate).
Generated docs, skill mirrors, and help snapshots synced.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Cut over hook pipeline docs from inline-list form (`hook = [{name =
"..."}, ...]`) to `[[hook]]` array-of-tables blocks. The deserializer
already accepts this shape — no code changes were needed; the change is
purely teaching. This mirrors what landed for aliases in #2144.
The summary in `Pipeline Ordering` now foregrounds the TOML shape
progression instead of a labeled taxonomy:
- `post-start = "npm install"` — one command
- `[post-start]` — one section of concurrent commands
- `[[post-start]]` — one of multiple sections, run in order
The bracket count itself tracks the structural escalation (value →
section → array-of-sections).
Also adds a module-level doccomment to `src/config/commands.rs`
describing the three primitive TOML shapes the hook deserializer accepts
(string / dict / list) and the asymmetry between dict-at-top (always
`Concurrent`) vs dict-in-list (1-entry → `Single(named)`). This is the
analysis that motivated the cutover and is retained so future readers
can orient on the deserialization rules without re-deriving them.
The Databases example in `hook.md` and `tips-patterns.md` got its first
pipeline step named (`set-vars`) since `[[post-start]]` blocks can't
hold anonymous bare strings the way inline lists could.
> _This was written by Claude Code on behalf of Maximilian_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Multi-entry table form for pre-* hooks currently runs serially, while
the same form for post-* hooks runs concurrently. The parser produces
`HookStep::Concurrent` either way, but the foreground executor flattens
steps and runs them serially. To unify the semantics — table form will
run concurrently for all hook types in a future version — this
deprecates the current form for pre-* hooks and auto-migrates it to
pipeline form, which is explicitly serial.
## Implementation
Follows the existing deprecation recipe in `src/config/deprecation.rs`:
- **Detection** and **migration** for top-level hooks (user/project
config) and per-project overrides (`[projects."id".pre-*]`).
- **Warning**: matches the terse `{old} → {new}` pattern of existing
deprecations (`[merge] no-ff → ff`, `post-create → pre-start`, etc.).
- **Auto-migration** at load time: table form rewrites to pipeline of
inline tables so current behavior (serial) is preserved until users run
`wt config update`.
## Docs
Replaces the transitional "concurrent for post-*, sequential for pre-*"
framing with a neutral three-form description (string / table /
pipeline), plus a note recommending pipeline form for pre-* hooks to
avoid the upcoming behavior change.
## Tests
- `snapshot_migrate_pre_hook_table_form` — TOML migration diff
- `test_config_show_displays_pre_hook_table_form_deprecation` — full
user-facing `wt config show` output, covering the "Project config" label
and multi-hook list form
- Unit tests for detection/migration of top-level and per-project
variants
- Existing integration test fixtures migrated to canonical pipeline form
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
New Reference page covering the three extension mechanisms — hooks,
aliases, and external subcommands — with a comparison table and enough
detail to orient readers before linking to the full references
(`hook.md`, `step.md#aliases`).
Moves the external subcommands section out of tips-patterns.md into the
new page as its canonical location. The aliases recipe in tips-patterns
stays (it's a specific recipe, not a duplicate).
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
## 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>
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>
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>
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>
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>
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>
Adds `wt config state vars set/get/list/clear` commands for storing
custom variables per branch in git config
(`worktrunk.state.{branch}.vars.{key}`). Variables are available in all
template contexts via `{{ vars.key }}` syntax (hooks, `wt step eval`),
with JSON dot access (`{{ vars.config.port }}`) and default filters (`{{
vars.env | default('dev') }}`).
Uses `KEY=VALUE` syntax for `set` — `wt config state vars set
env=staging` — matching Docker `-e`, Heroku `config:set`, Fly `secrets
set`, and worktrunk's own `--var KEY=VALUE` convention. Splits on first
`=` only, so values can contain `=` (URLs, JSON).
The database-per-worktree example now uses `vars` to store the
connection string during `post-start`, replacing the `.env.local`
heredoc pattern. The URL is accessible outside hooks via `$(wt config
state vars get db-url)`.
Includes vars data in `wt list --format=json` output and `wt config
state get` display. Builds on #1004 (`wt step eval`). Part of #947.
## Test plan
- [x] Unit tests for vars template injection (empty, with data, no
branch, JSON dot access, shell escaping)
- [x] Integration tests for vars CLI commands (set, get, list, clear,
clear --all, --branch flag)
- [x] Edge-case tests for KEY=VALUE parsing (values containing `=`,
empty values)
- [x] Integration tests for vars in JSON output (present with data,
absent when empty)
- [x] All 493 unit + 1294 integration tests pass
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Shell code blocks on the docs site had inconsistent syntax highlighting.
Blocks with `$ ` prompt prefixes rendered as a single flat color because
Syntect's bash grammar treats `$` as variable expansion. This PR adds `$
` prompts to all shell commands while preserving full Syntect
highlighting by routing through terminal shortcodes.
## Approach
All shell commands in `console` blocks use `$ ` prefix.
`convert_dollar_console_to_terminal()` (new library function in
`src/docs.rs`) detects `$ ` lines and emits Zola terminal shortcodes:
- **Single or multi-command blocks** (no `{{ }}`): Uses `cmd` parameter
with `|||` delimiter. The shortcode template splits, highlights each
line individually through Syntect, and wraps commands in `<span
class="cmd">` (CSS `::before` adds `$ `). Comment lines (`#`) are
highlighted as comments without a prompt.
- **Blocks with `{{ }}` template syntax**: Falls back to body approach
with `<span class="cmd">` (accent color only, since Tera would interpret
`{{ }}` in the `cmd` parameter).
The function runs in both the `--help-page` generator (CLI source →
docs) and the doc sync test (hand-written docs → terminal shortcodes).
Hand-written docs can use plain `console` fences with `$ ` and get
auto-converted.
## Key files
- `src/docs.rs` — New library module with
`convert_dollar_console_to_terminal()` and unit tests
- `docs/templates/shortcodes/terminal.html` — Template enhanced to loop
over `|||`-delimited commands, highlighting each through Syntect.
Supports self-closing `{{ }}` syntax for bodyless blocks.
- `src/help.rs` — Uses library function, updated pipeline docs
- `tests/integration_tests/readme_sync.rs` — Sync test runs conversion
on all docs (not just CLI-generated). Updated skill transformation to
handle both body and self-closing terminal shortcodes.
- All `src/cli/*.rs` — `$ ` added to all console blocks
- All `docs/content/*.md` — Auto-converted to terminal shortcodes (zero
`bash` blocks remain)
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <w@worktrunk.dev>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Replace all `post-create` references with `pre-start` across
documentation, skills, example config, and test names. The Rust
deprecation handling code (migration, alias, config parsing) remains
intact for users with existing configs.
Also removes `post-create` from the `wt hook show` value_parser — it was
listed as a valid hook type for display even though it's been
deprecated.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Rewrite the redundant summary caching sentence in llm-commits and
tips-patterns docs — the preceding paragraph already explains what
summaries are and how to enable them, so "Disabled by default — when
enabled, each branch's diff is sent to the configured LLM for
summarization" was restating known information. Replaced with "Summaries
are cached and regenerated only when the diff changes."
Also fix a broken `#security` anchor in step aliases docs — the hook
page has no such heading; the correct target is `#wt-hook-approvals`.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Add bare repo `worktree-path` example (`{{ repo_path }}/../{{ branch |
sanitize }}`) to config docs alongside existing examples
- Update `repo_path` variable description to note bare repo behavior
- Expand "Bare repository layout" section in tips-patterns: explains why
bare repos suit worktree workflows and documents the `git clone --bare
<url> project/.git` setup
## Test plan
- [x] `cargo run -- hook pre-merge --yes` passes
- [x] Doc sync tests pass
(`test_command_pages_and_skill_files_are_in_sync`,
`test_config_source_generates_example_toml`)
- [x] Help snapshots updated and accepted
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Move experimental badges from the start of description paragraphs to
after the heading text in web docs.
Uses empty `<span>` elements with CSS `::after` for badge text, so the
span doesn't affect Zola's heading slug generation or page TOC entries.
This avoids the need for `{#slug}` anchor overrides and keeps sidebar
TOC entries clean (no "experimental" suffix).
Before: `## wt step relocate` / `EXPERIMENTAL Move worktrees to expected
paths.`
After: `## wt step relocate EXPERIMENTAL` / `Move worktrees to expected
paths.`
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
For iOS developers, this PR add a `post-remove` hook recipe to Tips &
Patterns that cleans up Xcode's DerivedData when removing a worktree.
## Context
Xcode's DerivedData directory names are hashed from the `.xcodeproj`
path, making them hard to match by name alone. Each DerivedData
directory contains an `info.plist` with a `WorkspacePath` field that
records the original project path. This recipe leverages that to find
and remove only the matching cache.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace `.lvh.me` with `.localhost` in all Caddy subdomain routing
examples
(docs, CLI help, tests). `.localhost` resolves to 127.0.0.1 via the OS
on
macOS and Linux with systemd-resolved — no external DNS dependency.
Also fixes a flaky PTY test: `WORKTRUNK_TEST_DELAYED_STREAM_MS` was
missing
from `STANDARD_TEST_ENV` in shell_wrapper.rs, causing `git worktree add`
output to appear non-deterministically under heavy parallel load.
Closes#1334
> _This was written by Claude Code on behalf of maximilian_
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
## Summary
- Add per-page `<meta name="description">` to all doc pages — command
pages auto-generated from CLI `about`/`long_about` via new
`--help-description` flag, non-command pages manually written
- Add `<link rel="canonical">` URLs and JSON-LD structured data (WebSite
+ SoftwareApplication) on the homepage
- Add custom `sitemap.xml` template with `<lastmod>` dates and
descriptive homepage `<title>`
- Extract shared `extract_about_and_subtitle()` helper, eliminating
duplicated subtitle logic between `handle_help_description` and
`combine_command_docs`
- Fix broken anchor in faq.md (`#picker-summaries` →
`#branch-summaries-experimental`)
## Test plan
- [x] Full test suite passes (2713 tests via `wt hook pre-merge --yes`)
- [x] All lints clean (pre-commit, clippy, cargo fmt)
- [x] Doc sync test confirms auto-generated descriptions match CLI help
- [x] Zola build succeeds with all template changes
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Adds an LLM-generated Summary column to `wt list --full`, showing a
one-line description of each branch's changes
- Extracts shared summary module (`src/summary.rs`) from picker-specific
code for reuse across `wt list` and `wt switch`
- Summary and Message are both flexible columns — Summary expands first
(10→70 chars), then Message gets remaining space (10→100 chars)
- Gated on `[list] summary = true` config (disabled by default — each
branch's diff is sent to the configured LLM) plus `[commit.generation]`
and `--full`
- Labeled as experimental throughout docs
## Test plan
- [x] 539 unit tests pass
- [x] 1072 integration tests pass (including 9 new layout tests for
flexible Summary/Message allocation)
- [x] Pre-commit lints pass
- [x] Manual: `wt list --full` with LLM configured + `summary = true` —
Summary column appears
- [x] Manual: `wt list --full` without `summary = true` — no Summary
column
- [x] Manual: `wt list` (no `--full`) — no Summary column
- [x] Manual: `wt switch` picker — summary tab still works
> _This was written by Claude Code on behalf of max-sixty_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Migrate from static CSS syntax highlighting to Zola's giallo engine
with custom `worktrunk-light.json` theme
- Replace hardcoded `syntax-light.css` / `syntax-dark.css` with
theme-based class generation
- Design a warm "sunlit workshop" palette: amber commands, gold strings,
chartreuse quoted strings, rusty constants
- Add CSS sibling selector to differentiate quoted from bare strings
(giallo tokenizes both as `z-string`)
## Test plan
- [ ] Verify syntax colors on `/switch/` (bash: commands, flags,
strings, quoted strings)
- [ ] Verify TOML blocks on `/config/` (section headers, keys, values)
- [ ] Verify dark mode is unaffected (quoted string CSS rule scoped to
`prefers-color-scheme: light`)
- [ ] Check all tests pass (`cargo test`)
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Without this flag, lsof matches any process with a connection to the
port (including established client connections), which can accidentally
kill unrelated processes. Restricting to LISTEN sockets ensures only the
actual server process is targeted.
* fix: warn when --config path doesn't exist, fix shell quoting in docs
Bug fixes from adversarial testing:
1. --config path warning: When users pass --config with a non-existent
file path, wt now warns instead of silently falling back to defaults.
Note: WORKTRUNK_CONFIG_PATH env var doesn't trigger this warning since
it's commonly used for test isolation with intentionally absent paths.
2. Shell quoting in docs: Template variables are automatically shell-escaped,
so user-added quotes cause issues with special characters. Fixed examples
in hook docs and tips-patterns that incorrectly showed quoted variables.
Also adds:
- Test verifying --squash is correctly ignored with --no-commit
- Tests for ANSI escape sequence handling in branch names
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: skip ANSI branch name test on Windows
Git for Windows with MSYS2 bash behaves differently and may accept
branch names containing control characters. The test verifies Unix git
behavior, so restrict it to Unix platforms.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix worktree-path on tips page
Update tips page with correct `worktree-path` for bare repos. (Without `../` working dir is created inside `.git` directory.)
Second doc location missed in https://github.com/max-sixty/worktrunk/pull/320
* Fix worktree-path template in bare repository example
Update the worktree-path configuration to use a relative path traversal
("../{{ branch | sanitize }}") instead of a relative path within the
repository. This correctly positions worktrees as siblings to the bare
repository directory rather than subdirectories within it.
---------
Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>