Commit Graph

63 Commits

Author SHA1 Message Date
Maximilian Roos e1745db105 feat(list): abbreviate the table's SHA with git, not a fixed slice (#3676)
The Commit cell sliced `&head[..8]` while `--format=json`'s `short_sha`
carried git's `%h`, so one commit read `1b9f1d96` in the table and
`1b9f1d9` in JSON, and `core.abbrev` reached only the JSON. #3675 gave a
detached row's Branch cell the same slice, so the disagreement showed up
twice on one row.

`ListItem::short_sha` becomes the only abbreviation of `head` anywhere:
the Commit cell, a detached row's Branch cell, the statusline, and JSON
all render it. `abbreviated_head()` is gone.

## Column widths

`COMMIT_HASH_WIDTH = 8` is gone. The Commit column and the Branch
column's detached budget both measure the SHAs they will render, so
`core.abbrev = 12` no longer truncates mid-hash and the default 7 stops
reserving a column nothing fills — the freed character goes to Message.

## Latency

`collect()` folds `%h` onto the rows before layout instead of after the
skeleton. The batch carrying it already gates the skeleton for `%ct`
sort order, so this is a map lookup rather than new I/O, and both cells
are identity columns with no placeholder — they still paint in the first
frame.

Measured on a 40-worktree / 400-branch fixture: git subprocess counts
are identical (5 pre-skeleton, 108 for the full run). Pre-skeleton wall
time is unchanged; running both binaries in each order, the sign of the
difference follows run order rather than the binary (+1.5 ms with this
branch second, −0.5 ms with it first), so the residual sits inside
drift.

## Behavior change

Where the commit-details batch fails, the Commit cell is now empty
rather than a slice of a SHA git refused. Age and Message already report
that failure the same way, under the same warning, and two snapshots
show it. `render_text_cell` also stops styling empty text, so a blank
cell no longer emits an escape pair around nothing.

## Reading the diff

160 files, but the hand-written part is +91/−79 in `src/commands/list/`
plus a +71 test. The rest is generated. The docs mirrors and help
snapshots are symmetric. Of the snapshot lines, content is +799/−799 —
every changed line a 1-for-1 hash swap — while +1396 is insta `env:`
metadata refreshing on the 128 snapshots this happens to touch.

`test_list_abbreviated_sha_follows_git` pins the invariant: the table's
hash equals JSON's `short_sha` at git's default and at `core.abbrev =
12`, and a longer prefix is ruled out. It fails against the old fixed
slice.

> _This was written by Claude Code on behalf of max-sixty_
2026-07-30 18:33:30 -07:00
Maximilian Roos cfd6f099bc fix(codex): clear activity marker on session end (#3660)
Codex now exposes `SessionEnd`, but Worktrunk’s Codex plugin only
returned the activity marker to idle at turn end. This adds a
main-session exit hook that clears the marker, using Codex’s
three-second maximum hook timeout so cleanup has the best chance to
complete without delaying shutdown.

The CLI help, plugin-layout guidance, user documentation, generated
mirrors, metadata test, and help snapshot now describe and verify the
complete Codex lifecycle.

Tested with `cargo run -- hook pre-merge --yes` (4,564 tests passed; 1
skipped).

> _This was written by Claude Code on behalf of max_.
2026-07-29 20:57:26 -07:00
Maximilian Roos a27cbd42be feat(skill): create the worktree by name, fall back to a path (#3636)
Closes #3555. `/wt-switch-create` now creates through
`EnterWorktree({name})` for the common invocation, which asks the user
to confirm nothing, and falls back to `wt` plus a path entry for the
cases that route can't serve.

The confirmation Claude Code 2.1.206 added fires on a call that passes
`path` and targets a worktree outside `.claude/worktrees/`. `{name}`
passes no `path`, so it never fires. With the plugin installed, `{name}`
runs worktrunk's own `WorktreeCreate` hook (`wt switch --create`), so
the worktree is the one `wt` would have made, and it shows up in `wt
list`.

## Routes

- **New branch, this repo** → `EnterWorktree({name})`. No confirmation.
- **Anything else** → `wt -C <repo> switch --create … --format=json`,
then `EnterWorktree({path})`, keeping the outcome handling below.

Three things `{name}` cannot do, each announcing its own fallback in the
error it returns, so the skill tries the cheap call and reads the result
rather than pre-checking:

| Case | What `{name}` returns |
|---|---|
| Repo argument given | no `-C` equivalent; skipped before the call |
| Branch already exists | the hook exits nonzero with `✗ Branch <branch>
already exists` |
| Session already entered a worktree | `Already in a worktree session.
Pass `path` to switch into another existing worktree` |

## The tradeoff, taken deliberately

A `{name}` worktree the session never touched is removed when the
session ends, branch included, through the plugin's `WorktreeRemove`
hook (`wt remove`). Verified end to end: create, `/exit`, and neither
the worktree nor the branch remains; one untracked file is enough to
keep both. That is wanted at this scale, since a research task that
wrote nothing leaves nothing to prune, but it does mean "the worktree
persists" was no longer true of every route. Cleanup and the rationale
now say which worktrees persist; the user docs stay out of the
mechanics.

## A bug fixed along the way

A user declining the path-entry confirmation returns an ordinary
permission denial, which the skill's single rejection branch caught, and
that branch's documented recovery is to `cd` into the worktree and work
there. So a declined entry became a worktree entered anyway. Three
wordings that asked the agent to classify the denial text failed
context-blind probes in turn: branches labeled by who refused, "the
denial reports a decision", and that plus the verbatim denial string
quoted as an example.

Entry outcomes now split structurally instead. The tool's own `Cannot
enter` errors key the reachability test; a denial of the call stops and
asks by default, with one exception: a session with no user to ask (its
denial says it couldn't prompt) takes the recovery, since nothing was
decided. A recovery that follows a denial invites the model to route any
denial into it, so the denial branch leads with stopping. Context-blind
agents route both denial directions and the four invocation shapes above
through their intended routes.

## Verification

All of it re-run live against Claude Code 2.1.220 in scratch repos,
since the analysis in #3555 rested on properties pinned to 2.1.173/177
and could not be checked from CI. Two claims died that way: an earlier
draft credited the exit removal to the harness when it is worktrunk's
own hook, and asserted that a `permissions.deny` rule produces a
classifiable denial, when it removes the tool from the session entirely.

Also recorded in the rationale: the confirmation offers no always-allow
and `permissions.allow` cannot suppress it, it fires in `default`,
`acceptEdits`, and `auto` while `bypassPermissions` allows silently, and
a session that cannot prompt denies without asking.

> _This was written by Claude Code on behalf of Maximilian_

---------

Co-authored-by: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:07:36 -07:00
Maximilian Roos 4549715af1 docs(claude-code): link the statusline segment details instead of restating them (#3576)
The `## Statusline` section of `claude-code.md` paraphrased three facts
that the `wt list statusline` reference already owns:

| Fact | `claude-code.md` said | `src/cli/list.rs` says |
|---|---|---|
| Pace maths | "1.4× the pace that would exactly fill that window,
coloured by how much of it would be spent locked out at the cap" | "2.9×
the pace that would exactly fill that window… colour deepens with
severity as the forecast lockout grows" |
| Link degradation | "clickable links where the terminal supports them,
plain text where it doesn't" | "Both links are OSC 8, which a terminal
that doesn't support them discards" |
| CI fetch latency | "runs it in the background… 1–2 second CI fetch
invisible" | "reach the network for a second or two, so it fits a
statusline the host renders in the background" |

#3568 removed the near-verbatim copies from this section but left these
paraphrases behind, which is arguably the worse state: two wordings of
one fact drift apart independently, and a reader can't tell whether
they're the same claim or two different ones.

## What stays

The cut isn't to zero. The page shows an example line, so it needs
enough gloss for a reader to parse the line in front of them — a bare
pointer would make the section a stub. It keeps one sentence naming what
the stdin JSON contributes, with "how the links behave" folded into the
pointer that was already there.

The latency clause also stays, though it is the third duplicated fact.
Without it the opening paragraph only restates the command name, and it
is the one "why" that justifies the `settings.json` block below it.

## Side effect

This leaves the `OSC 8` wording in `src/cli/list.rs` as the single home
for the link behaviour. A precise term earns its place in an exhaustive
`--help` reference and grated on an end-user integration page — so the
jargon was really a symptom of the fact living in two places.

Docs-only. Mirrors regenerated by `test_docs_are_in_sync`; `zola build`
clean (14 pages, anchors resolve).

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

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:29:51 -07:00
Maximilian Roos 11a1498fa1 docs(list): render wt list statusline's help on the docs site (#3568)
`wt list statusline`'s `after_long_help` documents the three output
formats, the Claude Code stdin JSON contract, and the pace segment, but
`wt list`'s help had no `<!-- subdoc: statusline -->` placeholder, so
none of it reached `docs/content/list.md` or the skill reference. It was
terminal-only.

Adding the placeholder pulls it in as a `wt list statusline` section
under `# Subcommands`, matching how `wt step` and `wt config` expose
theirs.

## What reading it on the web surfaced

The help text needed edits once it rendered as a page rather than a
block below an Options list. A context-blind agent was given the two
rendered pages and asked to answer setup questions from them alone;
three of its snags were real.

**The format lists were stale and used private names.** They read
`branch status ±working commits upstream ci url`, while the Columns
table higher up the same page calls those `HEAD±`, `main↕`, `Remote⇅`.
They also omitted `main…±`, which `format_statusline_segments` has
emitted since that column became a default. The lists now use the column
names and include it, and `claude-code` is stated as a delta on `table`
rather than repeating it.

**The formats read as a fixed layout.** The example line on the Claude
Code page has nine segments against an eleven-name format string, with
no explanation. Three rules were in the code and in no doc: empty cells
are omitted, `claude-code` drops `branch` when `dir` already ends in
`.<branch>` (`filter_redundant_branch`), and an overlong line drops
whole cells worst-priority-first (`fit_to_width`). All three are now
stated.

**The latency caveat lived only on the Claude Code page.** A reader of
the command's own docs had no way to learn it reaches the network. It
moves to the reference. That exposed a contradiction with the
definition, "Single-line status for shell prompts", against a caveat
saying it is too slow for a synchronous prompt — so the definition
becomes "Single-line status for the current worktree". This is the one
user-visible string change here; it lands in `wt list --help` and the
three help snapshots.

## Deduplication with the Claude Code page

The pace paragraph and the OSC 8 paragraph were near-verbatim on both
pages, and would have rendered twice on the site. `claude-code.md` keeps
what is Claude Code-side (install, demo, the example line, and a plain
note that the links degrade to unclickable text where the terminal lacks
support) and links to the reference for the rest.

## Follow-ups folded in

The module docstring at `src/commands/statusline.rs:4` carried `±working
commits upstream`, the last occurrence in the tree of the labels this
branch retired, and opened "Statusline output for shell prompts" — the
framing the definition dropped. Both now match the help text.

## Not done

An audit of every `after_long_help` in `src/cli/` found 46 that never
reach a docs page. Most are editorial calls rather than oversights: `wt
config create` alone would embed ~450 lines of example TOML into a page
that already covers that ground by hand. The one that looks like a plain
oversight is `wt step rebase` / `wt step push`, the only two of twelve
step operations without a marker, and the only two the `## Operations`
list leaves unlinked. Covering them needs their openers rewritten first,
since both currently restate their definition. Left for a follow-up.

Verified with `wt hook pre-merge --yes` (4499 tests) and a `zola build`,
which checks internal anchors.

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 07:45:25 -07:00
Maximilian Roos f3e5c82604 feat(statusline): dim the dev-server URL until its port answers (#3561)
Follow-ups to #3550. The dev-server URL in the statusline dims until
something answers on its port, matching the `wt list` cell it was
already copying in every other respect.

```
 ~/w/worktrunk.feature  ↕|🤖  ↑2 ↓1  #3550  :17913     server up
 ~/w/worktrunk          ⊂|    ↑2 ↓1  #3550  :12107     server down (dim)
```

`url_active` was already collected on this path (the health check runs
under the statusline's full-column plan), so the dim consumes something
already paid for rather than adding a probe.

## The other half, and why it isn't here

The follow-up list also carried "detect OSC 8 support and render
accordingly", which is how `wt list` decides its URL cell: linked
`:3000` when the terminal supports hyperlinks, the URL in full when it
doesn't. I built that, and three review passes took it apart. Recording
the path, because the conclusion is the interesting part:

1. The first version probed `stderr`, since a shell prompt captures
stdout through command substitution, and forced links on for
`--format=claude-code`. An adversarial pass showed the exception
reintroduced the bug it was meant to fix: Claude Code re-emits OSC 8 to
the terminal rather than drawing it, so Claude Code inside Apple
Terminal got the link, and the URL collapsed to a bare `:3000` with its
host inside an escape the terminal discarded.
2. Reading the environment alone fixed that and deleted the exception.
Then a bug sweep measured what the plain-text fallback actually costs:
unlinked, the cell grows from 5 columns to the whole URL, and the URL is
the worst-priority segment, so `fit_to_width` drops it first. Against a
long dev hostname the linked line kept the URL down to 39 columns and
the unlinked one lost it below 82. At 80 columns in tmux the "fallback"
showed nothing where the old code showed a clickable port.
3. Separating the two decisions fixed that (collapse to `:port` always,
link only when the terminal renders it) but left the probe controlling
nothing but escape bytes. At which point an outer-loop pass asked
whether it should exist, and it shouldn't:

- The [OSC 8
spec](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
guarantees a terminal implementing OSC parsing per ECMA-48 "is
guaranteed not to suffer from compatibility issues … the target URI is
silently ignored and the supposed-to-be-visible text is displayed,
without artifacts." The named buggy set is VTE up to 0.48 (2018),
Windows Terminal up to 0.9, Emacs term-mode, and screen with 700+
character URLs. The statusline has emitted unconditional OSC 8 to shell
prompts since #199 in January, on that same reasoning.
- `supports-hyperlinks` is an allowlist, so its dominant error is the
false negative. xterm, foot, mintty, rio, contour and a configured tmux
all render OSC 8 and none are named. The probe's certain cost is
withdrawing a working link from those users, against a speculative and
shrinking risk.
- The escape-hatch argument I had for it was inert. Once the probe no
longer changed any text, `FORCE_HYPERLINK=0` could not reproduce what
the old hardcoded Claude Code gate did, which was print the URL in full
and copyable during that regression window.

So the answer to "shouldn't we detect?" is that `wt list` detects
because there the answer changes what the reader gets:
`estimate_url_width` reserves a column wide enough for the full URL, so
an unlinked cell can print it. The statusline reserves nothing, so it
has no second rendering to switch to. `format_url_cell` now says that,
in place of a stale note claiming the statusline's stdout was a pipe "to
its editor".

## Also here

- `isolate_subprocess_env` scrubs `FORCE_HYPERLINK`. It stands alone:
`wt list` probes with `on(stream)`, which is `(FORCE_HYPERLINK set ||
tty) && allowlist`, and tests pin `TERM=alacritty`, so a developer with
`FORCE_HYPERLINK=1` exported flips the `wt list` table snapshots from
full URL to linked `:port`.
- The `table` and `claude-code` format lists in `wt list statusline
--help` name the `url` segment, which they emit and never listed.
- A `--format=claude-code` test pins where the URL segment lands among
the directory, CI and model segments.

## Verification

Full gate green (`cargo run -- hook pre-merge --yes`, 4486 tests).
Beyond the suite, the built binary rendering a live and a dead port, and
the drop threshold measured at six widths to confirm the segment behaves
as before.

## Known, not addressed

A `[list] url` with no port is permanently dim on both surfaces:
`UrlStatusTask` only connects when a port parses, so `url_active` stays
`None`, and the `== Some(true)` test reads "never checked" as "nothing
listening". Pre-existing in `render.rs`, which this change doesn't
touch.

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

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:38:10 -07:00
Maximilian Roos 14d4d92532 feat(statusline): make the CI and URL segments clickable in Claude Code (#3550)
The statusline suppressed OSC 8 hyperlinks in Claude Code mode, so its
CI segment was colored but not clickable and its dev-server URL printed
in full. Claude Code renders OSC 8, so both segments now link the way
they already do in `wt list`.

```
 ~/w/worktrunk.statusline-osc8-hyperlinks  ↕|🤖  ↑2 ↓1  ^+93 -24  #3550  :17913  Opus 4.8
                                                                   └link┘  └link┘
```

## Why it was off

The gate dated from 2026-01-02, inside Claude Code's OSC 8 regression
window — links worked through 2.0.76, broke around 2.1.3, and got a
partial IDE-terminal-only fix in 2.1.42
([anthropics/claude-code#26356](https://github.com/anthropics/claude-code/issues/26356)).
The premise was correct when written and has since expired; that issue
was auto-closed for inactivity rather than on a fix, so the tracker
understates current support.

## Verification

PTY-captured the raw byte stream from Claude Code 2.1.218, with
`TERM_PROGRAM`/`VSCODE_*` stripped so it exercises the
standalone-terminal path that was reported broken. Claude Code doesn't
strip or blindly pass through — it parses OSC 8 into its frame model,
assigns a hyperlink id, and re-emits canonically (normalizing an ST
terminator to BEL):

```
PRE ESC]8;id=1l7bqdh;https://example.com/ST-PROBE BEL STLINKTEXT ESC]8;; BEL  MID …
```

It holds in both the normal and alt-screen (`CLAUDE_CODE_NO_FLICKER=1`)
render paths, with `FORCE_HYPERLINK` unset. Driving the real `wt` binary
through Claude Code end to end yields both links live:

```
LINK: https://github.com/max-sixty/worktrunk/pull/3550
LINK: http://127.0.0.1:17913
```

Degradation is graceful: a terminal or multiplexer that drops OSC 8
shows the same text, just not clickable (tmux only gained OSC 8 in 3.4;
zellij and Alacritty support it).

## Shape of the change

With links unconditional for the statusline, the plumbed `include_links`
flag had one value, so it collapses into the segment builder.
`format_url_cell` likewise takes the link decision from its caller
rather than probing the terminal, matching `PrStatus::format_cell` — the
statusline's stdout is a pipe, so `supports_hyperlinks` reports false
there even though the consumer renders OSC 8. That left
`hyperlink_stdout` with no callers, so it goes.

`format_cell` keeps its `include_link` parameter: `wt list` passes the
terminal probe, and the picker passes `false` because a `--prs` row
never reaches the strip path.

`format_url_cell` moved next to `estimate_url_width`, which budgets the
column against it — the two have to agree on when a cell collapses to
`:port` and were in separate files.

The URL segment also gets shorter: the URL rides inside the escape
sequence, so `http://127.0.0.1:17913` becomes `:17913`, returning 16
columns on a line that budgets by width.

## Safety of the truncation interaction

`truncate_visible` ends its cut with `\e[0m`, which resets colour but
leaves an OSC 8 link *open* — a severed link would make the rest of the
terminal line clickable, and `ansi_cut` really will sever one if
reached.

It can't be reached, because the two cuts never meet: `fit_to_width`
drops whole segments worst-priority-first and stops at one, so character
truncation only ever lands on a best-priority survivor — Directory (0),
Branch or Model (1) — none of which carry escapes beyond SGR. Every
link-bearing segment is strictly worse (CI 5, URL 9), so each is dropped
entire first.

Reviewing the branch turned up that the numbered comments in
`format_statusline_segments` had drifted from `COLUMN_SPECS` — CI was
labelled 9 (it is 5) and the URL 8 (it is 9), with branch-diff and
upstream also off — and the first version of the test had taken those
stale numbers as its specification. The comments are corrected and the
test now rests on the invariant above, which doesn't depend on where CI
sits. It sweeps widths 1–90 over both links, asserts it spans every drop
stage, and pins that the URL goes before CI.

A second test pins that the hidden URL costs no visible width
(`ansi_strip` drops OSC 8 for both terminators), so priority budgeting
isn't inflated.

## Docs

The Claude Code statusline page now says the segments are clickable —
it's the feature's own page and said nothing about it. The `wt list
--help` JSON field description gains the links but stays short: an
earlier, longer wording shrank the help table's Field column and wrapped
two dozen unrelated rows.

## One judgement call worth flagging

`format_statusline_segments` also feeds plain `wt list statusline`
(shell prompts) and the JSON `statusline` field. The CI link was already
unconditional on both before this change; what's new is that the URL
cell renders as a linked `:3000` rather than the full URL, so a consumer
that strips OSC 8 sees only `:3000`. The structured `url` /
`dev_server.url` field still carries the full URL, and `:3000` still
answers "which port", so this reads as the right trade — but it is the
one place the collapse to a constant reaches a renderer that isn't
Claude Code.

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

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:20:01 -07:00
Worktrunk Bot 512a94de2f feat(plugin): ship Codex-native activity hooks (#3364) 2026-07-06 23:05:15 -07:00
Maximilian Roos 830cc850dd feat(list): show the main…± column by default; --full gates only off-machine columns (#3236)
Move the `main…±` branch-diff column (line diffs since the merge-base) into the default `wt list` view — it's pure local git backed by a persistent content-addressed cache, so the original blocking-walk concern no longer applies. `--full` now gates only the two off-machine columns: CI status (network) and LLM branch summaries. The interactive picker (`wt switch`) follows suit and is effectively `wt list --full`; on narrow terminals with the preview shown, CI clips past the split and alt-p reveals it.

Also adds a `.typos.toml` ignore rule for truncated word fragments glued to the … ellipsis, so the narrower Message column's truncated quickstart embed doesn't get spell-"corrected" by pre-commit.ci.
2026-06-25 01:49:47 -07:00
Maximilian Roos 2f0ee196e6 statusline: color the rate-limit pace notice by expected-lockout severity (#3229)
The statusline pace segment (the rate-limit warning, e.g.
`1.4×(10am–3pm)`) was a flat yellow whenever it showed. This grades its
color by how bad the projected hit actually is, deepening dim →
dim-yellow → yellow.

## Two axes, separated

The segment already gated its appearance on **confidence** — `P(over)`,
the Bayesian probability of crossing the cap before the window resets.
But `P(over)` saturates near 1, so it can't also grade *severity*: a
near-certain cross in the last hour and one halfway through the week
both read ≈1.

Color is now driven by a second metric, **`expected_lockout`**: the
expected fraction of the window that will be spent throttled, integrated
over the posterior on the consumption rate (7-point Gauss–Hermite). It
keeps discriminating after `P(over)` has flat-lined, and degrades to ~0
on thin early-window bursts — so a 5× burst on Monday doesn't light up
the segment. The displayed number stays the raw `u/t` pace; only the
color carries the Bayesian severity.

## Per-window thresholds from duration, not magic numbers

The same lockout *fraction* costs very different real time across
windows: 0.30 of the 5-hour window is 90 min, 0.30 of the weekly window
is ~2 days. So `colorize_pace` compares a **duration-weighted cost** `=
lockout · √(window / 5h)` against one shared cutoff pair, rather than
two hand-tuned per-window pairs. The 5-hour window keeps its existing
calibration (weight 1); the weekly window is sensitized ~5.8×,
escalating on real time forfeited. Duration drives this (known exactly,
33.6× apart) — not σ, which is only 1.17× apart and already folded into
the posterior integral.

## Reviewing

Start at `src/commands/statusline.rs`: `expected_lockout` (the severity
metric + its module-header rationale), `lockout_cost` (the √-duration
weighting), and `colorize_pace`. The `-vv` trace now logs `lockout` and
the weighted `cost` for future threshold tuning.

Well-covered: the metric (bounds/monotonicity), the color tiers, the
cross-window weighting, and the refactor's behavioral equivalence of
`p_over` are all unit-tested; the 5-hour window is unchanged (snapshots
confirm). The threshold anchors (dim-yellow 0.10, yellow 0.30, exponent
0.5) are feel-calibrated and meant to be tuned from real traces.

## Note: included an unrelated CI fix

Merging main surfaced a pre-existing failure in `test (linux)`:
`switch_picker_alt_l_ignored_list.snap` was left stale by the #3226
merge race (it still rendered the old `Age` column while every sibling
picker snapshot already shows `Remote⇅`). Main pushes don't run the full
suite, so it slipped in and blocks any PR that merges with main. This PR
regenerates that one snapshot — it's unrelated to the statusline change
but required to get CI green.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:57:35 -07:00
Maximilian Roos a5622d1424 refactor(plugin): rework /wt-switch-create cross-repo handling around reachability (#3118)
The `/wt-switch-create` skill could create a worktree in another repo
but then leave the session unable to work in it cleanly — a real session
hit that and read it as a failure. This reworks the cross-repo handling
around what the harness actually does, verified live and against the
Claude Code 2.1.177 binary.

The model: `EnterWorktree` re-roots within the repo the cwd is in, and a
session reaches another repo's worktree whenever that repo is in
`permissions.additionalDirectories` (via `cd`). So the skill now creates
the worktree (`wt -C`), tries `EnterWorktree`, and on a cross-repo
rejection `cd`-tests reachability — working in place if it sticks, or
escalating (ask the user to add the repo, or a parent like
`~/workspace`, to `additionalDirectories`) when it doesn't. The
procedure dropped from five steps to three and no longer hands off to a
separate session.

`skills/wt-switch-create/rationale.md` now carries the harness-mechanics
reference inline (M1 `cd` / M2 `EnterWorktree`, how they compose, the
master gate), consolidated from a separate doc.
`docs/content/claude-code.md` is trimmed back to a two-sentence overview
— the mechanism detail belongs in the skill, not the website page.

Skill- and docs-only; `test_docs_are_in_sync` and
`test_plugin_layout_is_consolidated` pass.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:50:21 -07:00
Maximilian Roos fd3ae5bff4 feat(plugin): optional branch name and single-route flow for /wt-switch-create (#3058)
The `/wt-switch-create` branch argument becomes optional (a name is
picked from the task when omitted), and the skill's workflow is rebuilt
from a guard-heavy three-route flow into one route plus one error-driven
fallback.

**The new flow**: pick a branch name if none was given, create the
worktree with `wt -C <repo> switch --create <branch> --no-cd
--format=json` in Bash (rerunning without `--create` when the user named
an existing branch), re-root the session with `EnterWorktree({path})`,
and on a graceful rejection (different repo, pinned cwd, nested session)
work in the worktree via absolute paths instead.

**Why the rewrite**: the old flow's guards encoded hypotheses about
Claude Code that turned out wrong or untested. The load-bearing
discoveries, each verified by binary inspection, official docs, or live
tests and documented in the new `skills/wt-switch-create/rationale.md`:
`EnterWorktree({path})` accepts worktrunk's sibling layout on first
entry; every rejection it can raise is side-effect-free, so
try-then-fallback replaces prediction; the previous
`EnterWorktree({name})`/hook route silently auto-removes a clean
worktree at session exit, which is wrong for durable worktrunk worktrees
(path-entered worktrees persist); and the "pinned cwd" the old guards
defended against was a misdiagnosis of the harness resetting `cd` that
leaves the session's working directories.

**Hook fix**: both `WorktreeCreate` and `WorktreeRemove` pipelines are
wrapped in `bash -c 'set -o pipefail; ...'`. Without it the trailing
`jq` exits 0 on empty input, so a `wt` failure surfaced as a
"successful" hook with an empty path. The wrapper is explicit `bash`
because hooks spawn via `/bin/sh -c`, where dash rejects `set -o
pipefail` fatally. `xargs -I{}` also fixes worktree paths containing
spaces. Tested end-to-end in both directions (success exits 0 with the
path; failure exits 1 with empty stdout).

Docs, README, and plugin CLAUDE.md are aligned with the new mechanism.
The agent-isolation use of the `WorktreeCreate` hook is unchanged.

Review trail: the design survived an evidence audit (every rationale
claim independently re-verified), a code review (bare directory-named
tokens no longer parse as the repo; mid-session moves now stash
uncommitted work across), and a level check that considered and rejected
fixing this in `wt` itself (an idempotent `--create` cannot encode who
chose the branch name, which determines the correct recovery).

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:01:13 -07:00
Maximilian Roos 8f44e98636 feat(statusline): show used % instead of pace above 90% of the rate limit (#3057)
When the statusline rate-limit segment shows and the binding window is
above 90% used, it now displays the used percentage instead of the pace
ratio — `95%(8:30am–1:30pm)` rather than `1.1×(8:30am–1:30pm)`. Near the
cap, how much is left matters more than how fast it's going; the pace
form is unchanged below 90%.

The show condition is untouched (P(over) ≥ 0.5 via the existing Bayesian
gate), so this only changes which number renders once the segment is
already visible. Strictly greater-than 90: a window at exactly 90% still
shows pace. Unit test pins the switch and the boundary; the 95%- and
100%-used integration snapshots now render the percent form, and the
help text and claude-code docs describe it.

🤖 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 22:57:53 -07:00
Maximilian Roos ce7c7c4861 fix(statusline): drop the word "pace" from the rate-limit segment (#3053)
The Claude Code statusline's rate-limit segment read `2.9×pace(Tue–Tue
5pm)`. The word "pace" made the longest segment longer without earning
the space, so the segment now renders as `2.9×(Tue–Tue 5pm)` and `wt
list statusline --help` explains the reading instead: 2.9× the pace that
would exactly fill the window, plus a one-line gesture at why the
segment doesn't show on every over-pace reading ("Likely" is a Bayesian
forecast; early-window bursts don't trigger it).

While there, the help page's structure had drifted: the `claude-code`
bullet under "Output formats" and the "Claude Code mode" section both
described the stdin context and the added segments. The segment list now
lives in the bullet (parallel with the `table` bullet) and the section
carries the input fields and the pace explanation. Ranges use en-dashes
(`0–100`), and the hard-wrapped paragraphs are unwrapped to match
`after_long_help` convention elsewhere in the CLI.

`docs/content/claude-code.md` and its skill mirror drop the word from
the example line and segment description. CHANGELOG.md keeps the old
spelling as a historical record.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:29:34 -07:00
Maximilian Roos c511d3fa2b feat(list): show PR/MR number in the CI column (#3041)
The CI column in `wt list --full` (and the statusline) previously showed
a single colored dot. It now shows the branch's open PR/MR reference —
`#3035` on GitHub/Gitea/Azure DevOps, `!3035` on GitLab — colored by CI
status, dimmed when stale, and hyperlinked to the PR. When no number is
available (branch workflows without a PR/MR, pre-number cache entries,
or a number wider than the allocated column), the cell shows a bare `#`
in the same colors. Fetch errors always render `⚠`, even when a number
is known — Error and Conflicts share yellow, so a yellow `#3035` would
read as a conflicted PR.

The branch merges main's review-state feature (#3044): review colors
(magenta/cyan) and draft dimming apply to the number cells exactly as
they did to the dot, and the `--help` legend shows colored `#` samples
for all seven states (the interim version had dropped the colored
samples from the legend entirely).

## The width problem

`wt list` renders skeleton-first: column widths are fixed before any CI
data arrives, and the table never resizes mid-render. The PR number's
width therefore has to be known up front. The solution is a repo-level
ratchet cache (`.git/wt/cache/pr-number/max.json`) holding the largest
PR number any fetch has seen — PR numbers are monotonic per repo, so the
value needs no invalidation. Pre-skeleton, `collect` reads that one file
and sizes the column exactly; on a cold cache the estimate is 5 chars
(`#9999`). A number that outgrows the estimate renders as the bare `#`
for that run and sizes correctly on the next run once the ratchet
records it. The ratchet is deliberately separate from the per-branch
`ci-status/` entries so the width hint isn't coupled to branch-entry
retention, and `detect` re-ratchets on cache hits too, so a deleted or
racily regressed `max.json` heals from locally cached numbers instead of
waiting out the TTL.

## Reviewer's map

- `src/commands/list/ci_status/mod.rs` — `PrRef` (number + forge sigil,
`PrRef::pr`/`PrRef::mr` constructors), `PrStatus.number` (serde-default
so pre-existing cache entries still deserialize, rendering `#` until
their 30–60s TTL expires), `format_cell` width-aware renderer with the
Error guard, ratchet in `detect` (both cache-hit and fetch paths)
- `src/commands/list/ci_status/cache.rs` — `MaxPrNumber` ratchet
(read/ratchet/clear)
- `src/commands/list/ci_status/{github,gitlab,gitea,azure}.rs` — each
fetcher populates the number (`gh --json number`, `iid`, Gitea `number`,
`pullRequestId`); GitLab's mr-view-failure path carries the
iid/URL/review state into the error status so the `⚠` stays clickable
- `src/commands/list/layout.rs`, `collect/mod.rs` — width estimate
threading
- `src/commands/list/render.rs`, `model/item.rs` — table cell and
statusline both go through `format_cell`
- `src/commands/list/json_output.rs` — `ci.number` field
- `src/commands/config/state.rs` — ratchet shown by `state get`/`cache
get` (table + JSON) and swept with the CI cache category, including the
deprecated `ci-status clear --all` path
- `src/md_help.rs`, `src/help.rs` — legend colorization rules rewritten
from `●` to `#` (terminal + website)

Most of the diff is snapshot churn from the column width and glyph
changes plus regenerated docs mirrors.

Known trade-offs: concurrent statusline ratchet writes can transiently
lose an update (monotonic, re-learns on the next render, documented at
the write site); one anomalously high PR number widens the column until
`wt config state cache clear`; an open Azure DevOps PR still shows gray
`NoCI` instead of its pipeline status — a pre-existing gap, now marked
`TODO(azure-pr-pipeline)`.

Testing: unit tests for `format_cell` (including the Error-with-number
and oversized-number link cases)/`pr_ref_width`/ratchet/width estimates;
integration coverage for all four forges with real numbers (the Gitea
mocks now exercise the number path too), review-state × number
composition, the GitLab mr-view-failure `⚠` and branch-pipeline success
paths, cache-TTL expiry → refetch, the statusline number view, and the
`wt config state` surfaces.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:36:44 -07:00
Maximilian Roos 51e32f4425 fix(statusline): space the context gauge emoji from its percent (#2944)
The context gauge in the Claude Code statusline rendered the moon-phase
emoji flush against the percent (`🌕42%`). Most terminals draw emoji as
double-width and bleed the glyph into the cell to its right, so the moon
visually collided with the digits. This adds a trailing space — `🌕 42%`.

The flush-packing convention still holds for ASCII and narrow markers
(`@+1`, `↓1`, `●`); emoji are the exception. That distinction is now
documented in the `writing-user-outputs` skill so future output keeps it
consistent.

Tests, the three inline statusline snapshots, nine rate-limit `.snap`
files, and the hand-authored docs example (auto-synced to the skill
reference) are all updated.

> _This was written by Claude Code on behalf of max_

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:40:18 -07:00
Maximilian Roos 3be40ad36f docs: writing-prose cleanup (claude-code, hook, llm-commits, tips-patterns) (#2922)
Targeted writing-prose cleanup across four docs pages. Per-page summary:

**`claude-code.md`** — dropped the duplicate skill definition (the lead
paragraph already covered it) and rewrote the `## Configuration skill`
opener as "With the `/worktrunk` skill, the agent can help with:" so the
section stands without depending on its heading.

**`hook.md`** (edits in the `Hook` command's `after_long_help` in
`src/cli/mod.rs`) — four small fixes:
- Dropped "As usual, post-* hooks run in the background" (already
established earlier on the page).
- Split the perspective+cwd run-on paragraph; the three `cwd ≠
worktree_path` cases (`pre-switch`, `post-remove`, `post-merge` with
removal) are now a bulleted list.
- Folded the semicolon-spliced conditional-variables enumeration into
the existing template-variables table descriptions (added "switch/create
only" to `base`, "when target has a worktree" to `target_worktree_path`,
hook-type qualifiers to `pr_number`/`pr_url`). The follow-on guidance
about undefined variables and conditionals/defaults stays.
- Trimmed the redundant `sanitize` sentence from the filters paragraph
(the table above already says it).

The two hook-types tables (event×pre/post matrix + per-hook purpose) are
intentionally kept — they're complementary, not duplicate.

**`llm-commits.md`** — dropped the "How it works" stub that mostly
restated the lead, and the "There are sensible defaults, but templates
are fully customizable" hedge.

**`tips-patterns.md`** — three trims:
- The `wt step tether` recipe's middle "This matters because…" sentence
(covered by tether's own docs).
- The ports-deterministic line tightened to lean on the concrete example
rather than restate the abstract claim.
- "in real-time" filler dropped from the Monitor hook logs section.

Auto-synced skill mirrors (`skills/worktrunk/reference/*.md`) carry the
same edits.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:49:54 -07:00
Maximilian Roos e22ed71282 feat(statusline): add Claude Code rate-limit pace segment (#2899)
## Motivation

Claude Code exposes
`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}` on its
statusLine JSON. Without UI for it, users only learn they're over-pacing
once Claude throttles them mid-task. We want a single-glance warning
that surfaces *before* throttling, and only when continuing at the
current pace would actually blow the cap — not whenever raw `u/t > 1`,
which fires constantly on early-window bursts.

## Change

A new yellow segment `1.3×pace(10am–3pm)` appears in the
`--format=claude-code` statusline when either rate-limit window is
meaningfully likely to be hit before reset. For the weekly window it
formats as `1.9×pace(Mon–Mon 3pm)`. Only the worse-projected of the two
windows is shown; the segment is hidden entirely when both are safe.

The trigger is a Bayesian forecast on `P(final ≥ 100%)`: cumulative
consumption modelled as Brownian-with-drift, prior on the long-run rate
pulled toward "most windows finish under" (`m₀ = 0.8`), posterior
predictive Gaussian. Segment appears only when `P > 50%`. Early-window
noise (e.g., 5% used at 3% elapsed → naive `u/t = 1.67` but P(over) =
42%) stays correctly hidden.

The *displayed* number is the naive `u/t` ratio (one decimal), not the
Bayesian posterior — the user sees an honest measurement of "what you've
actually done over the elapsed window," while the smoothing stays
internal where it decides visibility. Window bounds in parentheses
locate which 5h or weekly window the pace was measured over.

Adjacent delimiter cleanup in the same file, so the new segment fits the
existing line:

- Drop the vestigial `| ` prefix on the model segment — the two-space
inter-segment separator already does the job
- Drop the space inside `🌕 42%` → `🌕42%`, matching the other prefix-char
segments (`@+1`, `↓1`, `?^|`)
- En-dash inside the parenthetical (`10am–3pm`, not `10am-3pm`) —
typographically correct for a range, visually distinct from the ASCII
`-3` used for line deletions

## Tests

Eight file snapshot tests cover hidden/well-paced, 5h binding, the `:mm`
time branch, 5h at-limit edge, 7d binding (day+time form),
both-windows-pick-worse, dirty-worktree combined state, and narrow-width
segment drop. Time and timezone are pinned via the existing
`WORKTRUNK_TEST_EPOCH` env hook plus a per-subprocess `TZ=UTC`.

23 new unit tests: erf reference-value accuracy, standard-normal CDF,
`p_over` boundaries + monotonicity + six Python-prototype calibration
points, `format_clock`, `format_window_bounds` for both window kinds,
`select_binding_window` selection logic across five scenarios,
`format_rate_limit_segment` shape, and `parse_rate_limits` JSON parsing
across four cases.
2026-05-25 18:22:04 -07:00
Worktrunk Bot ec62580c29 revert(hooks): keep docs on pre-start/post-start; code accepts both (#2857)
Per @max-sixty's [direction in
#2838](https://github.com/max-sixty/worktrunk/issues/2838#issuecomment-4509447593):
revert the docs portion of #2840 and keep the code. Docs continue to
recommend `pre-start`/`post-start`; both names work in code so anyone
who already followed the briefly-changed docs (e.g. @EcksDy) isn't
stranded once a release ships these aliases.

## User-visible — back to `pre-start`/`post-start`

- README, docs site, skill mirrors, `dev/*.example.toml`,
`plugins/worktrunk/README.md`, `flake.nix`, `.config/wt.toml`
- `src/cli/mod.rs` / `src/cli/config.rs` / `src/cli/step.rs` /
`src/help.rs` after_long_help and example snippets — and the auto-synced
`docs/content/` and `skills/worktrunk/reference/` mirrors
- `wt hook --help` canonical subcommand names; completion advertises
`-start` only
- `HookType` Display via strum, serde `rename`, and clap `ValueEnum`
name — all `pre-start`/`post-start`. The Rust variant identifiers stay
`PreCreate`/`PostCreate` (internal; we already paid for that rename in
#2840, and now the eventual flip is a Display-only change)
- `HooksConfig` serde canonical fields

## `*-create` still works (kept code)

- `wt hook pre-create` / `post-create` — CLI alias on the canonical
subcommand
- `pre-create` / `post-create` in config: top-level, `[hooks.*]`, and
per-project, in string, `[table]`, and `[[array-of-tables]]` form.
Mechanism: serde `alias = ...` on the field, plus a silent in-memory
rename in `migrate_content()` so the round-trip in `unknown_tree`
doesn't flag table forms as schema-unknown.
- The pre-0.32.0 `post-create` fatal-load-error machinery stays removed
— the name is reclaimed, and both forms load without error.

## Smaller bits

- `valid_user_config_keys()` / `valid_project_config_keys()` append
`pre-create` / `post-create` so the unknown-field round-trip skips them.
`test_valid_*_keys_all_deserialize` skips both aliases (they can't sit
alongside the canonical without a duplicate-field error).
- `DEPRECATED_SECTION_KEYS` drops the `pre-start`/`post-start` entries
#2840 added — `pre-start`/`post-start` are canonical again.
- `find_pre_start_from_doc` / `find_post_start_from_doc` /
`find_renamed_hook_key` / `is_non_empty_item` /
`migrate_start_hooks_doc` and their tests are removed; the migration
direction flips via a new `migrate_create_hooks_doc` (silent, mirrors
the prior shape).
- Test files `e2e_shell_post_create.rs` and `post_create_commands.rs`
rename back to `_post_start_` (via `git mv`, so the rename shows as a
rename).

## Testing

`cargo run -- hook pre-merge --yes` — 3806 tests pass; the 10 failures
are all `case_4` of `shell_wrapper::unix_tests::*` (nu-shell case; `nu`
isn't installed in this runner; same failures occur on `main`).

Also manually verified that a fresh `wt switch --create` against a
project config with `[post-create]` loads cleanly with no unknown-field
warning and the hook fires as `post-start`.

## Follow-up

Per @max-sixty: in a couple of weeks, once a release with
both-names-work is out and users have had a chance to upgrade, the docs
flip is straightforward (most of it is in `src/cli/mod.rs`'s
`after_long_help` and the doc-sync test propagates).

Re #2838.

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:03:03 +00:00
Maximilian Roos d7e3f88422 feat(hooks): rename worktree-creation hooks to pre-create/post-create (#2840)
Phase 1 of the staged hook rename tracked in #2838: the worktree-creation hooks `pre-start`/`post-start` become `pre-create`/`post-create`. The old names keep working with no deprecation warning yet (Phase 2, months out, adds the warning).

## What changes

- `pre-create`/`post-create` are canonical everywhere: the `HookType` enum, the `HooksConfig` serde fields, the `wt hook` CLI, completion, and all docs.
- Old names keep working: `migrate_content()` rewrites `pre-start`/`post-start` config keys to `-create` before serde, and `parse_hook_type` accepts the old CLI names as silent aliases. `wt config update` rewrites them on disk; `wt config show` shows the migration diff. `wt hook <type>` execution and `wt hook show` both accept the old names; completion and `--help` advertise only the canonical names.
- `detect_deprecations()` flags the old keys so `update`/`show` act on them, but `format_deprecation_warnings()` stays silent (Phase 2 adds the warning). A new empty-warnings guard in `check_and_migrate` keeps a `-start`-only config from emitting a stray hint.
- The dead pre-0.32.0 `post-create` machinery is removed: the fatal `POST_CREATE_REMOVED_MSG` load error, the vestigial `HooksConfig.post_create` merge-fold, and `find_post_create_from_doc`. `post-create` is reclaimed as the canonical background creation hook.

## Semantic flip

Before v0.32.0, the key `post-create` named a *blocking* hook. It now names the *background* one.

Since v0.44.0, a pre-0.32.0 `post-create` config is a fatal load error on the `check_and_migrate` paths: `ProjectConfig::load` and user/system config loading, which fire on essentially every `wt` command. A repo carrying one has been unusable ever since. The one path that skips that check is `project_config_at_ref` (the base-ref read behind `wt switch --create`), which applies only structural migration. A pre-0.32.0 `post-create` surviving solely on a base ref, never checked out into a worktree, would now load as a background hook rather than folding into the blocking `pre-start`. That edge case is accepted: once `post-create` is valid again, reclaiming the name and detecting the dead key are mutually exclusive.

## Reviewing this diff

205 files, but the substance is ~36 files under `src/`. The rest is regenerated snapshots and auto-synced doc mirrors. Start with:

- `src/config/deprecation.rs` — detection (`find_renamed_hook_key`), migration (`rename_hook_key`), removal of the fatal block, the empty-warnings guard, and the `DEPRECATED_SECTION_KEYS` entries that stop unknown-field detection from flagging the migrated keys.
- `src/config/hooks.rs`, `src/git/mod.rs` — the serde field and enum renames.
- `src/config/project.rs` — `ProjectConfig::load` deserializes `check_and_migrate`'s migrated content, so a current-worktree config using the old keys loads into the canonical fields.
- `src/cli/hook.rs`, `src/commands/hook_commands.rs`, `src/completion.rs`, `src/main.rs` — the CLI alias layer; `wt hook show` accepts the old type names as hidden value-parser aliases.
- `src/cli/mod.rs` — the `wt hook` docs, including the soft-deprecation note linking #2838.

The ~93 modified snapshots also pick up deterministic env-block lines (`GIT_*: ""`, `LLVM_PROFILE_FILE`) that pre-existing snapshots already carry. That is stale-snapshot drift surfaced by the regeneration, not a behavior change.

## Testing

Full suite green (3799 tests). New coverage: `snapshot_migrate_start_to_create` (migration preserves value shape and position), `test_deprecated_start_hook_key_runs_silently` and `test_standalone_hook_start_alias_runs_silently` (old config and CLI names run with no warning), `test_config_show_displays_start_hook_migration` (`config show` reveals the diff without an "unknown field" warning), and `test_hook_show_accepts_deprecated_start_hooks` (a current-worktree config using the old keys loads, and `wt hook show` takes both the canonical and the deprecated type arguments).

Part of #2838.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-20 19:31:50 -07:00
Maximilian Roos 5b2a41ee73 feat(plugin): detect Gemini extension; document OpenCode and Gemini install (#2819)
Closing two gaps a prior plugin-implementation review surfaced: Gemini's
native install path was invisible (no `wt config show` section, no
user-facing docs), and OpenCode — which has the most substantial
implementation of the four — was absent from the plugins page despite
being the only tool that ships activity tracking via a worktrunk-written
plugin.

`wt config show` now renders a `GEMINI CLI` section gated on
`which::which("gemini")`. Installed state is detected by parsing
`~/.gemini/extensions/worktrunk/gemini-extension.json` and checking
`name == "worktrunk"` — exactly where `gemini extensions install` clones
the extension. Fail-closed on a missing home, unreadable file, or
invalid JSON, matching the existing claude/codex/opencode detection.
Test infrastructure (`gemini_installed` field on `TestRepo`,
`setup_mock_gemini_installed`, `setup_gemini_extension_installed`) and
the two new snapshot tests mirror the OpenCode equivalents
line-for-line.

`docs/content/claude-code.md` retitled "Agent Integration" and
restructured around a four-tool capability table covering configuration
skill, activity tracking, worktree isolation, and `/wt-switch-create`.
OpenCode and Gemini get install subsections; the activity-tracking
section is generalized from Claude-only to Claude+OpenCode+Gemini so the
heading matches the table. A Codex-only sentence under "Worktree
isolation" was redundant with the table and dropped.

## Verified end-to-end

After **v0.52.0** went `Latest`, `gemini extensions install
https://github.com/max-sixty/worktrunk` resolves via `/releases/latest`
→ the v0.52.0 source tarball (which now carries the root
`gemini-extension.json` from #2807) → install succeeds. `gemini
extensions list` shows `Type: github-release`, `Release tag: v0.52.0`,
with both skills (`wt-switch-create`, `worktrunk`) loaded via
`${extensionPath}/skills/`. Live `wt config show` reports `✓ Extension
installed`. The bare URL is what the docs use — `owner/repo` shorthand
returns "Install source not found" against gemini-cli, so the full URL
is the right form.

Per the `release` and detection paths in gemini-cli 0.42
(`bundle/chunk-CHERUG6W.js` lines 44170–44290 / 44640–44675), the URL
install always tries `releases/latest` first when releases exist, and
only auto-falls-back to `git clone` if there's no release data at all —
so the work landed in #2807 on `main` only became user-visible once
v0.52.0 was cut. This PR ships the detection and docs that have now been
confirmed accurate against the live release path.

Pre-merge gate green locally (3747 tests, 0 skipped; pre-commit clean;
`test_docs_are_in_sync` green so the skill reference and `llms.txt` are
in sync).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 18:33:05 -07:00
Maximilian Roos 233cd493d0 fix(codex): drop activity-marker hooks Codex can't drive (#2786)
The Codex plugin shipped `hooks.json` with `SessionStart`→💬,
`UserPromptSubmit`→🤖, and `Stop`→💬. But codex-cli 0.130.0's
`HookEventNameWire` schema (extracted from the binary) has **no
`Stop`/turn-end event**: `PreToolUse`, `PermissionRequest`,
`PostToolUse`, `PreCompact`, `PostCompact`, `SessionStart`,
`UserPromptSubmit`. So on Codex the 🤖 marker set at `UserPromptSubmit`
could never return to 💬 — a Codex worktree stuck at "working" for the
entire session. That's worse than the docs claimed ("rests at 💬").

This removes the marker hooks entirely until Codex exposes a turn-end
hook event. The Codex plugin now ships only the configuration skill;
activity tracking joins worktree-isolation and `/wt-switch-create` as
Claude-Code-only. A re-enablement comment (exact conditions + the files
to restore) lives in `src/commands/config/codex.rs` and a new
`CLAUDE.md` → "Codex Plugin" section, which also documents the accepted
tradeoff that the `skills/` symlink exposes the Claude-only
`wt-switch-create` skill to Codex (harmless — Codex can't act on it).

Bundled adjacent fixes: `plugin.json` metadata drift
(`description`/`longDescription` no longer claim activity hooks;
`homepage`/`websiteURL` `https://worktrunk.dev/claude-code/` →
`https://worktrunk.dev`), and the install/uninstall/`--help`/docs text
made honest about Codex having only the configuration skill.

**Reviewer navigation:** `src/commands/config/codex.rs` +
`src/cli/config.rs` (runtime + `--help` text),
`docs/content/claude-code.md` (skill ref + `llms.txt` auto-synced),
`CLAUDE.md` (new section), `tests/integration_tests/config_show.rs`
(`test_codex_plugin_metadata_is_valid_json` now asserts `hooks` absent +
guards metadata regression). Snapshot env-block churn is stale-fixture
convergence to the repo's current `/nonexistent/wt/` convention, not a
leak.

**Testing:** Full pre-merge gate green locally (3704 tests, 0 skipped;
lints clean). The metadata test was strengthened; the marker behavior
itself is removed, so there's nothing runtime-testable to add.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:29:54 -07:00
Maximilian Roos f00b787e1b fix(codex): relocate plugin into subdir so the marketplace resolves it (#2782)
## Problem

#2780 put `.agents/plugins/marketplace.json` at the path codex-cli
reads, but the plugin was still **uninstallable**. Driving the Codex
`/plugins` TUI surfaced the real, complete set of constraints in
codex-cli 0.130.0:

1. A plugin `source` must be the **object** form
`{"source":"local","path":"./plugins/<name>"}` — a bare string `"./"`
fails with `local plugin source path must not be empty`.
2. The path must be a **non-empty subdirectory**. Codex resolves it
relative to the marketplace root (repo root), so a repo-root plugin
(`./.codex-plugin/` at the top) can't be referenced at all — every Codex
marketplace plugin lives in
`./plugins/<name>/.codex-plugin/plugin.json`, exactly like the
OpenAI-curated marketplace.

So the plugin had to move into a subdirectory.

## Changes

- Move `.codex-plugin/` → `plugins/worktrunk/.codex-plugin/` and
`hooks/` → `plugins/worktrunk/hooks/` (a plugin-root sibling, matching
the curated layout where `skills/` and `assets/` sit beside
`.codex-plugin/`).
- `plugins/worktrunk/skills` → symlink to `../../skills` so the shared
`worktrunk` + `wt-switch-create` skills stay single-source (no
duplication; the repo still auto-syncs `skills/worktrunk/reference/`).
- Rewrite `.agents/plugins/marketplace.json` with the object `source`,
`policy.installation: "AVAILABLE"`, and `category` that Codex actually
parses.
- `plugin.json` `hooks` → `./hooks/hooks.json` (plugin-root relative).
- Update the install hint (`src/commands/config/codex.rs`), docs,
auto-synced skill reference, the validity test, and the install
snapshot.

## Verification

Verified end-to-end against codex-cli 0.130.0: repointed a local
marketplace at the worktree and drove `/plugins`. The `Worktrunk`
marketplace now appears (plugin count 123 → 124), the plugin shows as
**Available** with **both skills resolved** (`worktrunk:worktrunk`,
`worktrunk:wt-switch-create` — the symlink works), and it installs.

`test_codex_plugin_metadata_is_valid_json` now asserts the object
`source`, `policy.installation`, `interface.displayName`, and the new
paths. `test_docs_are_in_sync` and the install snapshot are updated;
full `config_show` module (119 tests) + pre-commit pass.

**Caveat (documented, not a regression):** plugin-bundled *command*
hooks remain gated by Codex's `plugin_hooks` feature flag — already
covered in the troubleshooting docs (`codex features enable
plugin_hooks`). The pre-install plugin detail view shows "No plugin
hooks", consistent with every one of the 123 curated plugins (none
bundle command hooks, so this path has no working precedent to verify
against in-TUI). Marker behavior with `plugin_hooks` enabled is the
remaining thing to confirm in a real installed session.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:29:03 -07:00
Douglas Soares de Andrade 2c675e00f7 Add Codex support (#2512)
## What problem does this solve?

Worktrunk already has a Claude Code integration, but Codex users do not
have an equivalent first-class path for:

- discovering Worktrunk guidance inside the agent environment
- bundling hooks that can show active Codex work in `wt list`
- learning the Codex-specific worktree workflow and its differences from
Claude Code
- installing the integration through a Worktrunk CLI command

This PR adds Codex support while preserving the important semantic
difference between Claude Code and Codex: Claude Code can route
agent-created worktrees through `wt` lifecycle hooks, but Codex
currently needs users/agents to invoke `wt switch --create` and `wt
remove` directly.

## What changed?

### Codex plugin package

- Adds repo-local Codex plugin metadata in `.codex-plugin/plugin.json`.
- Adds a Codex marketplace entry in `.agents/plugins/marketplace.json`.
- Adds `hooks/hooks.json` for bundled activity hooks:
  - `SessionStart` sets the marker to `💬`
  - `UserPromptSubmit` sets the marker to `🤖`
  - `Stop` sets the marker back to `💬`
- Reuses the existing Worktrunk skill/reference docs as the plugin skill
payload.

### CLI integration

- Adds `wt config plugins codex install`.
  - Runs `codex plugin marketplace add max-sixty/worktrunk`.
- Clearly tells the user to open `/plugins` in Codex and install
Worktrunk from the marketplace.
- Adds `wt config plugins codex uninstall`.
  - Removes the Worktrunk marketplace entry.
- Intentionally leaves already-installed plugins and global Codex hook
feature flags unchanged, because users may have configured hook settings
for other hooks.
- Adds a `CODEX` section to `wt config show` when the Codex CLI is
available.
  - It reports Codex CLI availability.
- It avoids claiming the plugin is installed, because the CLI path only
detects the Codex binary.

### Documentation

- Adds a new Codex integration page with installation, bundled activity
hooks, worktree workflow, LLM commit setup, and a Claude Code
comparison.
- Updates README, overview docs, tips/patterns, FAQ, and generated skill
references so Codex is listed alongside Claude Code/OpenCode where
relevant.
- Keeps the Claude Code docs as the place for Claude-only worktree
lifecycle hook behavior.

## Why this shape?

The implementation keeps Codex separate from the existing Claude plugin
command instead of abstracting over both. That is intentional: Claude
installs a plugin directly, while the Codex flow configures a
marketplace and still requires the user to install from `/plugins`. A
shared abstraction would hide those differences and make the user-facing
behavior easier to misread.

The docs also avoid presenting Codex as feature-identical to Claude
Code. Codex gets skills and bundled activity hooks here, but not
automatic worktree lifecycle routing.

## Review guide

- Plugin packaging: `.codex-plugin/plugin.json`,
`.agents/plugins/marketplace.json`, `hooks/hooks.json`
- CLI behavior: `src/commands/config/codex.rs`, `src/cli/config.rs`,
`src/commands/config/show.rs`
- Test helpers and coverage: `src/testing/mod.rs`,
`tests/integration_tests/config_show.rs`,
`tests/integration_tests/help.rs`
- Primary docs: `docs/content/codex.md`
- Generated docs/snapshots: `skills/worktrunk/reference/codex.md`,
config/help snapshots

## Tests

- `cargo fmt --check`
- `cargo check --lib --bins`
- `cargo test --lib --bins`
- `cargo test --test integration test_docs_are_in_sync`
- `cargo test --test integration "test_help"`
- `cargo test --test integration config_show`
- `git diff --check HEAD`

## Local pre-merge note

`cargo run -- hook pre-merge --yes` could not complete locally because
this environment does not have `pre-commit` or `cargo-nextest`
installed. The hook's doctest/doc portions did run and passed.

---------

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-05-16 20:30:32 -07:00
Maximilian Roos 52555982a8 feat(plugin): /wt-switch-create — optional repo arg and -- task delimiter (#2751)
The `/wt-switch-create` skill now accepts `<branch> [<repo>] [--
<task>]`. The optional second token names a different repository to
create the worktree in (instead of the session's current one); the skill
`cd`s into that repo with a `Bash` call before invoking `EnterWorktree`
(which has no repo parameter), then verifies the new worktree landed
under the requested repo as a safety check.

The `--` cleanly separates the task from the rest. Without one, the
parser treats a path-shaped second token (absolute, `~`-relative,
`./`/`../`-relative, or an existing directory) as the repo and the
remaining tokens as the task — anything else after the branch is task
text, so the old shorthand `/wt-switch-create my-branch fix the bug`
keeps working.

Examples in the skill:

```
/wt-switch-create my-feature -- fix the parser bug
/wt-switch-create my-feature ~/workspace/other-repo -- fix the parser bug
/wt-switch-create my-feature
```

Also tidied existing prose: the nesting + different-repo case now stops
rather than silently running the task in the wrong repo, and the scope
note mentions the repo arg.

The follow-up commit updates the public docs page
(`docs/content/claude-code.md`) so the `/wt-switch-create` signature
shown at worktrunk.dev matches the new grammar;
`skills/worktrunk/reference/claude-code.md` re-synced via
`test_docs_are_in_sync`.

No code or test changes.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:49:27 -07:00
Maximilian Roos 5a07bc09bd feat(plugin): ship the wt-switch-create skill in the Claude Code plugin (#2737) 2026-05-11 21:10:35 -07:00
Maximilian Roos 8abefbce82 refactor(docs): unify AUTO-GENERATED marker form; share constants across crate boundary (#2418)
## Summary

Follow-ups from #2417 review, plus adjacent simplifications.

- **Cross-crate share of marker constants.** `MARKER_OPEN_PREFIX` and
`MARKER_CLOSE` now live in `src/docs.rs`. `src/help.rs` (the
`--help-page` producer of help-region markers) and
`tests/integration_tests/readme_sync.rs` (the consumer + producer of
snapshot/section markers) both reference them — drift becomes a build
error rather than a silent mismatch.
- **Drop `MARKER_OPEN_HTML_PREFIX`.** The `-HTML` variant emitted
visually identical wrapping; the suffix only signalled "in-place
refreshable" but the `.snap` ID + terminal-shortcode body in
`DOCS_SNAPSHOT_MARKER_PATTERN` already discriminate that. Standardised
on a single open prefix and stripped the `-HTML` literals from
`README.md`, the four standalone docs files, and the test file.
- **Help-page marker uses the shared prefix.** The mirrored close (`<!--
END AUTO-GENERATED from \`wt <cmd> --help-page\` -->`) stays as-is
because adjacent regions need unambiguous pairing, but the open prefix
now goes through `MARKER_OPEN_PREFIX`.
- **Drop `MarkerType::output_format()` / `extract_inner()`.** Both had a
single trivial non-panic branch that existed only to enforce "no
Snapshot markers in README" via `unreachable!()`. Replaced with one
explicit assertion in `sync_readme_markers` — surfaces the invariant as
an actionable error message instead of a panic.
- **Collapse `format_replacement`.** `wrap_in_marker` is invoked once
for both output formats; only body construction varies.
- **`__WT_QUOT__` rename in `tests/integration_tests/user_hooks.rs`.**
Inlined the single quotes — the `.replace('__WT_QUOT__', \"'\")` was
unnecessary indirection (the outer raw-string literal already tolerates
`'`, and TOML / Tera don't care about embedded `'`). Removes a
name-conflation footgun with the unrelated `__WT_QUOT__` placeholder in
`src/docs.rs`.

Net diff: -7 lines.

## Test plan

- [x] `cargo test --test integration` — full integration suite (1559
tests) pass
- [x] `cargo test --test integration readme_sync` — all 13 sync tests
pass; `test_readme_examples_are_in_sync` now produces a stable README on
a clean run (verified by re-running multiple times — no further updates
after the initial regeneration)
- [x] `cargo test --test integration
test_args_indexing_and_length_in_hook_template` — confirms the
user_hooks single-quote inlining works through TOML and Tera
- [x] Visual check via local Zola dev server: \`/worktrunk/\`,
\`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`,
\`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal
blocks cleanly with no leaked marker strings (`AUTO-GENERATED-HTML`,
`__WT_OPEN2__`, trailing `|||`)
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 16:57:39 -07:00
Maximilian Roos 7ae28e1d16 docs: trim needless parenthetical qualifiers (#2289)
Parenthetical examples (e.g., `origin/HEAD`) read naturally, but
qualifiers and conditions belong in prose. This trims three cases:

- `Checks git config worktrunk.default-branch (single command)` → drop
the redundant `(single command)` (the command fragment already shows
it's a single command).
- `queries git ls-remote (100ms–2s)` → `queries git ls-remote —
typically 100ms–2s`.
- Statusline doc: `fetches from the network (often ~1–2 seconds), making
it suitable for async...` → em-dash clause so the timing detail doesn't
interrupt the main claim.

Auto-synced: `docs/content/config.md`, both
`skills/worktrunk/reference/*.md` copies, and the
`help_config_state_default_branch.snap` help snapshot.

> _This was written by Claude Code on behalf of Maximilian_

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-18 12:04:00 -07:00
suyua9 d3fda9a8c1 docs: clarify plugin install command (#1906)
## Summary
- document `wt config plugins claude install` as the recommended plugin
install path
- keep the existing Claude marketplace commands as the manual equivalent
- align the docs page with the shipped CLI help and plugin installer
behavior

## Testing
- compared the docs change against `src/cli/config.rs`, which already
advertises `wt config plugins claude install`
- verified the diff stays limited to `docs/content/claude-code.md`

## Notes
- docs-only change

---------

Co-authored-by: ming <silverchris@foxmail.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 19:49:26 +00:00
Maximilian Roos d6e84ed258 feat: add --format=json to wt switch and CC worktree hooks (#1959)
Adds `--format=json` to `wt switch` and WorktreeCreate/WorktreeRemove
hooks to the Claude Code plugin, so agent-created worktrees go through
`wt` instead of raw `git`.

**`wt switch --format=json`** prints structured JSON to stdout
(`action`, `branch`, `path`, etc.) without changing any behavior —
hooks, `--execute`, shell integration, and cd all run normally
regardless of format. A `SwitchFormat` enum (`text`/`json`) keeps the
help text clean (only valid values shown).

**Plugin hooks** are inline one-liners in `hooks.json`:
- `WorktreeCreate` — pipes CC's JSON through `jq` to extract the branch,
calls `wt switch --create --format=json`, extracts the path
- `WorktreeRemove` — extracts the worktree path from CC's JSON, passes
it directly to `wt remove -D --foreground`

Approval prompts still run — the hooks don't pass `--yes`, so `wt`'s
non-interactive detection applies normally.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-06 19:17:03 -07:00
Maximilian Roos d8292f9a8b fix: use terminal shortcode for installation commands (#1828)
The installation block on the Claude Code docs page used raw `{%
terminal() %}` with manual `<span>` tags, bypassing the `cmd` parameter
path — no Syntect syntax highlighting and no `$ ` prompts, unlike every
other command block on the site.

Switched to the `{{ terminal(cmd="...") }}` shortcode format via the
standard `$ ` console block convention.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 20:14:32 -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 5c1672932f docs: remove deprecated post-create from documentation (#1776)
Replace all `post-create` references with `pre-start` across
documentation, skills, example config, and test names. The Rust
deprecation handling code (migration, alias, config parsing) remains
intact for users with existing configs.

Also removes `post-create` from the `wt hook show` value_parser — it was
listed as a valid hook type for display even though it's been
deprecated.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-27 17:38:57 -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 91a505603a feat: add Summary column to wt list --full (#1100)
## Summary

- Adds an LLM-generated Summary column to `wt list --full`, showing a
one-line description of each branch's changes
- Extracts shared summary module (`src/summary.rs`) from picker-specific
code for reuse across `wt list` and `wt switch`
- Summary and Message are both flexible columns — Summary expands first
(10→70 chars), then Message gets remaining space (10→100 chars)
- Gated on `[list] summary = true` config (disabled by default — each
branch's diff is sent to the configured LLM) plus `[commit.generation]`
and `--full`
- Labeled as experimental throughout docs

## Test plan

- [x] 539 unit tests pass
- [x] 1072 integration tests pass (including 9 new layout tests for
flexible Summary/Message allocation)
- [x] Pre-commit lints pass
- [x] Manual: `wt list --full` with LLM configured + `summary = true` —
Summary column appears
- [x] Manual: `wt list --full` without `summary = true` — no Summary
column
- [x] Manual: `wt list` (no `--full`) — no Summary column
- [x] Manual: `wt switch` picker — summary tab still works

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-18 16:25:46 -08:00
Maximilian Roos 1d85de945f Migrate docs syntax highlighting to giallo with warm theme (#1080)
## Summary

- Migrate from static CSS syntax highlighting to Zola's giallo engine
with custom `worktrunk-light.json` theme
- Replace hardcoded `syntax-light.css` / `syntax-dark.css` with
theme-based class generation
- Design a warm "sunlit workshop" palette: amber commands, gold strings,
chartreuse quoted strings, rusty constants
- Add CSS sibling selector to differentiate quoted from bare strings
(giallo tokenizes both as `z-string`)

## Test plan

- [ ] Verify syntax colors on `/switch/` (bash: commands, flags,
strings, quoted strings)
- [ ] Verify TOML blocks on `/config/` (section headers, keys, values)
- [ ] Verify dark mode is unaffected (quoted string CSS rule scoped to
`prefers-color-scheme: light`)
- [ ] Check all tests pass (`cargo test`)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 11:32:21 -08:00
Maximilian Roos 3ab31bd73f feat(statusline): add --format=json and --format=claude-code options (#875)
* feat(statusline): add --format=json and --format=claude-code options

Add `--format=json` to output current worktree as JSON (same structure as
`wt list --format=json` but single-element array).

Migrate `--claude-code` to `--format=claude-code` as the canonical syntax.
The old `--claude-code` flag is hidden for backwards compatibility (no
deprecation warning).

Also fixes nested worktree detection: previously used `starts_with()` prefix
matching which would incorrectly identify the parent worktree. Now uses
`git rev-parse --show-toplevel` via `repo.worktree_at().root()` with path
canonicalization, matching the approach in `wt list`.

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

* fix(statusline): require current_dir in Claude Code JSON

When parsing Claude Code JSON context, treat missing `.workspace.current_dir`
as invalid input (return None). The caller already falls back to
`env::current_dir()` when no valid context is parsed.

This is cleaner than fabricating a current_dir value - if Claude Code sends
JSON, it should include the required fields.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 16:36:19 -08:00
Maximilian Roos 1f239aced7 docs: add Quick Start section to front page (#864)
Reapply Quick Start documentation with fix for README rendering.
The terminal output blocks now include explicit `$ ` prompts before
commands, which makes GitHub's console syntax highlighting work
correctly (commands styled differently from output).

Changes:
- Add Quick Start section showing switch, list, merge workflow
- Add snapshot tests for Quick Start terminal output examples
- Fix strip_html() to convert .cmd spans to `$ ` prefixed commands
- Sync skill reference files from docs

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-25 22:55:09 -08:00
Maximilian Roos a6d39d053f Revert "docs: add Quick Start section to front page (#851)"
This reverts commit 69b3e47509.
2026-01-25 22:34:42 -08:00
Maximilian Roos 69b3e47509 docs: add Quick Start section to front page (#851)
* docs: add Quick Start section to front page

Shows the basic workflow with clear directory paths at each stage:
- Create worktree with wt switch --create
- Check status with wt list
- Two merge paths: PR workflow (push + gh pr create + wt remove)
  and local merge (wt merge)
- Parallel agents example with -x flag

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

* docs: improve Quick Start with realistic examples and snapshot sync

- Add snapshot-based output sync for quickstart examples (switch, list, merge)
- Make examples more realistic: 53 lines of auth module changes instead of 9
- Show uncommitted changes in wt list demo (WIP state, not already committed)
- Add wt step commit to PR workflow since changes are now uncommitted
- Remove redundant git push from PR workflow (gh pr create auto-pushes)
- Suppress worktree-path hint in quickstart tests for cleaner output
- Add transform_zola_to_github() for converting HTML terminal markers to plain code blocks

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

* Simplify code block formatting in README and tests

Convert bash code blocks to console format with command prompts and output
in a single block. Update regex pattern to optionally strip redundant bash
preamble when converting AUTO-GENERATED-HTML terminal markers.

* docs: remove redundant bash blocks before terminal examples

The terminal shortcodes already include the command with $ prefix,
so separate bash blocks showing just the command were redundant.

Also fix broken anchor link in config.md.

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

* feat(docs): make terminal prompt non-copyable via CSS

Use CSS ::before pseudo-element to generate the $ prompt, making it
structurally non-copyable. This works with both manual text selection
and copy buttons.

Changes:
- Add .cmd::before { content: "$ "; } to generate prompt via CSS
- Remove explicit <span class="prompt">$</span> from HTML output
- Extract command from snapshot YAML header instead of parsing HTML
- Update expand_command_placeholders for command pages (list.md, etc.)

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

* fix(docs): show commit step in quick start merge example

The merge example now shows staged changes being committed as part of
the wt merge workflow, reflecting a realistic user experience where
code is staged but not yet committed before merging.

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

* style: fix cargo fmt formatting

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

* fix(tests): use cross-platform mock LLM for quickstart_merge test

The quickstart_merge test was using a shell script for the mock LLM
which doesn't work on Windows. Now uses the mock-stub system which
creates a cross-platform mock binary.

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

* fix(test): use to_slash_lossy for Windows path compatibility

Windows paths with backslashes trigger shell metacharacter handling,
which wraps the command in `sh -c`. Bash can't parse Windows paths
like `C:\Users\...`. Converting to forward slashes with
to_slash_lossy() makes the path bash-compatible.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-25 22:26:46 -08:00
Maximilian Roos 632c6ca28a docs: Update stale documentation since v0.18.2 (#853)
- Update deprecated [commit-generation] config format to [commit.generation]
  with single command string (per f8aaead5b)
- Add context window gauge (moon phase 🌕→🌑) to statusline docs
- Add remote-only branch CI status detection note for --remotes flag
- Update demo build script to use new config format
- Sync auto-generated docs and skill references

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-25 15:15:34 -08:00
Maximilian Roos cd8ac3e060 perf(tests): add pre-built fixture to speed up Windows CI (#634)
* perf(tests): add pre-built fixture to speed up Windows CI

Instead of creating repos from scratch for each test, copy a pre-built
fixture with worktrees and remote already configured. This should
significantly reduce test time on Windows where process spawning is slow.

- Add tests/fixtures/standard/ with repo, 3 worktrees, and bare remote
- Update TestRepo to copy fixture instead of running git init/commit
- Add fixture cleanup to doc-generating tests for clean output
- Remove git sample hooks and boilerplate from fixture

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

* fix(tests): correctly clean fixture state for isolated tests

- Change `remove_fixture_worktrees()` to take `&mut self` and clear the
  worktrees map so `add_worktree()` can recreate branches
- Add remote removal to select and approval tests to prevent
  `origin/main` from appearing in snapshots
- Update select snapshots for fixture-based commit hashes

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

* fix(tests): use relative paths in fixture for portability

The fixture gitdir files contained absolute paths to the local
development machine, causing CI failures. Updated to use relative
paths which work after the _git → .git rename during fixture copy.

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

* fix(tests): remove invalid remote from bare repo fixture

The bare repository in the fixture had a remote configured with an
absolute path to the local machine, which wouldn't exist on CI.
Bare repositories don't need remotes configured.

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

* fix(tests): remove origin in approval tests for consistent project_id

Tests manually create approvals using a computed project_id based on
repo path. With the fixture's origin remote, worktrunk computes a
different project_id from the remote URL. Removing origin makes both
use the same fallback (directory path).

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

* fix(tests): improve fixture copy robustness for CI

- Add platform-specific copy: `cp -r` on Unix, `robocopy` on Windows
- Add verification that essential paths exist after copy
- Add verification that origin.git is a valid git repository after rename
- Include stderr in error messages for better debugging

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

* fix(tests): add .gitkeep to preserve empty git directories in fixture

Git doesn't track empty directories, so objects/info, objects/pack, and
refs/tags were missing on CI after checkout. These directories are
required for a valid git repository.

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

* fix(tests): ensure LF line endings in fixture git files

Add .gitattributes to force LF line endings for git internal files
(packed-refs, HEAD, config, etc.) to prevent CRLF corruption on Windows.

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

* fix(tests): handle file copies separately from directories on Windows

robocopy only works with directories, not files. Use Rust's std::fs::copy
for individual files like .gitattributes, and robocopy only for directories.

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

* fix(tests): enable rerere in test git config for consistent output

Git's rerere feature produces "Recorded preimage" messages during
conflicts. Enable this in test config so output is consistent between
local machines and CI.

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

* fix(tests): restore accidentally deleted hook_clear tests

The tests test_hook_clear_no_approvals and test_hook_clear_with_approvals
were accidentally removed during the fixture refactoring. This restores them
with the necessary remote removal so project_identifier matches "repo"
(directory name) instead of the fixture's remote URL.

Also adds remote removal to test_hook_show_approval_status for consistency.

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

* chore: remove unreferenced ping_pong snapshots

These snapshots used feature_a but the tests now use feature, leaving
these orphaned.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 18:48:39 -08:00
Maximilian Roos 8d56331210 docs: document the Claude Code configuration skill (#477)
* docs: document the Claude Code configuration skill

The Claude Code integration page previously focused only on activity
tracking (🤖/💬 markers), burying installation and not mentioning
that the plugin includes a skill.

Restructure to make both features clear:
1. Configuration skill — documentation Claude Code can read
2. Activity tracking — status markers in wt list

Move installation to the top where users expect it.

Relates to #475

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

* sync skill reference with docs

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-08 13:03:57 -08:00
Maximilian Roos 49c15097ff refactor(tests): simplify snapshot testing with auto-bound settings (#396)
* refactor(tests): simplify snapshot testing with auto-bound settings

- Auto-bind snapshot settings in TestRepo::new() via _snapshot_guard field
- Remove manual setup_snapshot_settings().bind() wrappers from tests
- Inline make_snapshot_cmd calls directly in assert_cmd_snapshot! macros
- Remove unused helper functions (json_settings, bind, bind_json)
- Use auto-naming from test function names (explicit names only for multi-snapshot tests)
- Fix directive file guard lifetime in test_remove_internal_mode
- Clean up orphaned snapshot files and let git detect renames

Net reduction: -183 lines in test files

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

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

* fix: update statusline tests for fish-abbreviated paths

The auto-bound snapshot settings from TestRepo use `_REPO_` filters
for full paths, but the statusline command uses fish-style path
abbreviation (e.g., `/p/v/f/.../repo`). These abbreviated paths
weren't matched by the existing filters.

Updated `claude_code_snapshot_settings()` to:
- Filter fish-abbreviated paths ending in `/repo` to `[PATH]`
- Strip leading ANSI reset codes from output
- Remove unused `repo` parameter

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

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

* fix: normalize statusline paths across platforms

The statusline output varies by platform:
- Linux: Raw path filtered by auto-bound settings to `_REPO_`
- macOS: Fish-style abbreviation bypasses auto-bound filters

Updated claude_code_snapshot_settings() to normalize both cases
to a consistent `[PATH]` placeholder for cross-platform tests.

🤖 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-03 01:00:10 -08:00
Maximilian Roos 5dac49806a docs: add demo GIFs to command pages (#324)
* docs: add demo GIFs to command pages

Add demos to switch, list, merge, llm-commits, and claude-code pages.
Position demos after the intro sentence for consistent layout.

Changes:
- Add wt-switch, wt-list, wt-commit, wt-statusline to DOCS_DEMOS
- Update doc pages with <figure class="demo"> elements
- Update CLAUDE.md with new available demos list

Note: Demos involving `claude` (wt-switch, wt-statusline) need fixes
to the Claude Code mock before the GIFs look correct.

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

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

* fix: add demo markers to cli.rs for switch, list, merge commands

Demo markers in after_long_help allow demos to persist through page
regeneration by test_command_pages_are_in_sync.

Positioned after intro sentence (matching wt-select pattern).

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

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

* fix: add new demo pages to lychee exclude_path

These files have root-relative asset paths that lychee can't resolve
locally (assets are fetched at deploy time from worktrunk-assets repo).

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

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

* chore: update help snapshots for demo markers

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

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

* refactor: exclude asset GIF links by pattern instead of by file

Broadened `/assets/wt-.*\.gif` to `/assets/.*\.gif` to match all asset
directories including `/assets/docs/{light,dark}/`. This is cleaner than
excluding entire files from link checking.

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

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

* fix: pin lychee to 0.20.x in CI

Version 0.22.0 has a regression where exclude patterns for root-relative
paths (like /assets/*.gif) don't work properly - they're treated as
errors instead of being excluded.

🤖 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-31 01:37:41 -08:00
Maximilian Roos 3471fee63b Update docs and help text to clarify default branch references (#275)
* Update docs and help text to clarify default branch references

* Replace "main" with "default branch" in docs and help text

Update references throughout documentation and help snapshots to use
"default branch" instead of hardcoded "main" for clarity. Changes include:
- Help text descriptions for list, merge, switch, and remove commands
- Configuration file documentation and examples
- JSON output field descriptions
- Status symbol explanations
- Example commands and shortcuts

Also update environment variable from GIT_EDITOR to GIT_CONFIG_GLOBAL
in test snapshots and add missing RUST_LOG variable.
2025-12-21 08:49:03 +00:00
Maximilian Roos ad6ec2351b Replace emojis with single-width Unicode symbols (#255)
* Replace emojis with single-width Unicode symbols

Switch from graphical emojis (🔄, ✅, ❌, etc.) to single-width Unicode
symbols (◎, ✓, ✗, etc.) for message indicators:

- ◎ Progress (bullseye)
- ✓ Success (checkmark)
- ✗ Error (x mark)
- ▲ Warning (triangle)
- ↳ Hint (corner arrow)
- ○ Info (empty circle)
- ❯ Prompt (shell prompt)

Benefits:
- Better terminal compatibility (emojis render inconsistently)
- More serious/professional aesthetic
- Consistent single-character width for alignment

Also reduces gutter indentation from 3 to 2 columns to align with 1-char
symbols, and adds colors to the symbols themselves (red for errors, green
for success, etc.).

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

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

* Update shell integration test snapshots

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

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

* Update documentation and comments to use new symbols

Update help text examples, doc comments, and .claude rules files
to reflect the new single-width Unicode symbols.

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

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

* Update .claude rules to use symbol terminology

Rename emoji→symbol consistently in documentation guidelines.

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

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

* Use cstr! for pre-colored symbols

Symbols now have their semantic colors embedded using color_print's cstr!
macro, which creates colored &'static str constants. This ensures symbols
are always consistently colored without needing to wrap each usage site.

- PROGRESS_SYMBOL: cyan ◎
- SUCCESS_SYMBOL: green ✓
- ERROR_SYMBOL: red ✗
- WARNING_SYMBOL: yellow ▲
- HINT_SYMBOL: dim ↳
- INFO_SYMBOL: dim ○
- PROMPT_SYMBOL: cyan ❯

Message formatting functions now apply color only to the text content,
since symbols are pre-colored.

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

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

* Update shell integration test snapshots for pre-colored symbols

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

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

* Add filter for shell-escaped paths in test snapshots

When shell_escape quotes paths, the filters replace the path portion
but leave the quotes. Add filters to normalize '[REPO]' to [REPO].

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

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

* Filter syntax-highlighted [REPO] placeholders

Shell-escaped paths get green syntax highlighting. Add filter to
normalize colored [REPO] placeholders back to plain [REPO].

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

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

* Fix snapshot filters for shell-escaped path formatting

CI (Ubuntu/Windows) produces different ANSI formatting around
shell-escaped paths than local macOS. Add filters to normalize
the dim formatting codes around [REPO] placeholders.

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

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

* Update snapshots for single-width Unicode symbols

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

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

* Update PTY test snapshots for new symbols

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

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

* Update snapshots for new symbols after merge

🤖 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-19 17:34:44 -08:00
Maximilian Roos 58f00ef8b3 Remove bold styling from current branch in wt list (#254)
The current worktree is already distinguished by the @ gutter symbol
and its position at the top of the list, so bolding adds no additional
information.

This simplifies the styling code by removing the position_style function
entirely and only applying dimming for removable worktrees.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 12:25:09 -08:00
Maximilian Roos bb86da253b Show paths relative to main worktree in wt list (#251)
Previously, paths were shown relative to a computed common prefix, which could
degenerate to `/` when worktrees were in unrelated locations, resulting in
confusing output like `./Users/max/project`.

Now paths are computed relative to the main worktree:
- Main worktree: `.`
- Children: `./subdir`
- Siblings: `../project.feature`

Uses `pathdiff` crate for cross-platform relative path computation.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 17:43:20 -08:00
Maximilian Roos 7f0c927240 Unify git config state storage under worktrunk.state.<branch>.* (#234)
Changes from separate namespaces to unified format:
- CI cache: worktrunk.cache.<branch>.ci-status → worktrunk.state.<branch>.ci-status
- Markers: worktrunk.marker.<escaped> → worktrunk.state.<branch>.marker

Benefits:
- Numeric branch names (10704) now work - git subsections have no naming restrictions
- No escaping needed for slashes, underscores, hyphens
- Unified namespace for all branch-keyed state
- Removed ~260 lines of escape/unescape code and tests

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 00:16:53 -08:00