Commit Graph

75 Commits

Author SHA1 Message Date
Maximilian Roos 5c42c5b7d5 feat: machine-readable approval state and branch-removal outcomes (#3710)
Two of the five machine-readable-output requests NathanaelRea opened
(#3696–#3700), reviewed as a set and implemented where the gap was real.

## `wt config approvals list --format=json` (#3698)

The command already computed the four distinctions an orchestrator needs
— no commands, approved, approval-required, and stale — read-only,
without prompting or writing. It had no `--format` flag, so the only way
to learn that a non-interactive run would stop for approval was to run
the operation and catch `NotInteractive`, or to pass `--yes` and approve
whatever was there.

```json
{
  "state": "approval_required",
  "commands": [
    {"phase": "post-start", "name": "dev", "template": "npm run dev", "approved": false},
    {"phase": "pre-merge", "template": "cargo test", "approved": true}
  ],
  "stale": ["some removed command"]
}
```

`state` is what a caller branches on. `stale` stays a separate list
rather than a fourth `state`, because it co-occurs with all three — and
those are the approvals `--yes` would silently re-approve after their
command template changed, which is exactly what an orchestrator
preserving the approval model needs to see.

A flag on the existing read command rather than a new `status` verb,
matching `wt config show`, `wt config state get`, `wt config state
logs`, and `wt list`.

Closes #3698.

## `branch_outcome` on removal (#3700, partly)

`wt remove --format=json` reported the branch as one boolean, collapsing
five internal outcomes into two values:

| Internal outcome | `branch_deleted` was |
|---|---|
| `Deleted` | `true` |
| `Deferred` — handed to a detached process, result never observed |
`true` |
| `NotAttempted` — no branch, or `--no-delete-branch` | `false` |
| `Retained` — a sibling worktree has it checked out | `false` |
| `Retained` — **the CAS refused; the ref moved under us** | `false` |

The last row is the exact race #3700 asks for protection against.
Worktrunk already deletes with `git update-ref -d <ref> <oid>` and
already fails closed when the ref has moved — then reported it as the
same `false` that means "you asked me not to". And `Deferred` reported
`true` on intent.

`branch_outcome` names it instead: `deleted`, `deferred`,
`not_attempted`, `retained_unmerged`, `retained_checked_out`,
`retained_raced`, `retained_failed`. A caller that sees `retained_raced`
knows to re-read the ref and retry, which is what the guard detects it
for.

**This does not close #3700.** That issue asks for an *input* — a
caller-supplied expected OID that makes `wt` fail closed against the
orchestrator's own observation. This is an *output*. They land in the
same place on the default path, because the integration check already
refuses to delete unintegrated content, so the caller was never going to
lose commits — they just couldn't classify the refusal. Where the gap is
real is `--force-delete` / `-D`, which takes the early return in
`delete_branch_if_safe` and runs `git branch -D` with no integration
check and no CAS. If an `--expected-oid` flag lands, it has to gate that
path.

## Notes

- **Output-format break.** `branch_deleted` is replaced, not
supplemented, on `wt remove --format=json` and on `wt step prune
--format=json`'s live path. Per CLAUDE.md, output formatting is on the
flexible side of the interface line; flagging it here so the release
changelog picks it up.
- **`wt step prune --dry-run` keeps `branch_deleted`.** A dry run
predicts; it runs nothing to have an outcome. Different thing, different
name, documented as such.
- **`retained_raced` and `retained_checked_out` have no deterministic
CLI trigger.** Both come from windows between `wt`'s own fresh read and
the ref mutation, which no hook can be scheduled inside. They're covered
at the unit level (`branch_fate_from_result_mapping`,
`branch_fate_json_outcome_is_distinct_per_fate`, and
`cas_rejects_delete_when_branch_advances` in `src/git/remove.rs`, which
drives the race with a stale snapshot). The integration tests cover the
two reachable contrasts: `retained_unmerged` via a `pre-remove` hook
that commits, and `not_attempted` via `--no-delete-branch`.
- **`print_json` lives under `src/commands/list/`** and now has a third
caller from outside that module. Worth a more central home; not moved
here.

## The other three

Reviewed but not implemented:

- **#3696** — already possible. `wt --config-set 'list.json-schema = 2'
list --format=json` pins the schema per invocation above every config
layer, as does `WORKTRUNK_LIST__JSON_SCHEMA`. Answered on the issue;
what's left is a docs gap and making an out-of-range value fail rather
than degrade in JSON mode.
- **#3697** — the machine-readable error channel. A real gap and the one
policy call in the set; not started.
- **#3699** — aimed at `wt config state logs --format=json`, which is a
directory listing reconstructed from paths, under a model that
overwrites. The append-only run record it wants is `commands.jsonl`.

## Testing

`cargo run -- hook pre-merge --yes` green: 4533 tests, clippy, fmt,
doctests, rustdoc, docs sync.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:17:27 -07:00
Worktrunk Bot 4af04c2260 fix(remove, prune, merge): target the named worktree, retain shared branches (#3533)
Follow-up to the discussion on #3480: [@max-sixty
asked](https://github.com/max-sixty/worktrunk/pull/3480#issuecomment-5039137116)
to have `wt remove` honor an explicit path generally. #3607 has since
landed the resolution half, `Repository::resolve_worktree`, which turns
a path into a worktree. This is the removal half, and it is the part
resolution cannot decide: which worktree to act on is a naming question,
whether the branch may be deleted is not.

## Problem

`wt remove` threw the resolved answer away. For any non-current worktree
it re-targeted by branch name, and `prepare_worktree_removal` mapped
that branch back to git's *first-listed* worktree. Two failures
followed, both silent.

**The wrong worktree is removed.** With `feature` checked out twice:

```console
$ git worktree list
…/dup    [feature]
…/first  [feature]

$ wt remove …/first
◎ Removing feature worktree & branch in background (same commit as main, _)
```

`…/dup` is gone. `…/first`, the one named, is still there.

**The survivor is left broken.** The shared branch is deleted with it.
Worktrunk deletes branches with `git update-ref -d`, git's
compare-and-swap primitive, which unlike `git branch -d` does not refuse
a ref that is checked out somewhere:

```console
$ git worktree list
…/first  0000000 [feature]

$ git -C …/first rev-parse HEAD
fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
```

`wt step prune` reached the same deletion unattended, and `wt merge`
reached it with a freshly integrated branch, so nothing else declined. A
branch gets a second worktree only through `git worktree add --force`;
worktrunk never does it itself.

## Fix

**Remove the worktree that was named.** `wt remove` drops non-current
worktrees via `RemoveTarget::Path`. `wt step prune` does the same: its
candidates already carry a path, and targeting a *stale* entry by branch
name resolved to a live worktree that the same prune had just skipped as
too young, then removed it.

**Retain a branch another worktree holds.** One predicate,
`live_sibling_checkout`, answers "would deleting this ref orphan a
checkout?", and every path that can delete a branch asks it:
`prepare_worktree_removal`'s worktree and pruned-branch-only arms
(covering `wt remove`, `wt step prune`, and the picker, which already
targeted by path) and `wt merge`'s finish. A hit forces
`BranchDeletionMode::Keep`, the single chokepoint every deletion path
honors, and names the surviving checkout:

```console
$ wt remove …/dup
◎ Removing feature worktree in background
○ Branch feature retained; still checked out @ …/first
```

A sibling whose *directory* is already gone is stale metadata, not a
checkout with anything to lose, so it does not retain: removing the last
live checkout still deletes the branch.

**`-D` is refused out loud.** Everywhere else `-D` is the override that
wins, so one that cannot be honored warns rather than passing quietly:

```console
$ wt remove …/dup -D
◎ Removing feature worktree in background
▲ Branch feature retained despite -D; still checked out @ …/first
```

The ordinary single-checkout case is unchanged, and a retained branch
skips the integration check entirely rather than computing a verdict it
would discard.

#3480's duplicate-checkout hint now points at `wt remove <path>`, which
this makes the safe answer.

## Testing

Full gate green. New coverage, each case asserting the survivor still
resolves `HEAD`, which is the corruption in question:

- `remove`: by path, by name, refused `-D`, the pruned-directory
fallback, and the mirror case where a stale sibling must *not* retain.
- `step prune`: a stale entry whose branch is live in an age-skipped
worktree. This test is what surfaced the wrong-worktree bug in prune.
- `merge`: merging a branch that a `--force` duplicate also holds.

## Not addressed

`wt step prune`'s summary counts candidates rather than outcomes, so a
retained branch still reports `✓ Pruned 1 branch`. The per-item line
above it already says the branch was retained. Fixing the count means
threading removal outcomes back through prune's accounting, which is a
separate change.

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

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 09:57:34 -07:00
Worktrunk Bot f593438e7c feat(remove): add experimental --reap to kill worktree processes (#3396)
Implements the design agreed in #3365: an opt-in, experimental flag on
`wt remove` that reaps processes left running in the worktree.

## The name

`--reap` — it's the term the whole thread and the issue title already
use, it's concise (matching worktrunk's flag style), and the user-facing
messages read naturally (`◎ Reaping 2 processes under feature
worktree`).

## What it does

```console
$ wt remove --reap feature
◎ Reaping 2 processes under feature worktree
   ┃ 51234 node
   ┃ 51240 esbuild
✓ Reaped 2 processes
◎ Removing feature worktree & branch in background (same commit as main, _)
```

Processes are discovered by working directory (`lsof -d cwd`): any
process whose cwd is at or under the worktree path. Termination reuses
the existing `SIGTERM`→wait→`SIGKILL` escalation (`escalate_terminate`,
shared with the fsmonitor sweep).

## Data-safety posture

Killing a process the user didn't mean to kill — a terminal editor with
unsaved buffers — is exactly the silent loss-of-work the project refuses
without consent, so two guards keep `--reap` conservative:

- **Controlling-terminal exclusion.** A process holding a controlling
terminal (an interactive shell, or `vim`/`nvim`/`emacs -nw`) is never
reaped (`ps -o tty=`). Only detached processes — the dev servers and
watchers this issue is about — remain candidates. This also spares the
shell `wt remove` was run from.
- **Self-exclusion.** The current `wt` process is never a candidate.

The flag itself is the explicit opt-in; the list is printed before
signalling for transparency.

## Scope / limitations (matching the #3365 discussion)

- **Under-inclusive by design.** cwd discovery misses a daemon that
forked and `chdir`'d away, or one that reparented to `init` — they no
longer report a cwd under the path. Those are what [`wt step
tether`](https://worktrunk.dev/step/#wt-step-tether) is built to reap
(whole process group). `--reap` and `tether` cover different gaps and
are complementary, not substitutes — the docs say so.
- **Ordering.** Reaping runs before the worktree directory is
staged/renamed (cwd matching needs the directory in place), so it's
independent of foreground/background removal, trash-vs-delete, and
`--force`.
- **Unix only.** Windows has no cheap per-process cwd; `--reap` is
rejected there with a clear error.

## Tests

- Pure parsers for `lsof`/`ps` output (`parse_lsof_cwd`,
`parse_ps_tty`).
- End-to-end against the real `lsof`/`ps`: spawns a child with a cwd
under a tempdir, asserts `processes_under` discovers it, asserts the
controlling-terminal guard keeps-or-drops it in agreement with the
child's *actual* TTY state (so the test is host-independent — CI has no
TTY, a dev box does), then reaps it and confirms `SIGTERM`.
- CLI snapshot (`test_remove_reap_no_processes`) covering the
no-candidates path, deterministic whether or not `lsof` is installed on
the runner.

Help text, `docs/content/remove.md`, and the skill reference mirror are
regenerated and in sync.

One thing worth a maintainer's eye: I chose to print-then-signal rather
than add an interactive confirm/`--dry-run` in this first cut — the
opt-in flag + TTY exclusion + printed list felt like enough for an
experimental flag, and a confirm step is easy to layer on if you'd
prefer it.

Closes #3365.

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-09 15:13:46 -07:00
Maximilian Roos 3d0b99e072 Add WORKTRUNK_VERBOSE env var equivalent to -v/-vv (#3166)
Shell tab-completion runs the `wt` binary as its own subprocess (the
shell sets `COMPLETE=<shell>`), and that path returns from `parse_cli`
before `main` ever reaches `logging::init`. So when a tab-completion is
slow, there's no flag that turns on logging for it — `-v`/`-vv` never
run, and `RUST_LOG` only sets a level, not the `-vv` file sinks. There
was no way to profile a slow completion.

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

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

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

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

### Testing

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

> _This was written by Claude Code on behalf of max_

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

## Behavior

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

## Why

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

## Implementation

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

## Testing

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

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:04:19 -07:00
Maximilian Roos 41d5ef7349 docs(cli): render --format help as terse inline possible-values (#3108)
Every `--format` flag — `wt step
commit`/`squash`/`rebase`/`push`/`for-each`/`copy-ignored`, `wt remove`,
`wt switch`, `wt merge`, `wt config show` — rendered a verbose `Possible
values: - text: … - json: …` block in long help and the generated doc
pages. clap auto-generates that block from the per-variant doc comments
on the shared `SwitchFormat` enum.

Dropping those variant doc comments makes clap render the terser inline
`[possible values: text, json]` instead, slimming every `--format`
flag's long help in one place. The variant names are self-describing, so
the descriptions added nothing.

The sibling `OutputFormat` enum gets the same trim for `Table`/`Json`,
but `claude-code` keeps its description — "reads context from stdin"
conveys real, non-obvious behavior the bare name doesn't. That one
surfaces in `wt list statusline --help`.

Doc mirrors (`docs/content/`, `skills/worktrunk/reference/`) and help
snapshots are regenerated.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:49:43 -07:00
Maximilian Roos 11129d3811 fix(plugin): fail worktree hooks before side effects; document path args (#3060)
Hardens the plugin's worktree-lifecycle hooks against malformed
payloads, and documents two things they rely on.

**Hooks** (`plugins/worktrunk/hooks/hooks.json`): in the pipeline form,
`jq -r .name | xargs … wt switch --create {} …` runs `wt` with whatever
jq printed — so a payload missing `.name` minted a real branch named
`null`, and `set -o pipefail` could only report the failure after the
side effect (verified under `/bin/sh`). Both hooks now validate the
field in a command substitution before `wt` runs (`name=$(jq -er .name)
|| exit 1; …`), which fails with nothing created and stays
whitespace-safe via quoted variables. The `bash -c` wrapper remains —
hook commands must parse under fish/zsh/bash and fish rejects
`name=$(…)` — but `set -o pipefail` is gone: the only remaining pipe
ends in `jq -er .path`, whose exit is the pipeline's. Exercised under
`sh -c` against the shipped JSON: missing field → exit 1, no branch;
fresh create → path on stdout, exit 0; existing branch → wt's real
error, nonzero; remove by path → removed.

**Docs**: `wt remove`'s positional also accepts worktree paths
(`resolve_worktree_arg` tries branches first, then paths) and the
`WorktreeRemove` hook passes a path — the help line now reads "Branch
name or worktree path". The plugin README lists `jq` as a hook
dependency, and the skill's branch-naming step asks for names consistent
with the repo's existing worktrees.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:33:58 -07:00
Maximilian Roos 2617348b22 docs: writing-prose cleanup (faq, config, list, remove) (#2925)
Continues the writing-prose pass on the remaining doc-site pages — the
items deferred at the end of #2922.

**`faq.md`** — dropped "Worktrunk creates files in four categories."
scaffolding; the four H3s below (Worktree directories, Config files,
Shell integration, Metadata in .git) make the count self-evident.

**`config.md`** (edits in the `Config` command's `after_long_help` in
`src/cli/mod.rs`) — four small fixes:
- Replaced the "For context:" three-bullet preamble in *User
project-specific settings* with a direct lead sentence. Side benefit:
fixes the "User configs _also_ has" grammar bug. The new sentence avoids
second-person ("for you") and third-person addressing ("for the user")
per the writing-prose indicative-mood rule.
- Dropped "also" from the system-config sentence — it was the only
signal the sentence was an orphan footnote relative to the table above.
- Dropped "Similarly," before the first-commit-prompt sentence; the
parallel "On first run … On first commit …" structure carries the
relation.
- Dropped "Note the single underscore after `WORKTRUNK` and double
underscores between nested keys." that restated what the env-var table
already showed.

**`list.md`** (edits in the `List` command's `after_long_help`) — folded
the three-dot diff detail into the `main…±` column description;
tightened the remaining footnote to just the label-stays-main point.

**`remove.md`** (edits in the `Remove` command's `after_long_help`) —
extracted the cap-detail appendix from the "Patch-id match" bullet,
which had four sentences while the surrounding five bullets averaged one
or two; it now sits as its own paragraph. Also dropped the two sentences
in *Force flags* that inverted the force-flags table just above; only
the new `--no-delete-branch` note remains.

**Skipped** (re-reviewed and judged not worth changing):
- `switch.md` fork material — mechanism + naming-rule, not duplication.
- `step.md` mixed-shape operations list — the asymmetry signals which
subcommands have subdoc sections.
- `step.md` "How it works" subsections — useful reference content, not
internal commentary.
- `step.md` `wt step promote` opener — opinionated voice framing.
- `step.md` `wt step tether` "Why" — now the canonical place for the
leakage rationale (the tips-patterns dup was already removed in #2922).

Auto-synced skill mirrors (`skills/worktrunk/reference/`) and
regenerated help snapshots carry the same edits.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:18:37 -07:00
Maximilian Roos 21e0b27e49 refactor(verbose): rename output.log → subprocess.log; -vv keeps Info on stderr (#2913)
## Motivation

The `-v` / `-vv` UX had three small issues that compounded:

1. **`output.log` is misnamed.** It holds the *uncapped raw
stdout/stderr of every subprocess `wt` spawns* — multi-MB possible (`git
log -p`, patch-id pipelines, etc.). "output" reads as "stuff `wt`
printed" — the small thing — when it's actually the big thing. Easy to
misread.
2. **`-vv` went fully dark on stderr.** PR #2892 moved the noisy debug
pipeline to files at `-vv`; in the process, the stderr layer was
disabled entirely. Users running `-vv` to see hook output (info-level,
which `-v` shows on stderr) suddenly couldn't.
3. **`-v` help text was a 150-char one-liner** packed into a
parenthetical, and the surrounding docs leaned on a "stderr stays
readable / `log::*` pipeline" framing that was Rust-jargon-flavored and
implied stderr-quiet at `-vv` — which is no longer true after change #2.

## Change

- **Rename `output.log` → `subprocess.log`.** Filename now matches
content. `OUTPUT` static → `SUBPROCESS`, plus the related
`OutputMakeWriter` / `OutputFileFormat` / `build_output_layer` symbol
renames.
- **`-vv` keeps the Info baseline on stderr.** `build_stderr_layer` no
longer returns `None` at `-vv`; debug-level records still route to file
layers only, so the terminal stays readable while info-level status
(hook output, template variables, the `Tracing to ...` pointer) shows
the same as at `-v`.
- **`-v` help text rewritten** to describe both levels cleanly without a
wall of detail.
- **`docs/content/faq.md` gets a "What does -v / -vv do?" section** with
a three-level table.
- **Docs cleanup**: drop "stderr stays readable" / `log::*` jargon /
"but not subprocess.log" negative framing from user-facing prose.

## Notes for review

- The only `log::info!` site in the codebase is
`commands/picker/mod.rs:389` (a single picker error message), so making
`-vv` show info-level on stderr doesn't add meaningful noise.
- `test_vv_log_pipeline_silent_on_stderr` is renamed to
`test_vv_debug_pipeline_silent_on_stderr` — its assertions only check
debug-level records stay out of stderr (they do); the old name implied
the whole `log::*` pipeline was silent, which was never quite true
(direct `eprintln!` always showed) and is less true now (info-level
routes to stderr).
- 67 of the 69 changed files are snapshot updates (help text and one
diagnostic snapshot) and auto-synced doc/skill mirrors. `git diff --stat
-- 'tests/snapshots/*' 'docs/content/*' 'skills/worktrunk/reference/*' |
tail -1` separates them.
- CHANGELOG: not touched. The historical entry that introduced
`output.log` (`#2201`) stays accurate for its release; this rename gets
a new line in the next release.

## Tests

3870 tests pass. Re-snapshotted all `test_help_*` snapshots, three
`step_alias` snapshots that quote the global help, and the diagnostic
file format snapshot.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:55:03 -07:00
Maximilian Roos 89ec4f70fa docs(remove): state that --force discards all uncommitted changes (#2869)
The FAQ and the `wt remove --force` help text said `--force` overrides
the untracked-files check "for build artifacts". `--force` actually
removes a dirty worktree including staged and modified *tracked* files,
not just untracked ones (`test_remove_force_with_modified_files`,
`test_remove_force_with_staged_files`). On a destructive command, that
wording can lead users to consent to more data loss than they expected.

Updates the `--force` arg help, the "Force flags" table and example in
`src/cli/mod.rs`, and the FAQ to say `--force` discards staged,
modified, and untracked files; the `help_remove_long` snapshot and the
`remove.md` / `faq.md` doc and skill mirrors are regenerated to match.

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

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:40:41 -07:00
Maximilian Roos 771739f6b7 perf(integration): bound patch-id squash-merge scan to 500 commits (#2752)
## Summary

`is_squash_merged_via_patch_id` runs `git log -p {merge-base}..{target}
| git patch-id` over the *entire* target-side history. The range is
unbounded: on a fast-moving repo with an old branch — tens of thousands
of commits since divergence — a single integration check takes seconds
to tens of seconds, which surfaces as `wt step prune` (and `wt list`)
going visibly silent on the parallel check tail in #1888.

This caps the scan at `PATCH_ID_SCAN_MAX_COMMITS = 500` commits via a
cheap graph-only `git rev-list --count` pre-flight. Above the cap, the
check returns `Ok(false)` — the safe direction (branch kept, not wrongly
deleted); `wt remove -D` still removes branches that fall past the cap.

## Why 500

Count is a rough proxy — per-commit cost scales with `changed_files ×
changed_lines`, not just count. Working back from "keep one check under
a few seconds":

- typical repo (~5-20 KB patches): 500 ≈ well under 1s
- heavy monorepo (~50-100 KB patches): 500 ≈ 2-5s

Branches merged within a normal review-and-cleanup cycle sit well inside
this. Anything older is `-D` territory.

## Scope

The cap is in the shared probe, so it applies to `wt list`'s status
column, `wt remove`, `wt merge`, and `wt step prune` — anywhere
`integration_reason` is called. Behavior change only when a branch was
squash-merged *and* `git merge-tree` conflicts (the same files were
modified again after the squash) *and* the default branch has advanced >
500 commits since the merge point. Narrow.

## Test plan

- Two new unit tests in `patch_id_cap_tests` using `git fast-import`
(501-commit history built in milliseconds): under-cap finds the squash,
over-cap bails despite the squash being in range. The pair pins the
difference to the cap, not the topology.
- Existing squash-merge integration tests
(`test_remove_squash_merged_*`, `test_prune_squash_merged_*`,
`test_list_integrated_when_squash_merged_*`) keep small histories well
under the cap and continue to pass — regression guard for the wiring.

Refs #1888.

@ortonomy — would you mind running this against the repo from #1888? The
simplest check is `cargo install --git
https://github.com/max-sixty/worktrunk --branch prune-speed wt` (or
build locally), clear the disk cache to force a cold run (`rm -rf
.git/wt/cache`), then time `wt step prune`. Most interested in whether
the silent stretch is gone; if it isn't, `wt -vv step prune` writes
`.git/wt/logs/trace.log` showing which `git` invocations are slow.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:17:40 -07:00
Worktrunk Bot a77c92e25b docs(help): point banner at the actual cli source path (#2665) 2026-05-10 08:14:37 +00:00
Maximilian Roos 4cbc8ca5fd refactor(docs): drop mirrored help-page close; align convert_console_blocks_in_docs error channel (#2422)
## Summary

Three follow-ups from #2419 review.

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

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

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

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

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

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

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

## Test plan

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

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

---------

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

Two follow-ups from #2418 review.

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

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

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

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

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

Net diff: −54 lines.

## Test plan

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

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

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

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

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

## What changed

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

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

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

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

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

## Test plan

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

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

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
2026-04-24 16:40:21 -07:00
Maximilian Roos 0253260503 Extend -v variable dump to aliases + help-table drift test (#2324)
Follow-ups from #2316.

## What's in here

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

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

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

## Example

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:39:29 -07:00
Maximilian Roos eec3b24883 Show resolved template variables under -v for hooks (#2316)
Closes #2309.

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

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

## Key files

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

## Testing

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:28:43 -07:00
Maximilian Roos 1a4eea3c03 feat(cli): promote --yes to a global flag (#2279)
## Summary

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

## Why

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

## Call-site survey

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

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

## Alias compat

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

## Navigating the diff

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

## Notes

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

## Test plan

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

> _This was written by Claude Code on behalf of Maximilian_

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-04-18 11:08:41 -07:00
Maximilian Roos 3e1b351ac1 feat(log): split -vv output into trace.log + output.log, drop -vvv (#2201)
## Motivation

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

## Change

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

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

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

## Fallback path

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

## Key files

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

## Testing

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-13 10:50:18 -07:00
Maximilian Roos c16150afc8 Skip hash suffix for already-safe log filenames (#2157)
`sanitize_for_filename` previously always appended a 3-char hash, giving
logs like `main-vfz/project/post-merge/clippy-vif.log`. Clean names now
pass through unchanged, so the common case reads
`main/project/post-merge/clippy.log`. Inputs that actually require
sanitization (path separators, invalid chars, empty input) still get the
hash suffix so they can't collide with an already-safe name.

Also adds a TODO noting that the trash-sweep log shouldn't be
branch-scoped — it piggybacks on `HookLog` with a fake `"wt"`
pseudo-branch, so its actual on-disk path is the awkward
`.git/wt/logs/wt/internal/trash-sweep.log`. Cleaner would be a top-level
`internal/trash-sweep.log` alongside the other shared logs
(`commands.jsonl`, `verbose.log`, `diagnostic.md`).

Snapshot tests updated to reflect the new pass-through behavior.

> _This was written by Claude Code on behalf of Maximilian_

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:01:56 -07:00
Maximilian Roos 2a6389a092 Centralise [wt-trace] emitter and fix -vv log verbosity (#2146)
## Summary

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

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

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

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

## Reviewer navigation

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

## Testing

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 18:17:33 -07:00
Maximilian Roos f8f291b372 refactor(logs): nest hook output by branch/source/hook-type/name (#2041)
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>
2026-04-09 16:47:35 -07:00
Maximilian Roos 4a7219b230 Sweep stale .git/wt/trash entries on wt remove (#2039)
## 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>
2026-04-09 13:54:41 -07:00
Maximilian Roos 85c3dadf17 feat: add --format=json to config state subcommands (#1969)
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>
2026-04-07 14:41:21 -07:00
Maximilian Roos 679fe539fe Deprecate --no-verify in favor of --no-hooks (#1932)
`--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>
2026-04-06 15:01:18 -07:00
worktrunk-bot f6e89009e5 fix: detect squash-merged branches when merge-tree conflicts (#1820)
## 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>
2026-04-01 19:05:54 -07:00
Maximilian Roos 33b87f0b95 Widen help text wrapping from 80 to 100 chars for web docs (#1786)
The web docs content area fits ~101 monospace characters on desktop. The
previous 80-char wrap caused unnecessary line breaks in option
descriptions — e.g., enum variant descriptions wrapping mid-phrase like
"Stage everything: untracked files + unstaged tracked / changes".

Widens the two `help_reference()` call sites in `src/help.rs` from
`Some(80)` to `Some(100)`. Also fixes a pre-existing issue where clap's
line wrapping would break bold (`<b>`) spans across lines —
`ensure_line_resets` now tracks active SGR styles and re-opens them on
continuation lines.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-29 12:38:00 -07:00
Maximilian Roos b6a6744380 Consistent code block convention for syntax highlighting (#1777)
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>
2026-03-28 12:18:48 -07:00
Maximilian Roos a4fe57b510 docs: tighten remove and hook help text (#1765)
Clarify branch cleanup description, remove migration notes, and clean up
small wording issues in remove and hook docs.

- Reword branch deletion condition to "when they would add no changes to
the default branch if merged"
- Add "empty working trees" qualifier to the dimming condition
- Use long flags consistently in prose (`--force-delete` not `-D`)
- Remove parenthetical examples from force flag table
- Remove `wt switch /path/to/worktree also works` from detached HEAD
section
- Remove hook migration section (legacy — should have been in changelog
only)
- Reorder pre/post-start table to lifecycle order

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-26 23:10:01 -07:00
Maximilian Roos acff4cd03b Replace parentheticals with prose in after_long_help docs (#1764)
Rewrites parenthetical asides in `after_long_help` text across switch,
list, merge, step, hook, and config docs. Qualifiers and conditions
become em-dashes or semicolons; examples and analogies stay in parens.

Changes: `src/cli/mod.rs` only (docs, skills, and snapshots are
auto-synced).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-26 22:54:26 -07:00
Maximilian Roos e1e47a3b04 feat: support path-based switch for detached worktrees (#1680)
Extends path-based worktree resolution to `wt switch`, matching the
path-based removal from #1665. Both the CLI (`wt switch
/path/to/worktree`) and the picker (pressing Enter on a detached
worktree) now work.

## Changes

- `plan_switch`: path-based fallback (Phase 2b) after branch lookup
returns `None` — tries the argument as an absolute or multi-component
relative path
- Picker: passes the worktree path instead of `"(detached)"` for
detached items when switching
- CLI arg description stays "Branch name" — path support is documented
in the remove page's "Detached HEAD worktrees" section, per user
guidance
- Reverts "Branch name or path" arg description from #1665 back to
"Branch name"

## Testing

- New test: `test_switch_detached_worktree_by_path` — verifies `wt
switch /path/to/worktree` works for detached worktrees
- All existing tests pass

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-22 23:12:11 -07:00
worktrunk-bot 47e6090502 fix: allow removing detached HEAD worktrees from picker (#1665)
## Problem

The switch picker (`wt switch` TUI) fails to remove detached HEAD
worktrees when pressing `alt-r`. All detached worktrees resolve to the
identifier `"(detached)"`, which is then passed to branch-based removal
— causing the error:

```
✗ No branch named (detached)
```

Additionally, there was no CLI way to remove a detached worktree from
outside it (`wt remove @` only works from within the worktree).

## Solution

Add path-based worktree resolution as a fallback in
`resolve_worktree_arg` for the `Remove` context. When branch-name lookup
fails, the argument is tried as a filesystem path. This means:

- **CLI**: `wt remove /path/to/detached-worktree` now works
- **Picker**: Uses `handle_remove_path()` — the same codepath as the CLI

The picker doesn't do anything the CLI can't do.

## Changes

- `resolve_worktree_arg`: path-based fallback for `Remove` context
(absolute or relative paths)
- `handle_remove_path`: new function for path-based worktree removal
- `validate_remove_targets`: handles `branch: None` (detached worktrees)
via path-based removal
- Picker uses `handle_remove_path` for detached worktrees instead of
directly calling `prepare_worktree_removal`
- CLI help text updated: arg description says "Branch name or path",
detached HEAD section added to `--help`

## Testing

- Existing test: `wt remove (detached)` → fails with "No branch named
(detached)"
- New test: `wt remove /path/to/worktree` → successfully removes
detached worktree
- All existing tests pass

Thanks to @mjakl for reporting in #1661

---
Closes #1661

> _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 <m@maxroos.com>
2026-03-22 22:05:21 -07:00
worktrunk-bot ce9ea1a6b6 refactor: consolidate .git/wt-* paths under .git/wt/ and stage removed worktrees in trash (#1583)
## Summary

- Moves the rename-based staging directory from a visible sibling path
(`project.wt-removing-<timestamp>`) into `.git/wt/trash/`, hiding it
from the user's workspace
- Consolidates all worktrunk-managed `.git/wt-*` directories under a
single `.git/wt/` parent
- Adds `Repository::wt_dir()` accessor as the single root for all
worktrunk state
- Renames `wt-relocate-tmp` to `wt/relocate-staging` for consistency
with `wt/promote-staging`
- Falls back to legacy `git worktree remove` if the trash directory
can't be created
- The `.git/` directory is always on the same filesystem as worktrees,
so the instant rename guarantee is preserved

## Path migration

| Before | After |
|--------|-------|
| `.git/wt-logs/` | `.git/wt/logs/` |
| `.git/wt-cache/summaries/` | `.git/wt/cache/summaries/` |
| `.git/wt-cache/ci-status/` | `.git/wt/cache/ci-status/` |
| `.git/wt-promote-staging/` | `.git/wt/promote-staging/` |
| `.git/wt-relocate-tmp/` | `.git/wt/relocate-staging/` |
| (new) `.git/wt/trash/` | Staging for background removal |

## Context

Users reported confusion when seeing `.wt-removing-*` directories in
their workspace after `wt remove` (#1572). By staging in
`.git/wt/trash/` instead, the directory is completely hidden — even if
the background `rm -rf` is slow or gets interrupted.

The `.git/wt-*` sibling directories were also consolidated into
`.git/wt/` for tidiness per review feedback.

## Test plan

- [x] Unit tests for `generate_removing_path` and
`build_remove_command_staged` updated and passing
- [x] All 105 remove-related integration tests passing
- [x] `test_remove_background_path_gone_immediately` — verifies instant
removal still works
- [x] `test_remove_background_fallback_on_rename_failure` — verifies
fallback when staging path is blocked
- [x] `test_remove_stale_staging_dir_from_crashed_removal` — verifies
stale dirs land inside `.git/`
- [x] All 2496 tests pass (lib + bin + integration)
- [x] Help snapshots, doc sync, and lint checks all pass

Closes #1572

🤖 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>
Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
2026-03-17 13:30:49 -07:00
worktrunk-bot 3080eb0922 docs(remove): update example heading to mention branches (#1449)
## Summary

- Updates example heading from "Remove specific worktrees:" to "Remove
specific worktrees / branches:" to clarify that `wt remove` also works
on branches

Ref #1415

## Test plan

- [x] Snapshot test updated and passing
- [x] Doc sync test 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>
2026-03-11 22:07:11 +00:00
Maximilian Roos 2bca89a886 Group help options into Picker and Automation headings (#1355)
Group `--branches`/`--remotes` under "Picker Options" and
`--yes`/`--no-verify` under "Automation" in `wt switch`, `wt remove`,
and `wt merge` help output, using clap's `help_heading`.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-08 14:07:58 -07:00
Maximilian Roos 1cf2c8203f Add page metadata, canonical URLs, and structured data to docs (#1167)
## 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>
2026-02-22 13:58:34 -08:00
Maximilian Roos 1f361b6f45 docs: improve wt remove help text (#792)
- Change subdefinition from "For finished feature branches. Removes the
  current worktree by default." to "Defaults to the current worktree."
- Add Hooks section documenting pre-remove and post-remove hooks

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 22:35:16 -08:00
Maximilian Roos fb011c37dd feat: reduce background hook output verbosity (#740)
* feat: reduce background hook output verbosity (#690)

Background hooks (post-start, post-switch) now show a single-line summary
by default instead of verbose per-hook output with command details:

  ◎ Running post-start hooks @ repo.feature: user:bg, project

Use `-v` to see detailed per-hook output with expanded commands.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: update comment to match actual output format

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add coverage for verbose background hook output

Add test_post_start_verbose_shows_per_hook_output to verify that -v shows
detailed per-hook output with command in gutter format.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: document -v flag for background hook verbosity

Adds a note in the hook types section explaining that background hooks
show a single-line summary by default, with -v for expanded details.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 19:20:24 +00:00
Maximilian Roos f64c7863b6 refactor: require -vv for debug logging, reserve -v for future use (#702)
Change verbosity threshold from -v to -vv for enabling debug logging and
diagnostic file generation. This frees up -v for other purposes.

Behavior change:
- -v: no effect (reserved for future use)
- -vv: debug logging + verbose.log + diagnostic.md (unchanged)

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-17 13:55:11 -08:00
Maximilian Roos d70860afbb docs: improve command documentation structure (#643)
* feat: include command definition at top of doc pages

The first `///` doc comment line (the "about" / definition) now appears
at the top of each command's documentation page, before the subtitle
and after_long_help content.

Previously, this definition only appeared in the Command Reference
section at the bottom. Now the page structure is:

1. Definition (short about)
2. Subtitle (long about, if present)
3. Conceptual documentation (after_long_help)
4. Command reference

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: improve command documentation structure

- Add definition to top of web doc pages (was missing)
- Combine definition + subdefinition into single lead paragraph on web
- Remove duplication between definition and after_long_help openers
- Fix em-dash spacing (spaced per style guide)
- Fix pronoun clarity ("Creates one" vs "Creates it")
- Use indicative mood instead of second person ("For finished feature branches" not "Use when you're done")
- Add documentation guidelines to docs/CLAUDE.md

Commands updated: switch, list, select, remove, merge, step, hook, config

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: update switch help snapshot for pronoun change

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 21:22:52 -08:00
Maximilian Roos 6ecc51feef docs: clarify --force vs -D flags in wt remove (#565)
Add a "Force flags" section to explain the difference between:
- `--force` (`-f`) for worktree removal with untracked files
- `--force-delete` (`-D`) for deleting unmerged branches

This addresses user confusion reported in #564 where the `--force` flag
for worktrees was only documented in the command reference, not the
prose section.

Also removes the Shortcuts section (not relevant to remove) and uses
consistent single-quote styling for 'target' and 'same commit'.

Closes #564

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 13:46:24 -08:00
Maximilian Roos 6665c128d4 feat(diagnostic): add -vv flag for diagnostic report generation (#472)
* feat(diagnostic): add -vv flag for diagnostic report generation

Add verbosity levels to the CLI:
- `-v` enables debug logging (existing behavior)
- `-vv` also writes a diagnostic report to .git/wt-logs/diagnostic.md

The diagnostic report includes:
- Command that was run and result
- Environment (wt version, OS, git version, shell integration)
- Worktree list
- User and project config contents
- Verbose log (if available)

When gh CLI is installed, shows a hint with the full `gh issue create`
command for easy bug reporting.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): make version check more robust in diagnostic test

The version string can be either "v0.9.5" (from git describe) or
"0.9.5" (from cargo), so check for "wt " instead of "wt v".

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): handle Windows path separators in diagnostic snapshot

The project config path uses backslashes on Windows (.config\wt.toml)
but forward slashes on Unix (.config/wt.toml).

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

Co-Authored-By: Claude <noreply@anthropic.com>

* test(diagnostic): add unit tests for config formatting functions

Add tests for:
- format_config_section: file not found, empty file, content, truncation
- strip_ansi_codes: ANSI code removal
- truncate_log: small and large content

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-08 06:56:07 +00:00
Maximilian Roos 75e1e039f7 feat(hook): add --foreground flag for debugging background hooks (#470)
* feat(hook): add --no-background flag for post-start/post-switch debugging

Allows running background hooks (post-start, post-switch) in the
foreground to see their output directly, useful for debugging.

Usage: wt hook post-start --no-background

Follows the existing --no-background pattern from wt remove.

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

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(cli): rename --no-background to --foreground across all commands

Rename the flag for running operations in foreground mode from
--no-background to --foreground for better UX. The old flag remains
as a hidden deprecated alias with a warning message.

Affected commands:
- wt remove --foreground
- wt hook post-start --foreground
- wt hook post-switch --foreground

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

Co-Authored-By: Claude <noreply@anthropic.com>

* test: add tests for deprecated --no-background flag

Add tests to verify the deprecated --no-background flag still works and
shows the deprecation warning. This covers the code paths for:
- wt remove --no-background
- wt hook post-start --no-background
- wt hook post-switch --no-background

These tests ensure the deprecation warnings are emitted and the old flag
continues to function as expected.

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-07 19:04:57 -08:00
Maximilian Roos 92abd99656 Format help text command references with backticks
Wrap command references in backticks (e.g., `wt merge`) in help text and documentation to render them as code. Updates both source documentation files and CLI help strings, with corresponding snapshot updates.
2026-01-07 12:54:52 -08:00
Maximilian Roos 0340924d47 refactor: improve CLI help text and documentation
Clarify Push command documentation and improve help text descriptions
for select, squash, and merge commands. Update test snapshots to reflect
these documentation changes.
2026-01-04 20:17:15 -08:00
Maximilian Roos 1154ff0744 Clarify branch cleanup target comparison logic
Reword the explanation of how the "same commit" check differs from
other checks in determining the target branch for comparison.
2026-01-03 21:11:43 -08:00
Maximilian Roos 143ec2a497 Clarify remove command help text
Update the `wt remove` command help to explicitly mention that branches
are only deleted if merged, and that the command defaults to removing
the current worktree. Adjust wording in branch cleanup section to use
"By default" for consistency.
2026-01-03 19:50:42 -08:00
Maximilian Roos 9e27a51d15 Standardize worktree/branch terminology in CLI and docs (#316)
Align with spec: "Worktrees are addressed by branch name."

CLI changes:
- Switch: "Branch or worktree name" → "Branch name"
- Remove: argument renamed from `worktrees` to `branches`
- Remove: "Worktree or branch" → "Branch name [default: current]"

Internal changes:
- GitError::UncommittedChanges field: `worktree` → `branch`
- Repository::ensure_clean_working_tree param: `worktree` → `branch`

Documentation:
- Added explanation: "Each worktree has exactly one branch, so Worktrunk
  uses branch names to address worktrees. The path is derived automatically."
- Appears in worktrunk.md (landing page) and `wt switch --help`

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 21:09:39 -08:00
Maximilian Roos 1a9ce24f36 Separate --force into --yes (prompts) and --force (removal) (#311)
* Add --force flag to wt remove for worktrees with untracked files

The --force flag now also passes --force to git worktree remove,
allowing removal of worktrees containing untracked files like build
artifacts (.vite/, node_modules/, etc).

- Extended existing --force flag semantics (was: skip approval prompts)
- Added -f short form for convenience
- wt merge now always forces worktree removal (build artifacts unneeded post-merge)

Fixes #301

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

Co-Authored-By: Claude <noreply@anthropic.com>

* Separate --force into --yes (prompts) and --force (removal)

The --force flag on wt remove had two meanings: skipping approval prompts
AND forcing worktree removal with untracked files. This was confusing and
potentially dangerous.

Now:
- --yes/-y: Skip approval prompts (all commands with prompts)
- --force/-f: Force worktree removal with untracked files (wt remove only)

This follows CLI conventions (apt, npm, etc.) where --yes skips prompts
and --force overrides safety checks.

Breaking change acceptable per project guidelines (early release mode).

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

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix unit test for NotInteractive error message

Update test to check for --yes instead of --force.

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 14:00:06 -08:00
Maximilian Roos 7e5057b1ef Add --force flag to wt remove for worktrees with untracked files (#310)
The --force flag now also passes --force to git worktree remove,
allowing removal of worktrees containing untracked files like build
artifacts (.vite/, node_modules/, etc).

- Extended existing --force flag semantics (was: skip approval prompts)
- Added -f short form for convenience
- wt merge now always forces worktree removal (build artifacts unneeded post-merge)

Fixes #301

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-30 12:34:03 -08:00