---
title: Base-branch picker
---

# Base-branch picker

## What it is

**One line:** When you start a new session, you can choose which git branch its
worktree forks from — and that same branch is where the session's work merges back
when it's done — instead of always branching off (and merging into) master.

### What it does (user-visible)

Omniscio isolates most sessions in their own git **worktree** (a separate working copy
on its own branch) so agents don't step on each other. Until now that worktree always
started from the repo's current `HEAD` (normally `master`), and the session's work
merged back into master when the session ended.

The base-branch picker adds one control to the **Start session** dialog: a **Base
branch** dropdown. Pick a branch (say `develop` or a feature branch) and:

- the new session's worktree is **forked from that branch** (it starts with that
  branch's code), and
- when the session finishes and its work merges back, it merges **into that same
  branch**, not master.

Pick nothing and everything behaves exactly as before (fork from master, merge to
master) — the default is byte-for-byte the old behavior.

## Where to find it

### Where it is

- The **Start session** dialog (the one you get from the inbox "Start session" action).
  A **Base branch** dropdown appears **below the Repo picker**, but only once you've
  picked a project and that project has branches. It's a searchable dropdown that
  defaults to the project's **current branch**; a tooltip reads *"The session forks
  from — and merges back into — this branch."*
- The branch list is fetched live from the chosen project's git repo (its local
  branches, current branch first). If the folder isn't a git repo, the dropdown simply
  doesn't appear — it never errors.

## How it behaves

### The one boundary to know

This is delivered entirely through Omniscio's **in-app merge** — the path interactive
sessions already use when they end. The background **auto-lander** (which merges
`ready-to-merge`-tagged branches) is deliberately **left untouched**.

**So do NOT tag a base-branch session `ready-to-merge`.** If you do, the auto-lander lands
the branch to **master** (its normal, unchanged behavior) — and, critically, **your chosen
target then never receives the work at all.** This is stronger than "the lander used the
wrong target": once the lander has landed the branch, the later in-app merge is **SKIPPED**.
`executeMerge` (`worktree-merge-queue.ts`)
computes `alreadyLanded = await isBranchDeliveredToBase(projectPath, branchName)` — true when the
branch has reached the base by ANY route — and short-circuits to a
synthesized `{ success: true }` **without ever calling `mergeWorktree`**, so the
`landTarget` it just read is never used. The worktree is retired down the shared success
path and the session is pushed as `status: 'merged'`.

  Note which function that is, if you are debugging a skipped merge:
  `isBranchDeliveredToBase` is the cold-path delivery check. It trusts the branch's
  `status: landed` tombstone first and only then falls through to the auto-lander event ledger —
  deliberately, so a land by `land:catch-up`, `merge-all-ready` or `safe-merge`, none of which
  write an `auto_lander_events` row, still counts as delivered. The inner, ledger-only
  `isBranchAlreadyLanded` also still exists, so **an empty `auto_lander_events` does not prove the
  skip could not have fired** — read the tombstone too.

Net effect for exactly that combination: **the UI reports success, and the branch you picked
is never touched.** The work is not lost — it is on `master` — but it is not where you asked
for it. (The skip itself is correct in isolation: the lander replays commits with FRESH
SHAs, so a plain `git merge` would FALSE-conflict on already-landed changes. What is missing
is any handling of a `landTarget` that the land bypassed.)

An earlier version of this page — and invariant 5 of the contract — described this as "safe,
just not the chosen target", which understated it. Landing into your branch happens via the
normal session-end / Merge / CLI merge **only when the auto-lander has not already landed the
branch.** Closing the gap properly is a lander/merge-queue behaviour change and is
deliberately not taken here (contract invariant 5: "Do not re-solve this in the lander
without an explicit decision").

### Status & limits

- Shipped for the **Start session** dialog. The **Quick Launch** composer picker and the
  **CLI/API** (`baseBranch` on `POST /project/:name/new`) are planned follow-ups.
- Desktop + mobile (the picker reuses the standard searchable `<Select>`; the branch-list
  read is mobile-reachable).

## For agents

### How it works (for agents)

- **Fork point.** `createWorktree(projectPath, sessionId, sessionName, options)`
  (`src/main/services/worktree/worktree-service.ts`),
  where the fork point is `options.baseBranch` — a field of `CreateWorktreeOptions`, alongside
  `explicitTarget` and `provenance`. **It is NOT a 4th positional string.** Pass a bare branch name
  in that slot and `options.baseBranch` is `undefined`, so the worktree silently forks from the
  repo's resolved head and the user's chosen base is ignored. (The options object replaced a
  positional list that had reached three, four and six arguments across three subsystems, precisely
  because a future insertion would mis-bind silently.) It validates the chosen ref (`git rev-parse --verify`) and passes it as the start point
  of `git worktree add -b <branch> --no-checkout <path> <baseRef>`. A bad/typo'd branch
  fails with a clear "Base branch not found" error, never a raw git dump. With no pick and no
  default branch resolving, it forks from `HEAD`, always passed as an explicit start point — with
  none, git guesses `--orphan` in an empty repository and dies against `--no-checkout`. A
  repository whose `HEAD` names no commit (brand new, or its `.git` damaged) is refused before any
  worktree exists with a clear "Nothing to branch from" error — `base-branch-invalid` (400) on
  `POST /worktrees/create`, never git's raw message — while any other failure of that check
  surfaces unchanged. And a create that *succeeds* is not a folder you can work in yet: on a busy
  box the route routinely answers `202 status: 'creating'`, and even its `200` carries a
  `readinessWarning` while `node_modules` keeps filling in behind the reply. Read it before you
  commit anything in the new folder.
- **Persistence.** The chosen branch is saved on the session as `land_target_branch`
  (a nullable column; NULL = master/today) so the merge step later knows where to land.
- **Merge back.** When the session's worktree merges (on session-end, the Merge action,
  or the CLI), the in-app merge queue reads that land target and merges into it via a
  **ref-only advance** — it updates `refs/heads/<target>` in git's object store and
  **never touches a live working tree**. If the target branch happens to be checked out
  in another worktree, it **refuses** (rather than corrupt that checkout) and reports
  it; a target branch that's gone is a harmless no-op.
- **Branch list IPC.** `PROJECT_BRANCHES_GET` (`project:branches-get`) returns
  `{ branches, currentBranch }` for a project; it's a read, safe to call from mobile.

The full invariants live in the repo's own `base-branch-picker-contract.md` (contributors only). The behavior that matters to a reader here: a base-branch session must not be tagged ready-to-merge, because the auto-lander would land it to base and the in-app merge into your chosen branch is then skipped.

## Related

Base branches are one part of a session's git story: [start-a-new-session.md](start-a-new-session.md)
covers the Start session dialog this picker lives in, [session-land-status-strip.md](session-land-status-strip.md)
shows where a finished session's branch sits in the auto-lander's queue, and
[auto-lander-dashboard.md](auto-lander-dashboard.md) is where the lander itself is watched, paused
and resumed.
