wt 0.2.0: worktree lifecycle — new, switch, list, rm, merge - #1
Open
VSN2015 wants to merge 15 commits into
Open
Conversation
need docker lived inside load_config_soft's config-present branch, so any repo with a worktree-kit.yml — even one that just omits runner:, which defaults to compose — made wt list die on a machine without the docker CLI. That broke the premise of the lifecycle layer: those commands need only git. Moved the check into require_config, after the HAS_CONFIG gate, so load_config_soft genuinely never exits and the ten pre-existing commands keep identical behavior (they all reach the check through require_config). Added a regression test that scrubs docker from PATH against a compose-defaulting config and asserts wt list still works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cmd_new handed the prepare hook to `wt run` as its payload, but `wt run` already runs hooks.prepare itself before execing its payload — running the hook twice on every `wt new`, on both runners. `wt new --server` made it three times, since `wt server` independently runs the hook as part of its own command string. Pass an inert `true` payload instead (wt run's own prepare step does the work), and skip the standalone prepare step entirely when --server is requested so `wt server`'s prepare run is the only one.
… removal Five review findings on the destructive path: the unmerged-commits guard failed open when a configured trunk did not resolve in the worktree; wt rm from inside the worktree being removed crashed after git worktree remove deleted the cwd, stranding the branch and skipping emit_cd; a failed server-stop was silently swallowed instead of reported; .dbready alone did not distinguish a kit-bootstrapped database from a merely-adopted one, so wt rm (and soon wt merge) could drop a hand-created database — added a narrower .dbowned marker and gated the drop on it; and the whole HAS_CONFIG=1 teardown branch (server-stop, the ownership gate, db_drop, redis_flush, the SLUG/N borrow) had zero test coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cmd_reset cleared .dbready but left a stale .dbowned behind. If the database is later replaced by hand and db_check adopts the replacement (touching only .dbready by design), the stale .dbowned would wrongly authorize wt rm/merge to drop a database this incarnation of the slug never created. Reset now clears both markers together; no heuristic added to recover ownership after a reset, per instruction — the accepted tradeoff is a "left in place" notice instead of a drop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d, untested guards covered - Rebase-conflict abort no longer trusts "rebase --abort" alone: falls back to checkout -f + an explicit update-ref of refs/heads/<branch> before the reset --hard, so a mid-rebase detached HEAD can never leave the branch ref silently rewritten while die claims it was restored. - Guarded the reset --hard fallbacks in the squash-commit-failure and fast-forward-failure paths so a failing reset can't have set -e kill the function before its recovery message (and the backup ref) reach the user. - Added cmd_rm's trunk-resolution guard (rev-parse --verify --quiet) so an unresolvable configured trunk dies with "cannot verify" instead of silently reading as "no commits ahead". - Test: feat/conflict now gets two commits so the squash always changes history shape, closing a same-second-timestamp coincidence that let the restore assertion pass even with the restore code deleted; added a rev-list --count shape assertion as the load-bearing check. - Test: backup ref assertion now checks value and timing (equals the pre-squash HEAD), not just that the ref name exists. - Test: added coverage for the trunk-not-at-primary and upstream-without---force guards, with master/branch "untouched" assertions rather than bare assert_fails — needed because both guards, when disabled, can still exit nonzero for unrelated incidental reasons (a fast-forward into the wrong branch, or git's own branch -d safety check) while real damage already happened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…_drop
- test/lifecycle.sh: the "yields a space" doctor assertion matched the
fixture's own path text regardless of whether the WARN line fired
(cmd_doctor echoes the raw template unconditionally before the check
runs). Assert on "wt new will refuse it" instead, which only the WARN
line can produce. Verified load-bearing (RED/GREEN) along with the
branch-unique assertion, which was already sound.
- templates/compose/django.yml: comment out db_drop — its db_bootstrap and
own_db_env reveal no connection details at all, so the db/postgres host
guess risked DROP DATABASE against the wrong server. redis_flush stays
active.
- templates/{compose,host}/laravel.yml: note that the redis_flush host is
an inference (this template exports REDIS_DB, not a REDIS_URL).
- bin/wt: usage() said shell-init "makes switch cd"; the wrapper actually
intercepts switch/new/rm/merge.
…t aborts diagnosable Root cause of a flake (1-in-5 observed): remove_worktree_and_branch was called with force=0, so branch teardown used plain "git branch -d". After a squash+rebase, the branch's tip is a new commit object, and git's -d refuses to delete a branch that is merged into HEAD but not merged into its own configured upstream tracking ref — a real but redundant check here, since a successful --ff-only merge already proves every commit is reachable from trunk. Whether that check fired depended on a same-second commit-timestamp coincidence in the squash, and when it fired, "wt merge" died under set -e; since the test's "wt merge ... --force" call was a bare statement, that nonzero exit killed the whole suite silently — no FAIL line, no LIFECYCLE FAIL, just truncated output. - cmd_merge now passes force=1 to remove_worktree_and_branch (git branch -D), with a comment explaining why this is safe and warning against reverting it. Accepted consequence: worktree removal also becomes --force (discards untracked files) — not routed around, since refusing at this point would strand the user with trunk already advanced and the database already dropped. - Wrapped every bare "$WT" new/merge invocation in the Task 5 test block (not already inside assert_fails) with "|| fail ...", so a future regression produces a visible FAIL line and reaches LIFECYCLE FAIL instead of silently truncating the run. Verified: reverting force=1 alone reproduced the flake immediately (now failing loudly instead of silently); with both fixes applied, 30 consecutive full-suite runs plus 4 dash runs, all clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…emoval
Whole-branch review of the lifecycle layer found seven defects that the
per-task reviews could not see, because each only spanned one command.
bin/wt:
- remove_worktree_and_branch ran four bare git calls AFTER teardown had
already stopped the server, dropped the database and deleted every state
file. A failure there aborted under set -e with only git's stderr: no
"removed" line, no emit_cd, so a shell-wrapper caller was left in a
deleted directory. The worktree removal now dies with a message saying
the reclaim already happened; branch deletion only notes. The reachable
trigger was `git branch -d`, which asks "merged into HEAD?" — not the
"merged into trunk?" question cmd_rm already answered — so removing a
sibling worktree could strand a branch wt had verified.
- teardown_worktree borrowed SLUG and N for the target but not WT_PATH,
which is what runner_exec keys off. hooks.prepare therefore re-ran in the
CALLER's worktree on every rm/merge of another worktree, and db_drop /
redis_flush resolved relative paths against the wrong tree. The unused
_tp parameter was the tell.
- expand_path fed {parent}, {repo}, {branch_raw} and {home} to sed as
replacement text, where & means the whole match. The default template
uses two of them, so a repo directory named "R&D-app" silently expanded
{repo} to "R{repo}D-app" — and that path's last segment names the slug,
the port, the redis slot and the database.
- $HOME was dereferenced unguarded under set -u in the {home} expansion and
the rc-file scan; an unset HOME (env -i, some CI runners) died bare.
- The fish wrapper used a single-command variable override, a syntax error
on older fish, which would break wt entirely for those users.
- The no-.dbowned notice claimed the database was "adopted, not created by
wt". That is false for every worktree provisioned before this release —
.dbowned did not exist — which is exactly who sees it first on upgrade.
- A failed restore in cmd_merge printed "could not restore" and then died
saying "restored to <sha>", on the one path where the user is panicking.
One helper now prints a single, true outcome.
test/lifecycle.sh (84 -> 110 assertions):
- Both PATH=/usr/bin:/bin scrubs would have misbehaved in the container
suites: docker lives in /usr/bin on Linux, inverting the server-stop
assertion, and the truncation can remove the YAML backend. They now drop
only directories that hold docker, shimming git and the backend first.
- Guarded ~20 setup calls and captures that ran bare under set -eu, so a
regression FAILs with a diagnostic instead of truncating the run.
- First coverage of worktrees.path from the committed config (only the
.local path was exercised); stdout purity for rm and merge; the reason
behind the three refusals that matter; a real rebase-state check; and the
removal of an assertion that had no code path able to fail it.
Docs: README spells out that removal deletes the whole directory including
gitignored files, that merge tears down like `rm --force` rather than `rm`,
that hyphens fold in {branch}, and how pre-0.2.0 databases behave. The
design spec is amended in three places where the implementation knowingly
diverged, each marked as a resolved deviation.
Verified on /bin/sh and dash, six consecutive runs, 110 assertions, no
flake. test/linux.sh not run — docker daemon is down.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…anup The db_drop bullet told upgrading users that `wt reset <slug>` plus an --own-db cycle would re-bootstrap and re-mark a pre-0.2.0 database. It cannot: reset clears both markers, but ensure_own_db then runs db_check, which succeeds because the database still exists, and returns having touched only .dbready. .dbowned is written solely on the db_bootstrap path, which runs only when the database is missing. The bullet therefore contradicted its own case (b) two lines further down. It now says to drop the database by hand first, and explains why reset alone is not enough. Also amends spec §10.1 step 7, which still listed four state files while the implementation removes .dbowned as well — a fourth divergence, marked like the other three, and the status header updated to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a worktree-lifecycle layer to
wt, so a worktree can be created, found, merged, and fully torn down — reclaiming the server, database, Redis slot, and state files the kit provisioned for it.Inspired by worktrunk, which solves the complementary half: it creates and destroys worktrees but knows nothing about databases, servers, or isolation, so its
removeleaks every resourcewtprovisions.New commands
Plus
wt doctorchecks for the path template,fzf, and shell integration;worktrees.path/worktrees.trunkandisolation.db_drop/isolation.redis_flushacross all ten stack templates; README documentation; and a bump to 0.2.0.Design
git.require_configis split intoload_config_soft(never exits, setsHAS_CONFIG) and the existing strict wrapper. The ten pre-existing commands keep the strict wrapper and behave identically — verified by tracing every call site.cdto it.git worktree list --porcelain, never a recomputed template — so it finds worktrees created by hand or by other tooling.Safety
wt rmandwt mergestop servers, drop databases, flush Redis slots, and delete branches.wt mergealso rewrites history and does not prompt.wt mergewritesrefs/wt/premerge/<slug>before the first mutation; every failure path restores the branch and names that ref.rebase --abortalone is not enough.--ff-only, so a merge commit is structurally unreachable..dbownedprovenance marker proves the kit bootstrapped it. Databases merely adopted bydb_check— including every database created before 0.2.0 — are left in place with a notice. This errs toward keeping data.Testing
New
test/lifecycle.sh: 110 assertions, git-only (no docker, no YAML backend required). Runs on macOS and is wired into the Debian and Alpine container suites.Verified on macOS under both the system shell and
dash, includingdashexecutingbin/wtitself.Not yet verified: the container suites (
./test/linux.sh) could not run — no docker daemon on the development machine.CONTEXT.md§5 records that three of this codebase's eight historical bugs surfaced under BusyBoxashfirst. Please run./test/linux.shbefore merging. Two tests derive a docker-freePATHand are the most likely to need adjustment on a Linux layout.Also unexercised: both picker branches (
fzfis not installed; the sandbox has no controlling terminal) and the emittedfishwrapper.Note on
docs/Commits
18c11f5andf936d19adddocs/superpowers/{specs,plans}/— the design spec and implementation plan, ~1,900 lines. These were previously untracked. If you would rather keep them out of the repo, they can be dropped without touching any code.Review pointers
The changes most worth a careful look:
remove_worktree_and_branchandteardown_worktree(bin/wt) — shared by both destructive commands; teardown borrows and restores theSLUG/N/WT_PATHglobals deliberately.cmd_merge's failure paths — the restore ordering is load-bearing.expand_path's_eschelper — path values becomesedreplacement text, where&would otherwise mean "the whole match".ensure_own_db— the one change to pre-existing code, adding the provenance marker.🤖 Generated with Claude Code