mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
codex/remove-codex-cloud-specific-tests
148 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
246c6bd919 |
fix(list): keep [list] columns out of the --format json plan (#3812)
Closes the `[list] columns` half of #3787, per the call in [this
comment](https://github.com/max-sixty/worktrunk/issues/3787#issuecomment-5273942067):
JSON always emits the same shape, and `list.columns` only affects the
actual columns.
Before, `--format json` planned `all_columns` (source `Default`)
*unioned* with the selection's forced-on columns, so the selection
reached JSON in one direction only — it couldn't narrow the emitted
fields, but a listed `ci` did force the forge fetch on without `--full`.
That made a presentation setting decide whether a machine-readable call
talks to GitHub, which is the thing the Neovim plugin in #3787 had to
pin `--config-set 'list.columns=[…]'` against. Now the JSON branch plans
`all_columns` alone; `--full` is the only switch for the gated data, and
it's the one a caller controls.
The table and the `wt switch` picker are untouched — a listed `ci` still
renders the CI column without `--full`, and the picker still unions the
selection in so its table matches `wt list`'s.
Only `ci` and `summary` are affected: every other column is ungated, so
`full_plan()` already covered them, and custom columns require no
background task.
**For the release note — this changes schema 1 too.** A caller with
`[list] columns = […, "ci"]` and no `--full` used to get the `ci` object
in schema-1 JSON and now won't; schema 1 has no `collected` envelope to
say why. The schema-1 `ci` row already documented `` `--full` only ``,
so the docs get *more* accurate, but the observable output changes for
anyone who was relying on the forcing path. Schema 2 reports the same
narrowing through `collected.ci`.
Docs updated in `after_long_help` (the `[list] columns` section plus the
schema-2 `pr`, `summary`, and `checks` rows — `summary` now names
`--full` alongside `[list] summary = true`, and `checks` names the
`--full` gate it shares with `pr`), with the generated mirrors,
`dev/config.example.toml`, and the `--help` snapshots regenerated. The
`CLAUDE.md` network inventory and the `collect` planning comment now
record the exemption too.
<details><summary>Test</summary>
`test_list_json_columns_selection_does_not_force_ci` in
`tests/integration_tests/list_config.rs` asserts schema 2's
`collected.ci` across three configs: unset (false), `columns =
["branch", "ci"]` without `--full` (false — the regression this fixes),
and the same with `--full` (true). `collected` records what the plan
requested rather than what a fetch returned, so the test needs no forge
and no `gh` on PATH. It sits next to
`test_list_json_ignores_columns_selection`, which owns the narrowing
direction, and `test_list_config_listed_column_overrides_full_gate`,
which owns the table's forcing behaviour and still passes unchanged.
Ran locally: full `cargo test --test integration` and `cargo test --lib
--bins`, plus `cargo clippy --all-targets` and `cargo fmt --check`. One
unrelated failure,
`test_copy_ignored_preserves_file_executable_permissions`, is a umask
artifact of this sandbox (expects `0644`, the runner's `umask 002`
produces `0664`); it touches no code in this diff.
The docs-row follow-up in
|
||
|
|
7f8ac8e5d7 |
feat(list): publish a JSON Schema for the schema-2 envelope (#3747)
`wt list --format=json` schema 2 now has a published, machine-readable contract at [worktrunk.dev/schema/list-v2.json](https://worktrunk.dev/schema/list-v2.json). The schema was already derived — `test_schema_generates` built one, asserted it compiled, and threw it away, with a comment saying "until the schema export ships." This ships it: `wt list --print-schema` prints the document (a developer entry point alongside `--help-page`, intercepted before clap), and a new step in `test_docs_are_in_sync` commits it to `docs/static/schema/list-v2.json`, the same generate-and-commit pattern as `llms.txt`. It shells out rather than calling `schema_for!` because `JsonEnvelope` lives in the bin-only `crate::commands` tree. Two things had to be fixed for the document to be usable. **The contract.** `schema_for!` generates under schemars' *deserialize* contract, which marks a `skip_serializing_if` field required — nothing supplies it on the way in. The first document I generated therefore required `default_branch`, `upstream`, `pr`, `checks`, `summary` and `vars` on every item, all of which the absence rule routinely omits, so it rejected the output it documents. Generating under `for_serialize()` fixes it. **The vocabularies.** Four fields — `checks.status`, `display.state`, `default_branch.integration.reason` and `worktree.operation` — were `&'static str`, so the schema described them as bare strings. They are now `JsonCheckStatus`, `JsonMainState`, `JsonIntegrationReason` and `JsonOperation`, each converted from its domain enum by an exhaustive match, so a new `CiStatus`, `MainState`, `IntegrationReason` or `InProgressOperation` variant is a compile error rather than a value silently missing from the published vocabulary. **The emitted JSON is unchanged**; the existing envelope snapshot passes untouched. <details> <summary>Before and after, for one item</summary> ```json // before — rejects its own output, and loses the vocabulary "required": ["default_branch", "upstream", "pr", "checks", "summary", "vars", "display"], "status": { "type": "string" } // after "required": ["branch", "head", "display"], "status": { "enum": ["passed", "running", "failed"] } ``` </details> ## Testing `test_schema_accepts_envelopes` validates a battery — every `CiStatus` over both sources, every `MainState`, a populated worktree row, an integrated row with an upstream and a dev server, plus the absent and null arms of the absence rule — against the same document `--print-schema` emits. Validating proves nothing about a type the battery never instantiates, so the test also pins every non-`Nullable_` type in the document to a path that must carry a non-null value. A new `Json*` type fails until the battery reaches it, and a row that stops populating one fails too — the check reports the type names rather than leaving the gap to a reader. This needed a `jsonschema` dev-dependency: schemars only generates, and derives the document from the types without ever seeing an envelope, so nothing otherwise tied the two together. The test was confirmed to fail on the bug it exists for. Reverting to `for_deserialize()` makes it report `pr`, `checks`, `summary`, `vars` and `display.columns` as wrongly required. The dependency is dev-only: `reqwest`, `rustls` and `async-trait` stay unselected so no HTTP stack comes along, and `cargo tree --package worktrunk --edges normal -i jsonschema` finds no path to it. One direction it deliberately does not cover: a *loosening*. If a field reverted to `&'static str` the schema would say `type: string` and anything would validate. That direction is held by the compiler instead, via the exhaustive matches. ## Notes for review - `schema_document()` lives in `json_v2.rs` beside the types, not in `help.rs`, so `--print-schema` and the test compile the same document rather than two constructions that could drift on the contract setting. - The lychee exclusion for `worktrunk.dev/schema/` follows the entry directly above it: a generated link that 404s until the site deploys. > _This was written by Claude Code on behalf of max-sixty_ |
||
|
|
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_ |
||
|
|
9eb473e056 |
feat(list): name a detached row by its short hash, not - (#3675)
The Branch cell hardcoded `"-"` for a worktree with no branch. It reads as missing data rather than as a state, and it was the odd one out: the skeleton row, the statusline, and `worktree_display_name` all reach for `branch_name()`'s `"(detached)"`, so the same cell changed label as the row settled. Detached worktrees aren't exotic here any more — Codex creates one per session under `~/.codex/worktrees/`, and they sit in `wt list` alongside everything else. The cell now carries the row's abbreviated HEAD in dim yellow. Yellow keeps it from reading as a branch that happens to be named like a SHA; dim keeps a row that isn't on a branch quieter than one that is. `should_dim`'s removable dim still reaches the row's Path and Message cells, so that signal survives the override. ``` Branch Status Path Commit Branch Status Path Commit @ main ^| . 1243e9c0 @ main ^| . 1243e9c0 + - ! ⚑↓ ../codex/… bdc5c663 → + bdc5c663 ! ⚑↓ ../codex/… bdc5c663 + 1p ⊂ ../wt.1p bdc5c663 + 1p ⊂ ../wt.1p bdc5c663 ``` ### What to look at `display_name()` gains a HEAD-prefix fallback so it answers before the `%h` batch lands post-skeleton — the skeleton and settled rows now print the same text in the same style, with no restyle as the row fills in. Both the Branch cell of a detached row and the Commit cell of every row render the new `abbreviated_head()`, so one commit gets one spelling: sourcing the Branch cell from `short_sha` (`core.abbrev`-aware) instead put `1b9f1d9` beside the Commit column's `1b9f1d96` on the same row. The Branch column budgets `COMMIT_HASH_WIDTH` when any row is detached. Sized off branch names alone it truncated the hash — and it already truncated the skeleton's `(detached)` to `(detac` behind a short branch set, so that was a latent bug rather than a new constraint. The picker's matcher text and the statusline follow the display. A detached row now filters by the hash on screen rather than a `"(detached)"` token that matches nothing visible and collapses every detached row onto one key, and a prompt names the same worktree the same way its `wt list` row does. `⚑` on a detached row stays as it was. The flag's axis is "not at home", and a worktree with no branch has no home path to be at — but the docs described only "branch name doesn't match the worktree path", which doesn't cover the case that has no branch at all. They now name it. ### Testing Covered by the existing detached-head list snapshots (all four now show the hash), a new layout test pinning the column width against a short branch set, a unit test for the `display_name` / `abbreviated_head` pair across the skeleton boundary, and the statusline detached test rewritten to assert the hash. Full suite green locally: 4493 tests, clippy and pre-commit clean. > _This was written by Claude Code on behalf of max-sixty_ |
||
|
|
79824f7122 |
feat(styling): underline every hyperlink (#3643)
The statusline underlined its PR reference but not the dev-server port, so nothing marked the port as clickable: ``` ~/w/worktrunk.test-suite-cpu ↕|💬 ↑17 ↓11 ^+585 -258 #3604 :11486 Fable 5 🌕 30% ``` Both links now route through a shared `worktrunk::styling::hyperlink(url, text)`, which emits the OSC 8 sequence and the underline together. It closes with `[24m` rather than a full reset, so a wrapping color (the CI verdict) or dim (a port nothing answers on) survives the link. The rule is recorded as policy in the `writing-user-outputs` skill. Link text is sized to fit a column (`#3604`, `:11486`) and reads as ordinary content, and color already carries state, so the underline is the only thing marking text as clickable. Text that is not a link stays plain: on a terminal without OSC 8 support, `wt list` still prints the dev-server URL in full, unadorned. Tests: a unit test pins the helper output, and a statusline test asserts both segments carry a helper-built link. Snapshots updated for the reordered escapes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) > _This was written by Claude Code on behalf of Maximilian Roos_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
04d8d587f2 |
feat(list): flag a branch checked out in more than one worktree (#3606)
## Problem Follow-up to #3480. That PR made a duplicated branch checkout (`git worktree add --force <path> <branch>`) visible at *resolution* time: `worktree_for_branch` warns once per branch, then resolves to whichever worktree git lists first. `wt list` said nothing about it. Two rows named `feature`, and no column that explains why. The nearest thing to a signal was accidental. A force-added duplicate usually lands off-template, since the original holds the template path, so it picks up `⚑` for the location mismatch — while the worktree *at* the template path, the one `wt` actually resolves to, carried no flag at all. Exactly backwards from what's useful. The framing from the request: a worktree in the wrong location gets a status flag; a worktree sharing its branch should get one too. ## Solution `⚑` now covers both, on every worktree of the duplicated branch, resolved one included. Which worktree `wt` picks is git's listing order, so singling out the shadowed rows would imply a legitimacy the ordering doesn't carry. ``` @ main ^| | . 05a4a45d 16h Initial commit + feature ⚑_ ../repo.feature 05a4a45d 16h Initial commit + feature ⚑_ ../repo.feature-dup 05a4a45d 16h Initial commit ``` This started as a seventh glyph (`⧉`) and collapsed onto `⚑` in the second commit. The Status column is a dense alphabet the reader has to learn, and the two states say one thing: this worktree's place in the branch ⇔ worktree map is irregular. Off-template path and branch-claimed-twice are both instances. The table already distinguishes them without a glyph — a repeated Branch cell is the duplicate, an odd Path cell the mismatch — so the flag only has to say "not a rendering glitch, look at the Path column". Sharing the glyph means sharing its dim-yellow styling, since the codebase treats a symbol's color as part of its identity; #3480's warning remains the loud channel, firing the moment any command resolves the branch. **The Path column comes along.** It previously appeared only for a location mismatch, on the reasoning that the path is otherwise redundant with the branch. A duplicate inverts that: the branch name no longer identifies the row, and the path is the only thing telling the two apart. The layout flag is renamed from `has_branch_worktree_mismatch` to `path_is_informative` to say what it now means. **The data model keeps the distinction.** JSON has no cardinality budget, and reporting a duplicate that sits at the template path as `branch_worktree_mismatch` would be false — its path does match. Schema 1's `worktree.state` names the cause (`"duplicate_branch"`), schema 2 gets its own `worktree.duplicate_branch` bool beside `branch_mismatch`, matching that schema's one-fact-per-field shape. The priority between the two `⚑` states now decides only which cause JSON reports. Detection is one pass over the worktree list (`duplicated_branches`, next to #3480's `worktree_paths_for_branch`), in memory, pre-skeleton, no git calls. ## Testing - `test_worktree_paths_for_branch_detects_duplicates` gains the set form and a detached-HEAD worktree, which has no branch to duplicate. - `test_metadata_worktree_state_priority` covers the two `⚑` states' ordering and both yielding to `⊟`/`⊞`. - `test_list_duplicate_branch` snapshots the table above, showing both flagged rows and the Path column earning its place. - `test_list_duplicate_branch_json` asserts both schemas flag exactly the two duplicated rows. The flag only fires on a state no prior test sets up, so the only snapshot churn is the help pages and the schema-2 envelope's new field. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f5d99d4fe5 |
fix(step): refuse to commit unresolved conflicts; one gutter symbol for every operation (#3579)
Two follow-ups from #3558, which stopped `wt merge` and `wt step rebase` from running out of a half-finished operation. Probing the sibling commands for the same root cause found one that was worse, and the symbol work is what that PR's detection change made possible. ## `wt step squash` committed conflict markers Mid-merge, invoked directly, it didn't refuse. It generated a commit message for the unresolved markers and committed them: ```console $ git merge side CONFLICT (content): Merge conflict in f $ wt step squash --yes ◎ Generating commit message and committing changes... (1 file, +6, no squashing needed) Merge branch 'side' This resolves a merge conflict between main and side branches. ✓ Committed changes @ e6262c6 $ git show HEAD:f <<<<<<< HEAD main ||||||| 56e6eb3 base ======= side >>>>>>> side ``` `MERGE_HEAD` is gone and the working tree is clean, so the broken merge reads as complete. `wt merge` was never exposed — its own gate stops it before it reaches squash — so this was the direct-invocation path only. ## The index is what knows The obvious fix is the gate #3558 added, and it is not sufficient. The hazard is narrower than "an operation is open", and also wider. `git add -A` collapses an unmerged path's three index stages into one entry. That resolves the conflict as far as the index is concerned, while `<<<<<<<` is still on disk — and it takes with it git's own refusal to commit an unmerged index, which is what would otherwise have stopped this. So the exposure is exactly the commands that stage on the user's behalf, and it does not need an operation to be open at all: ```console $ git stash pop CONFLICT (content): Merge conflict in f $ ls .git/MERGE_HEAD ls: .git/MERGE_HEAD: No such file or directory $ wt step commit --yes ✓ Committed changes @ 1f5387c # markers and all ``` A conflicted `git stash pop` leaves unmerged paths with no state file written, so no reading of `.git/` can see it. `WorkingTree::ensure_no_unmerged_paths` reads the index instead — `git diff --diff-filter=U`, the same question `git commit` asks — and both staging paths call it before staging: ```console $ wt step commit ✗ Cannot commit: 1 path with unresolved conflicts ┃ f ``` The paths are worth carrying where the operation refusal carried nothing: they are the one thing the user needs and git isn't being asked. `wt step squash` additionally takes the operation gate, ahead of its branch check, so mid-rebase it names the open rebase rather than blaming the detached HEAD and offering `git switch <branch>` — the one command that throws the rebase away, which is the same wrong remedy #3558 removed from `wt merge`. Both guards run before the pre-commit hooks and the LLM call. Neither is worth running for a commit that can't happen, and a refused commit runs no project commands — checked with a `pre-commit = "touch HOOK_RAN"` project config that never fires. `wt step commit --dry-run` is deliberately not gated. It mutates nothing, and guarding it displaced `test_step_commit_dry_run_propagates_git_add_failure`, which pins a distinct error path; a tested behavior is worth more than cosmetic parity. ## One symbol for every operation The Status column had `⤴` for rebase and `⤵` for merge, and nothing for the other three states git can leave open. A worktree stopped mid-cherry-pick, mid-revert, or mid-bisect rendered as idle — including the case #3558 closed, a multi-commit cherry-pick whose stop was resolved with `git commit`, which leaves a clean tree, no `CHERRY_PICK_HEAD`, and only the queued sequencer to say anything is wrong. Three more glyphs was the obvious fix and the wrong one. What the reader does about any of the five is identical: run `git status`, then finish or abort it. Splitting the column across a glyph per operation asked them to distinguish states that lead to the same next step, and the split is what left the other three invisible. So they collapse to one: ```console $ wt list Branch Status … Message @ main ↻^ … resolved by hand ``` `git status` names which operation it is, in git's own words — the same division of labor as the refusal message. `--format=json` keeps the identity the symbol drops, so a consumer that needs it still has it: `operation_state` gains `cherry_pick`, `revert`, and `bisect` alongside `rebase` and `merge`. That retires `ActiveGitOperation`, which existed only to re-encode `InProgressOperation` down to the two states the gutter knew. `WorktreeData.git_operation` is now `Option<Option<InProgressOperation>>` — outer `None` is "not loaded" — matching its neighbour `has_working_tree_conflicts`. `GitOperationTask` also stops swallowing errors: it reports a failed probe through the same `ctx.error` channel every other task uses, rather than reporting "no operation" for a probe that didn't answer. ## Verification Three integration tests, each asserting HEAD is unmoved rather than only matching the message: - `test_step_squash_refuses_mid_merge` — the case that committed markers. - `test_step_commit_refuses_unmerged_paths` — driven from a conflicted `git stash pop`, so it can only pass through the index read; the operation check cannot see that state. - `test_list_shows_symbol_for_bisect` — bisect because it is the only operation that leaves HEAD on the branch and the tree clean, so nothing else in the Status cell stands in for it. Snapshots pin both the symbol and `"operation_state": "bisect"`. Confirmed by hand against real repos: mid-merge, mid-rebase, and the conflicted-stash-pop state all refuse; a mid-merge commit whose conflicts *are* resolved still succeeds, as does a clean squash; a queued cherry-pick and a stopped revert both render `↻` and name themselves in JSON. Full gate green (4500/4500 tests, lints, fmt, doc sync), plus `cargo test --features shell-integration-tests` (2148/2148) and `cargo clippy --all-targets --features shell-integration-tests`, which the gate doesn't compile. > _This was written by Claude Code on behalf of Maximilian_ --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
d12c55a320 |
feat(config): wt config update adopts json-schema = 2 instead of pinning 1 (#3436)
Flips the direction of the `[list] json-schema` pending-default handling that #3411 introduced: `wt config update` now writes `json-schema = 2` (adopting the upcoming default) instead of pinning `= 1` (preserving the current one). Running update is the migration; staying on schema 1 is the deliberate manual edit. The `wt list --format=json` nag flips to match, keeping the adopt action last for easy copying: ``` ▲ JSON output is schema 1; a future release switches the default to schema 2 ↳ To keep this format set [list] json-schema = 1; to adopt the new schema, run wt config update ``` Why: with pin-to-1, every `wt config update` run during the deprecation window entrenched users on the schema being retired, leaving a pinned cohort the default flip could never migrate. With adopt-2, update moves users forward as a reviewed config edit, and after the flip `= 2` is just a redundant default a future rule can strip. The trade: `wt config update --yes` in a script switches JSON output as a side effect of unrelated migrations (the interactive path shows the diff first). The system-config gate is unchanged — when the system layer defines the key, update leaves the user file alone; the test now covers the sharper direction (system `= 1` must not be overridden by a user-file `= 2`). All detection/warning invariants carry over; the diff is ~6 semantic lines plus pin→adopt wording and 65 one-line snapshot flips. 🤖 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> |
||
|
|
e02453aa5f |
feat(list): demote branch-worktree mismatch from red flag to dim info (#3419)
Implements option 3 ("demote the presentation") from the design proposal
in #3415, for #3389: worktrees created by agent harnesses at their own
conventional paths (`.claude/worktrees/…`) made every `wt list` row
carry a red `⚑` at merge-conflict severity.
The list predicate, JSON fields, and Path-column behavior are unchanged;
only the presentation changes:
- `⚑` renders dim yellow (was red) in `wt list`, the picker, and the
`--help` legend: below the full-yellow actionable states (prunable,
locked), above plain dim.
- Priority within the status slot becomes `✘ > ⤴ > ⤵ > ⊟ > ⊞ > ⚑ > /`:
the yellow actionable states (prunable, locked) now outrank the
informational glyph instead of being masked by it. The JSON schema-1
`state` field follows the same priority; schema-2 booleans are
independent and unaffected.
- The inline notice on `wt switch` / `wt remove` / `wt merge` / `wt step
prune` is gone entirely: the `wt list` glyph is the state's one surface.
The `expected_path` plumbing is deleted end to end, so switching to an
existing worktree no longer expands the `worktree-path` template at all
(it only ran to feed the notice).
Before/after on a repo with agent worktrees:
```
+ claude/frosty-kilby-92c7d3 ⚑_ (red ⚑, reads as an error)
+ claude/frosty-kilby-92c7d3 ⚑_ (dim-yellow ⚑, reads as a note)
```
Option 1 from the design doc (narrowing the predicate so only genuine
collisions flag at all) can land separately on top of this.
Well-tested: the pre-merge gate passes (4390 tests), snapshots reviewed
line-by-line (glyph color and priority changes in list/help output; the
notice lines removed from switch/remove output), the removed-notice
tests renamed to pin the silence, and clippy is clean including
`--features shell-integration-tests`. Verified live: `wt list` shows the
dim-yellow flag; `wt switch`/`wt remove` print nothing about the
mismatch.
Thanks to @dmsmidt for the report in #3389.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
dac0f80608 |
feat(config): wt config update pins [list] json-schema while unset (#3411)
Wires the `[list] json-schema` nag into the standard config-update machinery, so the warning comes with the standard one-command fix. `wt config update` now pins `json-schema = 1` (the behavior-preserving choice) when the key is unset, via a new `PendingDefault` variant in `DEPRECATION_RULES`: applied on the update pass only — the load path must not pin, or an in-memory `Some(1)` would silence the nag — and scoped to user config through a `ConfigFileKind` enum threaded through detection in place of the old string labels. Two guards keep the pin honest: it stays inert when the system config layer already defines the key (a user-file pin would override a system-level `= 2` and flip resolved output), and the nag's hint offers `wt config update` only when the same detection update runs would actually write the pin — a missing, unreadable, or malformed user config falls back to naming the manual setting. The detection-equals-migration invariant holds with the warning relocated: the pin's warning fires at the JSON-emitting surface (`resolve_json_schema`) exactly when update would change the file, while config load stays quiet (`DeprecationKind::is_pending_default` filters it, and pin-only configs skip the warning-dedup machinery entirely), so `wt switch` users never see it. `wt config show` renders the pending pin's diff — including for empty config files — but keeps its TOML dump: a pin is additive, unlike a deprecation diff that supersedes the dump. This departs from the plan reviewed in `design/list-json-v2.md` (#3357), which deferred the `wt config update` integration to the default flip. Deliberate tradeoff: users who run update during the window land pinned on schema 1 and will see the `= 1` deprecation round after the flip; in exchange, the warning ships with its fixer. **Testing:** unit tests pin the rule's iff (unset ⟺ update changes the file), kind scoping (System/Project inert, load pass inert), the PendingDefault-rule/kind coupling, and placement in existing or implicit `[list]` sections; integration tests cover both hint variants, the update flow end-to-end (pin applied, second run clean), `--print`, and the system-config deferral. The invariant battery runs with an explicit pin appended so each case exercises only its own rule. Full pre-merge gate green (4386 tests) plus `--features shell-integration-tests` clippy. 🤖 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> |
||
|
|
56e20a4a78 |
feat(list): add JSON schema 2 behind [list] json-schema (#3357)
Adds a second JSON schema for `wt list --format=json` and `wt list
statusline --format=json`, selected by a new `[list] json-schema` config
key. Schema 2 is an envelope (`schema`, `repo.default_branch`,
`repo.forge`, `collected`) over items carrying independent facts; schema
1 (the current bare array) is byte-identical to today and remains the
default. Unset emits schema 1 plus a once-per-process stderr nag naming
both settings; `= 1` pins silently; an invalid value warns and degrades
like any other config problem. The nag is suppressed on the statusline
surface, which would otherwise corrupt prompts.
**The semantic core is the absence rule**: absent = nothing to report
(not applicable, not requested per `collected`, or determined-empty),
null = requested but undetermined (probe pending, timed out, fetch
failed). Three mechanisms keep it honest — `integration` derives from
the same committed-content signals `wt remove` trusts
(`check_integration`) rather than the cleanliness-gated display
collapse; skip-seeded conservative defaults are recorded on
`ListItem.seeded` and serialize as null instead of masquerading as
determined facts (invariant-pinned: no seed can fabricate a positive
integration match); and orphan sentinel counts are guarded so they can't
read as a same-commit match.
**For reviewers, in reading order**: `src/commands/list/json_v2.rs` (the
serializer and its `Tri` tri-state), `src/commands/list/mod.rs`
(`resolve_json_schema` + wiring), `src/commands/statusline.rs`
(`run_json`), and small model/collect extensions (`SeededFacts`,
`Collected`, `UpstreamStatus.upstream_short`, task-plan union so a
listed `ci` column forces the fetch for JSON like the table).
**Design doc**: the full rationale, field-by-field mapping, and
migration plan were reviewed as `design/list-json-v2.md`, which rode
this branch as its first commit. Design docs are review-only by repo
convention, so a final commit removes it from the net diff; it remains
readable at
|
||
|
|
4388defff5 |
feat(list): add git.branch.* template namespace for custom columns (#3319)
Adds a `{{ git.branch.* }}` template namespace for `wt list` custom
columns, parallel to `{{ vars.* }}`. It surfaces a branch's own git
config under `branch.<name>.*` — both convention keys you set yourself
(`branch.<name>.jira`) and the git-native `branch.<name>.description` —
without re-storing the values through `wt config state vars set`. This
is the gap left open in #3258: `vars.*` only reads worktrunk's
`worktrunk.state.<branch>.vars.*` namespace, so keys a user already
keeps in git config were invisible to the list.
## Before / after
With `branch.feature.jira` and `branch.feature.description` already in
`.git/config`:
```toml
[list.custom-columns.Jira]
template = "{{ git.branch.jira }}"
[list.custom-columns.Summary]
template = "{{ git.branch.description | lines | first }}"
```
```
Branch … Jira Summary
feature … HWINFCI-2810 Add telemetry instrumentation
main … ← no branch.main.*, empty cells
```
Previously the only way to populate these columns was to re-enter the
data via `wt config state vars set`; now the branch's own config is read
directly.
## Design
- A new `git` top-level namespace (rather than a flat `branch_config`)
so it can grow other git-derived per-branch fields later
(`git.upstream`, `git.remote`, …) without claiming a new top-level name
each time. Today it holds `git.branch.*`.
- `git.branch.<key>` maps 1:1 to `git config branch.<name>.<key>`. Note
git lowercases config variable names, so `branch.<name>.nvciShelf` reads
as `{{ git.branch.nvcishelf }}`; the git-native `description` is
multi-line, so `| lines | first` gives the summary line.
- Data comes from the existing in-memory bulk config snapshot — one
read, zero subprocesses per cell, on the same skeleton-first path as
`vars`. The reader shares a `subsection_map_from_snapshot(parse)` helper
with the existing `all_vars_from_snapshot`.
- Parsing splits `branch.<name>.<key>` with `rsplit_once('.')`, which is
correct because git variable names can't contain dots: dotted/slashed
branch names (`feature.foo`, `feature/bar`) keep their full subsection,
and git's section-level keys (`branch.sort`, `branch.autoSetupMerge`)
flatten to two segments and are skipped.
- Scoped to list custom columns only — no leakage into
hook/alias/pipeline template contexts.
## Testing
Unit tests for the parser/reader (dotted + slashed branch names,
variable-name lowercasing, `branch.sort` skip) and for rendering
(including `description | lines | first`); a JSON integration test
exercises the full `wt list` path end-to-end. Verified manually against
a scratch repo. Full pre-merge gate green (4297 tests, clippy, fmt,
rustdoc, docs-in-sync).
Closes #3258. Thanks to @cazador481 for the request.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a0f07ac6d7 |
fix(list): diff branches against the default branch's upstream tip (#3280)
## Problem
`wt list`'s `main↕` (ahead/behind) and `main…±` (branch diff) columns
measured every branch against the **local** default-branch tip. In a
fork whose local `main`/`master` lags its upstream, those columns
reported garbage. Local default branches go stale routinely: you fetch
far more often than you fast-forward local `main`, while the
remote-tracking ref refreshes on every fetch.
Concretely, in a fork of `skim-rs/skim` whose local `master` sat 42
commits behind `origin/master`, every feature branch displayed as `↑44`
ahead and `+∞ / -5K`, when each was only ~2 commits past the real
upstream tip. The workaround was a manual `git merge --ff-only
upstream/master` to un-stale the local base before `wt list` made sense
again.
## Root cause
`wt list` already carried two notions of "the mainline ref":
- **Integration status** (`⊂` / `↑` / `↓` …) resolves the default
branch's upstream and compares against the *superset* of `{local
default, its upstream}` via `Repository::integration_targets()`.
- **The two stat columns** ignored that and diffed against the raw local
`default_branch`.
That inconsistency is the bug. A stale local default inflated the counts
by every commit it was missing, and the integration symbol and the
numbers next to it could disagree.
## Fix
Route the two informational stat tasks through the same upstream-aware
superset ref the integration column already uses. A new
`TaskContext::comparison_base()` returns `integration_targets.primary`
(the superset side), falling back to the raw `default_branch` only when
integration targets could not be resolved (snapshot capture failed).
`AheadBehindTask` and `BranchDiffTask` now consume it instead of
`default_branch()`.
This reuses one mechanism rather than adding a second upstream resolver,
and is correct across all four local-vs-upstream relationships:
| Relationship | base | vs. before |
|---|---|---|
| no upstream / local == upstream | local | unchanged |
| local behind upstream (stale fork) | **upstream** | **fixed** |
| upstream behind local (unpushed local merge) | local | unchanged |
| diverged | local | unchanged |
The "upstream behind local" row is why naively preferring the upstream
ref would be wrong. After a local `wt merge` that has not been pushed,
the default branch leads its upstream, and sibling branches must still
measure against the local tip so the unpushed merge commits do not leak
into their counts. Reusing `integration_targets.primary` gets this case
right for free, because its superset selection already keeps the local
ref there.
### Deliberately unchanged
- **Conflict columns** (`✗ WouldConflict`, via `MergeTreeConflictsTask`
/ `WorkingTreeConflictsTask`) keep using the local default branch. They
predict the local `wt merge`, which targets the local ref.
- **`wt merge` target** and **`wt switch --base`** (writes) keep using
the local branch. You merge into and update a local ref, and a new
branch defaults onto a writable local branch; a remote-tracking ref is
neither.
### Side effect
The `↑`/`↓`/`↕` gutter symbols derive from these counts
(`is_same_commit` in `model/item.rs`), so they now also track the
upstream tip in a lagging fork. A branch sitting at the stale local-main
tip renders `⊂` (its content is already in the real mainline) rather
than `_`. Removal safety is unchanged: the safe-to-remove signals
(`is_ancestor`, `trees_match`) were already upstream-aware.
## Performance
The snapshot's batched `%(ahead-behind)` walk is keyed on the local
default-branch name, so the common case (superset == local) keeps the
batch. Only the lagging-fork case falls back to a per-pair
`ahead_behind_by_sha`, which is SHA-cache-backed, and that case had
wrong numbers before. The diverged case stays local, so it keeps the
batch too.
## Tests
Two regression tests in `tests/integration_tests/list.rs`:
- `test_list_branch_stats_use_upstream_when_local_default_lags` covers
the fix. It fails on `main` with `ahead 5` against the expected `2`.
- `test_list_branch_stats_stay_local_when_default_ahead_of_upstream`
pins that the base stays local when the default leads its upstream.
The full `wt hook pre-merge` gate is green (4262 tests, clippy,
docs-sync, rustdoc).
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
02c0621b3d |
fix(list): rank Integrated (⊂) above WouldConflict (✗) in main-state (#3278)
A squash-merged branch whose default branch later re-edited the same lines showed `✗` (WouldConflict) in `wt list`, while `wt step prune` correctly classified it as `⊂` (all changes in main) and removed it. The list and the prune verdict disagreed on the same branch, so `✗` read as "unmerged work that conflicts" when the branch was in fact fully integrated and safe to delete. The conflict is real but vacuous: a 3-way re-merge collides on the lines the default branch re-touched, yet the branch's whole diff already matches a commit on the default branch (patch-id), so resolving the merge just reproduces the default branch's tree — nothing added. That patch-id check is exactly how `prune` reaches `⊂`; the list simply ranked the downstream conflict above the integration verdict. This reorders gate 3 of the main-state column so the integration family (`⊂`/`_`) outranks `WouldConflict`, mirroring the existing rule that `Orphan` outranks it — show the root-cause state, not the downstream conflict. Integration stays fire-or-continue so the common `↑`/`↓`/`⊂` cases still render promptly; only `✗` is held until the integration verdict is final, so an integrated-but-stale branch never flashes `✗` before settling to `⊂`. A genuinely un-integrated conflict still shows `✗`. Well covered by unit tests on the priority logic and three new gate tests pinning the integrated-outranks-conflict, no-flash-while-loading, and genuine-conflict cases. The list's rendered table output is unchanged (the integrated-and-conflicting combination doesn't occur in existing fixtures); a follow-up commit reorders the `wt list` default-branch symbol docs and regenerates the `--help` snapshots to match the new priority. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
c3abee15ef |
docs(list): note --full requirement on summary and main.diff JSON fields (#3224)
Follow-up to #3220. That PR added the `--full` qualifier to the JSON `ci` field row; the `summary` field and the `main` object's `diff` sub-field have the identical omission. Both are gated by `--full` in the same `skip_tasks` block (`SummaryGenerate` and `BranchDiff` are dropped when not `--full`), and both Columns-table rows already say "`--full` only" while their JSON field rows didn't — so a consumer scripting plain `wt list --json` would see `summary`/`main.diff` always absent and misread the cause. This states the requirement on both rows, so all three `--full`-gated JSON surfaces (`ci`, `summary`, `main.diff`) read consistently. Edited in `src/cli/mod.rs` (the `after_long_help` primary source); the `docs/content/list.md` and skill-reference mirrors plus the two `--help` snapshots are regenerated. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2f47f29a6f |
docs(list): note --full requirement on the JSON ci object (#3220)
The `wt list --json` Fields table described the `ci` object as "absent when no CI", which omits the actual gate: `ci` is only populated under `--full` (or `[list] full`, or the statusline JSON path) — the same `--full` requirement the Columns table and the `[list] columns` note already document. A consumer scripting plain `wt list --json` sees `ci` always absent and reasonably misreads it as "no CI configured" rather than "needs `--full`". This states the requirement on the field row: `--full` only, then absent when no PR/MR or branch workflow. Source edit is in `src/cli/mod.rs` (the `after_long_help` primary source); the `docs/content/list.md` and skill-reference mirrors plus the two `--help` snapshots are regenerated. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3d0b99e072 |
Add WORKTRUNK_VERBOSE env var equivalent to -v/-vv (#3166)
Shell tab-completion runs the `wt` binary as its own subprocess (the shell sets `COMPLETE=<shell>`), and that path returns from `parse_cli` before `main` ever reaches `logging::init`. So when a tab-completion is slow, there's no flag that turns on logging for it — `-v`/`-vv` never run, and `RUST_LOG` only sets a level, not the `-vv` file sinks. There was no way to profile a slow completion. This adds `WORKTRUNK_VERBOSE=0|1|2` as the env-var equivalent of the `-v`/`-vv` flag count. It's read everywhere — including the completion path, which no flag can reach — and combined with the flag via `max`, so the env sets a baseline the flag can raise but never lower. Completion behaves *identically* to a flagged command at the same level: at level 2 it writes the same `trace.log`/`subprocess.log`/`diagnostic.md` under `.git/wt/logs/`, so a slow tab-completion can be profiled with: ```console $ WORKTRUNK_VERBOSE=2 COMPLETE=fish wt -- wt switch '' ``` then reading `trace.log`. (Set it inline like that, or as a one-off, rather than `export`-ing it — an exported value makes *every* TAB run as `-vv`, printing the "Writing to…" banner above your prompt and re-truncating the shared trace files on each keystroke. That's just normal `-vv` shared-sink behavior, but it's noisy interactively.) The one place completion deliberately diverges: it strips `WORKTRUNK_VERBOSE` from the environment of any forwarded `wt-*` custom-subcommand child, so the child doesn't re-run `logging::init` and clobber the trace files the parent completion just wrote (its stderr is discarded anyway). ### Testing Integration tests cover: `WORKTRUNK_VERBOSE=2` opens the trace files like `-vv` while `=1` does not; flag `-vv` combined with env `0` still writes (the `max`); and completion at level 2 writes `[wt-trace]`/`$ git` records to `trace.log` while candidates still go to stdout. A unit test pins the lossy parse (empty/garbage/out-of-range → `0`, never an error) so a stray value can't corrupt the completion candidate list. Docs (faq, config, the env-var table) and help snapshots are synced. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
61f3a296d9 |
docs(list): align status-symbol tables with JSON by type (#3139)
Restructure the `wt list` "Status symbols" docs so each subcolumn names its `--format=json` field, along the type the data actually has: Working tree is a product type (independent booleans that co-occur) and gets a Symbol | field | Meaning mapping; Worktree, Default branch, and Remote are sum types (one symbol at a time) and get Symbol | JSON | Meaning bridge tables. Fix the intro's "only the first matching symbol is shown", which was wrong for the co-occurring working-tree flags. Single-source the JSON-section meanings via links back to the bridges. Also correct an unreachable JSON value: the `/` symbol was documented as `worktree.state "no_worktree"`, but branch-kind items emit no `worktree` object, so it's mapped to `kind "branch"` and dropped from the worktree-object value list (matching the JsonWorktree.state doc comment). Docs-only change to `src/cli/mod.rs` after_long_help (primary source); mirrors and help snapshots regenerated. |
||
|
|
a17fc22904 |
Add --config-set for inline TOML config overrides (#3138)
A global, repeatable `--config-set <toml>` flag that overrides any user-config key for a single invocation, layered above config files and `WORKTRUNK_*` env vars. The value is a real TOML fragment, so arrays and tables work natively — no bespoke `key=value` grammar. ## Behavior - **Precedence**: config files → `WORKTRUNK_*` env vars → `--config-set` (highest). - **Merge semantics**: a later override replaces an earlier one for the same key; scalars and arrays replace the lower-layer value; nested tables deep-merge, so `--config-set list.full=true` leaves sibling `list.*` keys untouched. - **Global**: works in any position (`wt --config-set … list` or `wt list --config-set …`), like `-v` / `-y` / `--config`. - **Graceful degradation**: a malformed, ill-typed, or invalid override drops the whole `--config-set` layer with an attributed warning (`▲ Ignoring --config-set overrides: …`) and preserves the lower layers — consistent with how the existing `WORKTRUNK_*` env overlay degrades. ## Why This is the foundation for a follow-up that parameterizes `wt list`'s column set without baking it into config: e.g. an alias `list-fast = "wt --config-set list.columns=[...] list"` renders a smaller view while plain `wt list` stays full. Doing it as a generic config-override lever (rather than a `list`-specific flag) keeps one canonical path and composes with aliases and any future config key. ## Implementation `Cli.config_override` (`--config-set`, global, `Vec<String>`) is stashed in a process-global `OnceLock` (mirroring `set_config_path`) and applied in `UserConfig::load_with_warnings` via `apply_cli_overrides` as the top layer. New `LoadError::CliOverride` variant carries the raw values for attribution; the warning is rendered in `emit_user_config_warnings`. ## Testing 8 unit tests on `apply_cli_overrides` (sets, deep-merge-preserves-siblings, last-wins, array-replace, malformed, type-mismatch, validation-failure, empty) and 3 integration tests (overrides-the-file, malformed-warns-attributed, works-after-subcommand). Full suite green: lib + 1828 integration, clippy clean, docs in sync. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a7e29f79c7 |
feat(list): custom template columns and cached PR numbers in the picker (#3073)
Two display features for `wt list` and the interactive picker, developed together because they share the column-layout and progressive-rendering machinery. ## Custom columns (`[list.custom-columns]`) Each `[list.custom-columns.<Header>]` entry in user config adds a `wt list` column: a minijinja template rendered per row over `branch`, `worktree_path`, `worktree_name`, and `vars.*`, with optional `width` and drop priority. Values expand before the skeleton renders, from in-memory data only — `vars` come from the bulk git-config snapshot, so no subprocess runs per cell. Widths are measured from content like the Branch and Path columns; a column that is empty on every row is dropped. Unknown variables and misspelled filters abort `wt list` with the available-variables hint; undefined values render as empty cells (the intended sparse-column shape). `wt list --format json` gains a `columns` map per item, and its `vars` field now reads from the snapshot too (the previous `--get-regexp` line-parse truncated multiline values). The picker shares the row renderer, so the columns appear there as well; a broken definition degrades to no columns plus a stashed warning, since collect runs while skim owns the terminal. The key is `[list.custom-columns]`, not `[list.columns]`, to avoid colliding with the column-visibility toggles in #3065 (which claims `[list.columns]` as a flat map of built-in-column bools — a mutually exclusive serde shape for the same protected key). Namespacing here lets both land independently. Ref #1982 — the custom-columns proposal lives in that thread. The issue's own title is a separate directory-naming request, so this doesn't close it. ## Cached PR/MR numbers in the picker The picker skips the networked CiStatus task, so until now it had no CI column at all. Cached statuses are local data, though: collect now fills rows from `.git/wt/cache/ci-status/` when the task is skipped under a progressive handler, so PR/MR numbers fetched by earlier `wt list --full` or statusline runs render in the picker — aligned with the same `MaxPrNumber` ratchet width `wt list` uses, and with zero network access. A valid cache entry renders as-is. An entry whose TTL passed or whose branch head moved keeps its PR/MR number dimmed: the number still identifies the PR when the pipeline color may be outdated. Expired entries without a number are dropped. The CI column is allocated only when some row had a usable entry, and rows the cache can't fill resolve to blank rather than a pending placeholder, since no task repaints them. ## Key files - `src/config/expansion.rs`, `src/config/user/sections.rs`, `src/git/repository/config.rs` — column resolution, the template environment, and the bulk git-config snapshot. - `src/commands/list/layout.rs`, `src/commands/list/render.rs` — column width allocation and cell rendering. - `src/commands/list/ci_status/mod.rs` — `populate_from_cache`, the cache-only fill. - `src/commands/picker/mod.rs` — the dry-run dump (`WORKTRUNK_PICKER_DRY_RUN`) that makes picker row content assertable in tests. ## Testing Integration tests cover both features: custom columns (table render, JSON output, empty-column drop, invalid-template error) and the picker (cached PR numbers appear in the dry-run dump, uncached branches stay blank). Unit tests cover the cache-population logic (valid, expired-with-number, head-moved, dropped). Verified against the full `cargo run -- hook pre-merge --yes` gate locally. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0681a2e0d0 | feat(list): add structured repo metadata to JSON output (#3021) | ||
|
|
7421236021 |
feat(list): gutter sigils for local (/) and remote (|) branches (#3115)
The interactive `wt switch` picker widens its candidate list with `--branches` (local branches without a worktree) and `--remotes` (remote branches), but the shared list/picker gutter rendered a blank cell for every row without a worktree — so a local-branch-without-worktree and a remote branch looked identical. This adds gutter sigils that distinguish the three row kinds. ## Scheme The gutter marks each row by physical presence: ``` @ main worktree — current (bright) ^ main-repo worktree — primary (bright) + feature-x worktree — other (bright) / local-feature local branch, no worktree (dim) | origin/remote-feat remote branch (dim) ``` `@`/`^`/`+` are unchanged. The two new glyphs echo their Status-column twins — `/` is the WORKTREE_STATE "branch" glyph and `|` the in-sync upstream glyph, both already rendered dim — so the gutter reads as a left-edge summary of the row's kind, and bright-worktree / dim-ref reinforces the presence gradient. The glyph is the primary signal (survives `NO_COLOR`); dim is reinforcement only. All glyphs are single-width ASCII, deliberately, to dodge skim's `width_cjk` clipping (see `vendor/NOTES.md`) — the gutter is the worst place for a clipped row. ## Key decisions Local vs remote is recorded structurally as `ItemKind::Branch(BranchScope)` at construction (`new_branch` / `new_remote_branch`), not inferred from the branch name — a local branch may legitimately be named `origin/foo`, so a name-prefix heuristic would misclassify it. The gutter renders in both the final and skeleton paths with matching dim, so there's no flash or flicker on progressive reveal. A `#` sigil for PR rows (open PRs with no local branch) is intentionally **not** built here — see `TODO(pr-rows)` in `render.rs`. That row kind lives on a separate branch, and the picker skips CI/PR detection (a network call), so a PR-driven `#` would never render in the picker regardless. The scheme leaves `#` free for it. ## Files - `src/commands/list/model/item.rs` — `BranchScope` enum, `gutter_sigil()`, `new_remote_branch`. - `src/commands/list/render.rs` — gutter rendering (final + skeleton). - `src/commands/list/collect/mod.rs` — remote rows use `new_remote_branch`. - `src/cli/mod.rs` — new gutter legend in `wt list --help` (the gutter glyphs were previously undocumented); docs/skill mirrors regenerated. ## Testing Covered by the existing `wt list` / `wt switch` snapshot suite (regenerated under nextest); the rendered-table diffs are confined to the gutter cell. JSON output is unchanged (both branch scopes still serialize as `"branch"`; the remote-qualified name already carries the distinction for scripts). > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
47180a7035 |
refactor(cli): split StatuslineFormat from OutputFormat (#3116)
## Summary
`OutputFormat` carried a `claude-code` variant that only `wt list
statusline` uses. That forced two unrelated commands to work around it:
`wt list` and `wt config state get` set `hide_possible_values = true`
and hand-maintained an `Output format (table, json)` doc string to keep
`claude-code` out of their help, and their match arms carried a dead
`ClaudeCode` branch.
This splits the shared enum (removing the long-standing TODO that called
for it):
- **`OutputFormat { Table, Json }`** — `wt list`, `wt config state get`
- **`StatuslineFormat { Table, Json, ClaudeCode }`** — `wt list
statusline` only
With `claude-code` gone from `OutputFormat`, both commands drop
`hide_possible_values` and the parenthetical, and clap renders the value
list itself:
```
--format <FORMAT> Output format [default: table] [possible values: table, json]
```
`wt list statusline` is unchanged (`[possible values: table, json,
claude-code]`; `--help` keeps the expanded block with the `claude-code`
description).
## Behavior change (CLI flag)
This tightens parsing on two flags. Previously `wt list
--format=claude-code` and `wt config state get --format=claude-code`
were silently accepted and treated as `table`; they now fail fast:
```
error: invalid value 'claude-code' for '--format <FORMAT>'
[possible values: table, json]
```
The value was hidden, undocumented, and meaningless on those commands
(only `statusline` ever used it), so this is a fail-fast improvement
rather than a meaningful break.
## Testing
- `cargo run -- hook pre-merge --yes` — clippy, all pre-commit hooks,
3951 tests, doctests: green
- `test_docs_are_in_sync` regenerated `list.md` mirrors; 4 help
snapshots updated
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
214d82aeda |
Add PR/MR review state to wt list CI status (#3044)
Adds PR/MR review state to `wt list`'s CI status, merged into the existing CI dot color. ## What - New `ReviewState` (`approved` / `changes_requested` / `pending` / `draft`) on `PrStatus`, absent when the forge reports no review signal — branches with no review activity render exactly as before. - **GitHub**: `reviewDecision,isDraft` join the existing single `gh pr list --json` call (no extra API cost). Draft wins over the decision; an empty `reviewDecision` (no required reviewers, no reviews) maps to absent rather than `pending`, so solo repos don't show a perpetual waiting state. - **GitLab**: `draft` + `detailed_merge_status == "not_approved"` → `draft`/`pending`; MR list data carries no approved/changes-requested signal. Gitea/Azure: none. - **Display**: review state merges into the CI dot in `PrStatus::color()` — conflicts (yellow) > changes-requested (**magenta**) > running (blue) > failed (red) > review-required (**cyan**) > base CI color; draft dims like staleness. Cool colors mean waiting (blue: on CI, cyan: on a human), warm mean act; changes-requested outranks running because waiting can't clear it. Magenta was the one color not already carrying a meaning in the list row; cyan is reused from the working-tree symbols column, where the differing glyph disambiguates. - **JSON**: `ci.review_state` in `wt list --format=json`, vocabulary matching Claude Code's statusline `pr.review_state` so the two surfaces agree on names. ## Notes for review - The merge lives in `PrStatus::color()`/`style()` — `CiStatus` stays pure CI (cache values and JSON `status` strings unchanged). Both the table and the statusline render through `format_indicator` → `style()`, so there's a single chokepoint. - Old CI cache files deserialize unchanged (the new field is an `Option`). - `docs/content/list.md` and `skills/worktrunk/reference/list.md` are regenerated from `src/cli/mod.rs`. The HTML and terminal help colorizers learned magenta/cyan (they previously mapped only the five existing colors). Also fixed a pre-existing missing `repo_url` row in the ci-object doc table. - Testing: unit tables for the color merge and both forge mappings; integration snapshots (mocked `gh`) for changes-requested / review-required / approved / draft verifying the exact ANSI codes; the JSON field name is pinned by an inline snapshot. If this change is bad, it's most likely because the dot now encodes two axes (CI health and review attention) in one color — a consumer who reads green as "CI passed" still gets that, but red-vs-magenta now distinguishes who rejected the branch, which is more vocabulary to learn. The `--format=json` output keeps the axes separate for anyone who wants their own logic. Ref #2950 > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5da0d2c3e4 |
docs: alias template-expansion timing; fix narrow help-test env redaction (#3006)
## Docs: alias template-expansion timing
The main addition documents a gotcha that's easy to hit and hard to
diagnose: an alias body renders its `{{ … }}` once at dispatch, in the
invoking worktree, so a per-worktree variable like `{{ branch }}` is
baked to a single value *before* a nested `wt step for-each` / `wt
switch --execute` iterates — printing the same value in every worktree.
The fix is `{% raw %}…{% endraw %}` deferral (plus a quoted `sh -c '…'`
for `for-each`, since the deferred `{{ branch }}` contains spaces).
- `extending.md` — rewrote "Deferring expansion to a nested `wt`
command" around the `for-each` symptom; improved the `up` rebase recipe.
- `faq.md`, `troubleshooting.md` — symptom-first entries with `wt config
alias dry-run` as the diagnostic.
- `hook.md` / `config.md` / `step.md` — distinguish repo-level
(constant) vs per-worktree (active) variables; note `{{ default_branch
}}` needs no deferral; cross-link the `{{ default_branch }}` variable vs
the `wt config state default-branch` shell command.
## Factual corrections
- `integration_reason` JSON values are hyphenated (`trees-match`,
`no-added-changes`, `merge-adds-nothing`) — the docs had underscores.
Verified against `src/commands/list/model/state.rs`.
- `SKILL.md`: 7 → 10 hook types (5 events × pre/post), added an
aliases/multi-worktree task section, fixed stale anchor links.
## Test fix: narrow help-test env redaction
`test_help_list_narrow_terminal` built its own `insta::Settings` but
skipped `add_standard_env_redactions` (every other help snapshot routes
through `snapshot_help`, which calls it). Its snapshot env block
therefore leaked host-specific paths (`LLVM_PROFILE_FILE` = the
machine's temp dir, plus the `WORKTRUNK_*` paths), which churn whenever
the snapshot is regenerated on a different machine. Adding the one call
mirrors `snapshot_help` and makes the snapshot reproducible.
Worth noting (and a candidate follow-up): this gap was masked under
`cargo test` (libtest) because the `repo` fixture's `mem::forget`'d
`bind_to_scope()` guard leaks redaction settings across the shared
process's reused threads. Under nextest (process-per-test, what the
pre-merge hook uses) there's no leak, so a test missing its own
redactions is exposed. A few other help tests (`test_help_md`,
`test_version`, `test_nested_subcommand_suggestion`) have the same gap
and could be consolidated through one settings helper — left out of this
PR to keep it focused.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
2617348b22 |
docs: writing-prose cleanup (faq, config, list, remove) (#2925)
Continues the writing-prose pass on the remaining doc-site pages — the items deferred at the end of #2922. **`faq.md`** — dropped "Worktrunk creates files in four categories." scaffolding; the four H3s below (Worktree directories, Config files, Shell integration, Metadata in .git) make the count self-evident. **`config.md`** (edits in the `Config` command's `after_long_help` in `src/cli/mod.rs`) — four small fixes: - Replaced the "For context:" three-bullet preamble in *User project-specific settings* with a direct lead sentence. Side benefit: fixes the "User configs _also_ has" grammar bug. The new sentence avoids second-person ("for you") and third-person addressing ("for the user") per the writing-prose indicative-mood rule. - Dropped "also" from the system-config sentence — it was the only signal the sentence was an orphan footnote relative to the table above. - Dropped "Similarly," before the first-commit-prompt sentence; the parallel "On first run … On first commit …" structure carries the relation. - Dropped "Note the single underscore after `WORKTRUNK` and double underscores between nested keys." that restated what the env-var table already showed. **`list.md`** (edits in the `List` command's `after_long_help`) — folded the three-dot diff detail into the `main…±` column description; tightened the remaining footnote to just the label-stays-main point. **`remove.md`** (edits in the `Remove` command's `after_long_help`) — extracted the cap-detail appendix from the "Patch-id match" bullet, which had four sentences while the surrounding five bullets averaged one or two; it now sits as its own paragraph. Also dropped the two sentences in *Force flags* that inverted the force-flags table just above; only the new `--no-delete-branch` note remains. **Skipped** (re-reviewed and judged not worth changing): - `switch.md` fork material — mechanism + naming-rule, not duplication. - `step.md` mixed-shape operations list — the asymmetry signals which subcommands have subdoc sections. - `step.md` "How it works" subsections — useful reference content, not internal commentary. - `step.md` `wt step promote` opener — opinionated voice framing. - `step.md` `wt step tether` "Why" — now the canonical place for the leakage rationale (the tips-patterns dup was already removed in #2922). Auto-synced skill mirrors (`skills/worktrunk/reference/`) and regenerated help snapshots carry the same edits. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
21e0b27e49 |
refactor(verbose): rename output.log → subprocess.log; -vv keeps Info on stderr (#2913)
## Motivation The `-v` / `-vv` UX had three small issues that compounded: 1. **`output.log` is misnamed.** It holds the *uncapped raw stdout/stderr of every subprocess `wt` spawns* — multi-MB possible (`git log -p`, patch-id pipelines, etc.). "output" reads as "stuff `wt` printed" — the small thing — when it's actually the big thing. Easy to misread. 2. **`-vv` went fully dark on stderr.** PR #2892 moved the noisy debug pipeline to files at `-vv`; in the process, the stderr layer was disabled entirely. Users running `-vv` to see hook output (info-level, which `-v` shows on stderr) suddenly couldn't. 3. **`-v` help text was a 150-char one-liner** packed into a parenthetical, and the surrounding docs leaned on a "stderr stays readable / `log::*` pipeline" framing that was Rust-jargon-flavored and implied stderr-quiet at `-vv` — which is no longer true after change #2. ## Change - **Rename `output.log` → `subprocess.log`.** Filename now matches content. `OUTPUT` static → `SUBPROCESS`, plus the related `OutputMakeWriter` / `OutputFileFormat` / `build_output_layer` symbol renames. - **`-vv` keeps the Info baseline on stderr.** `build_stderr_layer` no longer returns `None` at `-vv`; debug-level records still route to file layers only, so the terminal stays readable while info-level status (hook output, template variables, the `Tracing to ...` pointer) shows the same as at `-v`. - **`-v` help text rewritten** to describe both levels cleanly without a wall of detail. - **`docs/content/faq.md` gets a "What does -v / -vv do?" section** with a three-level table. - **Docs cleanup**: drop "stderr stays readable" / `log::*` jargon / "but not subprocess.log" negative framing from user-facing prose. ## Notes for review - The only `log::info!` site in the codebase is `commands/picker/mod.rs:389` (a single picker error message), so making `-vv` show info-level on stderr doesn't add meaningful noise. - `test_vv_log_pipeline_silent_on_stderr` is renamed to `test_vv_debug_pipeline_silent_on_stderr` — its assertions only check debug-level records stay out of stderr (they do); the old name implied the whole `log::*` pipeline was silent, which was never quite true (direct `eprintln!` always showed) and is less true now (info-level routes to stderr). - 67 of the 69 changed files are snapshot updates (help text and one diagnostic snapshot) and auto-synced doc/skill mirrors. `git diff --stat -- 'tests/snapshots/*' 'docs/content/*' 'skills/worktrunk/reference/*' | tail -1` separates them. - CHANGELOG: not touched. The historical entry that introduced `output.log` (`#2201`) stays accurate for its release; this rename gets a new line in the next release. ## Tests 3870 tests pass. Re-snapshotted all `test_help_*` snapshots, three `step_alias` snapshots that quote the global help, and the diagnostic file format snapshot. > _This was written by Claude Code on behalf of max-sixty_ Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a77c92e25b | docs(help): point banner at the actual cli source path (#2665) | ||
|
|
3593606283 |
refactor: route every short-SHA display through git's abbreviation logic (#2576)
Every site that abbreviated a commit SHA was either slicing `&sha[..7]` or running its own ad-hoc `git rev-parse --short` call. 7-char prefixes regularly collide in repos with many commits, and none of the slicing sites honored `core.abbrev`. Cut over to a single canonical helper. ## Single helper, every display site `Repository::short_sha(&str) -> Result<String>` wraps `git rev-parse --short`. Routes display through git's own abbreviation logic so `core.abbrev` is honored and prefixes auto-extend on collision. Used by: - `step commit` / `step squash` success lines - `step push --no-ff` `Merged to @ <hash>` line — the original bug. Flagged on #2560 as a pre-existing third instance of the same pattern fixed there in `commit.rs` and `step_commands.rs`. - `{{ short_commit }}` template var in hook contexts (`command_executor.rs`, `template_vars.rs`) - post-remove hook context for the removed worktree - safety-backup ref display (`create_safety_backup`) - `(detached <sha>)` label in the orphan-check loop All seven sites previously sliced `commit[..7]` or called their own `rev-parse`. Now they route through one helper. ## Batched form for `wt list --format=json` The JSON list path emits one `short_sha` per worktree row. Looping `short_sha` would fork a subprocess per row, so the short SHA is folded into the existing `commit_details_many` batch instead — `%h` is added to the `git log --no-walk --format=...` call that already fetches timestamp and subject. One subprocess for the whole list, same `core.abbrev` behavior as every other site. `CommitDetails` gains a `short_sha: String` field with the same provenance as the timestamp and subject. The JSON schema is unchanged (`commit.short_sha` was already a field) — only its length now varies by `core.abbrev` instead of being hard-coded to 7. ## API change `TemplateVars::with_active_commit(commit, short_commit)` now takes both forms. Previously it sliced `commit.get(..7)` internally. The sole caller (`worktree/finish.rs`) resolves the short form via `Repository::short_sha` and passes both. ## Docs `{{ short_commit }}` is no longer documented as "(7 chars)" — `src/cli/mod.rs` and `src/config/project.rs` now describe `core.abbrev` behavior. Doc-sync regenerated `docs/content/hook.md` and the skill mirror. ## Tests Full suite passes (3463/3463). `commit_details_many` tests updated for the new tuple shape; `CommitDetails` fixtures in `layout.rs` get a `short_sha` field; `template_vars` tests pass both forms explicitly. Two integration tests previously asserting `short_commit` was exactly 7 chars (`post_start_commands.rs`, `user_hooks.rs`) still pass — fresh test repos default to `core.abbrev = 7`. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
4cbc8ca5fd |
refactor(docs): drop mirrored help-page close; align convert_console_blocks_in_docs error channel (#2422)
## Summary Three follow-ups from #2419 review. - **Bare close everywhere.** With inner snapshot wrappers gone from command pages (#2419), no `AUTO-GENERATED` markers nest inside the help-page region anywhere in `docs/content/*.md`. The mirrored close (`<!-- END AUTO-GENERATED from \`wt cmd --help-page\` -->`) was the only remaining variant of the close form; collapsed to bare `MARKER_CLOSE`. - `src/help.rs::PageMode::emit_footer()` no longer takes `subcommand`. - The help-page regex in `readme_sync.rs` matches via non-greedy `.*?` to bare `MARKER_CLOSE`, with a comment pointing at the new invariant test. - `AUTO_GENERATED_MARKER_PATTERN` strip regex built from the constants. Added **`test_no_nested_auto_generated_markers`** — walks `docs/content/*.md` and fails if any `AUTO-GENERATED` open ever appears inside an already-open region. This is the explicit invariant that bare-close pairing depends on; if a future change tries to re-introduce nesting (e.g., restore an inner snapshot wrapper around terminal shortcodes), the test catches it before the subtle "regex chops region at first inner close" failure mode lands. - **Aligned error channel.** `convert_console_blocks_in_docs` now returns `(Vec<String>, Vec<String>)` like its sibling sync steps, with per-file error capture for `read_dir`, dir entries, and `read_to_string`. The caller passes errors through the same `tag()` aggregation as everything else, so a transient I/O failure on one file no longer aborts the whole pipeline silently. Also fixes the matching clippy warning (`is_some_and(|e| e == \"md\")` → `is_none_or(|e| e != \"md\")`). - **Docs alignment.** `docs/CLAUDE.md` updated in two places where the prose still documented the mirrored close as the canonical form. Visual check via curl across 12 dev-server pages confirmed no leakage of any prior marker form. Adversarial review (subagent /popper-style) caught the stale prose; otherwise no regressions found. Net diff: +113/-49 (the growth is the new invariant test and explicit per-file error handling; structural simplification shows as -49). ## Test plan - [x] `cargo test --test integration readme_sync` — 12 sync tests pass (was 11; +1 for the new invariant test) - [x] `cargo test --test integration` — full integration suite (1558 tests) pass - [x] `cargo test --test integration test_no_nested_auto_generated_markers` — new guard test passes; manually verified it fires on synthetic nesting - [x] Single-pass convergence verified: `git checkout docs/ && cargo test --test integration test_docs_are_in_sync && git diff --stat` produces an empty diff after the first run - [x] Visual check via local Zola dev server: 12 docs pages (\`/worktrunk/\`, \`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`, \`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\`, \`/switch/\`, \`/config/\`, \`/faq/\`) — only HTML comments contain marker text, no rendered leakage - [x] `cargo clippy --all-targets --all-features` — clean - [x] `cargo fmt --check` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
66a5becc25 |
refactor(tests): consolidate three sync tests; drop dead inner snapshot wrappers (#2419)
## Summary Two follow-ups from #2418 review. - **Test brittleness fix.** Three previously-separate sync tests had data dependencies that nextest could interleave: `test_readme_examples_are_in_sync` reads `docs/content/*.md`, which `test_docs_quickstart_examples_are_in_sync` generates from snapshots. Under parallelism, README sync could see stale docs and produce content that required a second run to converge. Collapsed both into the existing `test_command_pages_and_skill_files_are_in_sync` pipeline, renamed to `test_docs_are_in_sync`. Steps run sequentially in dependency order; single pass converges from a clean working tree. Each step's errors and updated-file list are tagged with the pipeline stage (`[command pages]`, `[standalone docs]`, `[README]`, etc.) so a failure tells a developer which stage broke without reading the test source. README failure also now reports `(N of M section(s) updated)`. - **Dead inner snapshot wrappers.** `expand_command_placeholders` wrapped each terminal shortcode in command pages with `<!-- ⚠️ AUTO-GENERATED from <snap> --> ... <!-- END AUTO-GENERATED -->` markers. Command pages regenerate wholesale from `--help-page` each sync, so the inner wrapper served no in-place-refresh purpose — it was dead weight nested inside the outer help-page region's markers. Stripped from `expand_command_placeholders`'s HTML branch; net -32 lines across six command-page docs files. The outer help-page region's mirrored close stays — it's load-bearing whenever any nested `AUTO-GENERATED` marker appears in the body. Comment in `src/help.rs:emit_footer` updated to explain the role. (My follow-up note that the mirrored close was redundant turned out to be a misanalysis — non-greedy `.*?` only safely pairs the open with the right close when there are no nested closes; once the inner snapshot wrappers were gone, the mirror became technically unnecessary, but keeping it survives any future reintroduction of nesting at no cost.) Renamed the test in `CLAUDE.md` (4 sites) and `docs/CLAUDE.md` (2 sites) so the documented `cargo test` invocations resolve. Net diff: −54 lines. ## Test plan - [x] `cargo test --test integration readme_sync` — 11 sync tests pass (was 13; minus the two collapsed) - [x] `cargo test --test integration` — full integration suite (1557 tests) pass - [x] **Single-pass convergence verified**: `git checkout docs/ README.md && cargo test --test integration test_docs_are_in_sync && git diff --stat` produces an empty diff after the first run - [x] Visual check via local Zola dev server: \`/worktrunk/\`, \`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`, \`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal blocks cleanly with no leaked marker strings - [x] Adversarial review of the consolidation (subagent /popper-style): all 4 actionable findings (stale doc references, lost README count, missing stage tags, etc.) addressed - [x] `cargo clippy --all-targets --all-features` — clean - [x] `cargo fmt --check` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
5f126d21fe |
docs(pages): add static command output to sections dominated by GIFs (#2405)
## Summary Adds static command-output blocks to the docs pages dominated by GIFs (addresses #2403). The blocks are **driven from insta snapshots** so they stay in lockstep with what `wt` actually prints — one source flows to all three surfaces: - terminal `wt <cmd> --help` (plain text, gutter-formatted) - `docs/content/*.md` (colorized `{% terminal(cmd="...") %}` shortcode) - `skills/worktrunk/reference/*.md` (plain `$ cmd\noutput\n` block) ## What changed - **Six scripted snapshot tests** produce realistic output (`cargo nextest run`, `flyctl scale count 0`, LLM-generated commit messages): - `test_docs_merge_pre_merge_hook` — `wt merge` with pre-merge hook - `test_docs_step_commit_llm` — `wt step commit` with LLM - `test_docs_step_squash_llm` — three-commit squash with LLM - `test_docs_merge_squash_llm` — `wt merge` (squash + LLM + merge) for `llm-commits.md` - `test_docs_remove_pre_remove_hook` — `wt remove` with pre-remove hook - `test_docs_hook_pre_merge` — `wt hook pre-merge` direct invocation - **Sync pipeline extension** in `tests/integration_tests/readme_sync.rs`: - New write-back pass `sync_cli_mod_example_bodies` fills the ```console``` body in each `<!-- wt <cmd> (docs-example) -->` placeholder in `src/cli/mod.rs` from the registered snapshot. Runs before `--help-page` reads the file. - `COMMAND_PLACEHOLDER_PATTERN` extended to match three forms (```bash```, `{{ terminal() }}` self-closing, `{% terminal %} body {% end %}`). This **fixes a pre-existing bug** in `docs/content/list.md` where the HTML-mode expansion was silently broken. - Stripped trailing `|||` corruption that arises when blank lines in snapshot bodies are interpreted as empty commands by `convert_dollar_console_to_terminal`. - **Docs page migration**: - `src/cli/mod.rs` — replaced four hand-written ```console``` blocks (merge, step, remove, hook) with `<!-- wt <cmd> (docs-example) -->` markers. - `docs/content/llm-commits.md` — replaced three hand-crafted HTML blocks with `<!-- ⚠️ AUTO-GENERATED-HTML from X.snap -->` markers. - **Refactor follow-up** in a separate commit: - `BADGE_EXPERIMENTAL_HTML`, `SUBDOC_MARKER_PREFIX`, `DEMO_MARKER_PREFIX` hoisted to `worktrunk::docs` so producer and consumer stay in lockstep. - `normalize_clap_help_fences()` consolidates the `text→` + `console→bash` replacement pair shared by `--help-md` and `help_reference_inner`. - **CLAUDE.md note** documenting the `.gitattributes` `linguist-generated=false` exemption requirement when adding skill-only files (carryover from #2409). ## Test plan - [x] `cargo test --test integration readme_sync` — all 13 tests pass, idempotent - [x] `cargo test --test integration test_help` — help snapshots updated - [x] `cargo run -- {merge,remove,step,hook} --help` — clean gutter formatting, no visible HTML comments - [x] `cargo run -- hook pre-merge --yes` — full project gate green (3355 tests) - [ ] Visual check on dev site (maintainer — terminal shortcodes now render with colors via ANSI→HTML; dark/light variants both expected to look like the GIFs they replace) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
0253260503 |
Extend -v variable dump to aliases + help-table drift test (#2324)
Follow-ups from #2316. ## What's in here 1. **Alias `-v` variable dump.** Added `format_alias_variables(ctx)` alongside the existing `format_hook_variables(hook_type, ctx)`, with a private `format_variables_table` helper sharing the alignment + `(unset)` logic. Wired into `run_alias` before the announcement, symmetric with the foreground hook path. 2. **Help-table drift test.** `test_template_variables_table_matches_constants` parses the `## Template variables` table out of `src/cli/mod.rs`, extracts `(kind, var_name)` pairs, and asserts presence + group placement against `ACTIVE_VARS` / `REPO_VARS` / `EXEC_BASE_VARS` / `ALIAS_ARGS_KEY` / union of `vars_available_in(Hook(*))`. Uses public API only — no leak of the private `hook_extras` helper. Adding a var to the constants without updating the table (or vice versa) fails the test. Descriptions stay free-form. 3. **Shorter `-v` help text.** `Verbose output (-v: info logs + hook/alias template variable & output; ...)`. ## Example ``` \$ wt -v greet world ○ template variables: branch = feature worktree_path = _REPO_.feature worktree_name = repo.feature … args = ["world"] repo = repo … cwd = _REPO_.feature ◎ Running alias greet ○ Expanding greet echo hello {{ args }} → echo hello world hello world ``` > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eec3b24883 |
Show resolved template variables under -v for hooks (#2316)
Closes #2309. When a hook fires under `-v`, `wt` now prints a `template variables:` block listing every variable in scope for that hook type and the value it resolved to for this specific invocation. Vars that are in scope but not populated render as `(unset)` — which is exactly how `target_worktree_path` surfaces during `wt switch -`, the thing the issue reporter hand-rolled an echo-hook to discover. The block prints *before* the `◎ Running …` announce line so it describes what the hook is about to see, not what already ran. Works in both paths: the foreground path (`announce_command`, one block per command) and the background path (`announce_and_spawn_background_hooks`, one block per distinct hook type in the pipeline batch). ## Key files - `src/config/expansion.rs` — `format_hook_variables(hook_type, ctx)` emits the aligned `name = value` block ordered per the docs table (active → operation → repo → exec). `BASE_VARS` is split into `ACTIVE_VARS` / `REPO_VARS` / `EXEC_BASE_VARS` with a `base_vars()` helper, so `vars_available_in` and the printer share one source of truth. - `src/commands/command_executor.rs` — foreground wiring in `announce_command`, gated on `verbosity() >= 1`. - `src/commands/hooks.rs` — background wiring in `announce_and_spawn_background_hooks`; prints one table per distinct hook type (since within a pipeline only `hook_name` varies). - `src/cli/mod.rs` — updates the global `-v` help text and adds a pointer under `## Template variables` in the hook help. ## Testing - Unit: `test_format_hook_variables_groups_and_unset` snapshots a pre-switch context with `target_worktree_path` omitted so `(unset)` is covered; `test_format_hook_variables_scope_filters_operation` confirms pre-commit's narrower operation scope. - Integration: `test_hook_verbose_prints_variable_table` covers the foreground path; the existing `test_post_start_verbose_shows_per_hook_output` now also exercises the background path. No new CLI surface, no new config keys — only behavior added behind the existing `-v` flag. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1a4eea3c03 |
feat(cli): promote --yes to a global flag (#2279)
## Summary `-y, --yes` is now a top-level global clap flag. It moves off every subcommand's clap args and lives once on `Cli`, so `wt -y <anything>`, `wt <anything> --yes`, and `wt --yes <anything>` all skip any approval or confirmation prompt for that invocation. ## Why Follow-up to the top-level alias dispatch (#2266). The long-term plan is to unify approval-bypass behavior — `-y` should be a true global rather than duplicated across switch, remove, merge, commit, squash, prune, all ten hook subcommands, shell install/uninstall, plugin install/uninstall, and config update. A single canonical flag removes ~50 lines of duplicated clap definitions and one source of drift. ## Call-site survey Approval and confirmation prompt sources, as surfaced by `rg -l 'approve_|requires_approval|confirm_'`: - `approve_hooks` / `approve_hooks_filtered` — reads `ctx.yes`, already threaded via `CommandContext::new(..., yes)`. No change needed; the global now feeds those call sites from `handle_*_command` in `main.rs`. - `approve_command_batch` (merge, config `hook approvals add`) — takes `yes: bool`. Receives the global. - `approve_alias_commands` — takes `yes: bool`. Receives `global_yes || opts.yes` (see "Alias compat" below). - `handle_configure_shell` / `handle_unconfigure_shell` / `handle_config_update` / `handle_claude_install[_statusline]` / `handle_claude_uninstall` / `handle_opencode_install` / `handle_opencode_uninstall` — take `yes: bool` for confirmation-prompt bypass. All receive the global. ## Alias compat `AliasOptions` is hand-rolled (not clap) so it doesn't conflict with the clap global. Its post-alias `--yes` parsing is intentionally preserved here — `run_alias` does `let skip_approval = global_yes || opts.yes;` so both `wt -y deploy` and `wt deploy --yes` work unchanged. Removing the post-alias form is a separate cleanup, tracked by the user's long-term simplification plan. ## Navigating the diff - `src/cli/mod.rs` — new `yes: bool` on `Cli` with `global = true`, `short = 'y'`, `help_heading = "Global Options"`, `display_order = 103` (slots after `-v`). Removed the per-command `yes` field from `SwitchArgs`, `RemoveArgs`, `MergeArgs`. - `src/cli/step.rs` — removed `yes` from `CommitArgs`, `SquashArgs`, and `StepCommand::Prune`. Also dropped `help_heading = "Automation"` from the four step subcommands where the group was left with a single flag (`commit`, `squash`, `for-each`, `prune`); those flags now render under default Options instead of a single-item group. - `src/cli/hook.rs` — removed `yes` from all ten hook subcommands (`pre-switch`, `post-switch`, `pre-start`, `post-start`, `pre-commit`, `post-commit`, `pre-merge`, `post-merge`, `pre-remove`, `post-remove`). `--yes` stays in `KNOWN_HOOK_LONG_FLAGS` so the shorthand rewriter still recognizes it as a real flag rather than a template variable. - `src/cli/config.rs` — removed `yes` from `ConfigShellCommand::Install/Uninstall`, `ConfigCommand::Update` (and its now-redundant `conflicts_with = "yes"` on `--print`), `ConfigPluginsOpencodeCommand::Install/Uninstall`, `ConfigPluginsClaudeCommand::Install/Uninstall/InstallStatusline`. - `src/main.rs` — `dispatch_command` takes `yes: bool`; each `handle_*_command` accepts and threads it. `Cli` destructure adds `yes`. - `src/commands/alias.rs` — `try_alias`, `step_alias`, `run_alias` take `global_yes: bool`. `run_alias` OR's with `opts.yes` before calling `approve_alias_commands`. - `src/commands/custom.rs` — `handle_custom_command` accepts and passes the global to `try_alias`. - `docs/content/` + `skills/worktrunk/reference/` — auto-generated from `--help-page`; the global appears under "Global Options" on every subcommand. - `tests/integration_tests/approval_ui.rs` — six new tests: `test_global_yes_before_subcommand`, `test_global_yes_for_hook`, `test_global_yes_for_alias`, `test_post_alias_yes_still_works`, `test_global_yes_for_step_alias`, `test_global_yes_on_command_without_approval`. ## Notes - Does not remove `AliasOptions::yes` — deferred cleanup per the task brief. - Snapshot churn (~27 help files) is the expected fallout of adding a global flag; each subcommand's `--help` now shows `-y, --yes` under Global Options. ## Test plan - [x] 3242 tests pass (`cargo run -- hook pre-merge --yes`) - [x] Lints clean (clippy, cargo fmt, pre-commit) - [x] Doc sync test passes after regeneration - [x] New approval_ui tests cover before-subcommand, after-subcommand, alias, post-alias, step-alias, and no-approval positions > _This was written by Claude Code on behalf of Maximilian_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
1d7630e6de |
docs(cli): trim filler in list and step help text (#2277)
Third pass over the docs, this round in `src/cli/mod.rs` (which generates the command pages). - **`list`** — drop "The table displays instantly and columns fill in as results arrive." from the `--full` paragraph. The opening paragraph of `list`'s `after_long_help` already says: "The table renders progressively: branch names, paths, and commit hashes appear immediately, then status, divergence, and other columns fill in as background git operations complete." Full mode inherits that behavior. - **`step`** — trim the `<alias>` bullet to match the two other alias references in the file (lines 1843, 2020, both just "Command templates that run as `wt <name>`."). Drop the now-stale `[experimental]` tag — the Aliases section in `extending.md` stopped being marked experimental in #2271, leaving this the only remaining reference — and drop the `(see [Aliases](...))` parenthetical that linked to the same anchor as the bullet name. Snapshots updated via `cargo insta test --accept`. Follows #2271 and #2272. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3e1b351ac1 |
feat(log): split -vv output into trace.log + output.log, drop -vvv (#2201)
## Motivation At `-vv`, raw subprocess bodies were bounded (200 lines / 64 KB) on both stderr and `verbose.log`, with full output only available by rerunning at `-vvv`. Large captures (notably `git log -p` piped into `patch-id` during `wt list`) would routinely flood stderr with elision markers and force a second run just to see what was elided. ## Change The verbosity map is now `-v` (info) and `-vv` (debug); any `-v` count above 2 collapses to `-vv`. Captured subprocess stdout/stderr fan out through two log targets in `src/shell_exec.rs`: - `SUBPROCESS_TERMINAL_TARGET` → bounded preview on stderr, mirrored to `.git/wt/logs/trace.log` (new, replaces `verbose.log`) - `SUBPROCESS_FULL_TARGET` → uncapped body to `.git/wt/logs/output.log` (new), never stderr `src/log_files.rs` (renamed from `src/verbose_log.rs`) owns both file sinks behind a `LogSink` type and a `route(target)` helper that is the single source of truth for sink selection. `src/main.rs`'s env_logger format closure matches on the `Route` enum and emits once per sink. Diagnostic reports embed `trace.log` and reference `output.log` by path — multi-MB raw bodies would swamp a bug report. ## Fallback path When `RUST_LOG=debug` is set without `-vv`, neither sink is active. `FULL` records drop and the `TERMINAL` preview reaches stderr as before — preserving the bounded-stderr guarantee. The elision marker phrases its hint based on whether `output.log` was opened, so users in the fallback path see `rerun with -vv for full output` rather than a pointer to a file that doesn't exist. ## Key files - `src/log_files.rs` — new module; `LogSink`, `TRACE`, `OUTPUT`, `route`. - `src/shell_exec.rs` — two `pub const` targets, `log_output` emits on both, elision hint switches on `OUTPUT_LOG_AVAILABLE`. - `src/main.rs` — verbosity map + format closure. - `src/diagnostic.rs` — template splits inlined `trace.log` from referenced `output.log`. - `src/commands/config/state.rs` — diagnostic file recognition for the new names. ## Testing - `test_vv_splits_full_and_bounded_output` — `[wt-trace]` in `trace.log`, raw stdout in `output.log`, no trace records in `output.log`. - `test_vv_bounded_on_stderr_full_in_output_log` — 250-ref packed-refs trip the elision cap; asserts the marker on stderr + `trace.log`, full content in `output.log` without elision. - `test_rust_log_debug_fallback_without_vv` — no log files created at `-v 0 + RUST_LOG=debug`; bounded preview reaches stderr. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7afa2533e3 |
Collapse Status loading/timeout placeholders to · (#2177)
The Status column's loading glyph `⋯` (horizontal ellipsis) was too visually prominent in the tight per-position slots. Loading and the post-deadline timeout state don't really need to be distinguished via glyph — the surrounding context (progressive fill in `wt list`, picker appearing after deadline in `wt switch`) already tells the user which is which — so both states now render as a dim `·`. The change is centralized in a new `PLACEHOLDER` constant in `src/commands/list/render.rs`. Its TODO documents the future re-split: we'd like a subtle second glyph once we can evaluate candidates side-by-side in real tables. `render_list_item_stale` is kept as a separate entry point (passing the same `PLACEHOLDER` today) so the picker-side call site doesn't need re-auditing when the re-split lands. Most of the diff is mechanical: doc-comment references to the `⋯` glyph, test helpers counting `·` instead of `⋯`, regenerated insta snapshots, and the user-facing help table in `src/cli/mod.rs` collapsing two rows into one. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2a6389a092 |
Centralise [wt-trace] emitter and fix -vv log verbosity (#2146)
## Summary
Three changes to worktrunk's logging conventions, all motivated by `wt
list -vv` dumping raw `git diff-tree -p` bodies into the log stream at
debug level:
**1. `src/trace/emit.rs` owns the `[wt-trace]` grammar.** Previously the
grammar was emitted via ad-hoc `log::debug!("[wt-trace] ...")` format
strings in `shell_exec.rs`, with `src/trace/parse.rs` silently defining
it by how it parsed. `trace_instant`, `log_command_result`, the
`TRACE_EPOCH` static, and `thread_id_number` moved into the new emitter
module so `parse.rs` and the producer share one source. Wire format
byte-identical; `wt-perf` parsing unchanged.
**2. Level discipline.** `-v` → Info, `-vv` → Debug, `-vvv` → Trace.
Previously `-v` didn't touch the `log` crate at all and `-vvv` didn't
exist. The LLM prompt dump (`src/llm.rs`) and captured subprocess
stdout/stderr (`log_output` in `shell_exec.rs`) moved to `log::trace!`,
so `-vv` stops spilling thousand-line diff bodies and full LLM prompts.
**3. Bounded `log_output`.** At Debug, each stream caps at 200 lines /
64 KB with `… (N more lines, M bytes elided — use -vvv for full
output)`. At Trace, uncapped.
## Reviewer navigation
- **New file**: `src/trace/emit.rs` — the single-source emitter.
`command_completed`, `command_errored`, `instant` plus `trace_epoch` /
`now_us` / `thread_id` helpers.
- **Delete/delegate**: `src/shell_exec.rs` loses its duplicate
`TRACE_EPOCH` + `trace_epoch` + `thread_id_number`, and the four
`log::debug!("[wt-trace] …")` branches collapse into two calls into
`trace::emit`. `log_output` gains `log_stream_full` (Trace) and
`log_stream_bounded` (Debug) helpers.
- **Level map**: `src/main.rs:init_logging` has the new match on
`verbose_level`.
- **Help + test snapshots**: the `--verbose` help text change in
`src/cli/mod.rs:249` drives the large auto-synced snapshot / docs /
skill-reference diff.
- **Test rename**: `tests/integration_tests/diagnostic.rs` —
`test_v_does_not_enable_logging` → `test_v_does_not_write_log_files`
(the old name became inaccurate now that `-v` enables Info logging on
stderr).
## Testing
- 969 library unit tests pass.
- Integration tests for `diagnostic`, `step_alias`, `test_help` pass (82
tests).
- Lints clean.
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
d0bee7b260 |
docs: cross-link vars references to dedicated docs (#2034)
The vars feature is documented in three places (hook template variable
table, config state vars page, tips-patterns recipes), but these pages
didn't cross-reference each other. Readers encountering vars in one
place had no clear path to the other.
Added links so the two main vars pages link to each other
bidirectionally, and every other mention now points readers to either
the CLI reference (how to set/get) or the template reference (how to use
in hooks/aliases).
**Changes (5 locations — 4 in CLI source, auto-synced to docs/ and
skills/):**
- \`hook\` template variables table: \`{{ vars.<key> }}\` row now links
to \`wt config state vars\` docs
- \`wt list\` JSON output table: \`vars\` field now links to \`wt config
state vars\` docs
- \`wt config state vars\` "Template access" section: "hook templates"
now links to the template variables page
- \`wt config state\` keys list: the \`vars\` entry now links to its own
section
- \`tips-patterns.md\` "Database per worktree": "vars" now links to the
config page
No changes to aliases — their cross-references were already complete.
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
f6e89009e5 |
fix: detect squash-merged branches when merge-tree conflicts (#1820)
## Problem `wt step prune` (and `wt remove`) fail to detect squash-merged branches when the default branch has since modified the same files that the branch touched. This is because `git merge-tree --write-tree` reports conflicts when both sides changed the same files, and the code conservatively treated conflicts as "not integrated." The same issue affects `wt list` — the `WouldMergeAdd` task calls the same function, so the integration symbol `⊂` is not shown for these branches. ## Solution When `git merge-tree --write-tree` conflicts, fall back to **patch-id matching**: compute the branch's squashed patch-id (`git diff-tree -p merge-base..branch | git patch-id --verbatim`) and check if any commit on the target has a matching patch-id. This detects squash merges because the squash-merge commit on the target has the exact same content changes as the branch. Uses `--verbatim` (not `--stable`) to avoid false positives from whitespace normalization — `--stable` strips whitespace, so tabs-vs-spaces would produce matching patch-ids even though file content differs. The fallback is only triggered when merge-tree conflicts — the happy path (no conflicts) is unchanged. The merge-tree → patch-id sequence is extracted into `Repository::merge_integration_probe()`, a shared method used by both `wt list` (parallel tasks) and `wt remove`/`wt merge` (sequential path). This fixes a drift where the two call sites handled patch-id errors differently. ## Known limitation The patch-id fallback checks if the branch's diff was **ever** applied to the target, not whether the effect **persists**. If a squash merge is later reverted on the target and then the same files are modified (causing merge-tree conflicts), the historical squash-merge commit's patch-id still matches. This is documented in #1818. A follow-up could add a revert-detection check after finding a match. ## Testing - `test_remove_squash_merged_then_same_files_modified` — reproduces the exact scenario from #1818 (branch modifies file, squash-merged, then target modifies same file) - `test_prune_squash_merged_same_files_modified` — verifies `wt step prune --dry-run` detects the branch - All existing squash-merge tests continue to pass (5 remove + 22 prune tests) Closes #1818 > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
69edc3a337 |
feat: add wt config state vars for per-branch custom variables (#1006)
Adds `wt config state vars set/get/list/clear` commands for storing
custom variables per branch in git config
(`worktrunk.state.{branch}.vars.{key}`). Variables are available in all
template contexts via `{{ vars.key }}` syntax (hooks, `wt step eval`),
with JSON dot access (`{{ vars.config.port }}`) and default filters (`{{
vars.env | default('dev') }}`).
Uses `KEY=VALUE` syntax for `set` — `wt config state vars set
env=staging` — matching Docker `-e`, Heroku `config:set`, Fly `secrets
set`, and worktrunk's own `--var KEY=VALUE` convention. Splits on first
`=` only, so values can contain `=` (URLs, JSON).
The database-per-worktree example now uses `vars` to store the
connection string during `post-start`, replacing the `.env.local`
heredoc pattern. The URL is accessible outside hooks via `$(wt config
state vars get db-url)`.
Includes vars data in `wt list --format=json` output and `wt config
state get` display. Builds on #1004 (`wt step eval`). Part of #947.
## Test plan
- [x] Unit tests for vars template injection (empty, with data, no
branch, JSON dot access, shell escaping)
- [x] Integration tests for vars CLI commands (set, get, list, clear,
clear --all, --branch flag)
- [x] Edge-case tests for KEY=VALUE parsing (values containing `=`,
empty values)
- [x] Integration tests for vars in JSON output (present with data,
absent when empty)
- [x] All 493 unit + 1294 integration tests pass
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
bd9b9fe293 |
Syntect highlighting for template expression blocks (#1792)
Commands containing `{{ }}` template expressions (e.g., `wt step eval
'{{ branch | hash_port }}'`) were the only blocks that didn't get full
Syntect highlighting on the docs site — they fell back to accent-only
color because Tera would interpret `{{ }}` in the `cmd` parameter as
template expressions.
This uses text placeholders (`__WT_OPEN2__`, `__WT_CLOSE2__`) that pass
through Tera safely. The terminal shortcode template replaces them back
to real braces before Syntect processes them. Also fixes double-encoding
of `"` in cmd parameters (the old `"` was getting double-encoded by
Syntect to `&quot;`), using a `__WT_QUOT__` placeholder for the same
reason — Tera has no backslash-escape mechanism for string literals.
The skill file generator (`transform_docs_for_skill`) was updated to
handle the new format: extracting `cmd` parameter values and `|||`
delimiters into `$ `-prefixed bash blocks, converting legacy `<span
class="cmd">` body tags, and fixing the `[^)]*` regex that broke on `)`
inside cmd values.
Net effect: all code blocks on the docs site now have consistent
multi-color Syntect highlighting, and skill reference files have clean
`$ command` blocks instead of raw HTML.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
33b87f0b95 |
Widen help text wrapping from 80 to 100 chars for web docs (#1786)
The web docs content area fits ~101 monospace characters on desktop. The previous 80-char wrap caused unnecessary line breaks in option descriptions — e.g., enum variant descriptions wrapping mid-phrase like "Stage everything: untracked files + unstaged tracked / changes". Widens the two `help_reference()` call sites in `src/help.rs` from `Some(80)` to `Some(100)`. Also fixes a pre-existing issue where clap's line wrapping would break bold (`<b>`) spans across lines — `ensure_line_resets` now tracks active SGR styles and re-opens them on continuation lines. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
82d02239f3 | fix: correct --full example text and add removable worktree to docs (#1775) |