feat(workspaces): save repository set base branches (#3269)

This commit is contained in:
Carlos Florêncio
2026-09-11 16:18:10 +01:00
committed by GitHub
parent a4077cf9cf
commit e64d8e4cf3
89 changed files with 4481 additions and 857 deletions
+1 -2
View File
@@ -30,8 +30,6 @@ Git state.
### Requirements
- [Branch Policies](requirements/branch-policies.md)
- [Create a Local Repository During Task Creation](requirements/create-local-repository.md)
- [Empty Remote Repositories](requirements/empty-remote-repositories.md)
@@ -57,6 +55,7 @@ Git state.
- [Worktree Base Refresh](system-design/worktree-base-refresh.md)
- [Local Workspace Repositories](system-design/local-repositories.md)
- [Organization Units](system-design/org-units.md)
- [Repository Sets](system-design/repository-sets.md)
## Migration record
@@ -5,236 +5,155 @@ created: 2026-08-17
owners:
- kandev
---
# Repository Sets Requirements
## Overview
A workspace can hold dozens of repositories, and real work rarely touches one of them. A feature that runs from a web client through an API gateway into two backend services means the same four repositories are picked, one chip at a time, every time a task is created. The selection is knowledge the user already has and re-enters by hand on every task, and a repository forgotten on the third of four chips is only discovered once the agent cannot find the code.
A workspace can contain many repositories. Most tasks use a known subset of
those repositories. A repository set stores that reusable selection and its
order.
Some teams also start work from different base branches in each repository.
The set can store an optional base-branch preference for each member. This
preference reduces repeated task setup without changing repository defaults.
The workspace system owns this contract because it owns repository membership,
repository settings, and repository branch information.
## Terminology
- **Repository set:** A named and ordered group of repositories in one
workspace.
- **Set member:** One repository and its position in a repository set.
- **Saved base:** An optional base branch on a set member.
- **Task default:** The existing task-form branch selection when a set has no
saved base.
- **Repository default:** The current `default_branch` of a repository.
## Requirements
### REQ-WORKSPACES-REPOSITORY-SETS-001: Repository Sets
### REQ-WORKSPACES-REPOSITORY-SETS-001: Repository set management
**Intent:** A workspace can hold dozens of repositories, and real work rarely touches one of them. A feature that runs from a web client through an API gateway into two backend services means the same four repositories are picked, one chip at a time, every time a task is created. The selection is knowledge the user already has and re-enters by hand on every task, and a repository forgotten on the third of four chips is only discovered once the agent cannot find the code.
**Intent:** Users can reuse a known repository selection when they create a
task.
#### Acceptance criteria
- **AC-WORKSPACES-REPOSITORY-SETS-001.1:** A workspace owns any number of repository sets. A set has a name, an optional description, and an ordered list of repositories drawn from that same workspace.
- **AC-WORKSPACES-REPOSITORY-SETS-001.2:** Sets are managed from **Settings > Workspaces > _workspace_ > Repositories**, in a **Repository sets** section below the repository list. The section lists each set with its name, description, and member repositories, and offers create, rename, edit-members, reorder, and delete.
- **AC-WORKSPACES-REPOSITORY-SETS-001.3:** Sets can also be created from the task-creation dialog: **Save as set** captures the workspace repositories currently selected in the form, asks for a name, and creates the set without discarding the in-progress task.
- **AC-WORKSPACES-REPOSITORY-SETS-001.4:** A set name is trimmed, is 1 to 100 characters, and is unique within its workspace, compared case-insensitively. A second set named `Full-stack` in a workspace that already has `full-stack` is rejected and the existing set is named in the error.
- **AC-WORKSPACES-REPOSITORY-SETS-001.5:** A set holds each repository at most once. Ordering is user-controlled and is the order in which the set fills the picker.
- **AC-WORKSPACES-REPOSITORY-SETS-001.6:** Creating or updating a set requires at least one repository. A set that becomes empty because its repositories were deleted is kept, not removed.
- **AC-WORKSPACES-REPOSITORY-SETS-001.7:** Sets are workspace-scoped, not per-user: every user who can see the workspace sees and can apply its sets.
- **AC-WORKSPACES-REPOSITORY-SETS-001.8:** The repository row of the task-creation dialog gains a **Sets** control next to **add repository**, listing the workspace's sets by name with their repository count.
- **AC-WORKSPACES-REPOSITORY-SETS-001.1:** A workspace shall own any number of
repository sets. Each set shall have a name, an optional description, and an
ordered list of repositories from that workspace.
- **AC-WORKSPACES-REPOSITORY-SETS-001.2:** The repository settings page shall
list each set with its name, description, and members. Users shall be able to
create, rename, reorder, edit, and delete sets.
- **AC-WORKSPACES-REPOSITORY-SETS-001.3:** A set name shall contain 1 to 100
characters after trimming. Names shall be unique in a workspace under a
case-insensitive comparison.
- **AC-WORKSPACES-REPOSITORY-SETS-001.4:** A set shall contain each repository
at most once. A user-created set shall contain at least one repository.
- **AC-WORKSPACES-REPOSITORY-SETS-001.5:** When repository deletion removes the
last member, the system shall keep the empty set.
- **AC-WORKSPACES-REPOSITORY-SETS-001.6:** Every user with workspace access
shall see the same sets. Workspace write permissions shall control set
mutations.
- **AC-WORKSPACES-REPOSITORY-SETS-001.7:** A mutation shall update other open
clients through the repository-set event stream.
- **AC-WORKSPACES-REPOSITORY-SETS-001.8:** Set writes shall be atomic. A
rejected name, member, order, or saved base shall not cause a partial write.
## Migrated source detail
### REQ-WORKSPACES-REPOSITORY-SETS-002: Saved base branches
## Why
**Intent:** Users can reuse the base branch for each repository in a set.
A workspace can hold dozens of repositories, and real work rarely touches one of them. A feature
that runs from a web client through an API gateway into two backend services means the same four
repositories are picked, one chip at a time, every time a task is created. The selection is
knowledge the user already has and re-enters by hand on every task, and a repository forgotten on
the third of four chips is only discovered once the agent cannot find the code.
**User story:** As a user, I want to save each repository base branch, so that I
do not select every branch for each task.
A **repository set** is a named, reusable group of workspace repositories. Choosing a set fills the
task-creation repository picker with every repository in it, in one action. Branches remain a
per-task decision and are never stored in a set.
#### Acceptance criteria
## What
- **AC-WORKSPACES-REPOSITORY-SETS-002.1:** Each set member shall store zero or
one saved base. A member without a saved base shall use normal task-form
defaulting when the set is applied.
- **AC-WORKSPACES-REPOSITORY-SETS-002.2:** The set editor shall show selected
members as searchable, ordered rows. Each row shall show its repository and a
base-branch selector.
- **AC-WORKSPACES-REPOSITORY-SETS-002.3:** The selector shall include a
`Task default` choice. The choice shall show the current repository default
as supporting context when that value is available.
- **AC-WORKSPACES-REPOSITORY-SETS-002.4:** A reset action shall remove all saved
bases from the draft. The action shall not remove or reorder members.
- **AC-WORKSPACES-REPOSITORY-SETS-002.5:** Branch lists shall load when a user
opens a selector. Opening a set with many members shall not start one branch
request for every member.
- **AC-WORKSPACES-REPOSITORY-SETS-002.6:** A saved base that is no longer
available shall remain visible with an unavailable state. The system shall
not replace it with another branch.
- **AC-WORKSPACES-REPOSITORY-SETS-002.7:** The desktop editor shall use a modal
surface. The phone editor shall use a full-height surface with one scroll
region and a safe-area action footer.
- **AC-WORKSPACES-REPOSITORY-SETS-002.8:** All required actions shall support
keyboard, pointer, and touch input. Touch targets shall be at least 44 CSS
pixels in their active dimension.
- **AC-WORKSPACES-REPOSITORY-SETS-002.9:** The base control shall use the same
selector behavior as New Task. It shall include branch search, refresh,
remote-qualified names, and local or remote-origin labels.
### Defining a set
### REQ-WORKSPACES-REPOSITORY-SETS-003: Task form application
- A workspace owns any number of repository sets. A set has a name, an optional description, and an
ordered list of repositories drawn from that same workspace.
- Sets are managed from **Settings > Workspaces > _workspace_ > Repositories**, in a **Repository
sets** section below the repository list. The section lists each set with its name, description,
and member repositories, and offers create, rename, edit-members, reorder, and delete.
- Sets can also be created from the task-creation dialog: **Save as set** captures the workspace
repositories currently selected in the form, asks for a name, and creates the set without
discarding the in-progress task.
- A set name is trimmed, is 1 to 100 characters, and is unique within its workspace, compared
case-insensitively. A second set named `Full-stack` in a workspace that already has `full-stack`
is rejected and the existing set is named in the error.
- A set holds each repository at most once. Ordering is user-controlled and is the order in which
the set fills the picker.
- Creating or updating a set requires at least one repository. A set that becomes empty because its
repositories were deleted is kept, not removed.
- Sets are workspace-scoped, not per-user: every user who can see the workspace sees and can apply
its sets.
**Intent:** Applying a set fills the task form without removing user work.
### Applying a set
#### Acceptance criteria
- The repository row of the task-creation dialog gains a **Sets** control next to **add
repository**, listing the workspace's sets by name with their repository count.
- Choosing a set adds one repository row per member, in set order, leaving each row's branch empty
so the dialog's existing per-row branch defaulting fills it. The user then reviews and adjusts
branches exactly as when adding rows by hand.
- A single empty placeholder row is consumed by the first applied repository rather than left
behind. Rows the user already configured are never discarded or reordered by applying a set.
- Applying a set is additive and idempotent: a repository already present in the form is skipped,
so applying the same set twice changes nothing and applying two overlapping sets yields the union.
- A member repository that no longer exists in the workspace, or was deleted, is skipped. When any
member is skipped the dialog reports how many were skipped and why, as visible text.
- The same control appears in the **new subtask** form, which uses the same repository picker. It
does not appear in Quick Chat.
- The control is absent when the workspace has no sets **and** the current selection cannot be saved
as one, so a control that could only report "you have no sets" never appears. When the form does
hold a workspace repository, the control stays available with **Save as set** and no set entries:
otherwise the first set could never be defined from the flow that just chose the repositories.
- The control is absent in **Remote URL** and **No repository** source modes, where workspace
repositories are not what is being selected.
- The control is never disabled by executor capability. **Add repository** beside it is not
executor-gated either: the repository selection is what constrains which executor profiles stay
selectable, and the executor picker already marks an incompatible profile unavailable with its
reason. Applying a set on a single-repository executor therefore behaves exactly like adding the
same rows by hand.
- Applying a set changes only the form. No task, repository, or set is modified, and nothing is
persisted until the task is created.
- **AC-WORKSPACES-REPOSITORY-SETS-003.1:** New Task and New Subtask shall show a
`Sets` control next to the repository action. Each option shall show the set
name and member count.
- **AC-WORKSPACES-REPOSITORY-SETS-003.2:** Applying a set shall add one row for
each available member, in set order. The action shall consume one empty
placeholder row.
- **AC-WORKSPACES-REPOSITORY-SETS-003.3:** Applying a set shall skip a repository
that is already in the form. The action shall not change or reorder that
existing row.
- **AC-WORKSPACES-REPOSITORY-SETS-003.4:** Applying a set shall copy each saved
base into its new task row. A member without a saved base shall use normal
task-form defaulting.
- **AC-WORKSPACES-REPOSITORY-SETS-003.5:** The task form shall show an unavailable
copied base. The user shall select an available base or the task default
before task creation.
- **AC-WORKSPACES-REPOSITORY-SETS-003.6:** Local execution shall keep the saved
base separate from the checkout branch. Applying a set shall not silently use
the saved base as the local checkout branch.
- **AC-WORKSPACES-REPOSITORY-SETS-003.7:** Applying a set shall change only the
task draft. Later set changes shall not change existing tasks or open drafts.
- **AC-WORKSPACES-REPOSITORY-SETS-003.8:** When members are missing, the form
shall skip them and show the number skipped. Applying the same set twice
shall not add duplicate rows.
- **AC-WORKSPACES-REPOSITORY-SETS-003.9:** `Save as set` shall capture the
workspace repositories and effective bases shown in the task form. The dialog
shall state that it saves base choices. A `Task default` choice shall remain
an empty saved base.
When a repository appears on several rows, saving shall keep the first row and
its effective base. The dialog shall explain this rule and report additional
rows separately from rows that are not workspace repositories.
- **AC-WORKSPACES-REPOSITORY-SETS-003.10:** The control shall remain absent from
Quick Chat, Remote URL, and No repository modes. Executor capability shall not
hide or disable the control.
### Live updates
## Compatibility
- A set created, changed, or deleted in one client appears in every other client viewing that
workspace without a reload.
## Data model
Two new tables, both workspace-owned.
`repository_sets`
| Field | Contract |
| -------------------------- | ----------------------------------------------------------------- |
| `id` | Stable set identity. |
| `workspace_id` | Owning workspace; cascade-deleted with the workspace. |
| `name` | Trimmed, 1 to 100 characters, unique per workspace. |
| `description` | Optional free text, defaults to empty. |
| `created_at`, `updated_at` | Audit timestamps. |
`(workspace_id, name)` is unique in the database, and a second unique index on
`(workspace_id, LOWER(name))` makes that backstop case-insensitive too. The service rejects a
case-insensitive collision with a message naming the existing set; the index is what stops two
concurrent creates of `Full-stack` and `full-stack` from both landing.
`repository_set_items`
| Field | Contract |
| -------------------------- | ------------------------------------------------------------------- |
| `id` | Stable membership identity. |
| `repository_set_id` | Owning set; cascade-deleted with the set. |
| `repository_id` | A repository in the same workspace as the set. |
| `position` | Zero-based order within the set; contiguous after every write. |
| `created_at`, `updated_at` | Audit timestamps. |
`(repository_set_id, repository_id)` is unique: a set cannot list the same repository twice.
Sets deliberately store **no branch**. Branch choice belongs to a task, is already modelled on
`task_repositories.base_branch` / `checkout_branch`, and is what the user still decides per task.
Repositories are soft-deleted (`repositories.deleted_at`), so a foreign-key cascade does not remove
memberships. Deleting a repository removes its `repository_set_items` rows in the same transaction
that soft-deletes it, and every read of a set also excludes members whose repository is soft-deleted
or has moved out of the workspace. Either mechanism alone is sufficient; both are present so a set
can never surface a repository the user cannot select.
## API surface
Collection routes are workspace-scoped, item routes are flat, matching the existing repository
routes.
```
GET /api/v1/workspaces/:id/repository-sets
POST /api/v1/workspaces/:id/repository-sets
GET /api/v1/repository-sets/:id
PATCH /api/v1/repository-sets/:id
DELETE /api/v1/repository-sets/:id
```
Create request:
```json
{
"name": "Full-stack",
"description": "web + gateway + services",
"repository_ids": ["repo-web", "repo-gateway", "repo-orders"]
}
```
`repository_ids` is ordered and defines `position`. Update accepts `name`, `description`, and
`repository_ids` independently; a supplied `repository_ids` replaces the whole membership list,
which is also how reordering is expressed.
Response:
```json
{
"id": "set-1",
"workspace_id": "ws-1",
"name": "Full-stack",
"description": "web + gateway + services",
"repositories": [
{ "repository_id": "repo-web", "position": 0 },
{ "repository_id": "repo-gateway", "position": 1 },
{ "repository_id": "repo-orders", "position": 2 }
],
"created_at": "2026-08-17T09:00:00Z",
"updated_at": "2026-08-17T09:00:00Z"
}
```
The same five operations are available over the WebSocket dispatcher as
`repository_set.list|create|get|update|delete`, mirroring `repository.*`, so clients on the socket
do not need a second transport.
Set membership is also exposed on the boot payload as a workspace-keyed `repositorySets`
collection, so the task-creation dialog can offer sets on first paint without a fetch.
## Permissions
- Every route resolves the workspace and applies the same workspace-access authorization already
required for repository routes. A user without access to the workspace receives the same response
as for a workspace that does not exist.
- Item routes (`/repository-sets/:id`) resolve the set's workspace first and authorize against it;
a set id from another workspace is not readable, writable, or deletable.
- `repository_ids` are validated against the set's workspace. A repository id from another
workspace is rejected and does not reveal whether that id exists.
## Failure modes
| Situation | Result |
| ------------------------------------------------------------ | --------------------------------------------------------------- |
| Malformed body, blank name, name over 100 characters | `400`, no write |
| Empty or absent `repository_ids` on create, empty on update | `400`, no write |
| Duplicate id inside `repository_ids` | `400`, no write |
| Unknown workspace, unknown set, or no workspace access | `404`, no write |
| Name already used in the workspace, case-insensitively | `409` naming the existing set, no write |
| A `repository_id` is unknown, deleted, or in another workspace | `422` listing the offending ids, no write |
| Repository listing fails while building the boot payload | Boot succeeds with an empty set list; the dialog fetches instead |
| A set's members were all deleted | The set lists as empty and cannot be applied; it is not removed |
| Applying a set on an executor that forbids multi-repository | The set applies; the executor picker marks that profile unavailable |
Writes are atomic: name, description, and membership of one request either all land or none do,
because the store applies both halves in a single transaction. A set deleted between the read and
the write is reported as not found rather than resurrected.
## Persistence guarantees
- A created or updated set is durable before the response returns, and its membership positions are
contiguous from zero in the order requested.
- Deleting a workspace deletes its sets and memberships. Deleting a set deletes its memberships and
leaves every repository untouched. Deleting a repository removes it from every set, leaves the
sets themselves intact, and publishes each affected set's new shape so open clients stop offering
the deleted member.
- Applying a set writes nothing. The repositories that reach `task_repositories` are whatever the
form holds at submit, so a set applied and then edited persists the edited selection.
- Sets survive backup and restore with the rest of the workspace schema, and the schema
initialization is replay-safe: initializing twice against the same database is a no-op.
- Existing sets shall load with no saved bases and shall preserve current task
defaulting.
- Existing create and update clients that send ordered `repository_ids` shall
remain valid. Those members shall have no saved bases.
- Deleting a set shall not change its repositories or tasks that used it.
## Out of scope
- Branches, agent profiles, executor profiles, or workflows stored in a set. A set is repositories.
- Remote-URL and folder sources. Sets reference workspace repository entities.
- Quick Chat, and adding sources to an already-created task.
- Cross-workspace and per-user sets.
- Automatic set suggestions derived from task history.
- A shared branch value that is applied to every repository.
- Branch policies, branch templates, pull-request targets, agent profiles,
executor profiles, or workflows in a set.
- Remote-URL sources, folder sources, cross-workspace sets, and per-user sets.
- Automatic repair of unavailable saved bases.
## Traceability
- System design: [Repository sets](../system-design/repository-sets.md)
- Implementation plan: [Repository set base branches](../../../plans/repository-set-base-branches/plan.md)
@@ -0,0 +1,281 @@
---
status: draft
system: workspaces
requirements:
- REQ-WORKSPACES-REPOSITORY-SETS-001
- REQ-WORKSPACES-REPOSITORY-SETS-002
- REQ-WORKSPACES-REPOSITORY-SETS-003
---
# Repository Sets System Design
## Purpose and boundaries
The workspace system owns repository-set persistence, authorization, transport,
and settings behavior. The task form consumes a set as a reusable draft input.
A set stores an optional base for each member. Applying a set copies this value
into the task draft. The task service remains the authority for task-repository
snapshots and launch behavior.
The saved base does not change `repositories.default_branch`. It also does not
create a live link between a set and a task.
## Requirement mapping
| Requirement | Design sections |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `REQ-WORKSPACES-REPOSITORY-SETS-001` | [Persistence](#persistence), [Service and transport](#service-and-transport), [Events and boot state](#events-and-boot-state) |
| `REQ-WORKSPACES-REPOSITORY-SETS-002` | [Settings experience](#settings-experience), [Shared branch selector](#shared-branch-selector), [Branch loading](#branch-loading), [Responsive behavior](#responsive-behavior) |
| `REQ-WORKSPACES-REPOSITORY-SETS-003` | [Task form application](#task-form-application), [Local executor semantics](#local-executor-semantics), [Failure and recovery](#failure-and-recovery) |
## Data model
`repository_sets` keeps its current fields and workspace ownership.
`repository_set_items` adds `base_branch TEXT NOT NULL DEFAULT ''`. An empty
value means that the set does not override normal task-form defaulting.
The `RepositorySetItem` model and DTO add `BaseBranch` and `base_branch`.
Responses always include the ordered member list. A member can omit
`base_branch` when it has no override.
The set remains a draft template. It does not store a branch-policy ID,
checkout branch, generated task branch, or pull-request target.
## Persistence
The SQLite and PostgreSQL schema migration adds the new column before any query
uses it. The migration is replay-safe. The base schema includes the column for
new databases.
Repository-set create and update operations write metadata and all member
fields in one transaction. Membership replacement preserves the requested
order and assigns contiguous positions.
Existing rows receive an empty base. Backup and restore include the new column
through the existing database process.
## Service and transport
The service accepts ordered member inputs with `repository_id` and optional
`base_branch`. It validates these facts before the transaction:
- Each repository is live and belongs to the set workspace.
- Each repository occurs once.
- Each non-empty base passes `securityutil.IsValidBaseBranchRef`.
The service does not query Git during a write. Branch existence can change
after any check, and Git access can require credentials that are not available
to a settings request.
Create and update payloads add an ordered `repositories` field:
```json
{
"name": "Central",
"description": "Core services",
"repositories": [
{ "repository_id": "repo-document", "base_branch": "develop" },
{ "repository_id": "repo-classifier", "base_branch": "" }
]
}
```
The existing `repository_ids` field remains a compatibility input. Each ID
becomes a member with an empty base. A request that contains both member fields
is invalid because their order or values can conflict.
HTTP and WebSocket handlers use the same request type and service methods. The
same error categories remain stable. An unsafe base produces a validation
error and no write.
## Events and boot state
Repository-set DTOs include `base_branch` in list, get, create, and update
responses. The `repository_set.created` and `repository_set.updated` events use
the same member shape.
The boot-state projection uses the DTO without a second mapping. The workspace
store and WebSocket handlers preserve the complete member objects.
## Settings experience
The approved editor uses one inline row for each selected repository. The
desktop modal contains these elements:
- Name and description fields.
- A search field for the selected-member list.
- Ordered member rows with a repository label and base selector.
- An Add repository action.
- A Reset bases action.
- Cancel and Save actions.
The base selector starts with `Task default`. When available, supporting text
also shows `repositories.default_branch`. Choosing this option stores an empty
base and preserves current task-form defaulting.
The list shows only selected members. The Add repository action opens a
searchable list of remaining workspace repositories. Existing move controls
preserve explicit member order.
Set summary cards keep their current member chips. A member with a saved base
shows the base as secondary text or a compact suffix. The card does not fetch
branch lists.
## Shared branch selector
The set editor reuses the `Pill` popover/list primitive from
`task-create-dialog-pill.tsx`, the same primitive rendered by New Task's
`RepoChipBranchPill`. It does not create a settings-only branch selector or a
second lookalike popover.
The editor builds branch options with `sortBranches`, `branchOptionValue`, and
`branchToOption` from `task-create-dialog-branch-options.tsx`. This shared
option model supplies these behaviors:
- Search uses the same branch scoring and keywords as New Task.
- Remote branches keep qualified names such as `origin/main`.
- Local branches show the `local` badge.
- Remote branches show the remote name, such as `origin` or `followup`.
- Provider branches without a remote name show the `remote` badge.
- The shared popover keeps search and refresh in one row, groups branches under
`Branches`, and preserves the New Task keyboard, pointer, and portal behavior.
The editor supplies its dialog or drawer surface as the shared picker portal
container. Pickers remain inside the modal scroll-lock boundary but outside
the scrolling form. The outer surface allows overflow so it does not clip
the floating picker. Mouse-wheel and touch gestures scroll the option list.
`Task default` is a synthetic first option for base pickers. It does not change
the shared mapping, search terms, ordering, badges, or selected-branch display.
The shared `Pill` accepts an optional open-state callback for settings' lazy
branch loading. New Task keeps its current behavior when the callback is
absent.
## Branch loading
The trigger can show these values without a branch request:
- A saved base from the set member.
- The repository default from the workspace repository DTO.
- The loading or unavailable state.
The editor enables `useBranches` only while a row selector is open. This rule
prevents an immediate request for every member in a large set.
After the list loads, the selector compares the saved base with all available
branch option values from `branchOptionValue`. A missing value remains in the
selector as a disabled fallback option with an unavailable marker.
The frontend can save an unchanged unavailable value. A user cannot select a
new value that is absent from the current list. The backend still validates the
Git-ref syntax for all values.
## Responsive behavior
Desktop uses the existing dialog pattern. The dialog has a bounded height and
one internal scroll region for member rows.
Phone layouts use the full-height drawer pattern from repository branch-policy
settings. The drawer uses `100dvh`, one scrolling body, and a fixed footer. The
footer includes safe-area padding.
Each member row stacks its repository label above a full-width base selector on
phones. Add, remove, move, reset, save, and cancel actions have touch targets of
at least 44 CSS pixels.
The phone editor uses the same searchable `Pill` popover as New Task. The
popover stays inside the viewport and owns the temporary option-list scroll. It
keeps the origin badges, refresh action, focus return, and keyboard behavior.
The domain draft, validation, mutations, and branch option mapping are shared
between desktop and phone layouts. Only the surface composition changes.
## Task form application
`RepositorySetItem` supplies the saved base to `applyRepositorySet`. A new
`TaskRepoRow` keeps the copied base separate from checkout state.
Applying a set follows this order:
1. Resolve the member against the current workspace repository list.
2. Skip the member when the repository is missing or already in the form.
3. Add a row with the saved base and no branch-policy selection.
4. Let normal row defaulting resolve an empty saved base.
The action never changes an existing row. This rule preserves manual edits and
keeps repeated or overlapping set application idempotent.
The task branch selector shows the effective base. If a copied saved base is
unavailable, the row shows the value and blocks submission. The user must select
an available branch or `Task default`. Selecting `Task default` clears the
copied base so normal task-form branch autoselection runs again.
`Save as set` sends ordered member objects. It captures each workspace
repository and the effective base in the draft. A `Task default` choice sends
an empty base. The visible summary states that base choices are included.
Saving keeps the first row for each workspace repository, including its effective
base. The dialog counts additional rows separately from non-workspace rows and
explains that only the first row is saved. Neither exclusion changes the draft.
## Local executor semantics
The task draft keeps these values separate:
- The effective base from the set member or repository default.
- The checkout branch that represents the current local working branch.
For worktree-based execution, the effective base remains the source for
`base_branch`. For local execution, submission sends the effective base as
`base_branch` and the local working branch as `checkout_branch` when they
differ.
The task row shows both values when local execution makes them different. A
copied saved base never changes the local checkout branch as a side effect.
## Failure and recovery
| Condition | Behavior |
| -------------------------------------------- | ----------------------------------------------------------------- |
| A member repository is missing | Applying skips the member and reports the count. |
| A saved base is unsafe | The service rejects the set write with no partial change. |
| A saved base disappears | Settings and task creation keep the value visible as unavailable. |
| A branch request fails | The open selector shows retry behavior and keeps the draft value. |
| An old client sends `repository_ids` | The service stores members with no saved bases. |
| An open task draft already contains a member | Applying keeps the existing row unchanged. |
The frontend does not substitute a repository default for an unavailable saved
base. This rule prevents tasks from starting from an unintended branch.
## Permissions and security
Repository-set routes keep their current workspace authorization. Item routes
resolve the owning workspace before read or write access.
The service validates all repository IDs against the set workspace. It does not
reveal whether a cross-workspace ID exists. Safe Git-ref validation prevents a
saved base from becoming a command argument with unsafe syntax.
## Localization and accessibility
All new labels, hints, unavailable states, errors, and summaries use i18n keys.
The five supported catalogs contain the same keys and placeholders.
The editor uses semantic controls and visible labels. Selector triggers expose
accessible names. Focus returns to the trigger after a selector closes.
## Verification strategy
- Repository tests cover fresh schema, replay, existing-row migration, ordered
base persistence, atomic replacement, and SQLite/PostgreSQL behavior.
- Service and handler tests cover validation, compatibility payloads, DTOs,
events, and authorization.
- Frontend unit tests cover API mapping, store events, apply rules, unavailable
bases, save-as-set input, and local base-versus-checkout submission.
- Component tests cover search, add, remove, reorder, reset, lazy branch loads,
and responsive surface selection.
- Desktop and mobile Playwright tests create a set, save member bases, apply the
set, and create a task with the expected repository bases.