mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
dependabot/cargo/dirs-7.0.0
177 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
89326f14af |
Publish shell integration, add a footer, dedupe global options (#4000)
Guides and site presentation: one home per topic, a shell-integration
page, a footer, and three rendering fixes.
**Global options were emitted once per command reference**
clap repeats the same ~20-line `Global Options:` block in every
reference it renders, so a page assembled from subdocs stacked 11 copies
on `/config/` and 13 on `/step/`. That padded the pages and gave site
search that many near-identical hits — "squash" returned both
`#command-reference` and `#command-reference-2`. `take_global_options`
cuts each reference at the heading as it is built, keeping only the
first; one `kept` flag threads through the subdoc expansion and the page
streams out rather than accumulating. Terminal `--help` renders through
clap directly and is unchanged.
The config page also carried colliding anchors — two "Hooks" (`#hooks`,
`#hooks-1`), two "Aliases", and seven "Examples" (`#examples` …
`#examples-6`) — now qualified at their source in `src/cli/config.rs`:
User/Project hooks, User/Project aliases, and
Approval/Alias/State/Cache/Log/Variable examples.
`/step/` still has its own set (eight "Examples", two "Options", two
"Arguments", plus "Staging" and "Dry run" pairs). Qualifying those moves
existing `/step/#examples-N` anchors, so it wants a pass of its own with
the inbound links audited; the deduplication above already removes 13
Global Options blocks from that page.
**Shell integration has a page**
Shell-integration debugging was skill-only: five named warning messages,
a PowerShell checklist, and the wrapper mechanism, with no site page —
while the FAQ's answer to "`wt switch` didn't cd" was to install the
Claude Code plugin. It is now `/shell-integration/`, offered first, with
the plugin as the second route. The `llms.txt` listing serves every page
as `/<slug>.md` from a hand-created symlink, so a new page was a 404 the
listing still advertised; the symlink is added and the sync now fails
when a listed page has none.
**Presentation**
- A site footer carries the version (read from `Cargo.toml` at build
time), releases, changelog and license. No page named any of them, and
`/code-signing/` was reachable only from inside a collapsed block on the
homepage. Starlight's `Footer` is wrapped rather than replaced.
- `wt list --full` renders 1157px inside an 800px content column, so 40%
of it sat behind a horizontal scrollbar with the pane beside the column
empty. A terminal frame now takes the whole pane where there is slack,
measured with a query container rather than recomputed from Starlight's
layout formula.
- The `wt-command-reference` frames offered a copy button for 3,877
characters of generated help text; they now expose no copy control. A
console block listing several commands is as often a menu of
alternatives as a recipe, and nothing in the markup tells them apart, so
every command line in such a block carries its own copy control
alongside the block's.
- The four command demos and the two hand-written figures get captions;
the 2.33 MB homepage GIF below the fold loads lazily.
**Sidebar order is pinned**
`site-navigation.mjs` told readers a
`test_sidebar_matches_frontmatter_order` would fail when the authored
sidebar and the pages' `sidebar.order` disagreed. No such test existed,
and the disagreement it describes is exactly what the survey found:
`remove` listed before `merge`, Agent integration ahead of
lower-numbered pages. The test is written, so the sidebar and the
`llms.txt` ordering derived from the frontmatter can't drift apart
again.
<details>
<summary>Guide corrections</summary>
- Tips & patterns was 26 flat H2 recipes in no order, all 26 in the
sidebar. They group under five H2s — setup and layout, aliases and
hooks, per-worktree services, working with agents, status/commits/logs —
with each recipe demoted to H3. Anchors are level-independent, so
existing `/tips-patterns/#…` fragments still resolve.
- `-x 'opencode run'` has been broken since 0.75.0 made `-x` a literal
program: it is `-x opencode -- run '<task>'`.
- The branch-summary preview moved from tab 5 to 6 when the unified-diff
tab landed; the recipe names the `summary` tab instead of a number.
- The Caddy recipe claimed `feature-auth` hashes to port 16460 — that is
`fix-auth`'s port. It is 18283.
- `_` in `wt list` is same-commit *and clean*; the
same-commit-with-changes glyph is `–`, which is not safe to delete.
- `wt step prune` removes branches with no worktree too, and the min-age
guard ages a worktree by its creation time and a bare branch by its
oldest reflog entry.
- `wt step eval -v` prints fifteen variables; the example showed two
under a lead calling them "the available template variables".
- A filter applied to `{{ vars.<key> }}` acts on the placeholder the
preview substitutes, so `{{ vars.port | default('8080') }}` previews as
`{{ vars.port }}`, filter gone.
- The `.git/wt/cache/` table was missing `picker-preview`, and `wt
config state clear` prompts unless `--yes`.
- `skills/worktrunk/reference/README.md` was a symlink to the repo
README that `SKILL.md` never referenced, and the plugin mirror
dereferenced it into a 262-line copy carrying the star-history token,
share links, and a logo path resolving nowhere. Nothing generated it, so
deleting the symlink is the whole fix.
- One home per topic: agent handoffs stay in tips-patterns, activity
markers in `claude-code.md`, alias-template deferral in `extending.md`,
and the `codename` filter's two `worktree-path` recipes give way to the
config page that owns path templates. The FAQ's "Running tests" and "How
can I contribute?" duplicated the README's Contributing block down to
the share URLs.
- The FAQ linked `/worktrunk/#install`, the `noindex` compatibility
route; the plugin hook shim's Windows Terminal hint pointed there too.
Both use `/#install`, where the new sidebar Install entry goes.
- Example names settle on `myproject` / `feature-auth`; "sibling to main
repo" becomes "sibling to the main worktree", and `wt remove`'s "target
worktree" becomes "the worktree being removed" per the project's own
terminology rule.
</details>
UX survey items: `#36`, `#37`, `#38`, `#39`, `#45`, `#47`, `#48`, `#49`,
`#50`, `#51`, `#52`, `#61`, `#94`, `#95`, `#96`, `#97`, `#99`, `#100`.
Reviewable files: the hand-written pages under `docs/src/content/docs/`
(notably the new `shell-integration.md`, `tips-patterns.md`, `faq.md`),
`docs/src/components/Footer.astro`,
`docs/src/plugins/worktrunk-terminal.mjs`,
`docs/src/site-navigation.mjs`, `docs/tests/*.mjs`, `src/help.rs`,
`plugins/worktrunk/hooks/wt.sh`. Generated mirrors and snapshots are
regenerated.
> _This was written by Claude Code on behalf of max-sixty_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01XAUYWFN9d9oh6jyoQiouHb
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
134f9eb471 |
Default wt list JSON output to schema 2 (#4038)
`wt list --format=json` and `wt list statusline --format=json` now emit the schema 2 envelope when `[list] json-schema` is unset. Explicit `json-schema = 1` keeps the legacy bare array, while invalid values warn and fall back to schema 2. This removes the completed pending-default migration and its `wt config update` prompt, and refreshes help, docs, config examples, and snapshots. Tests: - `cargo run -- hook pre-merge --yes` - `npm --prefix docs run build` > _This was written by Codex on behalf of max-sixty_ |
||
|
|
7d08f6775f |
Add Pi activity tracking plugin (#3594)
## Summary - add `wt config plugins pi install|uninstall` - install a profile-aware Pi / oh-my-pi hook with atomic writes - map Pi lifecycle events to Worktrunk working, waiting, and cleared activity markers - honor Pi's agent-directory, config-directory, and profile environment variables - cover profile paths, explicit directory overrides, uninstall behavior, and CLI output snapshots ## Validation - `cargo fmt --check` — passed - `cargo test --test integration test_pi_ -- --nocapture` — 3 passed - the same snapshot tests passed again without `INSTA_UPDATE` - `cargo clippy --bin wt -- -D warnings` — passed Closes #3571 --------- Signed-off-by: Aditya Datta <crazyme07071996@gmail.com> |
||
|
|
8e420cb8c4 |
Load the OpenCode plugin under OpenCode 2 (#4018)
## Problem `wt config plugins opencode install` writes a plugin OpenCode 2 silently ignores, reported in #4014. OpenCode 2's loader decodes a plugin module's default export against a schema that accepts `{ id, effect }` or `{ id, setup }` — a bare default-exported function, which is what this plugin has always been, fails the decode and the load is dropped without a message ([`packages/core/src/plugin/module.ts#L60`](https://github.com/sst/opencode/blob/b2cecc6350d377c382e1ec32ee66ec63ad68f715/packages/core/src/plugin/module.ts#L60) on the `beta` branch, which is where the OpenCode 2 prereleases come from — `dev` is the 1.18.x line). The plugin also ran `wt` through Bun's `$`, which the host only supplies when it is itself running under Bun. ## Solution One file, both runtimes: the default export is now `{ id, setup, server }`, and `wt` is spawned with `node:child_process` instead of Bun's shell. OpenCode 2 reads `setup` and subscribes to session events; OpenCode 1.16+ reads `server` and gets exactly the hooks it has today. Each version ignores the other's key, so the install path, filename, and marker behavior are all unchanged. Two details in the `setup` arm that differ from the port in the issue's thread — both from reading the shipped `@opencode-ai/plugin@beta` types: `session.status` carries `status.type ∈ {idle, busy, retry}` and `session.idle` is annotated deprecated, so the status is read rather than the event name; and `session.status` with an `idle` status sets 💬 rather than 🤖. **The part worth a maintainer's call**: this keeps the OpenCode 1 shape rather than switching cleanly to v2 alone. OpenCode 2 is still prerelease (`npm dist-tags` for `@opencode-ai/plugin`: `latest` 1.18.29, `beta` 0.0.0-beta-19192), so a v2-only file would make every current stable install fail with `Plugin … must default export an object with server()`. The cost is the `server` arm plus its two local types — say the word and I will cut it to `{ id, setup }`. Either way OpenCode below 1.14.19 loses the plugin: that is where `readV1Plugin` — the function that admits an object-shaped default export — first appears, and older releases accept only the bare function. 1.16 is the floor worth documenting, though, because it is where the host began filtering events to each plugin instance's directory ([v1.16.0](https://github.com/sst/opencode/blob/6cb74317a6efacd656483cb0489d8e7e3701c12e/packages/opencode/src/plugin/index.ts#L258-L259)); [v1.15.0](https://github.com/sst/opencode/blob/2662a4f955e563fd22cd5c4873ca350d21745275/packages/opencode/src/plugin/index.ts#L246) fans every bus event to every instance, so on 1.14.19–1.15.x the marker follows the worktree the instance was created for rather than the session that is active — the same behavior today's plugin has, not a regression. ## Verification `cargo test --test integration test_docs_are_in_sync`, the twelve `opencode` integration tests, and `packaged_assets` pass. Beyond that I ran the new file through both loaders' own logic under `node --experimental-strip-types`, with a stub `wt` on `PATH` recording its arguments. <details><summary>Loader checks and recorded marker calls</summary> Decoding the real module with the exact schema OpenCode 2 uses (`effect@4.0.0-rc.112`, the version its plugin package depends on), and with OpenCode 1's [`readV1Plugin`](https://github.com/sst/opencode/blob/337fd144d2ba144743368f78d9579a99cce175bd/packages/opencode/src/plugin/shared.ts#L272) detect-mode logic: ``` OpenCode 2 module decode : ok, id=worktrunk OpenCode 1 readV1Plugin : ok, id=worktrunk v2 setup returned cleanup: true v2 setup on a 1.18 context: bails, no throw ``` The last line covers a case worth calling out: OpenCode 1.18 also carries a `{ id, setup }` loader, but the context it passes has neither `location` nor `event`. `setup` returns early there instead of throwing, and `server` drives the marker. Feeding a fake event stream, the commands the plugin actually issued: ``` v2: set 🤖 (status busy) set 🤖 (status retry) set 💬 (status idle) — skipped, event location is another worktree — set 💬 (session.idle) clear (session.deleted) clear (cleanup) v1: set 🤖 / set 💬 / clear (unchanged from today) ``` What I could not check from CI is the plugin running inside a real OpenCode 2 session — @pragmaticivan, if you are still up for testing, `wt config plugins opencode install` off this branch should behave the same as your local port. Install location is unchanged and confirmed on `beta`: plugins are still discovered as `.ts`/`.js` files under `plugin/`, `plugins/` in each config directory ([`source-directory.ts#L7`](https://github.com/sst/opencode/blob/b2cecc6350d377c382e1ec32ee66ec63ad68f715/packages/core/src/plugin/source-directory.ts#L7)). </details> Closes #4014 --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
51fd3b2b7e |
docs(config): name the bare repo's own location in the worktree-path example (#4006)
## Problem
The `worktree-path` examples in the user-config guide are introduced as
being **"for repo at `~/code/myproject`"**, and every example states the
path it produces. For all but one, that arithmetic checks out. The
bare-repository example doesn't:
```toml
worktree-path = "{{ repo_path }}/../{{ branch | sanitize }}"
```
Heading claimed `~/code/myproject/feature-auth`. With `repo_path` at
`~/code/myproject` as the section says, `{{ repo_path
}}/../feature-auth` resolves to `~/code/feature-auth` — one directory up
from the stated result.
## Fix
The template is right; the heading silently switched the repo's location
without saying so. `{{ repo_path }}` for a bare repo is the bare
directory itself (as the variable list a few lines above states), so the
claimed result holds only when that directory is a hidden child — the
`myproject/.git` layout that [tips-patterns.md
documents](https://github.com/max-sixty/worktrunk/blob/main/docs/src/content/docs/tips-patterns.md#bare-repository-layout),
and that `wt switch`'s bare-repo offer writes this exact template for.
So the heading now names it:
> Bare repository cloned to `~/code/myproject/.git`
(`~/code/myproject/feature-auth`):
Edited in `src/cli/mod.rs` (the primary source); the four generated
mirrors and two `--help` snapshots are regenerated output.
## Testing
No regression test — this is a documentation string with no behavior
attached. The generated mirrors are pinned by the existing sync tests,
which is what caught them here:
- `cargo test --test integration readme_sync` — 18 passed (regenerates
`dev/config.example.toml`, `docs/src/content/docs/config.md`, and both
`skills/.../reference/config.md` mirrors).
- `cargo insta test --accept --test integration -- test_help` — 47
passed (`help_config_create`, `help_config_long`).
- `cargo fmt --check` — clean.
- `cargo test --test integration` — 2048 passed, 1 failed.
<details><summary>The one integration failure is a sandbox artifact, not
a regression</summary>
`step_copy_ignored::test_copy_ignored_preserves_file_executable_permissions`
expects `0644` and gets `0664`. The tend sandbox runs with `umask 0002`
(group-writable) rather than the `0022` the test assumes.
Confirmed unrelated: it reproduces identically with this branch's
changes stashed, i.e. on the merge base. Both `ci` and `coverage` on
`main` are green at `2026-09-03T10:17:26Z`. This diff touches only doc
strings and snapshot files and cannot reach file-permission code.
</details>
<details><summary>Checked against the in-flight docs PRs</summary>
#4000, #3999, and #3998 each touch the same five files (`src/cli/mod.rs`
plus the four config mirrors), so I checked for the duplication that
sank #4001. None of them edits the `worktree-path` examples region, and
`git merge-tree` against each reports a clean merge with this branch.
</details>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
|
||
|
|
541f6d204d |
Revert "feat(list): add [list] sort for configurable row order" (#3952)
(not determinate, but want to rethink whether this is useful) |
||
|
|
e9c839a194 |
feat(list): add [list] sort for configurable row order (#3933)
Row order in `wt list` and the `wt switch` picker was fixed — current worktree first, primary second, then committer date descending — while `[list] columns` already let a user pick and order the columns. This adds `[list] sort`, a list of column names (most significant first, `-` prefix for descending) that orders the rows, restricted to the columns whose value is known before the table paints, per the maintainer's call on #3922. ```toml [list] columns = ["path", "branch", "status"] sort = ["path"] # the reporter's case: the table mirrors the directory layout # sort = ["-age"] # oldest commit first # sort = ["branch", "-age"] ``` Verified with `cargo run -- hook pre-merge --yes`: 4725/4728 passed, the three failures being sandbox-environment artifacts that fail identically on the base commit (a PowerShell profile and a Nushell autoload dir this runner does have, and a `0664` umask). New coverage is 7 unit tests in `src/commands/list/sort.rs` and 7 integration tests in `tests/integration_tests/list_config.rs`. Closes #3922 <details><summary>Design decisions</summary> **Only skeleton-time columns are sortable** — `branch`, `path`, `commit`, `age`, `message`, i.e. exactly the built-ins with no background task. The rest (`status`, `ci`, `upstream`, the diff columns, custom columns) stream in behind the first frame, so ordering on one would mean either holding the table for a network round trip or reordering rows under the cursor, which the progressive table doesn't do. Naming one is an error that says why rather than a silently ignored setting. `test_sortable_keys_are_exactly_the_task_free_columns` pins the correspondence to `ColumnKind::required_tasks`, so a new task-free column can't quietly become unsortable (or a streamed one sortable). **A spec replaces the default order outright, pinned prefix included.** `sort = ["path"]` exists to make the table read in path order; keeping the current and primary worktrees pinned to the top would defeat exactly that. Newest-commit-first survives as the final tiebreak, which means the empty spec and the fallback are one code path — and rows a spec can't separate keep the order they have today, e.g. branch-only rows under `sort = ["path"]`, since only worktrees have a path. **`age` ascending is newest first.** The column counts up from the commit date, so the smallest age is the newest commit — the same direction as the default order — and `-age` puts the oldest on top. **Rows sort within their group, never across it**: worktrees, then branch-only rows, then remote rows, as before. **`--format json` follows `sort`**, unlike `columns`. The reasons `columns` is excluded are the every-field contract and not letting a display setting decide whether a machine-readable call reaches a forge (#3787); reordering an array narrows no payload and adds no fetch, so neither applies. **A bad key aborts `wt list` and degrades the picker**, the same fork `[list] columns` takes — the picker can't surface an abort mid-render, so it stashes a warning and falls back to the default order. </details> <details><summary>Changes</summary> - `src/commands/list/sort.rs` (new) — `SortKey`/`SortTerm`, `parse_sort_spec`, and the `compare` comparator over per-row `SortFacts`. - `src/commands/list/collect/mod.rs` — parse the spec before the sort, thread it through `sort_worktrees_with_cache` and the new `sort_branch_rows` (which replaces the generic `sort_by_timestamp_desc_with_cache`; both call sites passed the same `Vec<(String, String)>`). Facts are precomputed per row, so the commit-details lookup stays one per row rather than one per comparison. - `src/config/user/sections.rs` — `ListConfig::sort`, merging wholesale like `columns` (a spec is an ordering; merging terms across layers would invent an order neither layer asked for). - `src/cli/mod.rs` — a "Row order" section under `wt config`'s `[list]` docs; the mirrors under `docs/`, `skills/`, `plugins/`, `dev/config.example.toml`, and the help snapshots are regenerated. </details> --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
48b8b3802a |
Stabilize mature interfaces and remove retired config (#3949)
Marks the mature `wt step eval`, `wt step for-each`, `wt step prune`, LLM summary, config-state vars, and commit-template append interfaces as stable. Removes compatibility for the long-deprecated `template-file` and `squash-template-file` settings, and retires the special migration for the already-ignored `switch.picker.timeout-ms`. These keys now follow the ordinary unknown-field path; the troubleshooting guide explains how to move file contents into inline templates. The change also removes the redundant project CI-platform accessor. Validation: `cargo run -- hook pre-merge --yes` (4,710 tests passed; one skipped), plus a production documentation build. > _This was written by Codex on behalf of @max-sixty_ |
||
|
|
bcdd44d133 |
Count untracked files in list diffs (#3925)
`HEAD±` stopped counting untracked files to avoid creating blobs, which made a moved file look like a large deletion. This uses a temporary intent-to-add index so Git can pair moves before counting line changes, while keeping the real index and object database unchanged. The behavior now applies to default and full list output, the picker, and statusline. Tests cover exact and edited moves, unusual path bytes, sparse checkouts, and generated output. > _This was written by Codex on behalf of @max-sixty_ |
||
|
|
8597036a4f |
Remove Codex compatibility entry point (#3904)
Removes `dev/codex.sh` so `.codex/cloud.sh` is the only Codex Cloud setup entry point. Tested with Bash syntax and usage checks, repository-wide reference search, `git diff --check`, and the applicable pre-commit hooks. > _This was written by Codex on behalf of @max-sixty_ |
||
|
|
e66da8e6f2 |
Consolidate Codex environment setup (#3901)
Collects Worktrunk's Codex setup under `.codex`. Desktop worktrees delegate to `wt hook post-start`; this intentionally relies on one-time interactive Worktrunk approval and leaves the hooks backgrounded because they also launch the docs server. Cloud keeps its Linux bootstrap in `.codex/cloud.sh`, with `dev/codex.sh` forwarding for existing environment settings. Tested with Bash syntax checks, entry-point parity checks from inside and outside the repository, TOML parsing, `wt hook post-start --dry-run`, and the applicable pre-commit hooks. > _This was written by Codex on behalf of @max-sixty_ |
||
|
|
8faeabf239 |
refactor(setup): run Codex Cloud as root and simplify both setup paths (#3841)
Consolidates the two remaining Codex Cloud drafts, #3835 and #3838, onto what #3839 landed, then cuts what was left. Main's setup is a 163-line script and a 26-line README under `scripts/codex-cloud/`; this is one 79-line file at `dev/codex.sh`, beside the repo's other development files, with the README folded into its header. That empties `scripts/` — the directory existed only for this. **Codex Cloud runs as root** (from #3835). Setup used to replace `/root/.cargo/bin/cargo` with a wrapper that re-executed cargo as a UID-1000 `ubuntu` user under `tini`, and maintenance chowned the checkout, the rustup home, and three cache directories to match. All of it existed to keep the suite's permission tests from skipping, since root can write to a read-only file. Worth stating plainly: ten tests now skip on Codex Cloud. The pair carrying the most weight is `test_remove_foreground_succeeds_with_stuck_directory` and its `_detached` twin, the only automated coverage of `wt remove` against a directory it cannot delete. It is not a new hole — `setup-web` creates no non-root user, so the Claude Code web environment has always skipped them, and `tests/integration_tests/approval_pty.rs:157` carries a standing TODO about it. Codex Cloud was the one environment buying an exception, and a cargo wrapper, `tini`, `runuser`, and four chown passes were the price. Both environments now agree about what the suite observes, and that TODO is the single place to fix it for both. Three of the ten decided that skip by reading `$USER` rather than by probing the filesystem, which fails open: a container that runs as root without exporting `USER` runs them and asserts an error root never gets. They probe now, through one helper, like the other seven. **Task, the checksums, and the retries are gone.** Nothing invoked Task on Codex Cloud once #3839 stopped routing the launchers through it, so it is no longer installed. The archive checksums follow the Taskfile digest for the same reason that one went: HTTPS authenticates GitHub and the container is disposable and secret-free, so verifying each download bought a helper pair and a 64-char line per tool for very little. Each install is now `curl | tar` and an `install`. The version numbers stay. The gate runs `--all-features`, so nu and pwsh drive PTY snapshots their own versions can move, and `.github/actions/test-setup/action.yaml` pins cargo-insta, cargo-nextest, and nu to the same three versions — unpinning those would make the environment and CI disagree about snapshot output. Nothing under `.github/` pins PowerShell, so CI runs whatever the runner image ships and that version answers to nothing but these scripts. **`setup-web` catches up** (from #3838). It gains `lsof`, installs Nushell from its release archive rather than checking that one is already present, bootstraps `uv` with pre-commit, and puts `$HOME/.local/bin` on PATH. `wt` installs from the debug build produced a few lines earlier instead of through a second full compile. Its `cargo install` of cargo-insta and cargo-nextest stays unconditional, as on main: neither form pins, and a `command -v` guard would have frozen whatever versions the image happened to carry. ## Testing `cargo run -- hook pre-merge --yes` passes, shellcheck is clean at warning level, and `task --list` still parses. The `.tar.xz` and `.tar.gz` extractions were run against the real release archives to confirm the tar flags and the paths inside them. The three converted tests pass unprivileged; their skip branch rests on the same probe the other seven root-skipping tests already use. Neither setup path is executable from a dev machine — Codex Cloud needs Linux and root on the universal image, `setup-web` needs a web image — so the first real exercise is the next environment build. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
aa9d8c43df |
feat: add remote_repo variable (#3745)
Add a `remote_repo` variable that returns the repo name from the remote URL. Unlike `repo`, it stays consistent even if the clone was renamed. Feel free to reject, or suggest other names for the variable. But this change would improve my workflow. I hope you don't mind my submitting a PR before opening an issue. Thanks for an amazing developer tool! AI Disclosure 🤖: I used Claude Code to generate the changes, but reviewed every line and made adjustments. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
bdce107d91 |
fix(config): rank env vars and --config-set above project entries (#3790)
Fixes #3788. Layer and specificity were separate steps. `load_with_warnings` flattened system config → user config → `WORKTRUNK_*` env vars → `--config-set` into one document, and the accessors then resolved specificity on that document, so a `[projects."<id>"]` entry answered for the global key of the same name whichever layer set it. `WORKTRUNK_WORKTREE_PATH` could therefore not override a project's `worktree-path`, and a global `--config-set` hit the same wall. Per @max-sixty in the issue thread — "env vars should indeed take precedence over the user project config, we should fix this throughout" — the two invocation layers now cross the axes: they're typed for one run, so they outrank a project entry as well as the global key. Load applies them at both scopes (`apply_invocation_layer_over_projects`, the last step before `finalize`): whatever the layer set is dropped from every project entry, leaving the global key it also set to answer for it. Two kinds of key are held back: - **Keys the layer restates under `projects."<name>"`** — `--config-set 'projects."github.com/owner/repo".worktree-path = …'` is both the highest layer *and* the most specific key, so it still wins over the same layer's global key. - **Composing keys** — hooks, aliases, and `step.copy-ignored.exclude` — whose project-scoped values append to the global ones rather than replacing them. Both already apply, so an env-set hook was never outranked, and dropping the project's copy would silently stop it running. Hook names come from `HooksConfig`'s schema, so a new hook can't be forgotten. Two sections have to go as a unit rather than leaf by leaf. `[commit.generation]`'s mutually exclusive pairs: `template` and `template-file` clear one another in `merge_with` *and* are rejected together by `validate`, so overriding either has to displace both at project scope — otherwise the project's partner would still win the merge. `exclusive_sibling` names those pairs. And `[list.custom-columns]`, which `ListConfig::merge_with` extends per whole column, so a partial removal leaves the project's column replacing the global one anyway — and `ListColumnConfig::template` is required, so it can also strand a column that no longer deserializes. `is_atomic_section` names that table. Both are enumerations, so the pass degrades as a unit behind them: the removals land on a candidate, kept only if it still deserializes and validates. That is the guarantee the env and `--config-set` layers already have, and without it the next required field would answer a stranded leaf with `UserConfig::default()` — costing the user their whole config for that invocation rather than one project entry's precedence. The precedence table now reads: | Source of `worktree-path` | Loses to | |---|---| | `--config-set 'worktree-path = …'` | — | | `WORKTRUNK_WORKTREE_PATH` | `--config-set` | | `[projects."github.com/owner/repo"]` in a config file | either invocation layer | | global `worktree-path` in a config file | all of the above | ## Docs The help text had no precedence section at all — the gap that made this read as a bug — so this adds one under **Environment variables**, plus a pointer from **User project-specific settings**. That supersedes #3789, which documented the old behavior; I'll close it in favour of this. ## Testing Nine unit tests in `src/config/user/tests.rs` cover the table-level rule (both layers, pattern entries, restated project-scoped overrides, untouched sibling keys, composing keys, the exclusive pair, the atomic custom column, a rolled-back layer, and the no-override no-op), and `test_switch_create_invocation_layers_outrank_project_worktree_path` proves it end-to-end — a real process is the only thing that reads `WORKTRUNK_WORKTREE_PATH` off the environment. That test keeps a control showing the project entry still beats the config file's own global key, so it can't pass by project entries having stopped applying. The reproduction from the issue now lands where it says it should: ```console $ WORKTRUNK_CONFIG_PATH="$tmp/wt.toml" WORKTRUNK_WORKTREE_PATH="$tmp/from-environment" \ wt switch --create feature --no-cd --no-hooks --yes --format=json {"action":"created","branch":"feature","path":"/tmp/tmp.jQiTAuNPFd/from-environment",…} ``` <details><summary>Local suite</summary> `cargo test --lib --bins` and `cargo test --test integration` are green apart from `test_copy_ignored_preserves_file_executable_permissions`, which fails in this sandbox because its umask is `0002` (file created `0664`, test expects `0644`) — unrelated to this change and not reproducible on a `0022` runner. `cargo fmt --check` and `cargo clippy --all-targets --all-features` are clean. </details> --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
2203c414f5 |
fix(docs): convert a config-example link whose text holds a bracketed span (#3731)
`wt config create --project` writes a comment into the user's `.config/wt.toml` — and `wt config create --help` prints the same text — carrying a raw, unresolvable Zola link: ``` # When many repositories share one self-hosted host, name it once in user config with a [pattern-keyed `[projects]` entry](@/config.md#user-project-specific-settings) instead of repeating this block in each repo. ``` Every other cross-reference in that file is a plain URL (`… see \`wt hook\` (https://worktrunk.dev/hook/) …`), because `transform_config_source_to_toml` converts the `after_long_help` markdown to plain text on the way into `dev/wt.example.toml`. This one link isn't converted: `convert_markdown_links_for_config` matched link text with `[^\]]+`, which stops at the first `]` — here the one closing the nested `` `[projects]` `` span — so the regex failed to match and the markdown survived verbatim. The line arrived with #3701; it's the only link in either generated example file with a bracketed span in its text. ## The fix **One rule for `]` in link text.** `ZOLA_LINK_PATTERN`, earlier in the same file, already solves this problem for the skill mirrors — it alternates a backticked code span with any non-`]`-non-backtick char, which is why `skills/worktrunk/reference/config.md` renders this very sentence with a resolved URL while the TOML example didn't. `convert_markdown_links_for_config` now uses that same class rather than a second, weaker one. Brackets in these link texts always sit inside a code span, so the class fits the shape exactly, and it covers `[[…]]` array-of-tables names as well — these sections already document `[[projects."…".post-start]]` pipelines, so a link naming one is the next form to arrive. Regenerating produces the intended form: ``` # When many repositories share one self-hosted host, name it once in user config with a pattern-keyed `[projects]` entry (https://worktrunk.dev/config/#user-project-specific-settings) instead of repeating this block in each repo. ``` **A shape the regex declines now fails loudly.** Widening the class fixes the shapes we know about; it can't fix the next one. `finalize_skill_content` already handled that risk with a guardrail — after the rewrite it scans for a stray `](@/…md` and panics with the offending line, precisely because "the regex declined on an unexpected character in the link text" is the expected failure mode. `transform_config_source_to_toml` had no equivalent, which is why this one reached `dev/wt.example.toml` and the `--help` output. That check is now extracted into `assert_no_untransformed_zola_links` and called from both surfaces, so the next unsupported shape is a test failure naming the line rather than a raw `@/config.md` target in a user's config file. ## Why nothing caught it `test_project_config_source_generates_example_toml` compares `dev/wt.example.toml` against the output of this same transform, so an unconverted link is "in sync" by construction — the sync test can't see the difference between a link that converted and one the regex declined to match. Two tests close that gap: - `test_config_markdown_links_convert_to_plain_text` asserts the transform's output directly. It fails on `main`'s regex with exactly the reported symptom, and pins the forms already working (Zola page, Zola page + anchor, absolute URL, two links on one line, the `[[…]]` array-of-tables name) plus the case that must *not* convert — a bare `` `[forge]` `` span is not a link and has to survive verbatim. - `test_untransformed_zola_link_fails_the_config_transform` covers the backstop itself: an unbalanced backtick in link text makes the rewrite decline, and the assertion turns that into a panic naming the line. ## Files - `tests/integration_tests/readme_sync.rs` — the shared link-text class, the guardrail extraction and its second call site, and both tests. - `dev/wt.example.toml` — regenerated by the sync test (one line). - `tests/snapshots/…help_config_create.snap` — the same line, as `wt config create --help` renders it. Ran locally on the final state: `readme_sync::` (15), `test_help` (47), `cargo clippy --tests --all-features`, and `cargo fmt --check`. All green; the generated files are byte-identical under the new class, so the sync tests pass without regenerating. The full gate runs in CI. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
02a12c7f59 |
feat(config): match [projects."…"] keys by pattern, and carry forge there (#3701)
## Problem Forge platform is readable only from project config (`[forge].platform`) or a brand substring in the remote hostname. A self-hosted host carrying none of `github`/`gitlab`/`gitea` — a GitLab at `git.company.example`, a company git server — needs the same `[forge]` block in every repository's `.config/wt.toml`. Closes #3678. The user-level `[projects."…"]` table is where per-repository settings already live without touching each repo, but its keys are exact, so covering a host means one entry per repository. ## Solution **Pattern keys.** A `[projects]` key containing `*` matches any run of characters, `/` included, so one entry covers every repository on a host, nested groups and all. `*` is the only metacharacter. ```toml [projects."git.company.example/*"] forge.platform = "gitlab" [projects."git.company.example/platform/*"] worktree-path = ".worktrees/{{ branch | sanitize }}" ``` Every matching entry applies, least- to most-specific, so a narrower key wins where two set the same field and leaves the rest alone. A literal key is the most specific of all; specificity is the count of non-`*` characters. Rules and rationale: the `project_match` module docstring. **`forge` on `[projects]`.** Same shape as the repository's own block, carrying `platform` and `hostname`. Both describe the host rather than the repository — which is why an SSH alias resolved through `~/.ssh/config`, a name local to one machine, belongs in user config rather than a repository's committed one. A repository's own `[forge]` still wins field by field, being the more specific of the two: a repository that sets only `platform` still takes a matching entry's `hostname`. **One resolver.** `wt list`, its statusline, `wt switch pr:`, and CI-platform detection each read project config separately, so a configured platform could resolve in one command and read `unknown` in the next. They now share `Repository::configured_forge_platform` (and `forge_hostname` for the API host). ## Approvals `approved-commands` matches by the same rules, so a pattern entry approves its commands for every repository it covers. That widening is the user's to opt into — only a hand-written key is ever a pattern: - `wt config approvals add` and the interactive prompt record under the exact project identifier, so approving in one repository never reaches another. An identifier that itself contains `*` (a starred remote URL or no-remote path fallback) is refused outright — persisting it verbatim would create an entry reads treat as a pattern; the interactive flow degrades to a warning plus a per-run approval. - `wt config approvals clear` empties only the exact entry, leaving a pattern other repositories share intact — and both its outcomes end with a hint naming any pattern entries still approving commands for the project, so a surviving approval is traceable to the hand-written entry supplying it. - `--stale` judges only the exact entry, so one repository's config can't revoke approvals the others rely on. ## Tests `project_match` unit tests cover `*` spanning `/`, `.` staying literal, specificity ordering, and the lexicographic tie-break. Config tests cover a host-wide entry applying to nested groups, exact-over-pattern precedence, field-by-field layering, hooks appending across both entries, and forge platform/hostname. Forge resolution tests cover the unbranded host, nested groups, a narrower entry winning, project config overriding, falling through to inference, and an invalid value leaving the host unresolved. Approvals tests cover pattern lookup plus the two exactness guarantees above. ## Docs `src/cli/mod.rs` (the primary source) gains "Matching several repositories with one entry" and "Forge platform and hostname" under user project-specific settings, plus a pointer from the project-config forge section. Generated mirrors and `--help` snapshots regenerated. ## Review hardening An adversarial review pass surfaced eight findings, all fixed: - **Approval widening (moderate)**: the starred-identifier refusal above. Previously such an approval persisted verbatim and silently approved its commands for every repository the star matched. - **Literal-key tie (moderate)**: a pattern whose stars all match empty (`github.com/owner/repo*`) ties the exact key on literal count and sorted after it, so its values won the fold. Literal keys now outrank any pattern outright. - **Docs vs behavior (moderate)**: the layering paragraph claimed "most specific wins" for everything; hooks and aliases actually append across matching entries (all run, least-specific first). Docs now say so, and state the forge field-by-field precedence. - **Minor**: `matches()` is a two-pointer byte glob (was a per-call regex compile, ~0.7 ms per pattern key, a few hundred calls per `wt list`), pinned by an exhaustive differential test against a reference matcher; the invalid-platform diagnostics name their two possible config homes; a root `[forge]` in user config now points at `[projects."<id>"].forge`; docs note a host-wide key should end in `/*`; `approve_command` delegates to `approve_commands`, unifying their dedup predicates. ## Relationship to #3681 This is an alternative to #3681, which adds a bespoke `[forge-hosts]` section for the same issue. Both can't land — they'd be two ways to write one sentence. This one puts the setting in the table that already carries per-repository user config, and the pattern keys are reusable for the workspace-scoped ask in #3654 where repositories share a host or namespace. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
0f2d562541 |
fix(forge): classify a forge by the brand in the hostname, not by DNS label (#3673)
Reverts the branded half of the exact-label classifier and deletes the diagnostic built to explain it. `github-enterprise.acme.com`, `mygithub.com`, `gitlab-internal.company.com`, and the `github-personal` SSH alias resolve to their forge again, so CI status, `wt switch --prs`, and `repo.provider` work with no config. ## Why the label boundary goes It looked like an ownership check and wasn't one. An attacker controls their own DNS, so `github.attacker.example` has the exact label `github` and classified fine; `gitlab.evil.co.uk` likewise. What the rule actually excluded was the self-hoster who put the brand in a hyphenated name. It failed open for the adversary and closed for the customer. The residual case for it doesn't survive either. The hostname comes out of the user's own `.git/config`, and whoever can put a host there can put code there too — the trust decision happens at clone time, and by the time worktrunk reads the remote the user is already building from it. All the classification decides is which forge CLI (`gh`, `glab`, `tea`, `az`) runs against it. So the rule is recall-first: any host carrying `github`, `gitlab`, or `gitea` matches, first match winning. The cost is a host that merely sounds like a forge getting a forge CLI run at it, which surfaces as that CLI's error rather than as silence — the better of the two failures, and `forge.platform` overrides it. ## What stays Azure DevOps keeps suffix matching on its two service domains, and for a reason unrelated to security: those are service domains rather than a brand in the host, so every real hosted instance already matches, and the on-prem edition carries neither string. `dev.azure.com.attacker.example` and `evil-visualstudio.com` are outside the domains and carry no brand to fall back on, so they stay unclassified. Userinfo still resolves to the network host, so `https://github.com@attacker.example/…` is `attacker.example`. ## What goes `LegacyForgeAlias`, `Repository::legacy_forge_alias`, `legacy_forge_alias_diagnostic`, and its three emit sites in `wt list`, `wt switch --prs`, and `wt config show --full`. Every host the diagnostic fired on now classifies, so it could only ever return `None`. A host with no brand at all still reaches the existing generic hint, which is the right message there — there is no platform to infer. The end-to-end warning-dedup test goes with it, since no warning is raised from both the collect and `--prs` threads any more; `stash_warning_preserves_order` keeps the mechanism covered. ## Docs `## Forge platform` in `src/cli/mod.rs` described the override as being for SSH aliases and self-hosted instances, which now detect on their own. It states the rule and scopes the override to hosts carrying no brand — a Forgejo instance at `forge.example.com`. Mirrors, `dev/wt.example.toml`, and the two `config` help snapshots regenerate from it. --- The branch's history has a false start — the first commit widened the diagnostic, the second deletes it in favour of relaxing classification — plus a merge of `main` after v0.71.0 shipped. The net diff is the second approach; it all squashes on merge. Also supersedes [#3672](https://github.com/max-sixty/worktrunk/pull/3672), the triage bot's PR for the same issue — it widens the diagnostic rather than removing the need for one, so it should be closed too. Closes #3671 > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
63071709af |
feat(config): deprecate list.task-timeout-ms (#3615)
Removes the `[list] task-timeout-ms` per-task command timeout and the thread-local machinery behind it. The key stops having any effect immediately; a config that still carries it loads, warns, and is stripped by `wt config update`. The timeout killed any git command that outlived its budget, on every collect worker, through a thread-local that `Cmd::run` consulted on each invocation and clamped an explicit `.timeout()` against. Progressive rendering removed the reason for it: `wt list` and the picker paint from local data and stream results in behind the frame, so no single git command can hold up the first paint. What it bounded instead was completion, which `[list] timeout-ms` already bounds directly, and the drain falls back to a hardcoded 120s `DRAIN_TIMEOUT` whenever that is unset (`collect/mod.rs` `drain_deadline`), so removing this cannot introduce an unbounded wait. Both keys default to unset, so the default path never had a per-task timeout at all. `[list] timeout-ms`, the wall-clock budget for the whole collect phase, stays. It is the surviving knob and the more direct expression of the same goal. With the thread-local gone, the two-source `min()` in `Cmd::run` collapses to the command's own `self.timeout`, so every explicit `.timeout()` caller keeps its bound unclamped: `PROBE_TIMEOUT` in `git/reap.rs`, the fsmonitor stop/lsof bounds in `git/remove.rs`, the version check in `config/show.rs`, and `REMOTE_DETECTION_TIMEOUT`. ## Deprecation A `Structural` row in `DEPRECATION_RULES` strips the key from `[list]` in both the section and inline forms, top-level and per-project, following the `[switch.picker] timeout-ms` precedent (also a strip with no equivalent key to migrate into): ``` ▲ User config: list.task-timeout-ms is no longer used — list.timeout-ms bounds the collect phase ``` The env overlay (`WORKTRUNK__LIST__TASK_TIMEOUT_MS`) and `--config-set` route through the same rule and migrate silently, since neither layer has a file for `wt config update` to materialize. Neither errors. ## Testing New unit tests cover detection and migration for the section, inline, and per-project forms plus the warning text, and two cases join the `test_warning_fires_iff_update_changes` battery that pins the warn-iff-update-changes invariant. The three integration tests that exercised the feature are gone; `Cmd::timeout` keeps its own coverage in `shell_exec.rs`, so dropping the four thread-local unit tests loses nothing for the surviving path. Verified end to end that a config carrying the key loads, warns, and has it stripped by `wt config update` with sibling keys intact. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b00e6b612a |
fix(plugin): scope OpenCode marker commands to the plugin's directory (#3554)
Follow-up to #3552, as invited there. The OpenCode host injects the process-global Bun shell as `$` — a single instance shared by every plugin loaded in the process. The activity-marker plugin issued its commands unscoped, so each one ran in whatever the process-wide cwd was at spawn time; under concurrent parallel-agent sessions that means a marker write can land in a directory other than the one the event came from. This pins every command with the promise-level `.cwd(directory)`, per the review notes on the issue: - `directory` pulled from `PluginInput` (the factory now destructures `{ $, directory }`), and `session.deleted` scopes the same way as `set`. - Promise-level `.cwd(directory)` only — the instance-level `$.cwd(...)` would mutate the shared default for every plugin in the host process, so the call-site comment documents why the per-command form is load-bearing. Scope note, consistent with the issue discussion: this closes the "runs `wt` in the wrong directory" mechanism on the plugin side; it does not claim to explain the parent-directory `rename(2)` from the original report. The host-level question (injecting a scoped shell instead of the global one) belongs upstream with opencode — happy to raise it there as discussed. Co-authored-by: 4i3n6 <4i3n6@users.noreply.github.com> |
||
|
|
568b6de85f |
docs: consolidate duplicated explanations and trim slop (#3494)
Sweep of the docs for repetition and filler, from an audit of the hand-authored pages, the command pages' source in `src/cli/mod.rs`, and the plugin skill. Net −574 lines; every cut either had a surviving canonical home or restated an adjacent sentence. **One home per mechanism** (other mentions now link to it): - `template-append` — the LLM commits guide owns the rendering mechanism; the user- and project-config sections keep the key, an example, and what is unique to them (the project approval gate and the only-`template-append`-from-project scoping). Was explained in full in three places. - `-vv` diagnostic files — `wt config state logs` owns the four file descriptions; the FAQ summarizes in one sentence and links. - fsmonitor/trash cleanup — the FAQ's two overlapping `wt remove` bullets merge into one that distinguishes own-daemon teardown from the orphan sweep; troubleshooting.md's restatement compresses to a pointer, keeping its unique wedged-daemon-on-live-worktree guidance. - copy-ignored built-in excludes — the `wt step copy-ignored` page owns the directory list (previously enumerated 4×); the config sections state the rule and link. - hook-types table — `wt hook` owns it; the extending guide replaces its verbatim copy with a sentence. - LLM tool commands — unchanged, deliberately: the apparent hand-synced duplication between llm-commits.md and the config example is already machine-pinned by `test_llm_docs_commands_match_config_example`, so it cannot drift. **Cut-over debt**: the deprecated `wt config state ci-status` section shrinks to a deprecation pointer — its status table, fetch order, and caching notes all duplicated the `wt list` CI-status section. **Structure**: `wt step copy-ignored`'s "Features" list dissolves into the sections that owned its facts (excludes → "What gets copied", reflink → "Performance"); the four trailing "Note: This command is experimental…" lines go (the `[experimental]` badge already appears twice per section); shell-integration.md described the directive-file mechanism twice and now describes it once. **SKILL.md** drops from 339 to 181 lines: the permission-model section duplicated the config-types bullets, the hook-type mapping appeared twice, and the "Determining Which Config to Use" / "Validation Before Adding Commands" scaffolding enumerated judgments an agent makes on its own. The approvals-escalation and agent-handoff sections are untouched. **Deliberately not addressed**: `wt list`'s schema 1 JSON tables (still the default schema; the wholesale deletion lands when the default flips) and corpus-wide em-dash density (a house-style decision, not a per-page fix). Generated mirrors (docs command pages, skill references, plugins mirror, `dev/*.example.toml`, help snapshots) regenerated via `test_docs_are_in_sync` and `cargo insta`. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Sonnet 5 <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> |
||
|
|
c532d2dabd | docs(llm-commits): bump Codex commit-generation model to gpt-5.6-luna (#3430) | ||
|
|
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>
|
||
|
|
fec7edc72c |
feat(list): force a listed column on past the --full preset gate (#3295)
## Problem
A `[list] columns` entry could narrow the table but never *force a
column on* past its gate. Listing `ci` without `--full` still hid the CI
column; the gate won. But `--full` is a column-selection preset — it
bundles CI/summaries into the default table — not a feature-approval
switch, so "I listed `ci`" being silently overridden by "you didn't pass
`--full`" is surprising.
## Solution
A listed column now overrides the **preset** gates while still
respecting the **data-source** gates. The distinction is a new
`ColumnSource` (`Default` | `Listed`) threaded through the task planner:
- **Preset gates** — `--full` (`show_full`), `[list] summary`
(`summary_enabled`) — bundle columns into the *default* table. A
`Listed` column overrides them: listing `ci` shows it without `--full`;
listing `summary` shows it whenever an LLM command exists.
- **Data-source gates** — `[commit.generation]` (`has_llm_command`),
`[list] url` (`has_url_template`) — are hard prerequisites. They apply
even to a `Listed` column: a listed `summary` with no LLM command, or
`url` with no template, stays hidden, because listing can't conjure data
that isn't configured.
`Default`-source behavior (the default table, the picker, and `--format
json`, which all plan over `all_columns`) is byte-for-byte unchanged —
`column_renders(kind, Default, gates)` reduces to the old function.
### Before / after
`columns = ["branch", "ci"]`, no `--full`:
```
before: Branch # ci dropped — gated by --full
after: Branch CI # listed → forced on
```
### Picker consistency
The picker plans tasks from `all_columns` (it needs every column's data
for its preview tabs) but renders the `[list] columns` selection. So it
now plans the **union** of its preview set and the selection's forced-on
columns, keeping its table identical to `wt list`'s. This matters for
one case: a listed `summary` with an LLM command configured but `[list]
summary` off — `wt list` shows it, and without the union the picker
would have hidden it.
## Testing
- **Unit** (`test_required_tasks_for_render`): both sources × every gate
combination — listed-`ci`-without-`--full`, listed-`summary` overriding
the presets, listed-`summary` still needing an LLM command, listed-`url`
still needing a template, and `Default`-source parity.
- **Integration**
(`test_list_config_listed_column_overrides_full_gate`): end-to-end —
`columns = ["branch", "ci"]` renders the `CI` header with no `--full`,
where the default set hides it. Asserts on column enrolment (the
rendered header), independent of whether `gh` is on `PATH`.
- **Layout**: the filter test was repurposed
(`test_layout_renders_column_iff_task_planned`) to test "render iff task
planned" in isolation, since the planner no longer produces the old
"listed but unplanned `ci`" pairing.
- Full `pre-merge` gate (all suites, fmt, clippy, docs-sync, PTY picker
snapshots) green after merging `main`. The picker's 44 PTY tests pass
with no snapshot churn.
## Docs
The `[list] columns` help prose was rewritten ("Listing a column forces
it on, space permitting…") in the `src/cli/mod.rs` source and
regenerated across all four render contexts (terminal `--help`,
`config.example.toml`, `docs/content/config.md`, skills reference). The
`CLAUDE.md` network-access inventory also now notes that a listed `ci`
column reaches the wire without `--full`.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d5be156092 |
perf(list): plan background tasks from the columns being rendered (#3274)
## Problem `[list] columns` filtered purely at the layout layer. A narrowed selection like `columns = ["branch", "path"]` hid the unselected columns but still ran every per-worktree git task — `git status`, working/branch diffs, ahead/behind walks, merge-conflict probes — then threw the results away. On the kind of repo that motivated #3133 (27 dirty worktrees) that discarded work is the bulk of the wall-clock cost, so a "just branch and path" view was no faster than the full table. This was flagged in the [trace-based diagnosis on the issue](https://github.com/max-sixty/worktrunk/issues/3133#issuecomment-4816169750): `columns = ["branch","path"]` and the default set produced an **identical** command list. @max-sixty [confirmed](https://github.com/max-sixty/worktrunk/issues/3133#issuecomment-4819594461) it's a bug and asked for the fix. ## Solution `wt list` decides which background tasks to run in **one canonical stage**, driven by the columns it will render. The plan flows through the whole pipeline as a **positive set of tasks to run** — no skip-list, no inversion, no blanket default. `collect` computes `tasks` = the union of each rendered column's `required_tasks()`, gated by the conditions that turn a column off (`--full`, `[list] summary` + `[commit.generation]`, a url template). The spawn loop fires exactly that set; the layout renders exactly the columns it feeds. The rendered set is the `[list] columns` selection for the table; the picker and `--format json` plan from every column, because their consumers — the picker's preview tabs, JSON's every-field contract — need the full data set, not just what renders. This started as additive pruning layered on the old `skip_tasks` denylist; review (thanks @max-sixty) pushed it to the canonical, positive form: - **One column→task map.** `ColumnSpec::requires_task` is deleted; `ColumnKind::required_tasks()` is the single source, driving both the spawn plan and the layout visibility filter (`renders_given_run` — a column renders iff one of its tasks is in the plan). The two maps can no longer drift, so the reconciliation test is gone; the `cover_every_task` drift guard stays and gains teeth (an unconsumed task would never run, not merely be computed and discarded). - **A positive run set, end to end.** `CollectOptions` carries `tasks` (the run set), not a skip set — `collect` threads the plan straight into the spawn loops, the layout, and `max_pr_number` with no complement step. - **No blanket default.** `CollectOptions::for_columns(columns, gates)` derives the plan; nothing hand-writes a task set. The statusline declares what it renders (the full column set under full gates, no LLM summary) instead of leaning on "default everything". The picker rides `show_full` on `ShowConfig::Resolved`. - **One mechanism for the summary.** The per-item `SummaryGenerate && llm.is_none()` spawn guard is dropped: the column plan is the single authority on whether the summary runs, and `SummaryGenerateTask` already returns a clean error on a missing command. A branch/path `ls` alias over many dirty worktrees now runs no `git status`, diffs, or ahead/behind walks (#3133), while a column gated off elsewhere stays off. Behaviour is otherwise unchanged across default/selection × full/non-full × table/JSON/picker/statusline — no rendered-output snapshots move (the `help_config_*` snapshots move only from the columns-doc rewrite). ## Testing - Planner + filter units: `test_required_tasks_for_render` (the default set needs every task; a branch/path or custom-column view needs none; `Status` pulls in every status-feeding task; the gates drop `ci`/`url`/`summary` even when those columns are explicitly selected) and `test_renders_given_run` (the "render iff a task is planned" filter, including `Status` surviving while any signal runs). - `test_required_tasks_cover_every_task` drift guard retained: the union of `required_tasks()` across all built-ins equals the full `TaskKind` set, so no task can fall out of the now-load-bearing map. - End-to-end: `test_list_config_columns_prune_unused_tasks` (default set runs `git status --porcelain`; `columns = ["branch", "age"]` runs none) and `test_list_json_ignores_columns_selection` (`--format json` emits every field regardless of selection). - Reviewed by independent finder passes (line-by-line + removed-behavior, cross-file + picker/JSON equivalence, altitude + conventions) — no findings; each confirmed the task set is preserved bit-for-bit. Full `pre-merge` gate (all suites, fmt, clippy, docs-sync, PTY picker snapshots) green after merging `main`. Closes #3133 > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Maximilian Roos <m@maxroos.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. |
||
|
|
16deb5e6c4 | fix: use --safe-mode for Claude commit.generation to preserve apiKeyHelper auth (#3170) | ||
|
|
0fc757f08d |
feat(list): add [list] columns to select and order built-in columns (#3141)
Adds `[list] columns` — an ordered selection of which columns `wt list` renders. A non-empty list is exhaustive: only the listed columns appear, in the given order (a subset and/or reorder). Both built-in columns and `[list.custom-columns]` headers are selectable in one list. This is the follow-up to the merged `--config-set` work (#3138): a reduced "fast" view is `wt --config-set 'list.columns=[…]' list`, or an alias over it. ## Usage `columns` takes a TOML array in config files or `--config-set`: ```toml [list] columns = ["branch", "status", "ci", "path"] ``` Built-in names: `branch`, `status`, `working-diff`, `ahead-behind`, `branch-diff`, `summary`, `upstream`, `ci`, `path`, `url`, `commit`, `age`, `message`. A custom column is named by its `[list.custom-columns]` header, so a selection can mix both (`columns = ["branch", "Ticket", "ci"]`); a built-in wins over a custom header that collides with its name. When `columns` is set it is exhaustive — a custom column omitted from a non-empty list is hidden. Omit `columns` entirely to keep the default set, where custom columns append automatically. `WORKTRUNK__LIST__COLUMNS` is deferred for now: the env overlay only delivers scalars, so the env var is rejected with a warning and ignored rather than silently dropped. An inline TODO plans to add it later by parsing the value as TOML (matching the array form), not by splitting on commas. ## Design - **Drop-priority threading by filter, not renumber:** selection filters the `COLUMN_SPECS` candidate list and overrides only the display sort; `base_priority` is left untouched, so the Summary drop-loop arithmetic and the `EMPTY_PENALTY` tiers stay correct (the allocator sorts by priority, never indexes it). Reorder is display-only — narrow-terminal drops follow the built-in importance order. - **Validation at the `wt list` edge** (`parse_selected_columns`), mirroring `[list.custom-columns]`, because `ColumnKind` lives in the bin crate and is unreachable from the config crate. Unknown/duplicate names error with the valid-name list; the picker degrades, foreground aborts. - Applies to `wt list` and the `wt switch` picker (cosmetic only — the picker fuzzy-searches a separate field, so hiding Branch never breaks search). Statusline and `--format json` are unaffected. Gated columns (`ci` needs `--full`, `summary` needs `[commit.generation]`) stay hidden; custom columns append when no selection is set. ## Key files - `src/commands/list/columns.rs` — built-in/custom name ↔ `ColumnKind`, `parse_selected_columns`, completeness test - `src/commands/list/layout.rs` — `ColumnSelection`, candidate filter (built-in + custom), display sort - `src/config/user/sections.rs` — `columns` field (plain `Vec<String>`), merge (replace-wholesale) - `src/commands/list/collect/mod.rs` — parse + thread the selection Supersedes #3065 (the earlier `[list.columns]`-as-bool-map design). ## Testing Unit tests (parser incl. built-in/custom mix and collision shadowing, completeness guard over `COLUMN_SPECS`, layout cases incl. gated-column-stays-hidden, custom include/hide, and drop-by-base-priority), integration tests (config file, custom-column select, `--config-set`, unknown-name error, env-var-not-yet-supported), plus regenerated config-doc mirrors and help snapshots. Full pre-merge gate green: 4051 tests, clippy, fmt, doc, doctest, lychee. > _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> |
||
|
|
0d9b9b8082 |
feat(commit-generation): deprecate commits squash template variable (#2985)
|
||
|
|
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>
|
||
|
|
3a4ae84715 | Expose commit bodies to squash templates (#2983) | ||
|
|
7d16e63fad |
refactor(commit-generation): drop the CLAUDECODE= nesting workaround (#2979)
worktrunk stripped `CLAUDECODE` before spawning the
`[commit.generation]` LLM command, and the recommended Claude Code
command carried a leading `CLAUDECODE=`, both to get past Claude Code's
nested-session check that rejected `claude -p` launched from inside
another Claude Code session.
That check is gone. It's absent from the `claude` binary across every
current build (2.1.157 through 2.1.162), and `CLAUDECODE=1 claude -p …`
runs cleanly (exit 0, empty stderr). So the workaround is no longer
needed.
This drops it: `src/llm.rs` no longer calls `.env_remove("CLAUDECODE")`,
and the recommended command loses the `CLAUDECODE=` prefix in the source
of truth (`dev/config.example.toml`), the `wt config --help` text, and
the Taskfile bench command. Docs, the skill reference, and the config
help snapshots are regenerated to match.
Caveat: there's no published guarantee the nested-session check stays
gone, and a Claude Code old enough to still have it (older than roughly
late May 2026) would again block nested commit generation. Since the
check is absent from the implementation across all current builds, that
risk is low.
Tests: 1236 lib + 693 integration pass; fmt and clippy clean.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d65d5028fa | docs(commit-generation): bump Codex model to gpt-5.4-mini (#2949) | ||
|
|
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> |
||
|
|
ec62580c29 |
revert(hooks): keep docs on pre-start/post-start; code accepts both (#2857)
Per @max-sixty's [direction in #2838](https://github.com/max-sixty/worktrunk/issues/2838#issuecomment-4509447593): revert the docs portion of #2840 and keep the code. Docs continue to recommend `pre-start`/`post-start`; both names work in code so anyone who already followed the briefly-changed docs (e.g. @EcksDy) isn't stranded once a release ships these aliases. ## User-visible — back to `pre-start`/`post-start` - README, docs site, skill mirrors, `dev/*.example.toml`, `plugins/worktrunk/README.md`, `flake.nix`, `.config/wt.toml` - `src/cli/mod.rs` / `src/cli/config.rs` / `src/cli/step.rs` / `src/help.rs` after_long_help and example snippets — and the auto-synced `docs/content/` and `skills/worktrunk/reference/` mirrors - `wt hook --help` canonical subcommand names; completion advertises `-start` only - `HookType` Display via strum, serde `rename`, and clap `ValueEnum` name — all `pre-start`/`post-start`. The Rust variant identifiers stay `PreCreate`/`PostCreate` (internal; we already paid for that rename in #2840, and now the eventual flip is a Display-only change) - `HooksConfig` serde canonical fields ## `*-create` still works (kept code) - `wt hook pre-create` / `post-create` — CLI alias on the canonical subcommand - `pre-create` / `post-create` in config: top-level, `[hooks.*]`, and per-project, in string, `[table]`, and `[[array-of-tables]]` form. Mechanism: serde `alias = ...` on the field, plus a silent in-memory rename in `migrate_content()` so the round-trip in `unknown_tree` doesn't flag table forms as schema-unknown. - The pre-0.32.0 `post-create` fatal-load-error machinery stays removed — the name is reclaimed, and both forms load without error. ## Smaller bits - `valid_user_config_keys()` / `valid_project_config_keys()` append `pre-create` / `post-create` so the unknown-field round-trip skips them. `test_valid_*_keys_all_deserialize` skips both aliases (they can't sit alongside the canonical without a duplicate-field error). - `DEPRECATED_SECTION_KEYS` drops the `pre-start`/`post-start` entries #2840 added — `pre-start`/`post-start` are canonical again. - `find_pre_start_from_doc` / `find_post_start_from_doc` / `find_renamed_hook_key` / `is_non_empty_item` / `migrate_start_hooks_doc` and their tests are removed; the migration direction flips via a new `migrate_create_hooks_doc` (silent, mirrors the prior shape). - Test files `e2e_shell_post_create.rs` and `post_create_commands.rs` rename back to `_post_start_` (via `git mv`, so the rename shows as a rename). ## Testing `cargo run -- hook pre-merge --yes` — 3806 tests pass; the 10 failures are all `case_4` of `shell_wrapper::unix_tests::*` (nu-shell case; `nu` isn't installed in this runner; same failures occur on `main`). Also manually verified that a fresh `wt switch --create` against a project config with `[post-create]` loads cleanly with no unknown-field warning and the hook fires as `post-start`. ## Follow-up Per @max-sixty: in a couple of weeks, once a release with both-names-work is out and users have had a chance to upgrade, the docs flip is straightforward (most of it is in `src/cli/mod.rs`'s `after_long_help` and the doc-sync test propagates). Re #2838. Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d7e3f88422 |
feat(hooks): rename worktree-creation hooks to pre-create/post-create (#2840)
Phase 1 of the staged hook rename tracked in #2838: the worktree-creation hooks `pre-start`/`post-start` become `pre-create`/`post-create`. The old names keep working with no deprecation warning yet (Phase 2, months out, adds the warning). ## What changes - `pre-create`/`post-create` are canonical everywhere: the `HookType` enum, the `HooksConfig` serde fields, the `wt hook` CLI, completion, and all docs. - Old names keep working: `migrate_content()` rewrites `pre-start`/`post-start` config keys to `-create` before serde, and `parse_hook_type` accepts the old CLI names as silent aliases. `wt config update` rewrites them on disk; `wt config show` shows the migration diff. `wt hook <type>` execution and `wt hook show` both accept the old names; completion and `--help` advertise only the canonical names. - `detect_deprecations()` flags the old keys so `update`/`show` act on them, but `format_deprecation_warnings()` stays silent (Phase 2 adds the warning). A new empty-warnings guard in `check_and_migrate` keeps a `-start`-only config from emitting a stray hint. - The dead pre-0.32.0 `post-create` machinery is removed: the fatal `POST_CREATE_REMOVED_MSG` load error, the vestigial `HooksConfig.post_create` merge-fold, and `find_post_create_from_doc`. `post-create` is reclaimed as the canonical background creation hook. ## Semantic flip Before v0.32.0, the key `post-create` named a *blocking* hook. It now names the *background* one. Since v0.44.0, a pre-0.32.0 `post-create` config is a fatal load error on the `check_and_migrate` paths: `ProjectConfig::load` and user/system config loading, which fire on essentially every `wt` command. A repo carrying one has been unusable ever since. The one path that skips that check is `project_config_at_ref` (the base-ref read behind `wt switch --create`), which applies only structural migration. A pre-0.32.0 `post-create` surviving solely on a base ref, never checked out into a worktree, would now load as a background hook rather than folding into the blocking `pre-start`. That edge case is accepted: once `post-create` is valid again, reclaiming the name and detecting the dead key are mutually exclusive. ## Reviewing this diff 205 files, but the substance is ~36 files under `src/`. The rest is regenerated snapshots and auto-synced doc mirrors. Start with: - `src/config/deprecation.rs` — detection (`find_renamed_hook_key`), migration (`rename_hook_key`), removal of the fatal block, the empty-warnings guard, and the `DEPRECATED_SECTION_KEYS` entries that stop unknown-field detection from flagging the migrated keys. - `src/config/hooks.rs`, `src/git/mod.rs` — the serde field and enum renames. - `src/config/project.rs` — `ProjectConfig::load` deserializes `check_and_migrate`'s migrated content, so a current-worktree config using the old keys loads into the canonical fields. - `src/cli/hook.rs`, `src/commands/hook_commands.rs`, `src/completion.rs`, `src/main.rs` — the CLI alias layer; `wt hook show` accepts the old type names as hidden value-parser aliases. - `src/cli/mod.rs` — the `wt hook` docs, including the soft-deprecation note linking #2838. The ~93 modified snapshots also pick up deterministic env-block lines (`GIT_*: ""`, `LLVM_PROFILE_FILE`) that pre-existing snapshots already carry. That is stale-snapshot drift surfaced by the regeneration, not a behavior change. ## Testing Full suite green (3799 tests). New coverage: `snapshot_migrate_start_to_create` (migration preserves value shape and position), `test_deprecated_start_hook_key_runs_silently` and `test_standalone_hook_start_alias_runs_silently` (old config and CLI names run with no warning), `test_config_show_displays_start_hook_migration` (`config show` reveals the diff without an "unknown field" warning), and `test_hook_show_accepts_deprecated_start_hooks` (a current-worktree config using the old keys loads, and `wt hook show` takes both the canonical and the deprecated type arguments). Part of #2838. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
7f36ece541 |
feat(config): show project identifier in wt config show (#2827)
## Summary Closes #2826. Surfaces the project identifier (`<host>/<owner>/<repo>` from the primary remote, or the canonical repo path as a fallback) in the PROJECT CONFIG section of `wt config show`, so users can find the right key for `[projects."..."]` in their user config without hand-deriving it. Also adds an `identifier` field to `wt config show --format=json`. Docs expand the *User project-specific settings* section with the new how-to-find-it note plus single/concurrent/pipeline hook examples — the reporter's TOML showed both pieces were unclear, even though the array-of-tables form they tried was correct. ## Drive-by `test_config_show_github_remote` and `test_config_show_gitlab_remote` used `git remote add origin …` against the standard fixture, which already has an `origin`. `git remote add` failed silently (output isn't checked) and the test snapshotted the fixture's original `../origin.git` URL instead of the platform URL it was meant to exercise. The new Identifier line in the regenerated snapshot made this visible; both tests now use `set-url`, matching the existing `test_config_show_full_gitea_remote`. ## Test plan - [x] `cargo run -- hook pre-merge --yes` (passes; nushell-only `shell_wrapper::*::case_4` failures are CI-environment, not regressions) - [x] `cargo test --test integration` — 1738/1738 passing - [x] `cargo test --lib --bins` — passing - [x] `cargo test --test integration test_docs_are_in_sync` - [x] Verified manually: `wt config show` in a repo with a `github.com:max-sixty/worktrunk.git` remote prints `Identifier: github.com/max-sixty/worktrunk` --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f522357e11 |
feat(commit): experimental project-level commit-message guidance (#2774)
## Summary
- Adds `[commit.generation] template-append` to **both** user config and
project config (`.config/wt.toml`). It *adds to* the commit/squash
prompt rather than replacing it (`template` still replaces). One field
covers both commit and squash.
- Each fragment is rendered as its own minijinja template with the same
variable context as the main template, then emitted in a
provenance-labeled block: `<user-guidance>` (the developer's own config
— no approval) followed by `<project-guidance>` (repo-supplied — gated).
Default templates render each block conditionally; `{{ user_guidance }}`
/ `{{ project_guidance }}` are exposed for custom user templates.
- The project fragment keeps the first-time approval gate — same
one-shot gate as project hooks. `wt merge` bundles it into the existing
hook-approval batch so the user sees one combined prompt. Declining is
non-fatal; the LLM runs with just the user fragment (if any). Skipped
automatically when no LLM command is configured.
- Marked experimental in CLI help, docs, and the user/project config
docstrings.
Discussion: #2758 (comment
[4454619614](https://github.com/max-sixty/worktrunk/issues/2758#issuecomment-4454619614))
## Test plan
- [x] Unit tests for `commit_template_append()` and the user-side
accessor (trim / empty / unset)
- [x] `Merge` test for user `template-append`
- [x] Snapshot tests confirming the default commit and squash templates
render the `<project-guidance>` block
- [x] Unit tests for user-side append: renders into `<user-guidance>`,
ordered before `<project-guidance>`, blank-is-unset, minijinja variable
expansion
- [x] PTY integration tests for the approval flow (accept / decline /
merge bundling), asserting `<project-guidance>`
- [x] `cargo run -- hook pre-merge --yes` — 3708 tests passing, clippy +
pre-commit clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
00c4508671 |
refactor(ci): move CiPlatform to the lib crate, cache on RepoCache, drop the "override" framing (#2692)
Follows up on #2686 (the original "dedup the warning" fix), which parked the configured-platform state in a process-wide `OnceLock` because `CiPlatform` lived in the binary crate and `RepoCache` in the library crate. **Move `CiPlatform` to the lib crate.** It's now `worktrunk::git::CiPlatform` (a new `src/git/ci_platform.rs` module), with `Repository::ci_platform(remote_hint)` replacing the free `platform_for_repo` and a private `Repository::configured_ci_platform()` reading `forge.platform` / `ci.platform`. The configured value caches on a `OnceCell` field of `RepoCache` instead of a process global — same once-per-`wt list` warning dedup, but the repo-scoped data no longer lives in a process static. `src/commands/list/ci_status/platform.rs` is now pure forge dispatch (`gh` / `glab` / `az`) over a `CiPlatform`; the inherent `detect_*` methods became free functions so the enum could move without dragging the bin-crate backends along. **Drop the "override" framing.** `forge.platform` / `ci.platform` is now described everywhere as "the platform, falling back to URL detection when unset" rather than "overriding detection" — config docs (`## Forge platform`), the `ProjectForgeConfig` / `ProjectCiConfig` struct and accessor docs, `wt config state ci-status` help, and a few test names (`test_list_full_with_configured_platform_github`, `test_list_full_with_invalid_configured_platform`, `test_switch_pr_gitea_forge_platform`). Genuine "override" uses elsewhere (config-path/env-var overrides, `wt config state default-branch set`) are untouched. **Fix a pre-existing bug.** `forge.platform = "gitea"` is a valid value — the `wt switch pr:` shortcut uses it to pick `tea` — but worktrunk fetches CI status only from GitHub/GitLab, so `wt list` used to print `Invalid CI platform in config: 'gitea'. Expected 'github' or 'gitlab'.` for it. `configured_ci_platform()` now recognizes `gitea` as a known forge it just doesn't show CI for: no warning, blank CI column. Added `test_list_full_with_gitea_forge_platform` (asserts blank CI and empty stderr). Branch is merged with `main`, so the dispatch and detection handle the new `AzureDevOps` variant alongside GitHub and GitLab. `cargo run -- hook pre-merge --yes` passes. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1ca73ab52c |
feat: experimental Azure DevOps support (#1256)
## Summary
Adds **experimental Azure DevOps support** alongside the existing GitHub and GitLab integrations:
- `wt switch pr:<N>` resolves Azure DevOps PRs via the `az` CLI (auto-detected from `dev.azure.com` / `ssh.dev.azure.com` / `*.visualstudio.com` remotes, or pinned via `[forge] platform = "azure-devops"`).
- `wt list --full` surfaces Azure DevOps PR and pipeline CI status.
- `wt config show --full` reports `az` install/auth state when Azure DevOps is the detected platform.
GitHub still wins in mixed-remote setups; `forge.platform` is the override. Requires the `azure-devops` CLI extension (`az extension add --name azure-devops`).
## Context
Originally proposed by @mikeyroush; reimplemented against current `main` to pick up the `[forge]` config section, `url.insteadOf` fallback, the `handle_switch` consolidation, and the new Gitea provider that all landed after the original branch was opened. The dispatch path (`choose_pr_provider`) is shared with GitHub/Gitea — Azure is just a fourth provider in the same priority chain.
Implementation notes worth a reviewer's attention:
- Azure DevOps URLs don't fit the standard `host/owner/repo` shape (`dev.azure.com/{org}/{project}/_git/{repo}`), so `Repository::find_remote_for_azure` matches on `org` + `project` + `repo` instead of the owner-based path used for the other forges.
- Pipeline/PR web URLs are constructed from `org/project/build-id`, not the API's `url` field (which is a REST endpoint).
- `*.visualstudio.com` legacy hosts encode the org in the hostname; the URL helpers handle both shapes.
Fixes #1144
## Test plan
- `cargo run -- hook pre-merge --yes` — 3631 tests pass, clippy + fmt clean
- Unit tests cover the host-aware URL helpers, `find_remote_for_azure` (all URL shapes + the same-org/different-project collision case), and `choose_pr_provider` dispatch
- Integration tests bring Azure to parity with the other forges (see Coverage):
- 13 `test_switch_pr_azure_*` tests mirroring the Gitea suite — same-repo, fork, `*.visualstudio.com` host, create/base conflicts, not-found, az-not-installed, `forge.platform` override, invalid JSON, generic server error, auth error, missing `azure-devops` extension, undeterminable org/host
- 9 `test_list_full_with_azure_*` tests covering `detect_azure_pr` (conflicts, queued, stale, retriable error) and `detect_azure_pipeline` (passed/failed/running, stale, no runs, retriable error)
- Manual validation: `wt switch pr:<N>` and `wt list --full` against an Azure DevOps repo
## Coverage
The `az`-shelling code (`fetch_pr_info`, `detect_azure_pr`, `detect_azure_pipeline`) is now exercised by integration tests via new `setup_mock_az*` helpers (modeled on `setup_mock_gh` / `setup_mock_glab`) — covering the happy paths plus the not-found / auth / extension-missing / generic-error / retriable-error branches. The non-`az` parts (URL parsing, provider dispatch, remote matching) remain unit-tested.
|
||
|
|
7fca05441e |
feat(switch): experimental Gitea PR support via pr: shortcut (#1320)
Add experimental Gitea PR support to `wt switch pr:<number>`.
The `pr:` syntax already resolved GitHub PRs; this teaches it to also
resolve Gitea PRs via the `tea` CLI. GitLab continues to use `mr:`.
## Dispatch
`pr:N` now goes through `choose_pr_provider`:
1. `[forge] platform` in `.config/wt.toml` if set (`github` / `gitea` /
`gitlab`)
2. Primary remote URL detection (host contains `github` / `gitea` /
`gitlab`)
3. CLI auth lookup: if `tea` is configured for this host (per
`~/.config/tea/config.yml`) but `gh` is not (per `gh auth token
--hostname <host>`), pick Gitea
4. Default to GitHub
There is no longer an "ambiguous" fallback that tries both providers and
wraps both errors — users on self-hosted Gitea instances either run `tea
login add <host>` (auto-detected) or set `[forge] platform = "gitea"`.
## New code
- `src/git/remote_ref/gitea.rs` — `GiteaProvider` implementing
`RemoteRefProvider` via `tea api repos/<owner>/<repo>/pulls/<n>`; reuses
the shared `cli_api_error` / `run_cli_api` helpers.
- `src/git/remote_ref/info.rs` — `PlatformData::Gitea { host,
head_owner, head_repo, base_owner, base_repo }`, wired into
`source_ref()`, `prefixed_local_branch_name()`, and `find_remote()`.
- `src/git/url.rs` — `GitRemoteUrl::is_gitea()`.
Shared helpers introduced in this PR:
- `mod.rs::extract_host_from_html_url()` (used by github + gitea;
identical 7-line chains collapsed).
- `github::is_authed_for()` (wraps `gh auth token --hostname`).
- `gitea::is_authed_for()` (reads tea's config.yml; never invokes `tea`
to avoid OAuth refresh on lookup).
## Docs
User-facing copy says "GitHub PR" by default; one paragraph in `wt
switch --help` mentions Gitea support, marked experimental.
## Tests
- 13 new integration tests covering Gitea same-repo, fork, error
responses (401/403/404/5xx/malformed JSON/deleted fork/no source
branch), `tea` not installed, `forge.platform` overrides,
GitLab-remote-with-`pr:` bail, self-hosted defaults-to-GitHub, and
self-hosted-with-`tea`-login routes-to-Gitea.
- Unit tests for `extract_source_branch` edge cases and the tea config
parser.
## Compatibility
No CLI flag or config file changes. The `tea` CLI is only required for
Gitea PRs; GitHub-only users see no change.
---------
Co-authored-by: worktrunk-bot <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
|
||
|
|
ecdb7bfc35 | feat(config): add codename template filter (#2641) | ||
|
|
1d97bb7a20 |
feat(config): add [remove] delete-branch config option (#2589)
## Summary Adds a `[remove]` config section that lets users default `wt remove` to keeping branches, requested in #2587. Setting `delete-branch = false` is equivalent to passing `--no-delete-branch` every time; CLI flags still override the config. ```toml [remove] delete-branch = false # never delete branches when removing worktrees ``` Implementation mirrors the existing `[merge]` pattern: new `RemoveConfig` section, a `[projects."<id>".remove]` override path, and a tri-state CLI flag pair (`--delete-branch` / `--no-delete-branch`) so users can override config either direction. ## Test plan - [x] Unit tests for `RemoveConfig` parse/default/merge + project override (`src/config/user/tests.rs`) - [x] Integration test: `[remove] delete-branch = false` keeps the branch on `wt remove` - [x] Integration test: explicit `--delete-branch` overrides the config - [x] All existing `wt remove` tests still pass (83 tests) - [x] `cargo test --lib --bins` and `cargo clippy --tests --all-features` clean - [x] Help/docs snapshots regenerated (`test_docs_are_in_sync`) Closes #2587. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f418729e21 | fix(step/copy-ignored): drop .pi/ from built-in excludes (#2527) | ||
|
|
86f4b37e48 |
docs(help): rename '[Aliases]' link to 'Extending Worktrunk guide' (#2330)
The bare `[Aliases](@/extending.md#aliases)` link text rendered in terminal help as "See Aliases for ..." — a self-reference when sitting inside a section already titled "Aliases" (`wt config user/project --help`), and a generic label elsewhere (`wt config alias --help`). Renamed to `[Extending Worktrunk guide]` in all three call sites. Also adds a short guideline under "Help text authoring" in CLAUDE.md: link text must stand alone when the URL is stripped (since terminal help strips URLs and keeps only the text). > _This was written by Claude Code on behalf of Maximilian_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a1be5645c4 |
feat(alias): dispatch aliases from top-level wt <name> (#2266)
`wt deploy` now resolves `deploy` against configured aliases before falling through to a `wt-deploy` PATH binary. Built-ins still win (clap matches before alias dispatch ever runs), and `wt step <name>` keeps working at runtime — only the docs cut over to the new form. ## Why `wt deploy` reads better than `wt step deploy`, and aliases as first-class commands lower friction for using them as everyday shortcuts. ## Precedence built-in (clap) → alias (user/project config, merged) → `wt-<name>` PATH binary → "unrecognized subcommand" error. User config wins over PATH binaries because aliases are how users customize wt — same model as git, where `[alias]` entries shadow `git-foo` externals. ## Navigating the diff - `src/commands/alias.rs` — refactored `step_alias` to share `run_alias` with the new `try_alias(name, rest) -> Result<Option<()>>`. Returns `Ok(None)` when the name isn't a configured alias or when not in a git repo; propagates config-load errors so a broken `wt.toml` fails loudly instead of silently turning into "unrecognized subcommand". Argument parsing is gated on alias-membership, so unrelated args meant for an external binary don't surface as alias parse errors. New `alias_names_for_suggestions()` mixes alias names into "did you mean" hints. `HelpContext` enum lets the help splice annotate "(shadowed by built-in)" against the right level (top-level builtins for `wt --help`, step builtins for `wt step --help`). The user-facing "shadow warning" was removed entirely — under the new model an alias named `commit` runs fine via `wt commit`, only `wt step commit` is shadowed. - `src/commands/external.rs` — `handle_external_command` calls `try_alias` first, then PATH lookup, then unrecognized-subcommand error. Suggestions include alias names. Non-UTF-8 args bypass alias dispatch (alias parser requires UTF-8; binary subcommands get raw `OsStr`). - `src/help.rs` + `src/main.rs` — early-parse pass returns `Option<HelpContext>`; help splice fires for both `wt --help` and `wt step --help`. - `src/completion.rs` — aliases injected at the top level in addition to `step`. - `src/cli/mod.rs` — long Aliases section moved out of `Step::after_long_help` into hand-authored `docs/content/extending.md`. New sync test `test_top_level_builtins_match_clap` keeps the `TOP_LEVEL_BUILTINS` constant aligned with the `Cli` enum. ## Tests 3221 tests pass, lints clean. New integration tests: `test_top_level_alias_dispatch`, `test_top_level_alias_with_step_builtin_name`, `test_top_level_alias_did_you_mean`. Removed `test_step_alias_shadows_builtin_plural` (warning gone). Reframed `test_step_alias_shadows_builtin` to verify shadow filtering of typo suggestions instead. Completion tests now isolate user config via `WORKTRUNK_CONFIG_PATH=/dev/null` — project config isolation is a noted gap (commented inline). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
407a899fc2 |
feat(config): deprecate switch.picker.timeout-ms (#2236)
`switch.picker.timeout-ms` bounded how long the picker blocked before rendering. After #2231 landed progressive rendering, the field was parsed but silently ignored — violating the CLAUDE.md rule against silently dropping old config keys. Wired through the standard deprecation path in `src/config/deprecation.rs`: detection flags the field at top-level and under `[projects."X".switch.picker]`, migration strips it, and the warning points users at `wt config update`. Handles both `[switch.picker]` section form and inline `picker = { ... }` form, matching existing helpers. Also removed the `SwitchPickerConfig::timeout_ms` field and `timeout()` accessor — migration strips the key before serde sees it, so the struct field is no longer needed. Tests referencing the old field updated or dropped. Example TOMLs and the picker comment cleaned up accordingly. > _This was written by Claude Code on behalf of Maximilian_ Co-authored-by: Claude <noreply@anthropic.com> |