mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
worktree-path-not-found-error
796 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
5a07bc09bd | feat(plugin): ship the wt-switch-create skill in the Claude Code plugin (#2737) | ||
|
|
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) | ||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
735756a46a | fix(docs): use __WT_QUOT__ placeholder in tips-patterns terminal blocks (#2496) | ||
|
|
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)
|
||
|
|
383774b38b |
docs: document conda-forge / pixi installation (#2425)
## Problem The README install section lists Homebrew, Cargo, winget, and pacman, but does not mention the conda-forge package — which has been published since 0.32.0 and is currently at 0.44.0 with builds for `linux-64`, `linux-aarch64`, `osx-64`, `osx-arm64`, and `win-64` ([anaconda.org/conda-forge/worktrunk](https://anaconda.org/conda-forge/worktrunk)). Reported in #2424. ## Solution Adds a "Conda / Pixi" entry to the install section in `docs/content/worktrunk.md`, between Cargo and the Windows expandable. README and the worktrunk skill reference are auto-synced from this source. ## Testing `cargo test --test integration test_docs_are_in_sync` passes. --- Closes #2424 — automated triage --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
4cbc8ca5fd |
refactor(docs): drop mirrored help-page close; align convert_console_blocks_in_docs error channel (#2422)
## Summary Three follow-ups from #2419 review. - **Bare close everywhere.** With inner snapshot wrappers gone from command pages (#2419), no `AUTO-GENERATED` markers nest inside the help-page region anywhere in `docs/content/*.md`. The mirrored close (`<!-- END AUTO-GENERATED from \`wt cmd --help-page\` -->`) was the only remaining variant of the close form; collapsed to bare `MARKER_CLOSE`. - `src/help.rs::PageMode::emit_footer()` no longer takes `subcommand`. - The help-page regex in `readme_sync.rs` matches via non-greedy `.*?` to bare `MARKER_CLOSE`, with a comment pointing at the new invariant test. - `AUTO_GENERATED_MARKER_PATTERN` strip regex built from the constants. Added **`test_no_nested_auto_generated_markers`** — walks `docs/content/*.md` and fails if any `AUTO-GENERATED` open ever appears inside an already-open region. This is the explicit invariant that bare-close pairing depends on; if a future change tries to re-introduce nesting (e.g., restore an inner snapshot wrapper around terminal shortcodes), the test catches it before the subtle "regex chops region at first inner close" failure mode lands. - **Aligned error channel.** `convert_console_blocks_in_docs` now returns `(Vec<String>, Vec<String>)` like its sibling sync steps, with per-file error capture for `read_dir`, dir entries, and `read_to_string`. The caller passes errors through the same `tag()` aggregation as everything else, so a transient I/O failure on one file no longer aborts the whole pipeline silently. Also fixes the matching clippy warning (`is_some_and(|e| e == \"md\")` → `is_none_or(|e| e != \"md\")`). - **Docs alignment.** `docs/CLAUDE.md` updated in two places where the prose still documented the mirrored close as the canonical form. Visual check via curl across 12 dev-server pages confirmed no leakage of any prior marker form. Adversarial review (subagent /popper-style) caught the stale prose; otherwise no regressions found. Net diff: +113/-49 (the growth is the new invariant test and explicit per-file error handling; structural simplification shows as -49). ## Test plan - [x] `cargo test --test integration readme_sync` — 12 sync tests pass (was 11; +1 for the new invariant test) - [x] `cargo test --test integration` — full integration suite (1558 tests) pass - [x] `cargo test --test integration test_no_nested_auto_generated_markers` — new guard test passes; manually verified it fires on synthetic nesting - [x] Single-pass convergence verified: `git checkout docs/ && cargo test --test integration test_docs_are_in_sync && git diff --stat` produces an empty diff after the first run - [x] Visual check via local Zola dev server: 12 docs pages (\`/worktrunk/\`, \`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`, \`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\`, \`/switch/\`, \`/config/\`, \`/faq/\`) — only HTML comments contain marker text, no rendered leakage - [x] `cargo clippy --all-targets --all-features` — clean - [x] `cargo fmt --check` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
66a5becc25 |
refactor(tests): consolidate three sync tests; drop dead inner snapshot wrappers (#2419)
## Summary Two follow-ups from #2418 review. - **Test brittleness fix.** Three previously-separate sync tests had data dependencies that nextest could interleave: `test_readme_examples_are_in_sync` reads `docs/content/*.md`, which `test_docs_quickstart_examples_are_in_sync` generates from snapshots. Under parallelism, README sync could see stale docs and produce content that required a second run to converge. Collapsed both into the existing `test_command_pages_and_skill_files_are_in_sync` pipeline, renamed to `test_docs_are_in_sync`. Steps run sequentially in dependency order; single pass converges from a clean working tree. Each step's errors and updated-file list are tagged with the pipeline stage (`[command pages]`, `[standalone docs]`, `[README]`, etc.) so a failure tells a developer which stage broke without reading the test source. README failure also now reports `(N of M section(s) updated)`. - **Dead inner snapshot wrappers.** `expand_command_placeholders` wrapped each terminal shortcode in command pages with `<!-- ⚠️ AUTO-GENERATED from <snap> --> ... <!-- END AUTO-GENERATED -->` markers. Command pages regenerate wholesale from `--help-page` each sync, so the inner wrapper served no in-place-refresh purpose — it was dead weight nested inside the outer help-page region's markers. Stripped from `expand_command_placeholders`'s HTML branch; net -32 lines across six command-page docs files. The outer help-page region's mirrored close stays — it's load-bearing whenever any nested `AUTO-GENERATED` marker appears in the body. Comment in `src/help.rs:emit_footer` updated to explain the role. (My follow-up note that the mirrored close was redundant turned out to be a misanalysis — non-greedy `.*?` only safely pairs the open with the right close when there are no nested closes; once the inner snapshot wrappers were gone, the mirror became technically unnecessary, but keeping it survives any future reintroduction of nesting at no cost.) Renamed the test in `CLAUDE.md` (4 sites) and `docs/CLAUDE.md` (2 sites) so the documented `cargo test` invocations resolve. Net diff: −54 lines. ## Test plan - [x] `cargo test --test integration readme_sync` — 11 sync tests pass (was 13; minus the two collapsed) - [x] `cargo test --test integration` — full integration suite (1557 tests) pass - [x] **Single-pass convergence verified**: `git checkout docs/ README.md && cargo test --test integration test_docs_are_in_sync && git diff --stat` produces an empty diff after the first run - [x] Visual check via local Zola dev server: \`/worktrunk/\`, \`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`, \`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal blocks cleanly with no leaked marker strings - [x] Adversarial review of the consolidation (subagent /popper-style): all 4 actionable findings (stale doc references, lost README count, missing stage tags, etc.) addressed - [x] `cargo clippy --all-targets --all-features` — clean - [x] `cargo fmt --check` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
8abefbce82 |
refactor(docs): unify AUTO-GENERATED marker form; share constants across crate boundary (#2418)
## Summary Follow-ups from #2417 review, plus adjacent simplifications. - **Cross-crate share of marker constants.** `MARKER_OPEN_PREFIX` and `MARKER_CLOSE` now live in `src/docs.rs`. `src/help.rs` (the `--help-page` producer of help-region markers) and `tests/integration_tests/readme_sync.rs` (the consumer + producer of snapshot/section markers) both reference them — drift becomes a build error rather than a silent mismatch. - **Drop `MARKER_OPEN_HTML_PREFIX`.** The `-HTML` variant emitted visually identical wrapping; the suffix only signalled "in-place refreshable" but the `.snap` ID + terminal-shortcode body in `DOCS_SNAPSHOT_MARKER_PATTERN` already discriminate that. Standardised on a single open prefix and stripped the `-HTML` literals from `README.md`, the four standalone docs files, and the test file. - **Help-page marker uses the shared prefix.** The mirrored close (`<!-- END AUTO-GENERATED from \`wt <cmd> --help-page\` -->`) stays as-is because adjacent regions need unambiguous pairing, but the open prefix now goes through `MARKER_OPEN_PREFIX`. - **Drop `MarkerType::output_format()` / `extract_inner()`.** Both had a single trivial non-panic branch that existed only to enforce "no Snapshot markers in README" via `unreachable!()`. Replaced with one explicit assertion in `sync_readme_markers` — surfaces the invariant as an actionable error message instead of a panic. - **Collapse `format_replacement`.** `wrap_in_marker` is invoked once for both output formats; only body construction varies. - **`__WT_QUOT__` rename in `tests/integration_tests/user_hooks.rs`.** Inlined the single quotes — the `.replace('__WT_QUOT__', \"'\")` was unnecessary indirection (the outer raw-string literal already tolerates `'`, and TOML / Tera don't care about embedded `'`). Removes a name-conflation footgun with the unrelated `__WT_QUOT__` placeholder in `src/docs.rs`. Net diff: -7 lines. ## Test plan - [x] `cargo test --test integration` — full integration suite (1559 tests) pass - [x] `cargo test --test integration readme_sync` — all 13 sync tests pass; `test_readme_examples_are_in_sync` now produces a stable README on a clean run (verified by re-running multiple times — no further updates after the initial regeneration) - [x] `cargo test --test integration test_args_indexing_and_length_in_hook_template` — confirms the user_hooks single-quote inlining works through TOML and Tera - [x] Visual check via local Zola dev server: \`/worktrunk/\`, \`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`, \`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal blocks cleanly with no leaked marker strings (`AUTO-GENERATED-HTML`, `__WT_OPEN2__`, trailing `|||`) - [x] `cargo clippy --all-targets --all-features` — clean - [x] `cargo fmt --check` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
1e053d2d88 |
refactor(docs): fix blank-line cmd-stream corruption; share AUTO-GENERATED marker constants (#2417)
## Summary Three small follow-ups + a placeholder rename, all from #2405 review feedback. - **Root-cause fix for `|||` corruption.** `convert_dollar_console_to_terminal` was promoting blank lines inside mixed `\$ cmd + output` blocks to extra `cmd=` entries, dropping the blank from the rendered body and emitting a stray `\$` prompt (e.g. `cmd=\"wt list|||\"`). Now distinguishes command-only blocks (blanks are visual spacing in `cmd=`) from mixed blocks (blanks belong to body). Removes the matching `trim_end_matches('|')` bandage in `expand_command_placeholders`. New unit test in `src/docs.rs` covers the mixed-block case. - **Single-pass classification.** Folded the two filter chains over `block_lines` into one loop with one match — fewer branches, no duplicated predicates. - **AUTO-GENERATED marker consolidation.** Within `tests/integration_tests/readme_sync.rs`, the `<!-- ⚠️ AUTO-GENERATED ... -->` literal appeared in four producer / regex sites with subtly different shapes (HTML vs plain, ID format). Factored into `MARKER_OPEN_PREFIX` / `MARKER_OPEN_HTML_PREFIX` / `MARKER_CLOSE` constants plus a `wrap_in_marker()` helper, and threaded those constants into the regexes via `regex::escape`. Pure refactor — no docs/skill files regenerated. - **`__WT_OPEN2__` / `__WT_CLOSE2__` → `__WT_OPEN__` / `__WT_CLOSE__`.** The `2` was meant to distinguish doubled-brace placeholders from hypothetical single-brace ones, but Tera only treats `{{`/`}}` (not single braces) as template delimiters — there's no second variant to disambiguate from. Updated the producer (`src/docs.rs`), the test-side decoder (`tests/integration_tests/readme_sync.rs`), the Zola template (`docs/templates/shortcodes/terminal.html`), the docs-site `CLAUDE.md`, and the regenerated `docs/content/{step,hook}.md`. ## Test plan - [x] `cargo test --lib docs::tests::test_convert_dollar_console_to_terminal` — exercises new mixed-block case + existing command-only / multi-cmd / comment cases - [x] `cargo test --test integration readme_sync` — 13 sync tests still pass after the refactor and bandage removal - [x] `cargo test --test integration` — full integration suite (1558 tests) pass - [x] Visual check via local Zola dev server: `/merge/`, `/step/`, `/remove/`, `/hook/`, `/llm-commits/`, and `/list/` all render terminal blocks cleanly with no `|||` artifacts and no leaked placeholder strings (`__WT_OPEN__` etc. don't appear in served HTML). The multi-command jq recipes block on `/list/` continues to render comments as bash-styled section headers. - [x] `cargo clippy --all-targets --all-features` — clean - [x] `cargo fmt --check` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6d0588de3c |
docs(faq): document new summary cache layout (#2408)
## Summary PR #2407 added the content-addressed summary cache at `.git/wt/cache/summary/{branch}/{hash}.json`, but the FAQ inventory in ['What files does Worktrunk create?'](https://worktrunk.dev/faq/#what-files-does-worktrunk-create) only listed the flat `.git/wt/cache/{kind}/*.json` shape. That row doesn't capture the nested layout (`{branch}/{hash}.json` instead of flat) or the LLM-summary purpose, so the on-disk surface now exceeds what the docs admit. Add a dedicated row matching how `wt config state get` already surfaces summaries as a separate `SUMMARY CACHE` table. Caught while reviewing recent commits in the nightly sweep. ## Test plan - [x] `cargo test --test integration test_command_pages_and_skill_files_are_in_sync` — passes (skill mirror auto-synced) - [ ] CI green Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5f126d21fe |
docs(pages): add static command output to sections dominated by GIFs (#2405)
## Summary Adds static command-output blocks to the docs pages dominated by GIFs (addresses #2403). The blocks are **driven from insta snapshots** so they stay in lockstep with what `wt` actually prints — one source flows to all three surfaces: - terminal `wt <cmd> --help` (plain text, gutter-formatted) - `docs/content/*.md` (colorized `{% terminal(cmd="...") %}` shortcode) - `skills/worktrunk/reference/*.md` (plain `$ cmd\noutput\n` block) ## What changed - **Six scripted snapshot tests** produce realistic output (`cargo nextest run`, `flyctl scale count 0`, LLM-generated commit messages): - `test_docs_merge_pre_merge_hook` — `wt merge` with pre-merge hook - `test_docs_step_commit_llm` — `wt step commit` with LLM - `test_docs_step_squash_llm` — three-commit squash with LLM - `test_docs_merge_squash_llm` — `wt merge` (squash + LLM + merge) for `llm-commits.md` - `test_docs_remove_pre_remove_hook` — `wt remove` with pre-remove hook - `test_docs_hook_pre_merge` — `wt hook pre-merge` direct invocation - **Sync pipeline extension** in `tests/integration_tests/readme_sync.rs`: - New write-back pass `sync_cli_mod_example_bodies` fills the ```console``` body in each `<!-- wt <cmd> (docs-example) -->` placeholder in `src/cli/mod.rs` from the registered snapshot. Runs before `--help-page` reads the file. - `COMMAND_PLACEHOLDER_PATTERN` extended to match three forms (```bash```, `{{ terminal() }}` self-closing, `{% terminal %} body {% end %}`). This **fixes a pre-existing bug** in `docs/content/list.md` where the HTML-mode expansion was silently broken. - Stripped trailing `|||` corruption that arises when blank lines in snapshot bodies are interpreted as empty commands by `convert_dollar_console_to_terminal`. - **Docs page migration**: - `src/cli/mod.rs` — replaced four hand-written ```console``` blocks (merge, step, remove, hook) with `<!-- wt <cmd> (docs-example) -->` markers. - `docs/content/llm-commits.md` — replaced three hand-crafted HTML blocks with `<!-- ⚠️ AUTO-GENERATED-HTML from X.snap -->` markers. - **Refactor follow-up** in a separate commit: - `BADGE_EXPERIMENTAL_HTML`, `SUBDOC_MARKER_PREFIX`, `DEMO_MARKER_PREFIX` hoisted to `worktrunk::docs` so producer and consumer stay in lockstep. - `normalize_clap_help_fences()` consolidates the `text→` + `console→bash` replacement pair shared by `--help-md` and `help_reference_inner`. - **CLAUDE.md note** documenting the `.gitattributes` `linguist-generated=false` exemption requirement when adding skill-only files (carryover from #2409). ## Test plan - [x] `cargo test --test integration readme_sync` — all 13 tests pass, idempotent - [x] `cargo test --test integration test_help` — help snapshots updated - [x] `cargo run -- {merge,remove,step,hook} --help` — clean gutter formatting, no visible HTML comments - [x] `cargo run -- hook pre-merge --yes` — full project gate green (3355 tests) - [ ] Visual check on dev site (maintainer — terminal shortcodes now render with colors via ANSI→HTML; dark/light variants both expected to look like the GIFs they replace) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
3a9d6402cb |
docs(hook): clarify when pre-start/post-start fire (#2360)
## Summary Refs #1571. Adjusts the existing `pre-start` and `post-start` rows in the hook types table to say when the hooks fire, rather than only what to put in them. Before: ``` | `pre-start` | Tasks that must complete before `post-start`/`--execute`: dependency install, env file generation | | `post-start` | Dev servers, long builds, file watchers, copying caches | ``` After: ``` | `pre-start` | Runs once when a new worktree is created, blocking `post-start`/`--execute` until complete: dependency install, env file generation | | `post-start` | Runs once when a new worktree is created, in the background: dev servers, long builds, file watchers, copying caches | ``` Matches the phrasing of neighbouring rows (e.g. `pre-switch` has \"Runs before...\", `post-merge` has \"Runs in the target...\"). No new section or explanation added — the adjustment is confined to the two rows whose definitions were silent on timing. Per @max-sixty's ask in https://github.com/max-sixty/worktrunk/issues/1571#issuecomment-4291227117, marked as draft for review. ## Test plan - [x] `cargo test --test integration test_command_pages_and_skill_files_are_in_sync` — passes after regenerating `docs/content/hook.md` and `skills/worktrunk/reference/hook.md` - [x] `cargo insta test --accept -- --test integration test_help` — no snapshot changes (the row is inside the table rendered from `after_long_help`; existing help snapshots already absorbed the old wording in their expected forms but don't anchor on it) Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ea505bebf9 |
fix(step): gate copy-ignored self-lower on WORKTRUNK_FOREGROUND=-1 (#2358)
## Summary Addresses #2342 — `wt step copy-ignored` has been much slower on macOS since v0.37.0 because `taskpolicy -b` throttles disk I/O, and that hits foreground callers (interactive use and `pre-*` hooks) as well as background ones. Per the [design proposed in the issue](https://github.com/max-sixty/worktrunk/issues/2342#issuecomment-4290580468): - Export `WORKTRUNK_FOREGROUND=-1` when spawning a background hook pipeline (detached `wt hook run-pipeline`). The env var is inherited by every descendant — shell, user command, nested `wt` invocations. - `wt step copy-ignored` self-lowers only when it sees that sentinel; interactive runs and synchronous `pre-*` hooks run at normal priority. - Documented in `wt step copy-ignored --help` under a "Background-hook priority (experimental)" section. Variable name and value flagged as not-yet-stable. Surface area: - `src/priority.rs` — new `FOREGROUND_ENV_VAR` / `BACKGROUND_HOOK_VALUE` consts and `in_background_hook()` helper, with a testable inner fn (`is_background_hook_value`) so we can unit-test the match without mutating process-global env. - `src/commands/process.rs` — `spawn_detached_exec` now sets the env var on the detached runner when the log variant is `HookLog::Hook`. Internal ops (removal, trash-sweep) don't set it. - `src/commands/step_commands.rs` — `copy-ignored` gates the existing `lower_current_process()` call on `in_background_hook()`. - `src/cli/step.rs` + auto-synced docs — experimental note, and removed a now-stale paragraph that claimed unconditional `nice 19`. - Integration test verifies `pre-start` sees the var unset and `post-start` sees `-1`. ## Test plan - [x] `cargo test --lib --bins` — 601 passed - [x] `cargo test --test integration test_background_hook_sees_worktrunk_foreground_env_var` — passes - [x] `cargo test --test integration step_copy_ignored` — 44 passed - [x] `cargo test --test integration user_hooks` — 106 passed - [x] `cargo test --test integration test_command_pages_and_skill_files_are_in_sync` — passes (docs auto-synced) - [x] `cargo test --test integration test_help` — 40 help snapshots pass - [x] `cargo clippy --all-targets` — clean --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
771918f29e |
docs(hook): promote Recipes to top-level H1 (#2351)
Follow-up to #2349. With the old "Designing Effective Hooks" umbrella heading removed, `## Recipes` was landing at H2 under `# Running Hooks Manually`, reading as a sub-topic of manual invocation — which it isn't. Promoting to `# Recipes` makes it a peer of the other top-level sections (`# Hook Types`, `# Security`, `# Configuration`, `# Running Hooks Manually`). Trailing `## See also` stays at H2 — consistent with how other command docs handle their "See also" footers. > _This was written by Claude Code on behalf of Maximilian_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
970375afd4 |
docs(hook): restructure recipes list and callout for copy-ignored (#2349)
Three tweaks to the hook docs: - Drop the "Designing Effective Hooks" heading and rename "More recipes" to "Recipes". - Make each recipe bullet lead with a specifically-named link to its Tips & Patterns section, so the list functions as a table of contents rather than a paragraph with a trailing URL. - Move "Copying untracked files" up next to the JSON context section and frame it as a specific command worth calling out, rather than leaving it as a lone sibling of "Recipes" under the old umbrella heading. Source is `after_long_help` in `src/cli/mod.rs`; `docs/content/hook.md` and `skills/worktrunk/reference/hook.md` are regenerated by the sync test. Dead post-processor entries in `src/help.rs` that used to convert the old bare-URL bullet format are removed. > _This was written by Claude Code on behalf of Maximilian_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
f0152a5a0d |
docs(hook): consolidate pipeline forms into one section (#2333)
The \`# Configuration\` intro introduced the string/table/pipeline forms, and a later \`# Pipeline Ordering [experimental]\` section re-explained the same three forms with different examples. Merge them under a single \`## Hook forms\` subsection inside \`# Configuration\`, keep the "when to use \`[[hook]]\`" guidance, and drop the stale \`[experimental]\` tag. Update the two \`#pipeline-ordering\` anchor links in \`extending.md\` to \`#hook-forms\`. Net: 6 files, -137 lines. No behavior change — help output unchanged (no snapshot updates needed). > _This was written by Claude Code on behalf of Maximilian Roos_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
18825c3e3c |
docs(hook): move progressive-validation and target-specific examples to tips-patterns (#2329)
Keeps "Copying untracked files" inline (the one example that materially uses worktree-specific mechanics) and moves the two plain-TOML patterns — progressive validation and target-specific `post-merge` — to `tips-patterns.md`. `hook.md` references them as `More recipes` bullets instead. > _This was written by Claude Code on behalf of Maximilian._ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
1a0a3e2f83 |
Widen Zola link regex to tolerate code spans in link text (#2327)
`ZOLA_LINK_PATTERN`'s link-text class `[^\]]+` rejected any `]`, so Zola links whose text contained a code span with `]]` (e.g. `` `[[block]]` ``) never matched and shipped as dead `@/…md` references in skill files. #2321 worked around two such links by rewording them — this fixes the root cause and restores the original wording. ## The regex change Alternate "a balanced `` `…` `` code span" with "any non-`]`-non-backtick char" inside the link text: ``` \[((?:`[^`]*`|[^\]`])+)\]\(@/([^)#]+)\.md(#[^)]*)?\) ``` Forbidding bare backticks in the single-char branch is load-bearing: without it, the regex can bridge across two unrelated code spans on the same line. `docs/content/faq.md:63` is a live example — `` [`worktrunk-sync`](https://github.com/…) `` followed much later by `` [custom subcommands](@/extending.md#custom-subcommands) ``. With a permissive single-char branch, the engine pairs backtick 2 with backtick 3 (spanning the `]` after `worktrunk-sync`) and matches the whole chunk as one "link." ## Guardrail Added an `UNTRANSFORMED_ZOLA_LINK_PATTERN` check in `finalize_skill_content`. After the transform, any leftover `](@/…md)` panics the sync test with the offending line — so a future regex miss fails loudly instead of silently shipping a dead link. ## Doc restoration Reverts the two rewordings from #2321 in `docs/content/extending.md` (`[[block]]` now sits inside the link text again). ## Testing `cargo test --test integration readme_sync` (12 tests) passes. Full `cargo run -- hook pre-merge --yes` green locally (3291 tests, all lints). > _This was written by Claude Code on behalf of Maximilian Roos_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0be0a28eae |
docs(hook): drop pre-start vs post-start section (#2326)
The section mostly restated content already covered by the "Hook Types"
table and the hook purpose table above it. Removing it and folding the
load-bearing guidance ("prefer `post-start` unless a later step needs
the work completed first") into the existing "most common starting
point" sentence near the top.
> _This was written by Claude Code on behalf of Maximilian_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
064f8940c0 |
docs(hook): consolidate copy-ignored recipe and trim redundant examples (#2323)
Docs consolidation sweep, same pattern as #2319. **Copy-ignored recipe** — collapses hook.md's "Copying untracked files" overlap with tips-patterns.md into a "More recipes" bullet pointing at the canonical recipe. Keeps a one-paragraph intro in hook.md (worktrees don't share untracked files → use `wt step copy-ignored`) since it's a fundamental worktree concept. Moves the concrete pnpm pipeline example into tips-patterns.md's canonical recipe, and switches it from `[[pre-start]]` to `[[post-start]]` — the pipeline form handles ordering without blocking worktree creation. `pre-start` is now called out only as the exception for when `--execute` needs the files immediately. **Trims** - Dropped hook.md's "Hook type examples" dump: most of its 10 entries duplicate existing recipes (dev server, database, cold starts) or the Progressive validation section. - Dropped tips-patterns.md's "Local CI gate": four-line recipe that duplicated hook.md's Progressive validation and merge.md's Local CI narrative. - Dropped hook.md's "Python virtual environments" snippet: step.md already owns language-specific notes. > _This was written by Claude Code on behalf of Maximilian Roos_ --------- Co-authored-by: Claude <noreply@anthropic.com> |