Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
# macOS ships zsh; ubuntu needs it installed so the
# shell-integration smoke tests run on both platforms
# instead of silently skipping on one.
- if: runner.os == 'Linux'
run: sudo apt-get update -qq && sudo apt-get install -y -qq zsh
- uses: actions/setup-go@v5
with:
go-version: stable
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## v0.1.0-alpha.3 - 2026-07-20

### Enhancements

- One `eval "$(wt shell-init zsh)"` line in `.zshrc` makes bare `wt` an interactive tree picker, `wt go` a fuzzy cd, and `wt new` land in the fresh tree — with zsh completions, an opt-in `WT_PROMPT` indicator, and a script-safe `wt ls --porcelain` fallback whenever no TTY is present. (#8)

### Documentation

- A shell-integration docs page covers the cd protocol, fuzzy-matching rules, completions, and the `WT_PROMPT`/starship prompt recipes. (#8)

### Infrastructure

- The ubuntu CI runner installs zsh, so the shell-integration smoke tests run on both platforms. (#8)

## v0.1.0-alpha.2 - 2026-07-20

### Enhancements
Expand Down
47 changes: 34 additions & 13 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,9 @@ so `wt shell-init zsh` emits:
(binary prints target path on stdout; function does the `cd`);
2. cobra-generated zsh completions;
3. an opt-in prompt hook: a zsh `chpwd` hook exporting `WT_PROMPT`
from a cached path check against known tree roots —
no git subprocess per prompt render —
from a live, builtins-only inspection of the tree's `.git` file —
no git subprocess and no cache to go stale,
recomputed only on cd, never per prompt render —
plus a documented starship `custom`-segment alternative.

The prompt indicator ships as **optional**
Expand Down Expand Up @@ -356,7 +357,7 @@ merged-branch slots, and `wt clean -n` previews every action.
| `wt` | Interactive fuzzy picker over trees → cd. Non-TTY: porcelain list. |
| `wt init` | Interactive setup: base branch, trees dir, pool y/n + size, prompt indicator, copy list; writes `.git/wt.toml`. |
| `wt new <branch> [--base <ref>]` | Default: create worktree + branch off base. Pool: claim a slot, reset, branch there. Prints tree path on stdout. |
| `wt ls [--json]` | List trees: branch, path, age, ahead/behind base, dirty, slot/lease state. |
| `wt ls [--porcelain] [--json]` | List trees: branch, path, age, ahead/behind base, dirty, slot/lease state. |
| `wt go [query]` | Fuzzy-jump: best match cd (with query) or picker (without). |
| `wt done [name] [--keep-branch]` | Finish a tree: safety checks, then remove (default) or release+reset slot (pool). Alias: `wt rm`. |
| `wt sync [--all]` | Fetch base, fast-forward it, re-park idle slots, report behind-counts. Never touches branches with user commits. |
Expand Down Expand Up @@ -490,7 +491,8 @@ branch protection live on `main` and `dev`.
- **Exit:** full default-mode lifecycle usable day-to-day from the raw binary
(no cd yet). Tag `v0.1.0-alpha.2`.

**Status (2026-07-20):** code complete, PR open against `dev`.
**Status (2026-07-20): complete.**
Merged to `main`; `v0.1.0-alpha.2` tagged and released.
Notable additions beyond the checklist:
`wt done` sweeps wt-planted `copy` files when their content still
matches the main checkout (an edited copy still trips the guard),
Expand All @@ -506,28 +508,47 @@ instead of leaking through the D13 contract;
tracked copy-list entries are left to git on both the plant
and sweep sides;
and `wt done` points prunable trees at `git worktree prune`.
Remaining before exit is met: merge, batch fragments,
tag `v0.1.0-alpha.2`.
Phase 3 (shell integration & navigation) is ready to be taken up
once the tag is cut.

### Phase 3 — Shell integration & navigation (M)

- [ ] `wt shell-init zsh`: `go:embed` shim template
- [x] `wt shell-init zsh`: `go:embed` shim script
`wt()` function, cd protocol, completions;
golden-file test of the emitted script plus a zsh smoke test
(`zsh -c 'eval "$(wt shell-init zsh)"; wt go x'`) under testscript.
- [ ] `wt go <query>`: sahilm/fuzzy over branch names + slot tickets;
- [x] `wt go <query>`: sahilm/fuzzy over branch names + slot tickets;
ambiguous → top-5 disambiguation on stderr, exit 3.
- [ ] Bare `wt` and bare `wt go`: go-fuzzyfinder picker with preview pane;
- [x] Bare `wt` and bare `wt go`: go-fuzzyfinder picker with preview pane;
**non-TTY fallback to porcelain list**
(testscript covers the non-TTY path;
the picker itself gets a manual test note).
- [ ] Optional prompt segment: chpwd-cached `WT_PROMPT`,
- [x] Optional prompt segment: chpwd-refreshed `WT_PROMPT`,
`--prompt` flag on shell-init; starship recipe in docs.
- **Exit:** the eval line in `.zshrc` gives cd-on-select, completions,
optional prompt. Tag `v0.1.0-alpha.3`.

**Status (2026-07-20):** code complete, PR open against `dev`.
Two D12 refinements surfaced by implementation:
the interactivity probe is **stdin + stderr**, never stdout —
the shim captures stdout to implement the cd protocol,
so a stdout check would make the picker unreachable
(the picker renders on `/dev/tty`;
a dumb or unset `TERM` also counts as non-interactive;
the porcelain fallback for scripts and agents is unchanged) —
and the porcelain fallback landed as an explicit
`wt ls --porcelain` flag so scripts can ask for it by name.
Fuzzy ranking normalizes away the matcher's name-length penalty:
a jump is decided by match quality alone,
so `feature` refuses to pick `feature/login` over `feature/logout`
just because it is a letter shorter.
The shim's cd set is bare `wt`, `wt go`, and `wt new`;
`wt path` stays cd-free plumbing.
Completions are bootstrapped via `eval "$(wt completion zsh)"`
inside the shim rather than inlined,
so they track the installed binary and golden files stay stable.
Remaining before exit is met: merge, batch fragments,
tag `v0.1.0-alpha.3`.
Phase 4 (pool mode) is ready to be taken up once the tag is cut.

### Phase 4 — Pool mode (L)

- [ ] `internal/lease`: atomic-mkdir lease + PID/start-time liveness —
Expand Down Expand Up @@ -673,7 +694,7 @@ stays short):
| R11 | Shared object store: gc/repack while trees are in use | Docs note; `wt clean` never triggers gc; watch-item, not v1 machinery | 6 |
| R12 | Locked worktrees (`git worktree lock`) | `ls` shows the lock flag; `done` refuses locked trees with an explanation | 6 |
| R13 | Unsigned-binary quarantine on macOS | Cask `postflight` xattr hook; notarization post-1.0 | 1 |
| R14 | Prompt hook slows every prompt render | chpwd caching, no git subprocess in the prompt path; feature optional | 3 |
| R14 | Prompt hook slows every prompt render | chpwd hook of zsh builtins only — no subprocess, recomputed on cd not per render; feature optional | 3 |
| R15 | Picker hangs agents/scripts | Non-TTY → porcelain, enforced by testscript | 3 |
| R16 | IDE state (VS Code) doesn't follow trees | Docs: `code $(wt path <name>)`; out of scope for v1 | 7 |
| R17 | Dev-server port collisions across parallel trees | Docs pattern: derive port from slot number in `.envrc`; not wt machinery | 7 |
Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,32 @@ Fallback, no Homebrew required:
go install github.com/loganthomas/wt/cmd/wt@latest
```

Then wire up the shell integration —
cd-on-select, completions, optional prompt indicator —
with one line in `~/.zshrc`:

```sh
eval "$(wt shell-init zsh)"
```

Details in [docs/shell.md](docs/shell.md).

## Commands

| Command | One-liner |
| -------------------------------- | ----------------------------------------------------------------------------- |
| `wt` | Interactive fuzzy picker over trees → cd. Without a TTY: porcelain list. |
| `wt init` | Set up wt for a repo (prompts, or `--yes` + flags); writes `.git/wt.toml`. |
| `wt new <branch> [--base <ref>]` | Create a worktree + branch off the base; prints the tree path on stdout. |
| `wt ls` | List worktrees: branch, path, state. |
| `wt new <branch> [--base <ref>]` | Create a worktree + branch off the base, and cd there under the shim. |
| `wt ls [--porcelain]` | List worktrees: branch, path, state. |
| `wt go [query]` | Fuzzy-jump: best match cds (with a query) or picker (without). |
| `wt done [name] [--keep-branch]` | Finish a tree: safety checks, remove it, delete its branch. Alias: `wt rm`. |
| `wt path [name]` | Print a tree's absolute path (plumbing). |
| `wt config [--edit]` | Show active config paths and merged values; `--edit` opens `$VISUAL`/`$EDITOR`. |
| `wt shell-init zsh [--prompt]` | Emit the shim, completions, and optional prompt hook for eval in `.zshrc`. |
| `wt --version` | Version, commit, build date. |

The full surface (`go`, `sync`, `clean`, pool mode, …)
The full surface (`sync`, `clean`, pool mode, …)
lands phase by phase; see [PLAN.md](PLAN.md).
Configuration reference: [docs/configuration.md](docs/configuration.md).
Scripting and agent contract: [docs/agents.md](docs/agents.md).
Expand Down
13 changes: 13 additions & 0 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,27 @@ then retry", never "wt is broken".
| ----------- | --------------------------------------------- |
| `wt new` | The new tree's absolute path, one line. |
| `wt path` | The resolved tree's absolute path, one line. |
| `wt go <q>` | The matched tree's absolute path, one line. Ambiguous: contenders on stderr, exit 3. No match: exit 1. |
| `wt ls` | One aligned row per tree. |
| `wt ls --porcelain` | One tree per line, three tab-separated fields: branch label, absolute path, comma-joined states (`-` when none). The field count never varies. |
| bare `wt`, bare `wt go` | Without a TTY on stdin and stderr: exactly the `--porcelain` listing, so agents never hang on the interactive picker. |
| `wt shell-init zsh` | The zsh integration script itself (it is the machine output — meant for `eval`). |
| `wt config` | Merged config as TOML; the two config file paths ride along as `#` comments, so the whole document stays parseable TOML. |
| `wt init` | Nothing (chatter on stderr). Non-interactive use requires `--yes` plus value flags; without a TTY, prompting is refused (exit 2) rather than hanging. |
| `wt done` | Nothing (chatter on stderr). |

`--json` on `ls`/`status`/`doctor` and the pool plumbing
(`wt claim` / `wt release`) land in later phases.

A porcelain line looks like:

```
feature/login /Users/you/acme.trees/feature-login -
main /Users/you/acme locked,prunable
```

Detached trees carry the literal branch label `(detached)`.

## Hooks

`hooks.setup` runs via `sh -c` inside the new tree.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ Pages land alongside the features they document
- [`agents.md`](agents.md) — machine-output contract: streams, exit codes
- `pool-mode.md` — monorepo pool setup and lifecycle (Phase 4)
- `recipes.md` — per-ecosystem examples (Phase 5+)
- `shell.md` — shim internals, prompt, completions (Phase 3)
- [`shell.md`](shell.md) — shim internals, cd protocol, prompt indicator, completions
- `faq.md` — doctor-adjacent questions (Phase 6)
129 changes: 129 additions & 0 deletions docs/shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Shell integration

One line in `~/.zshrc` turns `wt` from a path-printing binary
into a navigator:

```sh
eval "$(wt shell-init zsh)"
```

Add `--prompt` to also get the [prompt indicator](#prompt-indicator).
`wt` never edits `.zshrc` for you, and `wt uninstall` (Phase 7)
tells you the exact line to delete.

## The cd protocol

A child process cannot change its parent shell's directory,
so the binary and the shim split the work
(the zoxide/starship pattern):
the binary prints the target path on stdout,
and the `wt()` function the eval line installed performs the `cd`.

Only three invocations produce a cd —
bare `wt`, `wt go`, and `wt new`:

```sh
wt # pick a tree interactively → cd
wt go login # fuzzy-jump to the best match → cd
wt new feature/pay # create tree + branch → cd into it
```

Every other command passes through the wrapper untouched.
`wt path` deliberately never cds — it is plumbing,
built for things like `code "$(wt path login)"`.

Without the shim everything still works;
cd-producing commands simply print the path instead:

```sh
cd "$(wt go login)"
```

Capture works with the shim installed too:
`$(wt go login)` runs the wrapper in a subshell,
so the wrapper hands the path through
whenever stdout is not a terminal.

## Fuzzy matching rules

`wt go <query>` matches against each tree's branch name,
its sanitized directory form (`feature/login` → `feature-login`),
and its directory basename:

1. An exact spelling wins outright, branch names before directory names.
2. Otherwise the best fuzzy match wins —
but only when it strictly beats the runner-up on match quality.
3. A tie is ambiguous: the contenders are listed on stderr
and `wt` exits 3 without guessing.

Match quality ignores name length,
so `wt go feature` refuses to choose between
`feature/login` and `feature/logout`
rather than "winning" on the shorter name.

## The picker

Bare `wt` (and bare `wt go`) opens a fuzzy picker over all trees;
the preview pane shows each tree's `git status -sb` and last commit.
Enter cds to the selection, Esc cancels.

The picker only appears when stdin and stderr are TTYs.
Anything piped or captured — scripts, CI, coding agents —
gets the stable `wt ls --porcelain` listing on stdout instead,
so nothing ever hangs waiting for a human
(see [agents.md](agents.md)).

The picker TUI itself is verified by hand
(create a few trees, run bare `wt`, check preview/select/cancel);
everything around it — resolution, fallback, the cd protocol —
is covered by the test suite.

## Completions

The shim registers cobra-generated zsh completions lazily:
the real completion script is loaded from the binary
on the first `wt <Tab>`,
so completions always match the installed binary
and shell startup never pays for them.
They activate only if `compinit` has run before the eval line.

## Prompt indicator

Opt in with:

```sh
eval "$(wt shell-init zsh --prompt)"
```

A `chpwd` hook exports `WT_PROMPT` with the tree's directory name
while the cwd is inside a linked worktree, and unsets it elsewhere.
The hook is pure zsh — file stats and one `read`,
recomputed only when the directory changes;
it never runs a subprocess,
so your prompt latency is untouched.

Use it anywhere zsh expands prompts:

```sh
setopt prompt_subst
PROMPT='%~${WT_PROMPT:+ (⌂ $WT_PROMPT)} %# '
```

### Starship

Prefer [starship](https://starship.rs)? Skip `--prompt` and add a
[custom command](https://starship.rs/config/#custom-commands)
segment instead:

```toml
[custom.wt]
command = "basename $PWD"
when = '[ -f .git ] && grep -q "gitdir:.*worktrees" .git'
symbol = "⌂ "
style = "bold blue"
```

Starship runs `when` per prompt render;
it is two stats and a grep, comfortably under its 500ms budget.
Unlike the `WT_PROMPT` hook it only fires at a tree's root —
subdirectories of a tree show no segment.
13 changes: 11 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ go 1.25.8
require (
charm.land/huh/v2 v2.0.3
github.com/google/go-cmp v0.7.0
github.com/ktr0731/go-fuzzyfinder v0.9.0
github.com/pelletier/go-toml/v2 v2.4.3
github.com/rogpeppe/go-internal v1.15.0
github.com/sahilm/fuzzy v0.1.3
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.45.0
)

require (
Expand All @@ -27,15 +30,21 @@ require (
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/gdamore/tcell/v2 v2.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ktr0731/go-ansisgr v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/nsf/termbox-go v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.35.0 // indirect
)
Loading
Loading