fe84566b95
Co-authored-by: Kandev Agent <agent@kandev.dev>
14 KiB
14 KiB
Multi-branch tasks
Status: shipped Owner: Kandev backend Date: 2026-06-01 Related: ADR 0013
What
A single Kandev task can hold N (repository, branch) pairs — including multiple branches on the same repository. Each pair gets its own worktree under the task directory. PRs, sessions, changes, and review surfaces all key on the pair so the agent can fan out work across branches without fragmenting into sibling tasks.
Why
The previous model forced one task = one branch per repository. Users wanted:
- Stacked PRs — work that naturally splits across two branches against the same repo while staying in one conversation with the agent.
- Feature-flag rollouts — same repo, "with flag on" and "with flag off" branches reviewed side-by-side.
- A/B experiments — two implementations of the same change, opened as two PRs, compared from one task.
- Multi-repo, but stronger — the existing multi-repo (different repos in one task) was a natural neighbour of "same repo, different branches". Unifying them removed an arbitrary asymmetry.
Workarounds (sibling tasks, manually managing two worktrees) lost shared context: the agent's history, the kanban view, the chat thread.
Surface
Backend API
Service.AddBranchToTask(task_id, repository_id, base_branch?, checkout_branch?)— appends a newtask_repositoriesrow to a live task. Enforces uniqueness on the canonical 4-column key(task_id, repository_id, base_branch, checkout_branch)so both branch fields disambiguate siblings.Service.CreateTask— already accepted[]TaskRepositoryInput; now accepts duplicaterepository_identries whencheckout_branchdiffers.task_repositories.UNIQUE(task_id, repository_id, base_branch, checkout_branch)— relaxed from the legacyUNIQUE(task_id, repository_id)via themigrateTaskRepositoriesAllowMultiBranchmigration. Both branch columns participate because the worktree executor anchors the branch inbase_branch(leavingcheckout_branchempty) while the local executor inverts the split. Either column alone would miss one of the two shapes.
MCP
add_branch_to_task_kandev— new tool that takestask_id,repository_id,checkout_branch, optionalbase_branch. Backed byws.ActionMCPAddBranchToTaskandhandleAddBranchToTask.create_task_kandev— unchanged externally; agents can already submit multiple repository entries.
Worktrees
- Single-branch tasks:
~/.kandev/tasks/<task-dir>/<repo>/(unchanged). - Multi-branch tasks: first occurrence of each repo keeps
~/.kandev/tasks/<task-dir>/<repo>/; additional occurrences sit as siblings at~/.kandev/tasks/<task-dir>/<repo>-<branch-slug>/. The slug is derived deterministically fromCheckoutBranch(orBaseBranchwhen the checkout branch is empty) viaworktree.SanitizeBranchSlug. - The orchestrator (
buildRepoSpecs) detects same-repo duplicates and tags additional rows with aBranchSlug; the worktree manager applies the slug as a suffix at the task-root level. Sibling siting (rather than nesting inside the primary) keeps each worktree's git scope isolated. - Subsequent sessions on the same task, including handoffs and additional agents, reuse the task's existing worktree IDs for every
(repository, branch)pair. They do not create a new task directory or sibling worktree set unless the task itself gains a new branch/repository pair.
PRs
task_prs.UNIQUE(task_id, repository_id, pr_number)already permitted multiple PRs per (task, repo).task_prs.head_branchalready disambiguates which branch the PR tracks. No schema change.
Frontend
TaskRepository.checkout_branchwas already on the http type.- Worktrees are keyed by
worktree.idin the Zustand store, so two worktrees with the samerepository_idalready coexist. - Repo chips in chat-message renderers now key on
(repository_id, checkout_branch)so multi-branch tasks render distinct chips instead of collapsing. - In the task-creation dialog, every workspace, discovered-on-disk, and remote provider repository picker shows an accent-colored check (with an accessible
Already addedlabel) when another repository row already selects that repository. The current row does not mark its own selection. - Typing or pasting a supported remote URL stages it in the Remote repository picker. Kandev does not resolve or commit the URL until the user presses Enter, so the user can finish editing it first. A visible
Remote URLhint identifies URL-shaped input and tells the user to press Enter. - A committed remote URL can resolve repository branches and GitHub PR/issue metadata without an authenticated provider connection when the upstream resource is public. Private-resource access still requires the matching configured integration.
- Remote URL resolution failures remain attached to their repository row. The row shows the provider error and an explicit retry action; retry repeats both branch and PR/issue metadata lookups without requiring the user to delete and re-enter the URL.
- Marked options remain selectable so users can intentionally create a multi-branch task from one repository. Removing or changing the other row removes the marker immediately.
- Repository-chip tooltips keep long local paths inside a viewport-safe, wrapping surface. Closing a repository picker does not reveal its tooltip until the pointer leaves and deliberately hovers the chip again.
- Review surfaces expose one linked pull request at a time when a task has multiple PRs. A task-scoped selector defaults to the primary (oldest) PR, remembers an in-session override, and falls back to the primary PR when that override disappears.
- The Changes timeline keeps the last resolved PR file list visible while a background PR synchronization refresh is pending. A successful refresh replaces the list atomically, and a failed refresh retains the last resolved list; switching workspace/task or removing a PR must not expose files from the previous scope.
- Selecting a PR changes the remote PR diff contribution while preserving the existing source precedence: uncommitted worktree changes, then cumulative committed changes, then the selected PR. PR-only views and PR timeline rows resolve the exact PR rather than the task primary.
- The selector is available on desktop, phone, and coarse-pointer tablet. Phone uses a touch-sized bottom-menu treatment inside the existing Review surface; switching keeps Review open and exposes selected-PR loading, empty, and retry states.
- Full "+ Branch" UI affordance and grouped repo > branch tabs are deferred — agents drive multi-branch via the MCP tool today.
Task-creation scenarios
- GIVEN a task-creation repository row selects a workspace or discovered-on-disk repository, WHEN the user opens another repository selector, THEN that repository remains selectable and is visibly marked by a compact accent-colored check whose accessible label is
Already added. - GIVEN a task-creation Remote row selects a provider-backed repository, WHEN the user opens another Remote repository selector, THEN the same provider repository remains selectable and is visibly marked by the same compact accent-colored check.
- GIVEN a repository is marked because another row selects it, WHEN the user changes or removes that other row, THEN the marker disappears from the open or next-opened selector.
- GIVEN a row already selects a repository and no other row selects it, WHEN the user reopens that row's selector, THEN its current repository is not marked as a duplicate.
- GIVEN the Remote repository input contains a supported URL, WHEN the user pastes, edits, blurs, or tabs away without pressing Enter, THEN Kandev keeps the text editable and does not start repository resolution.
- GIVEN the Remote repository input contains a supported URL, WHEN the user presses Enter, THEN Kandev commits the trimmed URL, closes the picker, and begins branch plus applicable PR/issue metadata resolution.
- GIVEN the Remote repository input contains URL-shaped text, WHEN the picker is open, THEN a visible
Remote URLhint explains that Enter submits it. - GIVEN branch or GitHub PR/issue metadata resolution fails, WHEN the repository row renders, THEN it shows an actionable error and retry control while preserving the committed URL.
- GIVEN a failed remote repository resolution, WHEN the user retries and the provider responds successfully, THEN the error clears and the branch/metadata selection completes without re-entering the URL.
- GIVEN no GitHub or GitLab integration is configured, WHEN the user submits a public
github.comorgitlab.comrepository URL, THEN Kandev can discover its branches and create the task; private repositories continue to require credentials. - GIVEN a selected repository has a long unbroken local path, WHEN its tooltip opens, THEN the tooltip wraps the path and remains within the viewport instead of covering adjacent repository controls.
- GIVEN a user selects a repository from a repository-chip picker, WHEN the picker closes while the pointer remains over the chip, THEN no repository tooltip opens until the pointer leaves and deliberately hovers the chip again.
- GIVEN the Changes timeline displays resolved files for a linked PR, WHEN background synchronization advances that PR's sync timestamp and the replacement file request is still pending, THEN the existing PR Changes section and count remain visible without moving or remounting.
- GIVEN a background PR file refresh completes, WHEN the replacement response belongs to the current workspace, task, and PR, THEN the PR Changes section atomically displays the replacement file list without retaining removed files.
- GIVEN the Changes timeline displays resolved files for a linked PR, WHEN a background refresh fails, THEN the last resolved PR Changes section and count remain visible.
Non-goals
- Auto-stack PRs. Multi-branch lets you open N PRs; it does not detect base/branch relationships and stack them. Users do that themselves.
- Cross-branch merge orchestration. Each branch's PR lifecycle is independent.
- Branch deletion / cleanup automation. A
RemoveBranchFromTasksymmetric service method is planned but not in v1. - Aggregate multi-PR review. Review does not merge sibling PR diffs into one file list because two PRs can carry different revisions of the same repository path.
- Independent per-PR review history. Reviewed-file and pending-comment identity remain session/repository/path scoped. Switching PRs treats a different diff hash as a new visible revision; PR-qualified persistence is separate data-model work.
Risks
- Slug collision. Two distinct branches that sanitize to the same slug (e.g.
feat/avsfeat-a) would collide on disk. The service-layer dedup catches matchingCheckoutBranchexactly; near-identical names tripgit worktree addwith a clean error. - Repo-lock contention. Worktrees for different branches of the same repo serialize on the per-repo lock in
worktree.Manager. Multiple concurrent agents on the same task = lock queue, not parallelism. Acceptable for safety; revisit if it becomes a bottleneck. - Migration replay. The constraint relaxation migration is idempotent and triggers on a substring of the legacy DDL. Databases that already migrated skip cleanly.
Acceptance tests
TestSanitizeBranchSlug— slug determinism + handling of slashes, dots, special chars.TestTaskWorktreePath_BranchSlugNesting— empty slug stays flat, non-empty slug nests.TestCreateTask_AllowsSameRepoDifferentBranches— a task can be born with two rows on the same repo.TestCreateTask_RejectsSameRepoSameBranch— dedup guard still rejects exact duplicates.TestAddBranchToTask_HappyPath— second branch appended after the fact lands as a new row.TestAddBranchToTask_RejectsDuplicate— re-adding the same(repo, branch)errors.TestLaunchPreparedSession_MultiBranch_ReusesWorktreeIDsByBranchSlug— a follow-on session for the same task reuses each existing branch worktree instead of preparing a new task directory.- Task-creation component and mobile E2E coverage prove the accessible, compact selected-repository marker for workspace/on-disk and Remote provider selectors while preserving option selection, viewport-safe long-path tooltips, and post-selection tooltip suppression.
- Remote-entry component and mobile E2E coverage prove paste/typing remains editable until Enter, the
Remote URLhint is visible, failures preserve the URL and expose retry, and retry completes resolution. - GitHub and GitLab backend tests prove public branch lookup works without configured credentials, while public GitHub PR/issue metadata lookup supplies the task-creation enrichment used by the Remote selector.
- Web unit tests prove selected-PR default, override, task isolation, and removed-PR fallback behavior.
- Desktop and mobile Playwright tests prove a two-PR task can switch Review from the primary PR to a sibling PR without stale files, overflow, or closing the surface.
Open questions
- "Primary" branch for the kanban card / task title rendering — currently the lowest-position row. Acceptable until users complain.
- Whether
update_task_kandevshould accept the multi-branch shape for bulk edits, or whetheradd_branch_to_task_kandev+ a futureremove_branch_from_task_kandevis enough. Deferred to feedback. - "+ Branch" UI button — design and placement open. The MCP tool is enough for the agent-driven flow today.