Three follow-ups from the review of #3533, which landed shared-branch
retention across the removal surface. Each is small and independent;
they share a branch because they came out of the same pass.
## `wt step prune` counted kinds, not outcomes
A candidate's kind is what the scan selected, not what the removal took.
A branch a sibling worktree still has checked out is retained, so a
worktree candidate can take the worktree and leave the branch standing.
The summary counted the kind anyway:
```console
○ Worktree directory missing for feature; pruned
↳ branch checked out at ~/code/repo.feature
✓ Pruned 1 branch ← the branch is right there
```
`prune_summary` now counts the planned outcome, so that reads `✓ Pruned
1 worktree` — the stale entry, which is all that went. Both
`--format=json` payloads gained `branch_deleted` for the same reason: a
consumer reading `{"branch": "feature", "kind": "branch_only"}` would
reasonably conclude the branch is gone.
The predicate has one home now. `RemoveResult::to_json` already computed
`branch_deleted` inline; it moved to `RemoveResult::deletes_branch()`
and both callers share it. That incidentally fixes a detached worktree
reporting `"branch_deleted": true` beside a null branch — it has no
branch to delete.
`wt step prune --dry-run` had the same defect and no per-item line to
contradict it, so it now predicts retention too. A `Prunable` item has
no plan until `try_remove` prunes its stale entry, so the dry run asks
`live_sibling_checkout` — the same predicate the plan would.
## `RemoveTarget::Branch` now means "a branch with no worktree"
All three callers resolve first and pass `Path` for anything with a
worktree, so the arm handling "the branch turned out to have a worktree"
was unreachable at selection time — codecov confirmed it never executed.
Deleting it would have been enough, but the arm was a live hazard rather
than dead weight: the picker builds a fresh `Repository` and re-lists
worktrees between selecting a row and removing it, so a `wt switch` in
another terminal can give a branch-only row a worktree mid-flight. The
old code would then have removed that worktree, from a row that said
"delete this branch" — the same defect class #3533 fixed.
```console
✗ Branch feature gained a worktree @ ~/code/repo.feature since it was selected;
to remove that worktree, run wt remove ~/code/repo.feature
```
`test_prepare_removal_refuses_branch_that_gained_a_worktree` covers the
picker path. I checked it fails when the guard is neutralized rather
than passing trivially.
## `affected tests (windows, advisory)` was permanently red
git calls a file binary only when it finds a NUL byte in the first 8000,
and two of the committed loose objects under `tests/fixtures/*/_git/`
are zlib streams small enough to have none. Those diff as text, so `git
diff` writes raw deflate into its output, and `cargo affected` aborts
decoding it as a string before running a test.
Reproduced with `git show <commit> -- <object>` (invalid UTF-8, byte
`0x95` at position 5403) and confirmed fixed — the same command now
reports `Binary files … differ`. The rest of `_git/` stays text worth
reading.
## Testing
`prune_summary_counts_a_retained_branch_as_worktree_only` covers the
counting; `test_prune_retains_branch_checked_out_in_another_worktree`
gained an assertion on the summary line, and I verified it fails when
the guard is removed. The six `.snap` changes are all the same added
`branch_deleted` key.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dispatched to fix a reported ANSI dim-bleed after bash-gutter blocks: in
committed snapshots, multi-command hook announcements appear to inherit
an unclosed `[2m` from the preceding gutter line (e.g.
`post_start_named_commands.snap`, where the gutter line ends
`'Installing deps'[0m[2m`).
Diagnosis: the bleed doesn't exist in real output. Fresh `cat -v`
captures of `wt hook pre-merge --yes` and `wt switch --create` with
two-key hook tables show every gutter line closing with a full `[0m`,
and later `◎ Running …` lines rendering un-dimmed. The snapshots are
misleading by construction: the cross-platform filter in
`tests/common/mod.rs` deletes every line-final `[0m` before
snapshotting, and the formatter reopens dim after the last highlight
token before its per-line reset, so filtered snapshots end `[0m[2m` and
read exactly like a dangling dim.
The change removes that misleading byte pattern at the source: phase 2
of `format_bash_with_gutter_impl` strips the no-op reopened dim (and the
lone dim on blank lines) before appending each line's closing reset.
Rendering is identical; lines now end at their content plus one reset.
Also bundled:
- A CAUTION comment on the reset-stripping snapshot filter, so future
readers don't diagnose SGR bleed from `.snap` bytes.
- `config_show_theme` now binds the standard env redactions;
regenerating its snapshot leaked a host `LLVM_PROFILE_FILE` path and
tripped `test_no_host_specific_paths_in_snapshots` (pre-existing gap,
invisible until regeneration since insta never compares `info:` blocks).
- 108 regenerated snapshots. Verified mechanically: ANSI-stripped bodies
are byte-identical; raw diffs are confined to line-end SGR sequences
plus stale `env:` header refreshes (`RUST_LOG: warn` from an older
harness).
Possible follow-up, not done here: the line-final-reset filter itself
may be vestigial (anstream pass-through suggests piped output is
identical across platforms now); removing it would make snapshots
byte-truthful but churns nearly every snapshot and needs Windows CI to
confirm.
The first Windows CI run caught a real latent bug the strip exposed:
askama strips a template's final newline, so on a CRLF checkout (Windows
autocrlf) the fish wrapper render ends with a lone `\r` that the
formatter's pair-wise CRLF normalization missed. The `\r` reached
tree-sitter and came back as a trailing token after the highlight
closed, defeating the end-of-line cleanup (and historically invisible
because insta trims trailing whitespace when comparing). Fixed by
trimming trailing `\r` in the formatter's normalization, plus
`templates/* text eol=lf` in `.gitattributes` since CRLF templates
embedded at compile time would leak `\r` into the shell code
Windows-built binaries emit at runtime.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
The header comment in `.gitattributes` referenced
`test_command_pages_and_skill_files_are_in_sync`, but that test was
renamed to
`test_docs_are_in_sync` in #2419 when the three sync tests were
consolidated.
## Test plan
- [x] No code changes; comment-only fix
- [x] `git grep` confirms no other stale references to the old test name
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
Three small cleanups to the docs/skills sync pipeline (flagged by
`/simplify` review on #2404 but deferred at the time):
- **`readme_sync.rs`** — extract a `write_tracked(path, expected,
rel_path, updated)` helper that handles `fs::create_dir_all` +
`fs::write` + push-to-`updated_files`. Applied at five call sites:
`sync_command_pages`, `convert_console_blocks_in_docs`,
`sync_skill_files`, `sync_well_known_skills`, `sync_llms_txt`. Callers
keep control of the "is it different?" check so each site can apply its
own normalization (e.g., `trim_lines`) before comparing.
- **`sync_llms_txt`** — merge the two parse/group loops. Use `let-else`
to pull out `extra.group` when the frontmatter is first parsed, then
push directly into the `BTreeMap`. Drops both the intermediate
`Vec<(String, Frontmatter)>` and an unreachable `.expect("non-home pages
must declare [extra] group")` that only existed because the group was
looked up twice.
- **`.gitattributes`** — new file. Marks the 14 auto-generated outputs
as `linguist-generated=true` so GitHub collapses them in PR diffs.
Targets `skills/worktrunk/reference/*.md`, `docs/static/*.md` (the `.md`
symlinks), `docs/static/llms.txt`, and
`docs/static/.well-known/agent-skills/index.json`. Primary sources
(`docs/content/*.md`, `src/cli/mod.rs`) stay fully visible — `git
check-attr` confirms.
No behavior change; all 13 `readme_sync` tests pass, `cargo clippy
--all-targets --all-features -- -D warnings` clean, `cargo fmt` clean.
## Test plan
- [x] `cargo test --test integration readme_sync` — 13 passed
- [x] `cargo clippy --all-targets --all-features -- -D warnings` — clean
- [x] `cargo fmt --check` — clean
- [x] `git check-attr linguist-generated` on a sample of generated +
primary-source files — generated flip to `true`, primary sources stay
`unspecified`
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
This refactoring centralizes output logic into a global context, simplifying how messages, directory changes, and command executions are handled across different modes (interactive vs. internal).
Key changes:
- **New `output` module structure**:
- `output/global.rs`: Provides `initialize`, `success`, `change_directory`, `execute`, `flush`, `is_interactive` functions using thread-local storage for context-aware output.
- `output/interactive.rs`: Handles human-friendly output with colors, emojis, and direct command execution.
- `output/directive.rs`: Handles machine-readable output for shell integration using NUL-terminated directives.
- `output/handlers.rs`: Contains specific output formatting and handling for `switch` and `remove` commands, leveraging the global context.
- **Removed `src/output.rs`**: The old, monolithic output module is replaced by the new structured approach.
- **Updated `main.rs` and `merge.rs`**:
- Commands now initialize the global output context based on the `--internal` flag.
- Output calls are replaced with `output::success()`, `output::change_directory()`, `output::execute()`, and `output::flush()`.
- Removed `internal` parameters from output handler functions.
- **Snapshot test updates**: Adjusted expected output for various commands to reflect the new output formatting and directive structure.
- **Added `.gitattributes`**: Configures `*.snap` files to be treated as text for proper diffs.
This change improves maintainability, reduces code duplication, and provides a more flexible and consistent output experience.
Co-authored-by: Claude <no-reply@anthropic.com>