12 Commits

Author SHA1 Message Date
Maximilian Roos 89326f14af Publish shell integration, add a footer, dedupe global options (#4000)
Guides and site presentation: one home per topic, a shell-integration
page, a footer, and three rendering fixes.

**Global options were emitted once per command reference**

clap repeats the same ~20-line `Global Options:` block in every
reference it renders, so a page assembled from subdocs stacked 11 copies
on `/config/` and 13 on `/step/`. That padded the pages and gave site
search that many near-identical hits — "squash" returned both
`#command-reference` and `#command-reference-2`. `take_global_options`
cuts each reference at the heading as it is built, keeping only the
first; one `kept` flag threads through the subdoc expansion and the page
streams out rather than accumulating. Terminal `--help` renders through
clap directly and is unchanged.

The config page also carried colliding anchors — two "Hooks" (`#hooks`,
`#hooks-1`), two "Aliases", and seven "Examples" (`#examples` …
`#examples-6`) — now qualified at their source in `src/cli/config.rs`:
User/Project hooks, User/Project aliases, and
Approval/Alias/State/Cache/Log/Variable examples.

`/step/` still has its own set (eight "Examples", two "Options", two
"Arguments", plus "Staging" and "Dry run" pairs). Qualifying those moves
existing `/step/#examples-N` anchors, so it wants a pass of its own with
the inbound links audited; the deduplication above already removes 13
Global Options blocks from that page.

**Shell integration has a page**

Shell-integration debugging was skill-only: five named warning messages,
a PowerShell checklist, and the wrapper mechanism, with no site page —
while the FAQ's answer to "`wt switch` didn't cd" was to install the
Claude Code plugin. It is now `/shell-integration/`, offered first, with
the plugin as the second route. The `llms.txt` listing serves every page
as `/<slug>.md` from a hand-created symlink, so a new page was a 404 the
listing still advertised; the symlink is added and the sync now fails
when a listed page has none.

**Presentation**

- A site footer carries the version (read from `Cargo.toml` at build
time), releases, changelog and license. No page named any of them, and
`/code-signing/` was reachable only from inside a collapsed block on the
homepage. Starlight's `Footer` is wrapped rather than replaced.
- `wt list --full` renders 1157px inside an 800px content column, so 40%
of it sat behind a horizontal scrollbar with the pane beside the column
empty. A terminal frame now takes the whole pane where there is slack,
measured with a query container rather than recomputed from Starlight's
layout formula.
- The `wt-command-reference` frames offered a copy button for 3,877
characters of generated help text; they now expose no copy control. A
console block listing several commands is as often a menu of
alternatives as a recipe, and nothing in the markup tells them apart, so
every command line in such a block carries its own copy control
alongside the block's.
- The four command demos and the two hand-written figures get captions;
the 2.33 MB homepage GIF below the fold loads lazily.

**Sidebar order is pinned**

`site-navigation.mjs` told readers a
`test_sidebar_matches_frontmatter_order` would fail when the authored
sidebar and the pages' `sidebar.order` disagreed. No such test existed,
and the disagreement it describes is exactly what the survey found:
`remove` listed before `merge`, Agent integration ahead of
lower-numbered pages. The test is written, so the sidebar and the
`llms.txt` ordering derived from the frontmatter can't drift apart
again.

<details>
<summary>Guide corrections</summary>

- Tips & patterns was 26 flat H2 recipes in no order, all 26 in the
sidebar. They group under five H2s — setup and layout, aliases and
hooks, per-worktree services, working with agents, status/commits/logs —
with each recipe demoted to H3. Anchors are level-independent, so
existing `/tips-patterns/#…` fragments still resolve.
- `-x 'opencode run'` has been broken since 0.75.0 made `-x` a literal
program: it is `-x opencode -- run '<task>'`.
- The branch-summary preview moved from tab 5 to 6 when the unified-diff
tab landed; the recipe names the `summary` tab instead of a number.
- The Caddy recipe claimed `feature-auth` hashes to port 16460 — that is
`fix-auth`'s port. It is 18283.
- `_` in `wt list` is same-commit *and clean*; the
same-commit-with-changes glyph is `–`, which is not safe to delete.
- `wt step prune` removes branches with no worktree too, and the min-age
guard ages a worktree by its creation time and a bare branch by its
oldest reflog entry.
- `wt step eval -v` prints fifteen variables; the example showed two
under a lead calling them "the available template variables".
- A filter applied to `{{ vars.<key> }}` acts on the placeholder the
preview substitutes, so `{{ vars.port | default('8080') }}` previews as
`{{ vars.port }}`, filter gone.
- The `.git/wt/cache/` table was missing `picker-preview`, and `wt
config state clear` prompts unless `--yes`.
- `skills/worktrunk/reference/README.md` was a symlink to the repo
README that `SKILL.md` never referenced, and the plugin mirror
dereferenced it into a 262-line copy carrying the star-history token,
share links, and a logo path resolving nowhere. Nothing generated it, so
deleting the symlink is the whole fix.
- One home per topic: agent handoffs stay in tips-patterns, activity
markers in `claude-code.md`, alias-template deferral in `extending.md`,
and the `codename` filter's two `worktree-path` recipes give way to the
config page that owns path templates. The FAQ's "Running tests" and "How
can I contribute?" duplicated the README's Contributing block down to
the share URLs.
- The FAQ linked `/worktrunk/#install`, the `noindex` compatibility
route; the plugin hook shim's Windows Terminal hint pointed there too.
Both use `/#install`, where the new sidebar Install entry goes.
- Example names settle on `myproject` / `feature-auth`; "sibling to main
repo" becomes "sibling to the main worktree", and `wt remove`'s "target
worktree" becomes "the worktree being removed" per the project's own
terminology rule.

</details>

UX survey items: `#36`, `#37`, `#38`, `#39`, `#45`, `#47`, `#48`, `#49`,
`#50`, `#51`, `#52`, `#61`, `#94`, `#95`, `#96`, `#97`, `#99`, `#100`.

Reviewable files: the hand-written pages under `docs/src/content/docs/`
(notably the new `shell-integration.md`, `tips-patterns.md`, `faq.md`),
`docs/src/components/Footer.astro`,
`docs/src/plugins/worktrunk-terminal.mjs`,
`docs/src/site-navigation.mjs`, `docs/tests/*.mjs`, `src/help.rs`,
`plugins/worktrunk/hooks/wt.sh`. Generated mirrors and snapshots are
regenerated.

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

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

https://claude.ai/code/session_01XAUYWFN9d9oh6jyoQiouHb

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 12:44:47 -07:00
Worktrunk Bot c93b494bd0 test(configure_shell): pin the AUR PKGBUILD's fish completion to its generator (#4062)
The `worktrunk-bin` AUR package installs fish completions from a heredoc
in `.github/aur/PKGBUILD` that hand-copies what
`fish_completion_content("wt")` produces — makepkg runs no Rust, so
there is no way to call the generator from there. The PKGBUILD's own
`TODO` notes the duplication ("a bit hacky, but short enough that copy &
paste is OK for the moment"), but nothing checks it. The two copies are
identical today; if the generator changes, the next release ships a
stale completion to AUR users and the only symptom is someone's
tab-completion quietly producing nothing.

This adds a unit test next to the existing `fish_completion_content`
snapshots asserting the PKGBUILD still contains the generated string
verbatim, printing the replacement text on failure. It's a real surface:
the docs point at the official Arch package now, but `worktrunk-bin` was
deliberately kept publishing (#2052) and at least one user tracks it for
newer releases than the official package carries.

`.github/aur/PKGBUILD` is read at test time and so never appears in
coverage, which means a PKGBUILD-only edit maps to no test and would
slip through `cargo affected run`. It gets a
`workspace.metadata.affected.rule` for that — the same treatment
`readme_sync`'s runtime-read inputs already have.

`.gitattributes` pins the PKGBUILD to an LF checkout, because a byte
comparison against a `\n`-emitting generator is only meaningful if the
file on disk is LF everywhere. Git for Windows defaults to
`core.autocrlf=true` and the path carried no `text`/`eol` attribute, so
Windows checked it out CRLF and the assertion could never match — which
is how the first push went red on `affected tests (windows, advisory)`.
Pinning is right independently of the test (makepkg reads the file as a
shell script), it's the same mechanism `templates/* text eol=lf` already
uses, and unlike `readme_sync`'s `#![cfg(not(windows))]` it leaves
Windows actually running the drift check. The stored blob is already LF,
so nothing renormalizes; a Windows clone predating the attribute needs
the file re-checked out once (`git rm --cached .github/aur/PKGBUILD &&
git checkout -- .github/aur/PKGBUILD`), and CI checks out fresh.

**One thing I could not verify here:** `cargo-nextest` isn't reachable
from the CI agent sandbox (`tend: cargo-nextest came from the runner
home and is unavailable`), so I couldn't run `cargo nextest list -E
'test(/aur_pkgbuild/)'` to confirm the new filterset selects the test.
The expression mirrors the existing `test(/readme_sync/)` rule exactly,
and the repo's own nextest run will exercise it.

<details><summary>Verification</summary>

Run on Linux only — the sandbox has no Windows or macOS host, so the
cross-platform claim rests on CI (`test (windows)` and `full-tests
(windows-2025, windows)`), not on these:

```
cargo test --bin wt aur_pkgbuild        # passes
cargo test --bin wt configure_shell     # 33 passed
cargo fmt --check                       # clean
cargo clippy --bin wt --all-features    # clean
```

Drift is actually caught: with `--exclusive` deleted from the PKGBUILD's
`complete` line, the test fails and prints the correct replacement body.

The CRLF mechanism behind the Windows failure, reproduced on Linux
against a scratch repo with `core.autocrlf=true`:

```
without .gitattributes entry: CR count = 29
with    .gitattributes entry: CR count = 0
```

</details>

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-09-10 04:33:49 -07:00
Worktrunk Bot 80e795b271 fix(plugin): run the Codex Windows hooks through Git Bash, not the WSL launcher (#4008)
## Problem

Codex resolves a hook `command` through the platform shell — `/bin/sh
-lc` on Unix, `cmd.exe /C` on Windows
([`default_shell_command`](https://github.com/openai/codex/blob/main/codex-rs/hooks/src/engine/command_runner.rs)).
All four Codex hooks in the plugin manifest lead with a bare `bash`, and
under `cmd.exe` that resolves through the Windows PATH to
`System32\bash.exe` — the WSL launcher, not Git Bash. In a sandboxed
session the launcher refuses to start (`Access is denied. Error code:
Bash/Service/CreateInstance/E_ACCESSDENIED`), so every prompt,
permission request, turn end, and session end raises a `Hook failed`
banner (#4007).

## Solution

Git for Windows stays a requirement; only the bare *name* `bash` is the
problem, since `cmd.exe` resolves it through PATH. Each hook now also
carries Codex's per-handler `commandWindows`, which **replaces**
`command` on Windows (`command_windows.unwrap_or(command)` in
`codex-rs/hooks/src/engine/discovery.rs`). It calls a new `cmd.exe`
shim, `plugins/worktrunk/hooks/wt.cmd`, that locates `bash.exe` by path
the way
[`find_git_bash`](https://github.com/max-sixty/worktrunk/blob/2405b8b434cfa9d7a604405d3ace966ada60ff8a/src/shell_exec.rs#L481-L518)
does in `src/shell_exec.rs` — derived from `git.exe`'s install
directory, then the system-wide and per-user install defaults — and then
runs the same `hooks/wt.sh` that Claude, Gemini, and Unix Codex already
go through. Worktrunk's own binary resolution stays in that one script
instead of being spelled a second time in cmd.

Two details the shim inherits from the Rust resolver: `Git\bin\bash.exe`
before `Git\usr\bin\bash.exe`, because the former is the wrapper that
sets up the MSYS environment for a caller outside Git Bash (which is
what puts `uname` within reach of `wt.sh`); and a PATH-scoped lookup,
because an unscoped `where` searches the current directory first — for a
hook that's the user's project, so a `git.exe` committed to a repo would
otherwise choose the bash every event runs. The lookup is spelled
`"%SystemRoot%\System32\where.exe" "$PATH:git.exe"` on both counts: the
`$PATH:` prefix scopes what is searched, and the absolute path to
`where.exe` closes the same surface one level up — `where` is
`System32\where.exe` rather than a cmd built-in, so cmd resolves that
bare name from the current directory too.

`wt.sh` now clears `WT` before its branches. On Windows all of them can
be skipped (neither `git-wt.exe` nor `wt` on PATH), and a hook is handed
the caller's whole environment, so an inherited `WT` was what the final
`command -v "$WT"` check accepted and ran.

The Windows commands brace the plugin root as `${PLUGIN_ROOT}` because
Codex substitutes only that form textually, before the shell runs; the
unbraced `$PLUGIN_ROOT` the Unix commands use survives to `/bin/sh`, and
`cmd.exe` would pass it through literally. The tail is `|| exit /b 0`,
the cmd.exe spelling of the Unix `|| true`: a marker is decoration, and
a nonzero exit is what raises the banner. `.gitattributes` pins
`plugins/worktrunk/hooks/*.cmd` to a CRLF checkout, since cmd.exe
resolves a `goto` label by seeking through the file and can fail that
search on an LF-only batch file.

Two doc changes ride along. `plugins/worktrunk/CLAUDE.md` records why
every hook carries `commandWindows`, the shim's resolution order and the
two lookup-scoping decisions, and why `SessionEnd` keeps `timeout: 3`
(Codex's ceiling for that event, so the longer Windows chain has no more
budget to ask for). And because the CHANGELOG entry opens an `##
Unreleased` section, `.claude/skills/release/SKILL.md` step 9 now says
to rename that heading at release time rather than insert a new one
above it — otherwise the release ships a stale `## Unreleased`.

## Testing

Six tests, all new:

- `test_codex_hooks_carry_windows_commands` (all platforms) — pins that
every Codex command hook has a `commandWindows`, that it names neither
`bash` nor bare `wt`, that it calls the shim, and that the two
`PLUGIN_ROOT` spellings stay on their respective sides. Written first:
it failed on the manifest as shipped, with the exact hook command from
the report.
- `test_codex_windows_hook_commands_set_the_marker` (Windows leg of CI)
— runs the real `commandWindows` the way Codex spawns it, reproducing
both steps: the `${PLUGIN_ROOT}` substitution and `cmd.exe /C
"<command>"` with the command line as a single quoted raw argument. It
asserts `UserPromptSubmit` stores 🤖, `Stop` replaces it with 💬,
`SessionEnd` clears it, and that a hook which cannot find worktrunk
still exits 0. PATH is pinned to the shape a default Git for Windows
install produces — `Git\cmd` and nothing else from the install — so the
run covers cmd.exe's quote handling, the shim's search for bash, `wt.sh`
running under the bash it picks, and the emoji surviving both hops.
- `test_wt_sh_ignores_an_inherited_wt` (Windows leg) — points `WT` at a
real worktrunk, spelled the way bash can run it, on a PATH where `wt.sh`
finds none itself, and pins that the inherited value is not what runs.
- `test_shim_ignores_a_git_in_the_current_directory` (Windows leg) —
plants an unrunnable `git.exe` in the hook's current directory plus the
`bash.exe` the shim would derive from it, puts a real Git and worktrunk
on the pinned PATH, and asserts the shim prints a version line — which
it could not do had it taken the decoy.
- `test_shim_ignores_a_where_in_the_current_directory` (Windows leg) —
the same question one level up: a `where.bat` in the hook's current
directory names a decoy Git install whose `bin\bash.exe` exists, so an
unscoped `where` would reach the derive branch and leave `BASH` set and
unrunnable rather than falling through to a real install. A separate
probe pins the premise — that the planted `where` really does shadow
`System32\where.exe` — so the test cannot go green because the decoy was
never consulted.
- `test_shim_derives_bash_from_the_git_on_path` (Windows leg) — points
`ProgramFiles` and `LOCALAPPDATA` at an empty directory, which leaves
the derive branch as the only route to a bash. The two tests above go
red only when the shim picks the *wrong* bash; this one goes red when
the lookup finds nothing, so it is what observes that
`"%SystemRoot%\System32\where.exe" "$PATH:git.exe"` survives the quoting
`for /f` wraps it in.

<details><summary>What this does not verify</summary>

Nothing here drives a real Codex session on Windows, so the end-to-end
claim — that Codex selects `commandWindows` and that the banner stops —
rests on reading
`codex-rs/hooks/src/engine/{discovery,command_runner}.rs` rather than on
observation. What CI does exercise is the command string itself,
executed the way that source says Codex executes it.

The shim's two standard-install fallbacks (`%ProgramFiles%\Git`,
`%LOCALAPPDATA%\Programs\Git`) are unexercised by CI. Every test that
reaches a bash takes the derive branch above them, and the one test that
touches the fallbacks empties them to force that branch rather than to
exercise them; they share the whole tail with it.

Two adjacent things are deliberately left alone, as separate concerns:
the Gemini hooks at the repo-root `hooks/hooks.json` use the same bare
`bash` (Gemini's Windows hook execution isn't established here), and
Claude's `hooks/hooks.json` is unaffected because Claude Code runs hook
commands through Git Bash on Windows.

</details>

---
Closes #4007 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-09-06 16:26:17 -07:00
Maximilian Roos c63311db61 Collapse the remaining generated mirrors in PR diffs (#3990)
A help-text edit in `src/cli/mod.rs` regenerates three mirrors of the
same paragraphs, and `.gitattributes` collapsed only one of them. So a
PR touching help text — most of the docs-survey PRs — showed the text
three times over.

This marks the plugin skills tree and `dev/*.example.toml` generated as
well, each with a comment naming what rewrites it. The command pages
under `docs/src/content/docs/` deliberately stay visible: a reviewer
should read the rendered help once, and the rest of that directory is
hand-written primaries. Snapshots stay visible too — they are asserted
content, not a mirror.

Verified with `git check-attr linguist-generated` over a generated file
and a primary in each group, including the plugin mirror's copies of the
skill-only pages (`troubleshooting.md`, `shell-integration.md`), which
collapse there while their primaries under `skills/worktrunk/reference/`
stay exempt.

<details>
<summary>Considered and left alone:
<code>docs/public/schema/list-v2.json</code></summary>

It is generated by the same sync test, but `sync_json_schema`'s
docstring says a schemars upgrade rewriting it "should be a reviewed
diff" — collapsing it by default works against that.
</details>

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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 00:46:54 -07:00
Maximilian Roos e5f9589c5c Polish mobile docs terminals (#3936)
## Summary

- re-record and publish both mobile homepage demos with restored ANSI
colors
- preserve exact ANSI roles in every snapshot-backed website terminal
while keeping Markdown portable
- wrap source and command snippets on mobile, contain fixed terminal
tables, and add real WebKit layout and contrast regressions

The corrected GIFs are already published in `max-sixty/worktrunk-assets`
at `4de7bde`.

## Testing

- `cargo run -- hook pre-merge --yes` (4,721 tests passed, 1 skipped)
- `npm --prefix docs run check`
- `npm --prefix docs test`
- `npm --prefix docs run build`
- `npm --prefix docs run test:site`
- independent frame review of both mobile GIF themes
- independent architecture, systematic, and adversarial reviews

Closes #3930
Closes #3931

> _This was written by Codex on behalf of max-sixty._
2026-08-27 12:14:39 -07:00
Maximilian Roos 8e405bced9 docs: rebuild the site with Astro and Starlight (#3866)
The docs now build with Astro and Starlight instead of Zola. This
removes the Tera 2 migration blocker from #3827 while keeping the
published routes, anchors, generated references, metadata, and crawler
URLs stable. It replaces the approach closed in #3840.

## What changed

- Move the site into Starlight, with a custom Worktrunk homepage and a
shared copper, gold, paper, and ink design system.
- Keep generated docs as portable Markdown. The Rust sync pipeline no
longer needs Zola shortcodes, template escaping, ANSI-to-HTML
conversion, or reverse transforms for skill and README output.
- Preserve terminal semantics and command-only copying, add responsive
handling for short wide tables, retain stable heading IDs, and give
repeated command-reference headings useful search labels.
- Move docs CI and publishing to Node, Astro, and built-site contract
tests. The tests cover public routes, links, assets, metadata,
navigation, tables, terminal frames, and compatibility aliases.
- Record demo themes from isolated environments, add a mobile core demo,
and test the recording contract. The matching mobile assets are
published in `max-sixty/worktrunk-assets`.

Closes #3827.

## Verification

- `cargo run -- hook pre-merge --yes` (4,660 tests)
- `npm --prefix docs run check`
- `npm --prefix docs test` (9 tests)
- `npm --prefix docs run build` (16 pages)
- `npm --prefix docs run test:site` (12 tests)
- `pytest docs/demos/tests/test_recording.py` (4 tests)

> _This was written by Codex on behalf of max-sixty_
2026-08-22 00:01:58 -07:00
Maximilian Roos 4c845597b2 fix(prune, remove, ci): count what a removal took, not what it selected (#3633)
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>
2026-07-28 11:05:22 -07:00
Maximilian Roos f2a8cb696d refactor(styling): end bash-gutter lines at their content, not a reopened dim (#3056)
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>
2026-06-11 23:38:55 -07:00
Worktrunk Bot 76c8371b40 docs: fix stale test name in .gitattributes (#2434)
## 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>
2026-04-27 09:00:56 -07:00
Maximilian Roos 8b4e53f1b2 chore(tests): factor write_tracked helper; mark generated files (#2409)
## 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>
2026-04-24 08:27:23 -07:00
Maximilian Roos 2fb7f199cc Remove .gitattributes file
Co-authored-by: Claude <no-reply@anthropic.com>
2025-11-22 10:22:08 -08:00
Maximilian Roos a16c6bb6cc Refactor: Introduce global output context for consistent messaging
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>
2025-10-23 14:44:52 -07:00