feat(kit)!: the worktree belongs to the operator - #54
Merged
Merged
Conversation
`/dobby:scope` used to preflight a slug, create `worktree-<slug>` under `.claude/worktrees/` with the native `EnterWorktree`, and enter it; `/dobby:finish` assumed that exact worktree existed and tore it down by name. That ceremony — slug composition, collision and nesting detection, an orphan mode keyed by slug — duplicated what every host already does (`claude --worktree`, Claude Desktop, t3 code, an IDE's own worktrees, plain `git worktree add`), and it forbade running the lifecycle without a worktree at all: a goal on a branch in the main checkout had no path through the kit. dobby now runs wherever the session stands. `/dobby:scope` normalises the goal, writes `STATE.md` at the current workroot, and brings it up with `bunx dobby up` when `dobby.config.json` is present — it never creates, names, enters or preflights a worktree. The CLI's `scope preflight` command is gone with its helpers. `/dobby:finish` becomes symmetric: it merges behind the same gate, runs `bunx dobby down`, and only when the session stands in a LINKED worktree — whoever made it — offers to remove that worktree and its branch, trying `ExitWorktree` first (it restores the cwd when this session entered the worktree) and falling back to raw git from the main root; on a plain checkout it returns to `main`, pulls, and deletes the goal's branch. `finish --preflight` loses `--slug`, `candidates`, `mode` and `removeMechanism` and reports `inWorktree`, `worktreePath`, `mainRoot` and `branch` instead. The slug the kit derives elsewhere stays `basename(workroot)`: a checkout without worktrees has exactly one active goal, so its basename is the goal's name, and parallel goals are parallel worktrees the operator opens. BREAKING CHANGE: `/dobby:scope` no longer creates or enters a worktree — open one yourself or work on a branch. `dobby scope preflight` is removed. `dobby finish --preflight` no longer accepts `--slug` and its payload changed shape (see above). `/dobby:finish` on a plain checkout deletes the goal's branch after returning to `main`. `v0.16.md` walks it. This change was itself built without a kit-made worktree: a branch on the main checkout, `/dobby:dispatch` with three tasks, `bunx dobby up` reporting `slug: "dobby"` — the dogfood the decision promises. ADR-0033 records it with the rejected alternatives.
Greptile SummaryThis PR transfers worktree ownership from Dobby to the operator while retaining Dobby’s run lifecycle and guarded finish flow.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains in the previously reported worktree-install or return-branch cleanup paths.
|
| Filename | Overview |
|---|---|
| cli/src/preflight.ts | Replaces slug-targeted worktree preflight with current-workroot detection, per-workroot installation checks, and guarded return-branch resolution. |
| cli/src/preflight.test.ts | Rewrites coverage around plain checkouts, arbitrary linked worktrees, split installations, PR bases, dangling remote HEADs, and unknown return branches. |
| plugin/skills/finish/SKILL.md | Defines separate linked-worktree and plain-checkout cleanup paths with explicit destructive gates and fail-closed branch switching. |
| plugin/skills/scope/SKILL.md | Changes scope to operate in the operator-selected current workroot rather than creating and entering a kit-owned worktree. |
| docs/adr/0033-the-worktree-belongs-to-the-operator.md | Records operator ownership of worktrees and the PR base as the authoritative post-goal return branch. |
Reviews (6): Last reviewed commit: "docs(adr): finish returns the operator t..." | Re-trigger Greptile
…fault branch Review round 1 on #54 — two P1s on the finish preflight, both inherited from the model this PR removes. `dobbyInstalled` was still checked at the main checkout — a leftover from when the worktree was the kit's and the main checkout its origin. An operator-made linked worktree with its own `node_modules` (where `up` installed and where `down` runs) could be `blocked` while a bare main checkout was consulted instead. The probe now follows the workroot, in every shape; a plain checkout is unchanged because there the two roots coincide. The plain-checkout teardown said `git switch main`. On a repository whose trunk is `master` or anything else, that either fails and leaves the goal's branch behind or switches to an unrelated `main`. The preflight now reports `defaultBranch`, resolved from `origin/HEAD` with `main` as the fallback when there is no remote head, and the skill switches to and pulls that branch. The trunk's name is a fact the CLI resolves once, never a literal in prose.
Owner
Author
|
@greptileai review |
…nknown Review round 2 on #54. Round 1 replaced the hard-coded `main` with `origin/HEAD`, but fell back to the constant `main` whenever that symbolic ref was unreadable — which is exactly the case on a `master` or `trunk` repository that was never cloned (a `git remote add` leaves `origin/HEAD` unset). finish would then `git switch main`: a failing switch that leaves the goal's branch behind, or a switch to an unrelated `main`. `defaultBranch` is now resolved by an ordered cascade at the main root — `origin/HEAD`; then whichever of `origin/main` / `origin/master` exists; then whichever of local `main` / `master` exists — and is `null` when none does. finish's plain-checkout teardown reads it before switching: a name proceeds as before; `null` stops right there, with the PR merged and the run torn down, and tells the operator the two commands to run with the trunk they know. A wrong switch on the operator's own checkout is worse than asking.
Owner
Author
|
@greptileai review |
Review round 3 on #54. The cascade that resolves `defaultBranch` proved its fallback refs exist but trusted the `origin/HEAD` symbolic target as-is. Nothing refreshes that ref: after a remote renames its trunk (the `master` → `main` migration) and a pruning fetch, `origin/HEAD` can still point at `refs/remotes/origin/master`, which is gone — and finish would switch into nothing, or into a stale local `master`. The symbolic head now counts only when the remote-tracking ref it names still exists; otherwise the cascade continues through `origin/main` / `origin/master`, the local pair, and `null`. Every step proves the ref it answers with. A symbolic pointer is a hint, not a fact.
Owner
Author
|
@greptileai review |
…r a branch with no PR Review round 4 on #54. A stale `origin/HEAD` beside a legacy `origin/main` while the real trunk has a custom name made the cascade pick `main` on existence alone, and finish would switch to the wrong branch. No git-only heuristic can tell that apart — but finish runs after the goal's PR was merged, and the preflight already asks GitHub about that PR. Its base branch IS the trunk this goal merged into, by definition. `defaultBranch` now comes from the PR's `baseRefName` first; the git cascade (validated `origin/HEAD`, then `origin/main`/`origin/master`, then local `main`/`master`, then `null`) answers only for a branch that has no PR at all. The payload also reports `defaultBranchSource` so the operator sees which rule answered. The skill's plain-checkout teardown creates the local trunk from `origin/<trunk>` when it does not exist yet, and still stops with instructions when neither exists. Four rounds on this one field — an unreadable remote head, a blind constant, a dangling symref, an unrelated conventional name — and the resolution is the same each time: the trunk is a fact to resolve, never a name to assume. After the forge's own answer there is no higher one.
Owner
Author
|
@greptileai review |
…decision Review round 5 on #54 asked finish not to return to the merged PR's base branch when that base is a long-lived non-trunk branch such as `develop`. That inverts round 4, which asked not to trust a conventional name over the real target. The two cannot both hold, and the PR base is the authoritative one: finish closes a goal, and the branch the operator comes back to is the one the goal started from and merged into. A goal against `develop` belongs back on `develop`; moving it to a repository-wide trunk would take the operator away from where they were working. ADR-0033 now records this as the decision — the return branch is the PR base, reported with its source; the conventional cascade serves only a branch that never had a PR — so the next reader finds the reasoning instead of the code and a question.
Owner
Author
|
@greptileai review |
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.
Summary
dobby stops creating, naming, entering and preflighting the per-goal worktree. The operator owns the worktree; the kit runs wherever the session stands.
worktree-<slug>under.claude/worktrees/, finish's orphan-by-slug mode) duplicated what every host already provides —claude --worktree, Claude Desktop, t3 code, IDE worktrees,git worktree add— and made it impossible to run the lifecycle on a plain branch./dobby:scope: normalises the goal, writesSTATE.mdat the current workroot, brings it up withbunx dobby upwhendobby.config.jsonis present. No preflight, noEnterWorktree.dobby scope preflightdeleted (and its helpers); usage no longer listsscope./dobby:finish: merge (gated) +bunx dobby down --json+git pull, always. Only when the session stands in a linked worktree — whoever made it — it offers to remove it:ExitWorktreefirst, raw git frommainRootas fallback. On a plain checkout:git switch main,git branch -D <branch>.finish --preflight: drops--slug,candidates,mode,removeMechanism,slug; reportsinWorktree,worktreePath,mainRoot,branch(pluspr,dirty,dobbyInstalled,branchDeleteSafe,reasons,verdict).basename(workroot)everywhere: a checkout without worktrees has one active goal.CLAUDE.md,README.md,plugin/CONTEXT.md,cli/CONTEXT.md,cli/README.md, one-line fixes inonboard/migrate-config/learn/mark; ADR-0033 (decision, rejected options, consequences); upgrade notev0.16.md.Breaking — see
plugin/skills/upgrade/references/v0.16.md.Net −351 lines.
Test plan
bunx dobby checkgreen on the final tree (Biome · tsc · knip · vitest · manifest/frontmatter checks)cli/src/preflight.test.tsrewritten: 12 scope-removal cases, 9 finish slices on the new payload (plain checkout MERGED/OPEN/dirty, a worktree made by plaingit worktree add,--slugrejected, not-installed, text mode);registry.test.tsno longer advertisesscopeup --jsonfrom the main checkout →slug: "dobby";finish --preflight --jsonhere →inWorktree: false, ten keys exactly/dobby:dispatch— three tasks with test-first and an ADR, which is more than dispatch's own description ("a small fix or change") advertises; that gap is noted for/dobby:learn, not papered over here/dobby:finishfrom this very branch — theinWorktree: falsepath (merge →down→git switch main→git branch -D) runs for real against this checkout/dobby:finishfrom inside a worktree opened byclaude --worktree(theExitWorktreepath), and from one made bygit worktree add(the raw-git fallback)