feat(workspaces): save repository set base branches (#3269)
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user