mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
worktree-path-not-found-error
1003 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
5b2a41ee73 |
feat(plugin): detect Gemini extension; document OpenCode and Gemini install (#2819)
Closing two gaps a prior plugin-implementation review surfaced: Gemini's
native install path was invisible (no `wt config show` section, no
user-facing docs), and OpenCode — which has the most substantial
implementation of the four — was absent from the plugins page despite
being the only tool that ships activity tracking via a worktrunk-written
plugin.
`wt config show` now renders a `GEMINI CLI` section gated on
`which::which("gemini")`. Installed state is detected by parsing
`~/.gemini/extensions/worktrunk/gemini-extension.json` and checking
`name == "worktrunk"` — exactly where `gemini extensions install` clones
the extension. Fail-closed on a missing home, unreadable file, or
invalid JSON, matching the existing claude/codex/opencode detection.
Test infrastructure (`gemini_installed` field on `TestRepo`,
`setup_mock_gemini_installed`, `setup_gemini_extension_installed`) and
the two new snapshot tests mirror the OpenCode equivalents
line-for-line.
`docs/content/claude-code.md` retitled "Agent Integration" and
restructured around a four-tool capability table covering configuration
skill, activity tracking, worktree isolation, and `/wt-switch-create`.
OpenCode and Gemini get install subsections; the activity-tracking
section is generalized from Claude-only to Claude+OpenCode+Gemini so the
heading matches the table. A Codex-only sentence under "Worktree
isolation" was redundant with the table and dropped.
## Verified end-to-end
After **v0.52.0** went `Latest`, `gemini extensions install
https://github.com/max-sixty/worktrunk` resolves via `/releases/latest`
→ the v0.52.0 source tarball (which now carries the root
`gemini-extension.json` from #2807) → install succeeds. `gemini
extensions list` shows `Type: github-release`, `Release tag: v0.52.0`,
with both skills (`wt-switch-create`, `worktrunk`) loaded via
`${extensionPath}/skills/`. Live `wt config show` reports `✓ Extension
installed`. The bare URL is what the docs use — `owner/repo` shorthand
returns "Install source not found" against gemini-cli, so the full URL
is the right form.
Per the `release` and detection paths in gemini-cli 0.42
(`bundle/chunk-CHERUG6W.js` lines 44170–44290 / 44640–44675), the URL
install always tries `releases/latest` first when releases exist, and
only auto-falls-back to `git clone` if there's no release data at all —
so the work landed in #2807 on `main` only became user-visible once
v0.52.0 was cut. This PR ships the detection and docs that have now been
confirmed accurate against the live release path.
Pre-merge gate green locally (3747 tests, 0 skipped; pre-commit clean;
`test_docs_are_in_sync` green so the skill reference and `llms.txt` are
in sync).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ace32e0e2c |
fix: reap orphaned fsmonitor daemons in wt remove internal sweep (#2814)
## Why Companion to #2813. That PR stops the fsmonitor-daemon leak at its source (`wt remove` force-terminating a wedged daemon). This PR is defense-in-depth for daemons orphaned by paths that bypass `wt remove` entirely — plain `git worktree remove`, a manual `rm -rf`, or a crashed `wt` — which #2813 cannot cover. Background: a wedged `git fsmonitor--daemon` makes `git status` (and `wt list`) hang; orphans had accumulated ~80-deep on one machine. ## What Adds an orphan sweep (`src/git/fsmonitor.rs`: `classify_orphans`, `reap_orphan_fsmonitor_daemons`) hooked into the **existing** `wt remove` internal sweep op — no new always-on/timer mechanism, and deliberately not wired to `wt list` or any read-only command (killing processes as a side effect of a read-only command is out of bounds). It reaps only daemons whose IPC socket resolves to a worktree that no longer exists, SIGTERM → bounded wait → SIGKILL, socket-scoped via `lsof -a -p <pid>` (never broad process-name matching). A daemon serving a *live* worktree is never reaped — proven by unit tests, and the invariant holds even when the live-worktree set is unknowable: `live_git_dirs` returns `Option`, and an unknowable set disables resolved-socket reaping entirely rather than falling back to "reap everything". Shares low-level fsmonitor mechanics with #2813 and both touch `faq.md` / `troubleshooting.md` / `CHANGELOG.md`; whichever of the two lands second needs a small dedup and doc reconciliation. ## Testing `wt hook pre-merge --yes` green (3757/3757). 11 deterministic unit tests cover the pure logic, including `live_worktree_daemon_is_never_reaped` and `unknowable_live_set_spares_resolved_socket_daemons`. `src/git/fsmonitor.rs` is a new file, so it is entirely in patch scope: 94.5% line / 95.4% region. The uncovered lines are real-OS-signal and live daemon-enumeration paths only reachable by spawning real `git fsmonitor--daemon` processes; their pure logic is exhaustively unit-tested. A real-daemon end-to-end test was prototyped and dropped as flaky and net-negative (daemon-spawn races; risk of SIGKILLing unrelated dev-machine daemons). These uncovered OS-only lines are an accepted trade-off — if `codecov/patch` flags them it is a known false positive, not a gap to fill with a fragile test. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
045750f890 |
fix(remove): force-kill a wedged fsmonitor daemon on worktree removal (#2813)
## Why `wt remove` already calls `git fsmonitor--daemon stop` before staging a worktree to trash, but that stop is an IPC request to the daemon itself. When the daemon is *wedged* — the common failure that makes `git status` (and therefore `wt list`) hang — it can't answer the IPC, so the stop is a silent no-op and the daemon leaks. In practice this accumulated ~80 orphaned `git fsmonitor--daemon` processes on one machine, one of which wedged and hung `wt list` outright. ## What Adds a canonical `stop_fsmonitor_daemon` helper: after the IPC stop, it resolves the daemon's PID from its IPC socket and escalates SIGTERM → bounded wait → SIGKILL, scoped strictly to the removed worktree's own socket (never matched by process name, never another worktree's daemon). All three removal paths (library, foreground, detached-background) route through the one helper; the redundant `git ... fsmonitor--daemon stop` shell fragment in the detached path is deleted so there is a single canonical stop. Force-kill is Unix-only with a clean `#[cfg(not(unix))]` no-op (Windows keeps the IPC stop, where the daemon uses a named pipe). This is the source-level fix for the leak on the removal path. Companion PR #2814 adds a defense-in-depth sweep for daemons orphaned by paths that bypass `wt remove` (plain `git worktree remove`, manual `rm`, a crashed `wt`). The two branches share low-level fsmonitor mechanics and both touch `faq.md` / `troubleshooting.md` / `CHANGELOG.md`, so whichever lands second will need a small dedup and doc reconciliation. ## Testing Unit tests cover the deterministic logic: git-dir/socket resolution for a linked worktree, the SIGTERM-honored fast path, and SIGTERM-ignored → SIGKILL escalation (with a readiness handshake to avoid a signal-vs-trap-install race). `wt hook pre-merge --yes` is green. The `lsof`-failure and git-dir-error arms are single best-effort `log::debug!`+return lines that fall through to the existing fail-open behavior. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
1cc89d172e |
docs(claude): slim root CLAUDE.md, relocate detail to sibling files (#2816)
The root `CLAUDE.md` loads into every conversation, so its ~510 lines were paid on every session. This cuts it to ~190 lines without losing any documented behavior or invariant. Three things happened: derivable/stale content was removed (the JSON Output Format pointer, just a redirect to `wt list --help`), prose was tightened throughout, and task-specific detail was pushed down to the sibling `CLAUDE.md` that loads where that work happens — the root keeps a one-line pointer. **Relocations** (substance preserved at the destination, root keeps a pointer): - Testing commands, the `mock-stub` filtered-run gotcha, Claude Code web setup, the shell/PTY nextest SIGTTOU suspension, and codecov investigation mechanics → `tests/CLAUDE.md` - Doc sync taxonomy (the three categories + PRIMARY SOURCE), three-context help-text authoring, the `.gitattributes` `linguist-generated=false` exemption, config-TOML double-comment rule → `docs/CLAUDE.md` - Plugin layout, the Codex no-hooks re-enablement conditions, the accepted `wt-switch-create` tradeoff → `plugins/worktrunk/CLAUDE.md` (its back-pointers into the root were made self-contained so nothing dangles) - The add-a-CLI-command recipe → `src/commands/CLAUDE.md` **Kept in the root, tightened but substance intact:** the governing invariants — Data Safety, the Command Execution Principles (`shell_exec::Cmd`, structured output, network policy, signal handling), Project Commands Run Only After Approval (verbatim — the #2806 TOCTOU fix depends on its wording), the codecov merge gate, Config Deprecation, accessor naming, and the Worktree Model. A third commit consolidates the doc-sync explanation in `docs/CLAUDE.md`: the new "Doc sync taxonomy" section was overlapping with the file's existing "Command documentation" area on "edit `src/cli/mod.rs`" and `test_docs_are_in_sync`. Now the taxonomy is the single entry point and "Command page generation" reads as the category-1 mechanism details. Every CLAUDE.md heading quoted in a code comment is preserved verbatim so the breadcrumbs still resolve under a case-sensitive grep: `Plugin Layout` (`src/commands/config/codex.rs`), `Project Commands Run Only After Approval` (`src/commands/hook_plan.rs`, `src/commands/picker/mod.rs`), `Network Access` (`src/git/repository/config.rs`), and `Signal Handling` (`src/git/error.rs`, `src/commands/run_pipeline.rs`, `src/commands/command_executor.rs`). The first push downcased the latter two; the automated reviewer caught it and the second commit restored Title Case across all Command Execution Principles subheadings. `test_docs_are_in_sync` passes (docs-only change; no code touched). --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
408f4f5bee |
Add wt step tether: kill a command's process group when its worktree is removed
`wt step tether -- CMD…` runs CMD in its own process group and tears the whole group down when CMD exits or its worktree is removed (a 250ms portable poll; killpg on Unix, taskkill /T /F on Windows). Replaces the leaked-dev-server / fseventsd-saturation failure mode with a fire-and-forget supervisor needing only a single post-start hook. No unsafe, no new deps. Shell handling matches `wt step for-each`. Windows taskkill has a documented self-exit-detach edge. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
7d09c5da58 |
fix(merge): gate commit-append separately so declining it keeps hooks (#2802)
## Summary `wt merge` bundled the project commit-append into the hook-approval batch and shadowed `verify = false` on *any* decline. When the project hooks were already approved (and thus filtered out of the prompt), declining the lone commit-append prompt skipped every pre-merge/post-merge hook for that run — even though the user only meant to skip the append. This was flagged as a follow-up during review of #2774. The append is now resolved through the shared `approve_commit_template_append` gate — the same path `wt step commit` / `wt step squash` already use — so its decline drops only the append and never touches hook approval. Hooks are approved on their own; only a hook decline skips hooks. Trade-off: when both project hooks *and* the append are unapproved on a fresh repo, the user now sees two prompts instead of one bundled prompt. This matches the standalone commit/squash flow and is the canonical behavior; the old single-prompt bundling was what introduced the conflation. Already-approved appends still don't re-prompt. ## Also in this PR - **Message canonicalization** — `merge`, `removal`, `prune`, and `switch` now print `Commands declined, … without hooks`, matching the wording `step squash` / `step commit` already used. `wt hook` and `wt config approvals add` stay bare (they end on decline rather than continuing an operation). - **Docs** — the hook Security section now states that declining skips every project command for that operation (already-approved ones included) and that saved approvals are unaffected. Synced to the skill reference. - **Doc-comment fixes** — `HookGate` / `PreApprovedGuidance` comments no longer describe the removed bundled-prompt flow. ## Testing - New regression test `test_merge_decline_append_keeps_approved_hooks`: with the hook pre-approved, declining the append still runs the pre-commit hook and the merge succeeds (fails on the old code, passes now). - `test_merge_bundles_append_into_hook_approval` renamed to `test_merge_prompts_hooks_and_append_separately`, updated for the two-prompt flow. - Four decline snapshots regenerated (message text only). - Full pre-merge gate green: 3736 tests, clippy, fmt, doc-sync. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2ca0113a04 |
docs: remove unverified cmux recipes from tips-patterns (#2797)
## Problem The cmux recipes in `docs/content/tips-patterns.md` don't work as documented — see #2796. `wt switch` cd's the invoking shell *in addition to* the `pre-switch` hook selecting the per-worktree cmux workspace, so the original cmux workspace ends up pointing at the new worktree alongside the freshly-opened one. The Agent-handoffs cmux entry has a similar pollution issue. I initially proposed adding `[switch] cd = false` (prior revision of this PR), but I couldn't end-to-end verify the fix because cmux isn't installable in this CI environment. Per @max-sixty's call ([#2796 (comment)](https://github.com/max-sixty/worktrunk/issues/2796#issuecomment-4478578957)) — *"let's just remove the cmux recipes unless someone can certify that they work"* — the safer move is to drop the recipes until someone with cmux installed can re-add a verified configuration. ## What this PR does Removes both cmux blocks from `docs/content/tips-patterns.md` (and the auto-synced skill reference): - The `**cmux** (new workspace):` entry under **Agent handoffs**. - The full `## cmux workspace per worktree` section. Diff is the inverse of #1907 (the PR that added them): 33 + 36 = 69 deletions. The tmux and Zellij Agent-handoffs examples are unaffected. ## Testing - `cargo test --test integration test_docs_are_in_sync` ✓ (regenerated `skills/worktrunk/reference/tips-patterns.md`) - `git grep -ni cmux` returns no matches. --- Closes #2796 Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3848a9da7a | chore: bump the patch group with 3 updates (#2792) | ||
|
|
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>
|
||
|
|
233cd493d0 |
fix(codex): drop activity-marker hooks Codex can't drive (#2786)
The Codex plugin shipped `hooks.json` with `SessionStart`→💬, `UserPromptSubmit`→🤖, and `Stop`→💬. But codex-cli 0.130.0's `HookEventNameWire` schema (extracted from the binary) has **no `Stop`/turn-end event**: `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`, `SessionStart`, `UserPromptSubmit`. So on Codex the 🤖 marker set at `UserPromptSubmit` could never return to 💬 — a Codex worktree stuck at "working" for the entire session. That's worse than the docs claimed ("rests at 💬"). This removes the marker hooks entirely until Codex exposes a turn-end hook event. The Codex plugin now ships only the configuration skill; activity tracking joins worktree-isolation and `/wt-switch-create` as Claude-Code-only. A re-enablement comment (exact conditions + the files to restore) lives in `src/commands/config/codex.rs` and a new `CLAUDE.md` → "Codex Plugin" section, which also documents the accepted tradeoff that the `skills/` symlink exposes the Claude-only `wt-switch-create` skill to Codex (harmless — Codex can't act on it). Bundled adjacent fixes: `plugin.json` metadata drift (`description`/`longDescription` no longer claim activity hooks; `homepage`/`websiteURL` `https://worktrunk.dev/claude-code/` → `https://worktrunk.dev`), and the install/uninstall/`--help`/docs text made honest about Codex having only the configuration skill. **Reviewer navigation:** `src/commands/config/codex.rs` + `src/cli/config.rs` (runtime + `--help` text), `docs/content/claude-code.md` (skill ref + `llms.txt` auto-synced), `CLAUDE.md` (new section), `tests/integration_tests/config_show.rs` (`test_codex_plugin_metadata_is_valid_json` now asserts `hooks` absent + guards metadata regression). Snapshot env-block churn is stale-fixture convergence to the repo's current `/nonexistent/wt/` convention, not a leak. **Testing:** Full pre-merge gate green locally (3704 tests, 0 skipped; lints clean). The metadata test was strengthened; the marker behavior itself is removed, so there's nothing runtime-testable to add. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f00b787e1b |
fix(codex): relocate plugin into subdir so the marketplace resolves it (#2782)
## Problem #2780 put `.agents/plugins/marketplace.json` at the path codex-cli reads, but the plugin was still **uninstallable**. Driving the Codex `/plugins` TUI surfaced the real, complete set of constraints in codex-cli 0.130.0: 1. A plugin `source` must be the **object** form `{"source":"local","path":"./plugins/<name>"}` — a bare string `"./"` fails with `local plugin source path must not be empty`. 2. The path must be a **non-empty subdirectory**. Codex resolves it relative to the marketplace root (repo root), so a repo-root plugin (`./.codex-plugin/` at the top) can't be referenced at all — every Codex marketplace plugin lives in `./plugins/<name>/.codex-plugin/plugin.json`, exactly like the OpenAI-curated marketplace. So the plugin had to move into a subdirectory. ## Changes - Move `.codex-plugin/` → `plugins/worktrunk/.codex-plugin/` and `hooks/` → `plugins/worktrunk/hooks/` (a plugin-root sibling, matching the curated layout where `skills/` and `assets/` sit beside `.codex-plugin/`). - `plugins/worktrunk/skills` → symlink to `../../skills` so the shared `worktrunk` + `wt-switch-create` skills stay single-source (no duplication; the repo still auto-syncs `skills/worktrunk/reference/`). - Rewrite `.agents/plugins/marketplace.json` with the object `source`, `policy.installation: "AVAILABLE"`, and `category` that Codex actually parses. - `plugin.json` `hooks` → `./hooks/hooks.json` (plugin-root relative). - Update the install hint (`src/commands/config/codex.rs`), docs, auto-synced skill reference, the validity test, and the install snapshot. ## Verification Verified end-to-end against codex-cli 0.130.0: repointed a local marketplace at the worktree and drove `/plugins`. The `Worktrunk` marketplace now appears (plugin count 123 → 124), the plugin shows as **Available** with **both skills resolved** (`worktrunk:worktrunk`, `worktrunk:wt-switch-create` — the symlink works), and it installs. `test_codex_plugin_metadata_is_valid_json` now asserts the object `source`, `policy.installation`, `interface.displayName`, and the new paths. `test_docs_are_in_sync` and the install snapshot are updated; full `config_show` module (119 tests) + pre-commit pass. **Caveat (documented, not a regression):** plugin-bundled *command* hooks remain gated by Codex's `plugin_hooks` feature flag — already covered in the troubleshooting docs (`codex features enable plugin_hooks`). The pre-install plugin detail view shows "No plugin hooks", consistent with every one of the 123 curated plugins (none bundle command hooks, so this path has no working precedent to verify against in-TUI). Marker behavior with `plugin_hooks` enabled is the remaining thing to confirm in a real installed session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2c675e00f7 |
Add Codex support (#2512)
## What problem does this solve? Worktrunk already has a Claude Code integration, but Codex users do not have an equivalent first-class path for: - discovering Worktrunk guidance inside the agent environment - bundling hooks that can show active Codex work in `wt list` - learning the Codex-specific worktree workflow and its differences from Claude Code - installing the integration through a Worktrunk CLI command This PR adds Codex support while preserving the important semantic difference between Claude Code and Codex: Claude Code can route agent-created worktrees through `wt` lifecycle hooks, but Codex currently needs users/agents to invoke `wt switch --create` and `wt remove` directly. ## What changed? ### Codex plugin package - Adds repo-local Codex plugin metadata in `.codex-plugin/plugin.json`. - Adds a Codex marketplace entry in `.agents/plugins/marketplace.json`. - Adds `hooks/hooks.json` for bundled activity hooks: - `SessionStart` sets the marker to `💬` - `UserPromptSubmit` sets the marker to `🤖` - `Stop` sets the marker back to `💬` - Reuses the existing Worktrunk skill/reference docs as the plugin skill payload. ### CLI integration - Adds `wt config plugins codex install`. - Runs `codex plugin marketplace add max-sixty/worktrunk`. - Clearly tells the user to open `/plugins` in Codex and install Worktrunk from the marketplace. - Adds `wt config plugins codex uninstall`. - Removes the Worktrunk marketplace entry. - Intentionally leaves already-installed plugins and global Codex hook feature flags unchanged, because users may have configured hook settings for other hooks. - Adds a `CODEX` section to `wt config show` when the Codex CLI is available. - It reports Codex CLI availability. - It avoids claiming the plugin is installed, because the CLI path only detects the Codex binary. ### Documentation - Adds a new Codex integration page with installation, bundled activity hooks, worktree workflow, LLM commit setup, and a Claude Code comparison. - Updates README, overview docs, tips/patterns, FAQ, and generated skill references so Codex is listed alongside Claude Code/OpenCode where relevant. - Keeps the Claude Code docs as the place for Claude-only worktree lifecycle hook behavior. ## Why this shape? The implementation keeps Codex separate from the existing Claude plugin command instead of abstracting over both. That is intentional: Claude installs a plugin directly, while the Codex flow configures a marketplace and still requires the user to install from `/plugins`. A shared abstraction would hide those differences and make the user-facing behavior easier to misread. The docs also avoid presenting Codex as feature-identical to Claude Code. Codex gets skills and bundled activity hooks here, but not automatic worktree lifecycle routing. ## Review guide - Plugin packaging: `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json`, `hooks/hooks.json` - CLI behavior: `src/commands/config/codex.rs`, `src/cli/config.rs`, `src/commands/config/show.rs` - Test helpers and coverage: `src/testing/mod.rs`, `tests/integration_tests/config_show.rs`, `tests/integration_tests/help.rs` - Primary docs: `docs/content/codex.md` - Generated docs/snapshots: `skills/worktrunk/reference/codex.md`, config/help snapshots ## Tests - `cargo fmt --check` - `cargo check --lib --bins` - `cargo test --lib --bins` - `cargo test --test integration test_docs_are_in_sync` - `cargo test --test integration "test_help"` - `cargo test --test integration config_show` - `git diff --check HEAD` ## Local pre-merge note `cargo run -- hook pre-merge --yes` could not complete locally because this environment does not have `pre-commit` or `cargo-nextest` installed. The hook's doctest/doc portions did run and passed. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
771739f6b7 |
perf(integration): bound patch-id squash-merge scan to 500 commits (#2752)
## Summary
`is_squash_merged_via_patch_id` runs `git log -p {merge-base}..{target}
| git patch-id` over the *entire* target-side history. The range is
unbounded: on a fast-moving repo with an old branch — tens of thousands
of commits since divergence — a single integration check takes seconds
to tens of seconds, which surfaces as `wt step prune` (and `wt list`)
going visibly silent on the parallel check tail in #1888.
This caps the scan at `PATCH_ID_SCAN_MAX_COMMITS = 500` commits via a
cheap graph-only `git rev-list --count` pre-flight. Above the cap, the
check returns `Ok(false)` — the safe direction (branch kept, not wrongly
deleted); `wt remove -D` still removes branches that fall past the cap.
## Why 500
Count is a rough proxy — per-commit cost scales with `changed_files ×
changed_lines`, not just count. Working back from "keep one check under
a few seconds":
- typical repo (~5-20 KB patches): 500 ≈ well under 1s
- heavy monorepo (~50-100 KB patches): 500 ≈ 2-5s
Branches merged within a normal review-and-cleanup cycle sit well inside
this. Anything older is `-D` territory.
## Scope
The cap is in the shared probe, so it applies to `wt list`'s status
column, `wt remove`, `wt merge`, and `wt step prune` — anywhere
`integration_reason` is called. Behavior change only when a branch was
squash-merged *and* `git merge-tree` conflicts (the same files were
modified again after the squash) *and* the default branch has advanced >
500 commits since the merge point. Narrow.
## Test plan
- Two new unit tests in `patch_id_cap_tests` using `git fast-import`
(501-commit history built in milliseconds): under-cap finds the squash,
over-cap bails despite the squash being in range. The pair pins the
difference to the cap, not the topology.
- Existing squash-merge integration tests
(`test_remove_squash_merged_*`, `test_prune_squash_merged_*`,
`test_list_integrated_when_squash_merged_*`) keep small histories well
under the cap and continue to pass — regression guard for the wiring.
Refs #1888.
@ortonomy — would you mind running this against the repo from #1888? The
simplest check is `cargo install --git
https://github.com/max-sixty/worktrunk --branch prune-speed wt` (or
build locally), clear the disk cache to force a cold run (`rm -rf
.git/wt/cache`), then time `wt step prune`. Most interested in whether
the silent stretch is gone; if it isn't, `wt -vv step prune` writes
`.git/wt/logs/trace.log` showing which `git` invocations are slow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4cfc8e6acd |
docs(extending): note alias template timing and how to defer to nested wt (#2754)
Addresses [#2753](https://github.com/max-sixty/worktrunk/issues/2753) — adds a short subsection under Aliases documenting that templates render at alias dispatch (using the alias-invocation worktree's context), so a nested `wt` command's own template variables resolve against the outer worktree unless wrapped in `{% raw %}…{% endraw %}`. The example uses the reporter's pattern (`wt switch ... --execute 'echo {{ worktree_path }}'`) verified against the release binary in a scratch repo: - Direct: `wt switch other --no-cd --execute 'echo {{ worktree_path }}'` -> target path - Aliased without `{% raw %}`: -> outer worktree path (the bug) - Aliased with `{% raw %}{{ worktree_path }}{% endraw %}`: -> target path `test_docs_are_in_sync` propagates the change into `skills/worktrunk/reference/extending.md`. Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
52555982a8 |
feat(plugin): /wt-switch-create — optional repo arg and -- task delimiter (#2751)
The `/wt-switch-create` skill now accepts `<branch> [<repo>] [-- <task>]`. The optional second token names a different repository to create the worktree in (instead of the session's current one); the skill `cd`s into that repo with a `Bash` call before invoking `EnterWorktree` (which has no repo parameter), then verifies the new worktree landed under the requested repo as a safety check. The `--` cleanly separates the task from the rest. Without one, the parser treats a path-shaped second token (absolute, `~`-relative, `./`/`../`-relative, or an existing directory) as the repo and the remaining tokens as the task — anything else after the branch is task text, so the old shorthand `/wt-switch-create my-branch fix the bug` keeps working. Examples in the skill: ``` /wt-switch-create my-feature -- fix the parser bug /wt-switch-create my-feature ~/workspace/other-repo -- fix the parser bug /wt-switch-create my-feature ``` Also tidied existing prose: the nesting + different-repo case now stops rather than silently running the task in the wrong repo, and the scope note mentions the repo arg. The follow-up commit updates the public docs page (`docs/content/claude-code.md`) so the `/wt-switch-create` signature shown at worktrunk.dev matches the new grammar; `skills/worktrunk/reference/claude-code.md` re-synced via `test_docs_are_in_sync`. No code or test changes. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
761c293483 |
chore(plugin): drop off-spec version key, align skill frontmatter (#2747)
Tidies the plugin skills' YAML frontmatter against the [Agent Skills](https://agentskills.io/specification) spec, and cleans up a release-skill step the change makes stale. - `skills/worktrunk/SKILL.md`: drop the top-level `version: 0.49.0` key. The spec only defines `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools` at the top level (version, if kept, belongs under `metadata`), and Claude Code's own field set doesn't include `version` either. Also drop the `[package.metadata.release].pre-release-replacements` entry in `Cargo.toml` that existed solely to bump that line on release. - `skills/wt-switch-create/SKILL.md`: add `license` and `compatibility` so both plugin skills carry the same spec-valid metadata. - `.claude/skills/release/SKILL.md`: step 7 no longer applies `pre-release-replacements`, and `index.json`'s `SKILL.md` digest no longer goes stale on release (`cargo release` doesn't touch `SKILL.md` anymore) — drop the parenthetical and the now-no-op digest-resync sub-step. - Both `compatibility` lines now say "the `wt` CLI" rather than "worktrunk CLI". - `docs/static/.well-known/agent-skills/index.json` digest regenerated by `test_docs_are_in_sync`. Follow-up to #2737 / #2745. No `.rs` changes. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3c04588ccf | docs: refresh stale path references in docs/CLAUDE.md (#2739) | ||
|
|
5a07bc09bd | feat(plugin): ship the wt-switch-create skill in the Claude Code plugin (#2737) | ||
|
|
12e2ee2563 |
docs: drop stale docs/dev/cache-staleness.md (#2710)
The doc proposed and analyzed six options for closing the ref-keyed cache staleness class; the codebase adopted Option 5 (`RefSnapshot`) in #2528 and extended it with the content-addressed `ahead-behind/` SHA cache in #2704. The structural contract now lives in `src/git/repository/ref_snapshot.rs` (snapshot semantics, lifetime, construction) and the cache-kind landscape in `src/commands/list/collect/mod.rs`'s Caching section, both of which supersede what was in this file. What remained in the doc actively misleads: its `RepoCache` inventory table cites fields that no longer exist (`commit_shas`, `tree_shas`, `effective_integration_targets`, `integration_reasons`, `ahead_behind`, `head_shas`) and references a `batch_ahead_behind` helper that's been replaced. The file was an island — no inbound links from code, tests, or other docs, and `docs/dev/` is not a Zola content directory so it was never published — so deletion is clean. Anyone curious about the original design debate can find it at the merge commit for #2528. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0b088b5790 |
perf(list): cache ahead/behind counts SHA-keyed; skip the for-each-ref walk on warm runs (#2704)
## Summary
`wt list --branches` computed branch ahead/behind counts with one `git
for-each-ref --format='%(ahead-behind:BASE)'` walk, on every invocation,
in the post-skeleton setup scope. That walk is O(commits) — ~1.5s on
rust-lang/rust — and it runs *serially before the parallel task pool
opens*, because the per-branch tasks consume its result. So on a large
real repo roughly 40% of `wt list`'s wall time was a single-threaded git
graph walk that thread count couldn't help with (a follow-up from the
picker thread-count investigation in #2683/#2685).
Ahead/behind for `(base, branch)` is a pure function of the two commit
SHAs — content-addressed, never stale — exactly the shape of the
existing `.git/wt/cache/` SHA-keyed caches. This adds an `ahead-behind/`
cache kind and rewires the population path:
- New `ahead-behind/{base_sha}-{head_sha}.json` kind in `sha_cache.rs`,
identical machinery to `is-ancestor` / `diff-stats` (LRU-bounded,
cleared by `wt config state clear`, wiped by the bench
cache-invalidator).
- `Repository::ahead_behind_by_sha` is now cache-backed: check →
`compute_ahead_behind` → write.
- `RefSnapshot::capture_refs_with_ahead_behind` builds the snapshot's
ahead/behind map from the cache and only walks the graph for the
branches the cache doesn't cover:
- **a few misses** → left out of the snapshot map; the per-branch
`AheadBehindTask` recomputes (and caches) them by SHA in the parallel
pool. (That task already runs `merge_base_by_sha` for its orphan check,
so the merge-base `compute_ahead_behind` needs is already primed — the
fallback is just two cheap `rev-list --count` calls.)
- **everything cold** (fresh repo / after `wt config state clear`) → one
unscoped `for-each-ref %(ahead-behind:BASE_SHA) refs/heads/` walk — a
single shared traversal finds every merge-base — then results written to
the cache.
- **many cold but not all** → one `for-each-ref
%(ahead-behind:BASE_SHA)` *scoped to just the missed refnames* (`git
for-each-ref` takes a ref list, so it's still one shared traversal, only
over the cold subset), results written to the cache.
The few-vs-many threshold (`AHEAD_BEHIND_SCOPED_BATCH_MIN_MISSES`)
trades the per-pair path's parallelism (it runs in the pool) against the
batch's lower total work (one shared base-history traversal); a batch
here is serial, so it's only worth it once "many" misses make the
amortization clear.
### Cache-correctness details (Codex-prompted)
A few sharp edges, surfaced by `/review-codex`, that the seeding path
now handles explicitly:
- **`%(ahead-behind)` computed against the *resolved SHA*, not the
refname.** A tag named `main` shadows the branch in git's ref
resolution; passing `base_sha` to the batch makes it inert to that. The
fallback path (default branch isn't a local/remote-tracking ref) still
passes the refname and caches nothing.
- **Cache key from the batch's `%(objectname)`, not a separately-scanned
value.** If a `refs/heads/X` moved between `scan_locals` and the batch
(a sub-ms window, but possible), the counts are for the *new* SHA — and
the cache now stores them under that SHA, not under the staler
`b.commit_sha`.
- **Orphan branches normalized to `(0, 0)` before writing.** Git's
`%(ahead-behind:BASE)` for an orphan (no common ancestor) prints the two
disjoint history sizes; `compute_ahead_behind` returns `(0, 0)` and
signals orphan-ness separately. An orphan's `behind` count equals base's
total commit count, so one `git rev-list --count base_sha` on the
seeding path detects them. The cache invariant — cache hit equals what a
miss would recompute — holds for orphans now.
- **Bulk-write avoids O(N · dir_size) on the seeding path.**
`write_with_lru` scans the cache directory on every call to enforce the
LRU bound; the serial setup-scope seeding loop calls it N times.
`put_ahead_behind_bulk` writes all N entries first and sweeps once at
the end.
### Net effect
| | before | after |
|---|---|---|
| warm `wt list` on a large repo | ~1.5s serial `for-each-ref
%(ahead-behind)` in the setup scope | N small cache reads; no graph
walk; no serial blocker |
| `wt list` after committing on one branch | full ~1.5s walk again
(batch op — any change re-walks all) | only that branch recomputed (two
cheap `rev-list --count`, in the pool); the rest stay cached |
| many branches moved since last list | full walk | one `for-each-ref
%(ahead-behind)` scoped to just the moved ones |
| cold (fresh repo / after `state clear`) | one combined walk |
unchanged — one combined walk + one bulk cache seed |
| skeleton time (`WORKTRUNK_SKELETON_ONLY`) | — | unaffected (returns
before the scope this touches) |
No user-facing behavior change — same numbers, same columns. The picker
(`wt switch`) shares this path, so its preview pre-compute benefits
identically.
### Follow-ups left as TODOs in the code
- `TODO(ahead-behind-pool)` (`src/commands/list/collect/mod.rs`): the
cold-cache `%(ahead-behind)` walk still runs serially in the setup
scope, blocking the task pool from opening. Nothing downstream of
work-item generation needs the *counts* (only the per-row
`AheadBehindTask`, which has a per-SHA fallback) — only the cheap ref
scan does. So the walk could become a single work item in the pool,
overlapping the other ~N workers. Needs an inter-task dependency the
work-item model doesn't have today.
- `TODO(remote-ahead-behind-batch)`
(`src/commands/list/collect/tasks.rs`): the `Remote⇅` column's
per-branch `ahead_behind_by_sha(upstream, branch)` is already
cache-backed by this change, but has no cold-start batch primer. One
`for-each-ref --format='%(refname) %(upstream:track,nobracket)'
refs/heads/` would fill the `ahead-behind/` cache for it in a single
walk, mirroring what `%(ahead-behind:main)` does for `main↕`.
## Testing
- `sha_cache.rs`: round-trip + cache-read tests for the new kind;
`test_clear_all_covers_all_kinds` extended.
- `ref_snapshot.rs`: all-cold→unscoped-batch + cold→warm second-capture
reads the persistent cache; partial-warm (few misses) omits the moved
branch; many-misses uses the scoped batch; **orphan normalizes to
`(0,0)` in both the snapshot and the cache file**; remote-tracking base
resolves; unresolvable base degrades cleanly.
- Smoke-tested manually: `wt list --branches` populates
`.git/wt/cache/ahead-behind/`; tampering an entry → the next run shows
the tampered counts (proves the read path).
- Two passes of `/review-codex` — second pass clean.
- `cargo run -- hook pre-merge --yes` — full suite + lints + doc-sync
(3595 tests).
## Out of scope (flagged, not done here)
`docs/dev/cache-staleness.md` still references the in-memory
`RepoCache.ahead_behind` field and `batch_ahead_behind` — both removed
when `RefSnapshot` landed. Pre-existing staleness; rewriting that doc is
a separate cleanup. (The pre-skeleton `git log --no-walk` commit-details
batch is also content-addressed by SHA and could in principle be
cache-first, but it's already O(refs) and fast — not worth a TODO.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <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> |
||
|
|
014765252f |
feat(config): bare wt config alias show lists full definitions (#2691)
## Summary `wt config alias show` with no name showed a compact one-line summary listing (the `name command-summary (source)` table added in #2684). This swaps it for the full per-alias view: the same `○ Alias <name> (<source>):` header + gutter block that `wt config alias show <name>` prints, emitted for every configured alias in name order (user entry before project when a name is defined in both), with the shadowed-by-built-in stderr warning preserved (once per name). `--help` / `wt step --help` keep the compact names-only `Aliases:` section and point here. ``` ○ Alias landed (user): wt up && wt hook post-merge; git merge {{ default_branch }} --no-edit && { git diff {{ default_branch }} --quiet && git reset {{ default_branch }}; :; } ○ Alias sw (user): wt switch {{ args }} ○ Alias up (user): git fetch --all --prune && wt step for-each -- sh -c ' ... ' ``` Also drops the blank line between consecutive `format_entry` blocks — applied consistently to `wt config alias show` (the new listing), `wt config alias show <name>` (had a blank line between the user and project entries when a name is in both configs), and `wt config alias dry-run <name>` (same). The `○ Alias …` header is the delimiter everywhere. The compact-summary renderer (`render_aliases_section` + `format_alias_summary`) and its unit tests are removed — only #2684's no-arg listing used them; `--help`'s names-only section is the separate `render_aliases_help_section`, which is untouched. ## Changes - `src/commands/config/alias.rs` — `list_aliases()` rewritten to emit `format_entry` per alias; `handle_alias_show` / `handle_alias_dry_run` drop the inter-entry blank line; docstrings + module doc updated. - `src/commands/alias.rs` — delete `render_aliases_section`, `format_alias_summary`, and their tests; tidy the `render_aliases_help_section` doc. - `src/cli/config.rs` — `wt config alias show` help text reflects the new behavior. - Docs (`docs/content/config.md`, skill mirror) regenerated; three snapshots updated (one content change + cosmetic env drift on two). ## Test plan - [x] `cargo run -- hook pre-merge --yes` — 3581 tests pass, all lints green - [x] Manual: `wt config alias show`, `wt config alias show <name>`, `wt config alias dry-run`, `wt config alias show --help` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <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.
|
||
|
|
a3733dcce8 |
feat(config): list all aliases when wt config alias show has no name (#2684)
`wt config alias show <name>` shows one alias's configured template. With no name it now lists every configured alias — the same `Aliases:` block `wt --help` renders (name, source, one-line summary). This is the receiving end of a separate change that replaces the inline aliases block in `wt --help` with a pointer to `wt config alias show`. Implementation: `name` becomes optional with a single internal branch — no `--list` or `--all` flag. The listing reuses `render_aliases_section` / `load_aliases_for_listing` from `commands/alias.rs` (promoted to `pub(crate)`) rather than duplicating. It tolerates running outside a repo (user-config aliases still list, project-config ones are skipped) or outside a config, prints `No aliases configured` when empty, and pages like `wt hook show`. Top-level shadowing is annotated (`list` → `(shadowed by built-in)`). Audit of other `show`/`view`/`info`/`print` subcommands found nothing else to change: `wt hook show [TYPE]` already does this, `wt config show` / `wt config shell show-theme` / `wt config state get` take no positional and already show everything, and no `view`/`info`/`print` subcommands exist. Help text and the auto-synced docs/skill reference are updated; new snapshot tests cover the populated and empty listings, and the four `config_alias_show_unknown_*` snapshots pick up the `Usage: ... [NAME]` change. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
dc3142714c | docs(step): match the eval --dry-run separator to actual output (#2666) | ||
|
|
a77c92e25b | docs(help): point banner at the actual cli source path (#2665) | ||
|
|
7a61d38348 |
Release v0.49.0 (#2660)
Bump worktrunk to **0.49.0** — minor bump (semver-checks reports a
breaking change in `GitError::UncommittedChanges`, which gained a
`dirty_files: Vec<String>` field).
## Highlights
**Improved**
- New `codename(n)` template filter — deterministic friendly worktree
names from a ~1.26M-combo pool (`{{ branch | codename(2) }}` →
`malleable-opah`).
[#2641](https://github.com/max-sixty/worktrunk/pull/2641), thanks
@endigma
- Picker preview disk cache for Log / BranchDiff / UpstreamDiff, with
background refresh for stale Log decorations.
[#2628](https://github.com/max-sixty/worktrunk/pull/2628),
[#2646](https://github.com/max-sixty/worktrunk/pull/2646)
- Picker shares one `default_branch_sha` lookup across BranchDiff items
instead of forking N rev-parse subprocesses.
[#2658](https://github.com/max-sixty/worktrunk/pull/2658)
- "X has uncommitted changes" errors now list the dirty files inline.
[#2653](https://github.com/max-sixty/worktrunk/pull/2653)
**Fixed**
- `wt switch` integrates with `cd` aliases like zoxide (uses `builtin
cd`). [#2644](https://github.com/max-sixty/worktrunk/pull/2644), thanks
@xkumiyu for reporting
- Empty hook tables no longer panic during `wt switch --create`.
[#2635](https://github.com/max-sixty/worktrunk/pull/2635), thanks @topit
for reporting
- `Repository::current_worktree()` and five sibling helpers no longer
silently leak the process CWD when called via `Repository::at(p)`.
[#2652](https://github.com/max-sixty/worktrunk/pull/2652)
- `WorkingTree::is_linked` tolerates non-git CWDs (e.g. Nix sandbox).
[#2625](https://github.com/max-sixty/worktrunk/pull/2625), thanks
@DArtagan for reporting
- Picker no longer overlays `collect` warnings on the active TUI; clears
its frame on exit in inline mode.
[#2627](https://github.com/max-sixty/worktrunk/pull/2627),
[#2626](https://github.com/max-sixty/worktrunk/pull/2626)
- Failed `git log` no longer poisons the picker's preview disk cache.
[#2651](https://github.com/max-sixty/worktrunk/pull/2651)
- `wt step prune` says "removing branch" for branch-only candidates.
[#2619](https://github.com/max-sixty/worktrunk/pull/2619)
- Claude Code Windows integration uses a `wt.sh` wrapper to avoid
colliding with Windows Terminal's `wt.exe`.
[#1754](https://github.com/max-sixty/worktrunk/pull/1754), thanks
@lucaspimentel
**Documentation**
- cmux workspace integration recipe.
[#1907](https://github.com/max-sixty/worktrunk/pull/1907), thanks
@alvistar
See
[CHANGELOG.md](https://github.com/max-sixty/worktrunk/blob/release/CHANGELOG.md)
for the full list including internal CI / test-isolation changes.
## Test plan
- [x] `cargo run -- hook pre-merge --yes` (tests + lints clean)
- [x] `cargo semver-checks check-release -p worktrunk` (one breaking
change confirmed → minor bump)
- [x] CHANGELOG entries verified by subagent against actual diffs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
ecdb7bfc35 | feat(config): add codename template filter (#2641) | ||
|
|
404bd563a5 |
docs: add cmux workspace integration recipe (#1907)
## Summary - Adds cmux to the **Agent handoffs** section alongside tmux and Zellij - Adds a dedicated **cmux workspace per worktree** recipe with create/select/close lifecycle hooks - Documents the key gotcha: cmux socket restricts access to processes with cmux terminal ancestry, so `pre-*` hooks must be used instead of `post-*` ## Context [cmux](https://cmux.com) is a macOS terminal built on libghostty with workspace management via a Unix socket API. This recipe wires up worktrunk hooks so each worktree automatically gets its own cmux workspace. The `pre-*` vs `post-*` distinction was discovered through debugging: `post-*` hooks are detached background processes that lose the cmux process ancestry, causing the socket to reject connections with "Access denied — only processes started inside cmux can connect." ## Test plan - [x] Tested full lifecycle: `wt switch -c`, `wt switch`, `wt remove` — all correctly create, select, and close cmux workspaces - [ ] Verify docs render correctly on worktrunk.dev (Zola build) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com> |
||
|
|
f897aa7a9c |
Release v0.48.0 (#2618)
## Summary Release v0.48.0. See [CHANGELOG.md](https://github.com/max-sixty/worktrunk/blob/release/CHANGELOG.md) for the full notes. Highlights: - `--format=json` extends to seven step + hook commands (#2560) - `wt step commit` / `wt step squash` gain `--dry-run` (#2557) - New `dirname` / `basename` template filters (#2592, #2605) - New `[remove] delete-branch` config option (#2589) - `wt-perf timeline` subcommand (#2558) - Faster `wt list` on dirty worktrees (#2602) and faster alias dispatch (#2556, #2573) - Short-SHA display honors `core.abbrev` (#2576) - Cleaner `wt config show` shell-integration section for new users ## Test plan - [x] `cargo run -- hook pre-merge --yes` (3497 tests, lints clean) - [x] `cargo semver-checks check-release -p worktrunk` consulted; minor bump confirmed - [ ] CI green |
||
|
|
33db25618a |
docs(skill): document parallel sub-Agent worktree pattern (#2600)
## Summary
Adds a subsection under *Advanced: Agent Handoffs* in
`skills/worktrunk/SKILL.md` for the case where a single Claude Code
session spawns multiple sub-Agents to work autonomously in their own
worktrees — no terminal multiplexer, no human in the other pane. The
existing handoffs section only covers tmux/Zellij user-facing handoffs,
so future Claude sessions had no guidance pointing them at the canonical
pattern, and `Agent { isolation: "worktree" }` was the obvious-looking
but wrong default.
## What it says
- **Canonical recipe**: pre-create each worktree from the parent with
`wt switch --create <branch> --no-cd --no-hooks`, then call `Agent`
*without* `isolation: "worktree"` and name the path in the prompt.
- **Footgun call-out**: don't use `Agent { isolation: "worktree" }` with
this plugin. Claude Code passes its internal agent ID as `name` to the
`WorktreeCreate` hook, so `wt` creates the worktree as
`worktrunk.agent-<id>` on a throwaway branch. If the sub-Agent then
creates a feature branch on top, you end up with non-canonical paths,
orphan branches, and post-start hooks fired against the wrong branch.
The literal API name appears in the warning so a future Claude
pattern-matches the situation when it's about to call that exact tool.
## Test plan
- [x] `cargo test --test integration test_docs_are_in_sync` — passes
(regenerated `docs/static/.well-known/agent-skills/index.json` digest)
- [x] `cargo run -- hook pre-merge --yes` — 3491 tests pass, lints clean
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c82236e034 |
refactor(config): rename path-traversal filters to dirname/basename (#2605)
\`name\` was too generic — likely to collide with future filters operating on domain objects (a Branch, a worktree, a remote). Switch to the POSIX/jinja-convention pair so both filters travel together: ansible ships \`dirname\`/\`basename\`, shell users already know them, and \`dirname\` is no more ambiguous than \`parent\` was. Follow-up to #2592, which landed ~30 minutes ago — no users to break yet. > _This was written by Claude Code on behalf of @max-sixty_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
670cb2a7b0 |
feat(config): add parent and name path-traversal template filters (#2592)
Two new filters expose `Path::parent` and `Path::file_name` to
templates, enabling path traversal that previous filters couldn't
express. They unblock the bare-repo-in-hidden-directory layout
(`myproject/.git`), where `{{ repo }}` resolves to `.git`: users who
want a richer worktree-path naming scheme can now write `{{ repo_path |
parent | name }}` to recover `myproject`.
The interactive bare-repo prompt still suggests the simpler `{{
repo_path }}/../{{ branch | sanitize }}` template, since the bare layout
already nests worktrees inside a repo-specific wrapper directory — no
prefix needed for disambiguation. The filters are there for users with
different layout preferences (#1281 discussion).
Thanks to @seakayone for reporting #1279 and @Xilis for raising the
`parent_dir` question that prompted this approach.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
3593606283 |
refactor: route every short-SHA display through git's abbreviation logic (#2576)
Every site that abbreviated a commit SHA was either slicing `&sha[..7]` or running its own ad-hoc `git rev-parse --short` call. 7-char prefixes regularly collide in repos with many commits, and none of the slicing sites honored `core.abbrev`. Cut over to a single canonical helper. ## Single helper, every display site `Repository::short_sha(&str) -> Result<String>` wraps `git rev-parse --short`. Routes display through git's own abbreviation logic so `core.abbrev` is honored and prefixes auto-extend on collision. Used by: - `step commit` / `step squash` success lines - `step push --no-ff` `Merged to @ <hash>` line — the original bug. Flagged on #2560 as a pre-existing third instance of the same pattern fixed there in `commit.rs` and `step_commands.rs`. - `{{ short_commit }}` template var in hook contexts (`command_executor.rs`, `template_vars.rs`) - post-remove hook context for the removed worktree - safety-backup ref display (`create_safety_backup`) - `(detached <sha>)` label in the orphan-check loop All seven sites previously sliced `commit[..7]` or called their own `rev-parse`. Now they route through one helper. ## Batched form for `wt list --format=json` The JSON list path emits one `short_sha` per worktree row. Looping `short_sha` would fork a subprocess per row, so the short SHA is folded into the existing `commit_details_many` batch instead — `%h` is added to the `git log --no-walk --format=...` call that already fetches timestamp and subject. One subprocess for the whole list, same `core.abbrev` behavior as every other site. `CommitDetails` gains a `short_sha: String` field with the same provenance as the timestamp and subject. The JSON schema is unchanged (`commit.short_sha` was already a field) — only its length now varies by `core.abbrev` instead of being hard-coded to 7. ## API change `TemplateVars::with_active_commit(commit, short_commit)` now takes both forms. Previously it sliced `commit.get(..7)` internally. The sole caller (`worktree/finish.rs`) resolves the short form via `Repository::short_sha` and passes both. ## Docs `{{ short_commit }}` is no longer documented as "(7 chars)" — `src/cli/mod.rs` and `src/config/project.rs` now describe `core.abbrev` behavior. Doc-sync regenerated `docs/content/hook.md` and the skill mirror. ## Tests Full suite passes (3463/3463). `commit_details_many` tests updated for the new tuple shape; `CommitDetails` fixtures in `layout.rs` get a `short_sha` field; `template_vars` tests pass both forms explicitly. Two integration tests previously asserting `short_commit` was exactly 7 chars (`post_start_commands.rs`, `user_hooks.rs`) still pass — fresh test repos default to `core.abbrev = 7`. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
275eaead91 |
feat: add --format=json to seven step + hook commands (#2560)
Extends structured-output coverage beyond
`list`/`switch`/`remove`/`merge` to the remaining commands where JSON is
useful for scripting and Claude Code integration: `step rebase`, `step
push`, `step commit`, `step squash`, `step relocate`, `step
copy-ignored`, and `hook show`.
JSON shapes follow the existing pattern (additive on stdout; human prose
stays on stderr) and use stable snake_case `outcome` discriminators
where the result is one of several variants.
## JSON shapes
| Command | Payload |
|---|---|
| `step rebase` | `{target, outcome:
"rebased"\|"fast_forwarded"\|"up_to_date"}` |
| `step push` | `{target, outcome:
"fast_forwarded"\|"up_to_date"\|"merge_commit", commits, merge_sha?}` |
| `step commit` | `{commit, message, stage_mode}` (resolved mode, not
raw flag) |
| `step squash` | `{outcome:
"squashed"\|"no_commits_ahead"\|"already_single_commit"\|"no_net_changes",
commit?, message?, stage_mode?, target?}` |
| `step relocate` | `{dry_run, entries: [{branch, from, to}], skipped:
[{branch, reason}]}` |
| `step copy-ignored` | `{outcome, dry_run, from, to, entries: [{path,
kind}], files, bytes}` |
| `hook show` | `[{type, source, name, template, needs_approval,
expanded?}]` |
## Notable refactors
- `RebaseResult::Rebased` now carries `{target, fast_forward}`.
- `SquashResult::Squashed` now carries `{sha, message, stage_mode}`.
- `CommitOutcome` returned from `commit_staged_changes` and
`CommitOptions::commit` carries `{sha, message, stage_mode}` — the
*resolved* mode that was actually applied (CLI flag merged with config
defaults), not the raw `Option<StageMode>` from clap. Without this,
`--format=json` would emit `null` whenever the user didn't pass
`--stage`.
- `handle_push` / `handle_no_ff_merge` return `PushResult { target,
commit_count, outcome }` with outcome variants `FastForwarded` /
`UpToDate` / `MergeCommit { merge_sha }`. The merge-commit SHA was
previously discarded.
- `relocate::GatherResult` carries `template_error_branches` (was:
opaque count). Text mode already showed these in stderr; JSON now
surfaces them as `skipped` entries with `reason: "template_error"` so
automation can detect a broken `worktree-path` config rather than
reading `entries: []` as success.
## Behavioral guards
`--show-prompt` is rejected when combined with `--format=json` for `step
commit` / `step squash` — show-prompt emits raw LLM-prompt text that
would corrupt a JSON consumer's stdout.
## Reviewed by Codex
Two rounds. Round 1 caught the relocate template-error case (fixed in
this PR). Round 2 reported no remaining correctness issues.
## Tests
13 new integration tests covering the JSON shapes (parsed into
`serde_json::Value` and asserted against concrete keys). Full suite:
1604 / 1604 passing locally.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
e5d0c138ea |
Add wt step {commit,squash} --dry-run, hide --show-prompt (#2557)
Adds `--dry-run` to `wt step commit` and `wt step squash`. It renders
the prompt, prints the shell invocation that would call the LLM, calls
the LLM and prints the generated message in three labeled sections
(PROMPT, COMMAND, MESSAGE), then exits without staging, running hooks,
or committing. For commit, `--stage` is honored against a temp index —
the previewed prompt matches what a real run would send the LLM, but the
user's real index is never touched. Output is routed through
`show_help_in_pager` so the prompt section (50+ lines) doesn't scroll
off; piping (`| grep`, `| jq`) still works because the helper
TTY-detects.
`--show-prompt` is hidden via clap (`hide = true`) but kept working —
it's still the right shape for piping the cheap rendered prompt to
another LLM (`wt step commit --show-prompt | llm -m gpt-5-nano`), and
`GitError::LlmCommandFailed` still suggests it as a reproduction
command.
A new "When to page output" section in the `writing-user-outputs` skill
documents the policy: page long human-oriented stdout (`--help`, `wt
config show`, `wt hook show`, `--dry-run`); don't page pipe-first data
or output already paged by a delegated tool (`git diff`).
Follow-ups during review: `render_llm_invocation` now uses the shell's
basename (no install-path leakage), with an insta filter normalizing
`bash.exe` → `sh` for cross-platform snapshot stability. Added a
`run_git_capture` helper around the new direct `Cmd::new("git")` calls
that bails on non-zero exit (was a non-blocking reviewer observation —
without it, a failing `git diff --staged` would silently feed an empty
diff to the LLM). `stage_to_temp_index` was refactored to take argv
directly, eliminating a dead `StageMode::None` arm. Added unit tests for
`render_llm_invocation` and integration tests for `--dry-run
--stage=tracked` and `--dry-run` without LLM configured.
`codecov/patch` reports 95.29% with 9 misses. The remaining gap is split
between error-path fault-injection (`stage_to_temp_index` git-add
failure, `show_help_in_pager` spawn failure) and a codecov attribution
mismatch — local `cargo llvm-cov` shows `render_llm_invocation` as
covered by the new unit tests, but codecov doesn't credit them. Merged
with explicit override.
Test plan:
- [x] `cargo test --test integration -- merge::test_step_commit_dry_run
merge::test_step_squash_dry_run merge::test_step_commit_show_prompt
merge::test_step_squash_show_prompt`
- [x] `cargo run -- hook pre-merge --yes` (3438 tests pass, clippy
clean)
- [x] End-to-end smoke test in `/tmp/wt-dry-test`: untracked file
appears in dry-run prompt, real index stays untouched
- [x] `--dry-run` and `--show-prompt` are mutually exclusive (clap
`conflicts_with`)
- [x] CI green on Linux/macOS/Windows after the cross-platform shell
filter fix
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
8d5fff83c6 |
docs(homepage): replace "Scaling agents becomes trivial" intro line (#2553)
The line was a generic LLM-flavored flourish ("scaling," "becomes
trivial") that also overclaimed: it sat above the core-commands GIF,
whose own caption honestly says "Listing worktrees, switching, cleaning
up." The parallel-agents demo lives further down the page.
Drop the slop sentence above the basics demo, and move the zoom-out
claim down to the second demo's lead-in, where the GIF actually
substantiates it: `"Multiple parallel agents, same simple commands:"` —
ties back to the "as easy as branches" framing in the intro.
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
4b6316ac29 |
Release v0.47.0 (#2552)
Cuts v0.47.0. Highlights: - **Integration detection fix** across `wt list`, `wt remove`, `wt merge`, `wt step prune` for the local/upstream-diverged case (#2507, #2513, #2515). - **Faster shell-command dispatch** (~10ms per invocation, 1.49× on trivial aliases) via event-driven signal forwarding (#2537, #2538), plus parallel config load (#2543) and merged cold-start prewarm rev-parse (#2541). - **`wt switch <number>` suggests `pr:N` / `mr:N`** instead of just `--create` (#2516). - **Library API breaking changes**: `RefSnapshot` cutover (#2528, #2530) and the `for-each` direct-exec change (#2465). Full list in `CHANGELOG.md`. See `CHANGELOG.md` for the complete entry. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b12ac078b3 |
docs(extending): add stdin row to hooks-vs-aliases comparison (#2529)
The comparison table in `extending.md` covered invocation, positional handling, approval flags, source filters, and template-context extras — but never said anything about stdin. Hooks have always received the template context as JSON on stdin (documented in `hook.md`), and since #2380 aliases inherit the parent's stdin so pipes pass through and interactive TUIs (`wt switch`) keep the tty. Adding a row makes the contract discoverable from the aliases page rather than only from the CHANGELOG. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
845baeb976 |
refactor(git): RefSnapshot — close ambient ref-keyed cache staleness class (#2528)
## Summary Replaces the ambient ref-keyed caches in `RepoCache` with an explicit, point-in-time `RefSnapshot` value threaded through read paths. This closes the bug class behind PR #2507 (post-`update-ref` integration check seeing pre-write SHAs) — and several latent variants — by making the caches that could go stale unrepresentable. ## Why `RepoCache` cached ref-name → SHA in `commit_shas: DashMap<String, String>`, plus composite caches keyed off it (`integration_reasons`, `effective_integration_targets`, `ahead_behind`, `integration_target` OnceCell, `tree_shas`, `resolved_refs`). When wt itself moved a ref mid-command — most prominently `wt merge` running `git update-ref refs/heads/<target>` to advance the local target — those caches went stale, and any downstream read returned pre-write SHAs. PR #2507 patched one symptom by adding `ref_is_ancestor` (a cache-bypass helper) at the integration-check call site. That works locally; every other ref-name read after a write was a latent bug. The structural fix: there is no longer an ambient ref-name → SHA cache. Callers explicitly capture a `RefSnapshot` at known points and thread it through the read paths. After ref-mutating operations, callers capture a fresh snapshot — the discipline lives at the small set of write boundaries (currently one site, `finish_after_merge`), not at every ref-name accessor. Background analysis: `docs/dev/cache-staleness.md` (commit ba8b239) walks through six options and the trade-offs. ## What changed - **New `src/git/repository/ref_snapshot.rs`** — `RefSnapshot` value type. One `for-each-ref` over local + remote refs, optional ahead/behind batch. Cloned by value, no `OnceCell`/`Arc`. ~330 lines including unit tests (the freshness contract test mutates a ref and re-captures). - **Two-tier API** — `_by_sha` siblings of cached methods (`is_ancestor_by_sha`, `merge_base_by_sha`, `branch_diff_stats_by_sha`, `ahead_behind_by_sha`, `has_added_changes_by_sha`, `trees_match_by_sha`, `merge_integration_probe_by_sha`, …). The persistent on-disk `sha_cache` (already SHA-keyed) sits behind these. - **Snapshot-taking integration path** — `compute_integration_lazy`, `integration_reason`, `compute_integration_reason_uncached`, `integration_targets`, `delete_branch_if_safe`, `remove_worktree_with_cleanup` all take `&RefSnapshot`. - **Post-write fresh-snapshot pattern** — `wt merge`'s `finish_after_merge` captures a *fresh* snapshot AFTER `handle_no_ff_merge`/`handle_push` runs `update-ref`. This is the structural fix for PR #2507. The `ref_is_ancestor` workaround is deleted. - **`wt list` migration** — pre-skeleton phase captures one snapshot; `TaskContext` carries it; tasks call `_by_sha` siblings via the snapshot. No regression intended (one extra `for-each-ref` upfront, lose per-task `rev-parse` fallbacks). - **Cache deletions** — `commit_shas`, `tree_shas`, `effective_integration_targets`, `integration_reasons`, `ahead_behind`, `integration_target` OnceCell, `resolved_refs` all gone from `RepoCache`. `LocalBranchInventory`'s priming side-effects (writes to `commit_shas`/`resolved_refs`) removed. - **Uncached primitives** — `rev_parse_commit`, `rev_parse_tree`, `resolve_preferring_branch` no longer cache; consumers that need SHA-stable lookups thread a `RefSnapshot`. - **Persistent caches retained** — on-disk `sha_cache::*` and the in-memory `merge_base` / `branch_diff_stats` caches keep their SHA-keyed contract. ## Snapshot test deltas Three `list_*_with_nonexistent_default_branch.snap` snapshots no longer show the same-commit `_` symbol when the configured default branch is missing — that was a false positive (no ref to compare against). `list_warns_when_commit_details_batch_fails.snap` now embeds the resolved SHA in the error rather than the literal `"main"` ref name (strict improvement — the SHA is what was passed to git). ## Regression coverage - PR #2507's regression test (`test_remove_merged_locally_when_upstream_diverged`) passes without `ref_is_ancestor`. - PR #2513's `test_list_integrated_when_merged_locally_with_upstream_diverged` passes. - New unit test: `RefSnapshot` capture-mutate-recapture confirms two captures observe distinct SHAs after `update-ref`. ## Out of scope (deferred) Both flagged by `/reviewing-code` and explicitly deferred: - `prepare_worktree_removal` re-captures a snapshot per candidate inside `step_prune` — N+1 `for-each-ref` calls. Threading an optional `&RefSnapshot` would make `step_prune` reuse its entry snapshot. Affects display consistency in unusual hook setups (a `post-remove` hook that runs `git fetch` between candidates can desync the displayed integration target from the safety decision); not a safety bug. - `head_shas` cache retained. Different staleness model (per-worktree, mutated by `commit`/`reset` rather than `update-ref`); design doc lists it as a remaining vector. Out of scope for this PR's integration-target focus. ## Test plan - [x] `cargo run -- hook pre-merge --yes` (3413 tests pass before the merge of `origin/main`) - [x] 1071 lib + 607 bin + 1588 integration tests pass after merging `origin/main` - [x] `cargo check --all-targets` clean - [ ] CI green --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f418729e21 | fix(step/copy-ignored): drop .pi/ from built-in excludes (#2527) | ||
|
|
6eaf349711 |
refactor(for-each): exec argv directly, drop implicit shell (#2465)
## Problem
`wt step for-each` rebuilt the post-`--` command with `args.join(" ")`
before passing it to `sh -c`. That dropped the user's quoting and argv
boundaries, so anything with spaces, `;`, or shell metacharacters inside
an argv element broke. The reproduction from the issue:
```console
$ wt step for-each -- python3 -c 'import sys; print(sys.argv[1:])' 'a b'
sh: 1: Syntax error: word unexpected (expecting ")")
```
## Solution
Drop the implicit shell. The post-`--` argv is the program and its
arguments, exec'd directly via `Cmd::new(program).args(rest)`. Templates
expand into argv elements with `shell_escape=false`. Users wanting shell
features (pipes, redirects, `$VAR`, globs) write `sh -c '<snippet>'`
explicitly — the same pattern as `xargs`, `find -exec`, `kubectl exec
--`, and `docker run`.
This started as the smaller fix that landed on the previous commit
(shell-escape each argv element before joining, with an arity branch —
one arg meant "shell snippet," many args meant "argv to escape and
re-join"). On review the arity asymmetry felt wrong; the design
discussion concluded that direct exec is one rule with no quoting
recovery pipeline, and the #2461 bug becomes structurally impossible.
`for-each` is still tagged `[experimental]`, so the small breakage of
the documented snippet form is acceptable.
After the fix:
```console
$ wt step for-each -- python3 -c 'import sys; print(sys.argv[1:])' 'a b'
['a b']
$ wt step for-each -- sh -c 'git status | wc -l'
[per-worktree counts...]
```
### What changes for users
| Before | After |
|---|---|
| `wt step for-each -- 'git status \| wc -l'` | `wt step for-each -- sh
-c 'git status \| wc -l'` |
| `wt step for-each -- 'echo Branch: {{ branch }}'` | `wt step for-each
-- echo 'Branch: {{ branch }}'` (or wrap in `sh -c`) |
| `wt step for-each -- python3 -c '...' 'a b'` ✗ broken | `wt step
for-each -- python3 -c '...' 'a b'` ✓ works |
The `for-each` help/docs are rewritten around the simpler model (one
rule, two example blocks).
## Testing
- `test_for_each_preserves_argv_quoting` covers the exact reproduction
from the issue and is now exercising a load-bearing property of the
design rather than a join bug.
- `test_for_each_aborts_on_signal_exit` rewritten to use `sh -c
'<snippet>'` for the shell features it needs.
- `test_for_each_json_spawn_failure` simplified — direct exec means a
missing program is enough; the PATH/symlink dance for forcing sh-spawn
failure is no longer needed.
- `cargo test --test integration for_each` — 16 passed.
- `cargo run -- hook pre-merge --yes` — 3403 passed, 0 skipped, all
lints green.
---
Closes #2461 — automated triage
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
|
||
|
|
093f6b3fcf | Release v0.46.1 | ||
|
|
735756a46a | fix(docs): use __WT_QUOT__ placeholder in tips-patterns terminal blocks (#2496) | ||
|
|
01a8258b6e | Release v0.46.0 | ||
|
|
5dae8d8e92 |
feat(config)!: cap sanitize_db output at 48 chars (#2467)
## Summary - Lowers `sanitize_db`'s total output cap from 63 (PostgreSQL's identifier limit) to 48, leaving headroom for users composing the output into longer paths or identifiers — e.g., Unix socket paths capped at 107 bytes (#2397). - The 3-char hash suffix is unchanged (still derived from the original input), so collision avoidance is preserved at the new budget; only the truncated base shrinks. - **Breaking change** for branches whose current `sanitize_db` output exceeds 48 chars: the truncated base shifts, so the final identifier changes. Most branch names are well under 48 chars and pass through unchanged. Users who relied on the previous output as a stable database identifier should be aware. Companion to #2453 (added the `hash` filter so users can compose their own truncate-with-collision-avoidance recipes when 48 still isn't tight enough). Refs #2397. ## Test plan - [x] `cargo test --lib --bins -- sanitize_db` — 5 unit tests pass (including updated `test_sanitize_db_truncation`) - [x] `cargo test --test integration -- sanitize_db` — 3 integration tests pass (including updated `test_doc_sanitize_db_truncation`) - [x] `cargo test --test integration test_docs_are_in_sync` — passes (docs/skills auto-synced) - [x] `cargo insta test --accept --test integration -- "test_help"` — help snapshots regenerated; no surprise diffs --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
719a102409 |
feat(config): add hash template filter (#2453)
|