Skip to content

feat(kit)!: the worktree belongs to the operator - #54

Merged
kvnwolf merged 6 commits into
mainfrom
operator-owned-worktree
Sep 4, 2026
Merged

kvnwolf merged 6 commits into
mainfrom
operator-owned-worktree

Conversation

@kvnwolf

@kvnwolf kvnwolf commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

dobby stops creating, naming, entering and preflighting the per-goal worktree. The operator owns the worktree; the kit runs wherever the session stands.

  • Why: scope's worktree ceremony (slug preflight, collision + nesting detection, 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, writes STATE.md at the current workroot, brings it up with bunx dobby up when dobby.config.json is present. No preflight, no EnterWorktree.
  • CLI: dobby scope preflight deleted (and its helpers); usage no longer lists scope.
  • /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: ExitWorktree first, raw git from mainRoot as fallback. On a plain checkout: git switch main, git branch -D <branch>.
  • finish --preflight: drops --slug, candidates, mode, removeMechanism, slug; reports inWorktree, worktreePath, mainRoot, branch (plus pr, dirty, dobbyInstalled, branchDeleteSafe, reasons, verdict).
  • Slug stays basename(workroot) everywhere: a checkout without worktrees has one active goal.
  • Docs: glossary (Terminal host and five more entries), CLAUDE.md, README.md, plugin/CONTEXT.md, cli/CONTEXT.md, cli/README.md, one-line fixes in onboard/migrate-config/learn/mark; ADR-0033 (decision, rejected options, consequences); upgrade note v0.16.md.

Breaking — see plugin/skills/upgrade/references/v0.16.md.

Net −351 lines.

Test plan

  • Three tasks through the build loop (test-author → implementor + Exit gate → QA): scope + CLI, finish, docs — all QA-passed
  • bunx dobby check green on the final tree (Biome · tsc · knip · vitest · manifest/frontmatter checks)
  • cli/src/preflight.test.ts rewritten: 12 scope-removal cases, 9 finish slices on the new payload (plain checkout MERGED/OPEN/dirty, a worktree made by plain git worktree add, --slug rejected, not-installed, text mode); registry.test.ts no longer advertises scope
  • QA proofs against the real CLI, including this very branch: up --json from the main checkout → slug: "dobby"; finish --preflight --json here → inWorktree: false, ten keys exactly
  • Dogfood: this PR was built on a branch of the main checkout with no kit-made worktree, via /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
  • Live smoke, happening next: this PR will be closed with the NEW /dobby:finish from this very branch — the inWorktree: false path (merge → down → git switch main → git branch -D) runs for real against this checkout
  • Human smoke (deferred): /dobby:finish from inside a worktree opened by claude --worktree (the ExitWorktree path), and from one made by git worktree add (the raw-git fallback)

`/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-apps

greptile-apps Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR transfers worktree ownership from Dobby to the operator while retaining Dobby’s run lifecycle and guarded finish flow.

  • Removes the scope preflight and kit-managed worktree naming and creation.
  • Reworks finish preflight around the checkout where the session currently stands.
  • Adds validated PR-base and Git-ref fallback resolution for the return branch.
  • Updates lifecycle documentation, tests, upgrade guidance, and ADR 0033.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains in the previously reported worktree-install or return-branch cleanup paths.

Important Files Changed

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

Comment thread cli/src/preflight.ts Outdated
Comment thread plugin/skills/finish/SKILL.md Outdated
…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.
@kvnwolf

kvnwolf commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

@greptileai review

Comment thread cli/src/preflight.ts Outdated
…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.
@kvnwolf

kvnwolf commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

@greptileai review

Comment thread cli/src/preflight.ts
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.
@kvnwolf

kvnwolf commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

@greptileai review

Comment thread cli/src/preflight.ts
…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.
@kvnwolf

kvnwolf commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

@greptileai review

Comment thread cli/src/preflight.ts
…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.
@kvnwolf

kvnwolf commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

@greptileai review

@kvnwolf
kvnwolf merged commit 7ece226 into main Sep 4, 2026
3 checks passed
@kvnwolf
kvnwolf deleted the operator-owned-worktree branch September 4, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant