# Proposals: review a branch integration

A **Proposal** is Scribe's optional web review workflow. It describes the result
of integrating a source branch into a target branch, following new submits automatically. The interface
uses familiar **Review** and **Merge** actions, including for nested branches.

The CLI remains independent: ordinary CLI merges and submissions need neither
the web interface nor its database. No native protocol or on-disk format changes
are needed. A proposal is not a storage-level protected-branch rule.

## Workflow

1. Open a repository's **Proposals** tab and choose **New proposal**. Name the
   outcome, explain why it matters and how to test it, select the source and
   target branches, and optionally start as a draft. The initial review records both heads.
2. Publish the draft to request review. The Files changed tab compares the
   target with the proposed result. Text has a bounded line diff; images,
   textures, audio and models use the asset viewers on demand, and an image that
   exists in both the target and the proposed result opens in one viewer that
   compares them.
3. Discuss the whole proposal or a file. Every comment belongs to a review
   round, so an old discussion never silently claims to describe new bytes.
4. Resolve conflicts by explicitly keeping the target, using the source, or
   providing complete resolved UTF-8 text. Save the resolutions for review.
   They become part of a new round, rather than an unreviewed edit at merge time.
5. Another repository writer approves the round or requests changes with an
   explanation. One independent approval and approval from every requested reviewer
   are required. Authors cannot approve
   their own proposals. A request for changes remains blocking across rounds
   until that reviewer approves; automatic updates never silently dismiss it.
6. A repository writer confirms the merge. The server checks current heads,
   the approved round and all blockers before preparing the exact native merge.
   Storage then enforces current authorization, locks, path rules, and the
   target-head precondition. The accepted result is an ordinary merge commit
   with normal source provenance, visible in the existing history and graph.

Drafts, open proposals, merged proposals and closed proposals have separate
filters. Closing preserves discussion; reopening starts a fresh review round.
Open proposals and drafts follow branch changes automatically. Each observed pair
of new branch revisions is saved as an immutable round. Fresh approvals are
required only if the compared file content, modes or conflict resolution changes;
unrelated target-branch updates preserve valid approvals. A previously invalidated
approval never becomes valid again just because later work restores older bytes.
Title, description, labels and reviewer edits do not invalidate code approvals.
Requested reviewers still must approve, and reopening requires a fresh review.
Branches and history are never deleted.

Unrelated target-branch submissions preserve the saved review, its version and
its approvals. The web asks storage whether the reviewed paths, rename origins,
folded aliases, and ancestor/descendant dependencies changed. A source advance
or a changed target dependency follows the normal automatic review update.
Comparison failures block approval and merge while keeping the saved review
readable. Merge preparation uses that saved plan and its original pinned target;
the server checks acceptance again, including any changes racing the web read.

A separate background worker follows repositories with open proposals or drafts,
independently of browser sessions. Proposal mutations atomically schedule a
repository job in SurrealDB. Valkey dispatches deduplicated jobs with renewable
leases; expired leases are recovered after a crash, and durable jobs repopulate
Valkey after cache loss. Workers check branch heads once per bounded repository
batch every 30 seconds by default, recalculating only changed proposals. There is
no global storage/history scan or native change subscription. Jobs stop when a
repository has no active proposals. Read-only startup seeding scans indexed web
metadata to register active repositories; it makes no native requests.

The worker uses the separately configured **read-only credential file of a
service account** that reads the namespace (or holds `reader` on chosen repositories).
It never borrows browser credentials. A namespace-wide reader grant covers new
private repositories; with per-repository grants instead, each new private
repository needs its own `reader` grant for that account. Expiry,
revocation, unavailable services and comparison failures retry with bounded
backoff, preserving the previous round. See [background jobs](/docs/background-jobs.md)
for configuration, credential rotation, health and adding other job types.

Current REST/MCP reads and write preconditions retain the shared reconciliation
as a freshness guard. The web page polls every 15 seconds and on focus to display
persisted updates; only this display polling pauses while drafting. Background
processing continues. Historical reads never advance proposals. List/feed reads
use persisted, visibility-checked events, including worker-generated updates.

Automatic updates only write derived proposal metadata, never branch commits.
They use current repository read permission and produce a **Scribe** system event,
without claiming that the visitor authored the change or enrolling them in the
conversation. Concurrent readers converge through version compare-and-swap.
A last-moment branch change rejects approval, resolution or merge actions against
an unseen version. Storage checks the target head again before accepting a merge.
Closed, archived, merged and uncertain/merging proposals remain pinned.

Saved conflict resolutions carry forward only when that path's exact base,
source and target content/mode and default choice are unchanged. Changed paths
need a new resolution; the earlier resolution remains in its original round.
If comparison fails or exceeds admission limits, the last review remains
inspectable with an update warning, and stale review/merge actions stay blocked.


## Permissions and trust

- Repository visibility controls every read of metadata and file content.
- An authenticated reader can comment. Anonymous visitors can read public
  proposals but cannot participate.
- Existing writer/maintainer/namespace-admin authorization, through native
  upload authorization, controls creation, review and merging.
- The author manages the title, context, conflict resolutions and open/closed state while
  retaining repository write permission. A second writer supplies approval.
- Approvals attest the reviewer's verified writer permission **at review time**;
  credentials are not retained to keep an approval alive. Current read and write
  permission are checked for every new action. The merging user's authorization
  is checked again by storage. This is not a live role membership directory for
  past reviewers.
- Browser sessions use the existing same-origin checks and rate limits. PAT and
  MCP tokens use explicit `proposals:read` and `proposals:write` scopes. Scopes
  never grant repository access or bypass the same author/reviewer/merge checks.
  Write scope is opt-in when creating a token. Tokens remain audience-separated;
  MCP credentials use the MCP endpoint and PAT credentials use REST.

This initial policy intentionally has no self-approval switch, approval bypass,
forced dismissal of another reviewer's change request, or merge queue. A solo
user can continue to use ordinary CLI merges. Project-wide protected branch
policies would be a separate storage feature and must not depend on the web DB.

## Collaboration and activity

Conversation, Submits and Files changed are separate tabs. Submits pages show source
history after the pinned merge base. A New proposal shortcut on repository
history preselects the current branch and its parent when available.

The proposal header shows its pinned source → target branches. Editing replaces
the title and description in place; Save publishes both together using the
version at which editing began. Other proposal actions stay disabled while
editing, and existing comment drafts are retained. Descriptions and comments
share a Markdown editor with formatting tools, syntax highlighting, undo,
keyboard shortcuts and a rendered preview. The editor enforces the 16,000-character
limit without truncating existing text. Merge readiness sits between the
conversation timeline and the comment composer.

The Submits tab shows a count from its shared, paged history query. A `+` means
more submits remain to be loaded; the UI does not scan an entire branch just to
compute a badge. Files changed opens the latest proposal result with a filterable file navigator,
Added/Modified/Deleted/Unchanged/Conflict status badges and bounded text or asset
comparisons. File disclosure stays stable when the latest snapshot advances.
Collaboration metadata stays out of the file workspace. Ordinary changes have no
source/target dropdown: merge-result editing is a secondary disclosure, while
unresolved conflicts expose resolution controls immediately. Unsaved resolutions
must be saved or cancelled before submitting a review or switching snapshots.

Review changes opens the shared dialog with Comment, Approve or Request changes,
according to current permissions. It submits against the proposal version seen
when the dialog opened; an arriving update disables submission and offers a return
to the files. Existing server-side approval and stale-version checks still apply.
Review history is a secondary, date-led menu of immutable snapshots, loaded only
when opened. Earlier snapshots remain read-only with a Back to latest action.

Creators can assign reviewers and assignees by exact account name, and manage
up to twenty string labels. The picker suggests existing participants and also
accepts exact names; it does not expose an auth-service user directory or grant
access. Every individual reviewer/assignee addition and removal has a timeline
event. Adding a reviewer requires their approval without discarding approvals
from existing reviewers. Removing a reviewer never dismisses their existing
request for changes.

Creators can lock/unlock conversation. Locking blocks new comments; it does not
prevent independent review decisions. Archiving preserves discussion and all
review rounds without performing a merge, and makes the proposal read-only until
the creator reopens it. Closed proposals can still be discussed; archived ones
cannot. Reopening invalidates previous approvals.

Comments render safe Markdown and can include structured person, same-repository
proposal, and code-range references. `@name`, `@{email-or-account-name}` and
`#proposal-uuid` also create references. Code references name the source or target
side and 1-based line range in the comment's immutable review round; the API checks
that the file and lines exist. Links open and highlight the pinned range. Each
comment allows eight references including two code ranges of up to 200 lines within the first 5,000 displayed lines.
Code reference validation reads at most 512 KiB per file. A code reference the
review cannot resolve is refused with `400` and one code per reason: `code_path` (the file
is not in the review), `code_unresolved` (the result side of an unresolved
conflict), `code_file_missing` (the file does not exist on that side),
`code_too_large`, `code_not_text` (not UTF-8) and `code_out_of_range` (the lines
do not exist). Assignment/mention names
are addressing labels, not proof of another account's identity or permissions;
only their authenticated actions supply that proof.

Lifecycle events (created, including drafts, published, closed, reopened,
merged and archived) appear in the feed for anyone who can read the repository,
including the anonymous feed of a public one. Discussion events (comments,
reviews, reviewer, assignee and label changes, automatic updates, a rejected
merge) appear only for the proposal's audience: its author, reviewers,
assignees, anyone who commented or acted on it and mentioned accounts.
Participation is cumulative; removing an
assignment does not erase prior participation. The activity event and its bounded
recipient records are committed atomically with the proposal update. Each proposal
allows 256 participants. An index on deployment, recipient and event order serves
activity without scanning native history or every proposal.

Feed reads recheck repository access and incarnation before exposing any event.
They inspect at most 36 indexed candidates and emit at most twelve entries per
page. Empty pages can still have a cursor when candidates are inaccessible. The
home feed merges this stream with the existing bounded native-commit streams,
advancing only consumed entries. Its encrypted cursor remains identity-bound.

## Persistence, races and recovery

The `ProposalRepository` interface isolates persistence. Its SurrealDB adapter
stores review metadata, bounded events, operation fingerprints and pending merge
intents; repository bytes and accepted history remain in Scribe storage.
Records are scoped by configured endpoint/namespace, repository ID and incarnation.
Indexed pages contain at most twenty summaries, excluding discussion, resolved
text and prepared objects. Timeline records are paginated in batches of 50; compact review state retains current decisions and the latest review boundary. Durable operation fingerprints live in separate records.

Every metadata action carries a UUID and expected proposal version. Atomic
compare-and-swap prevents concurrent comments, reviews, automatic updates and merge
starts from overwriting each other. Exact retries are idempotent; reuse of an
operation ID for different input is rejected. The browser saves the exact request
before sending and offers retry after uncertainty.

Before native transmission, one atomic write changes the proposal to `merging`
and saves the complete prepared intent, native operation ID, and initiating
identity. Mutations freeze while the outcome is unknown. Only the initiating
account can resume; it may sign in again. Retries resolve native operation history
and reuse the original intent. A lost response never means rejection and never
causes a fresh merge ID. Concurrent recovery calls converge on the same recorded
native outcome. A confirmed rejection reopens the proposal with a new round.
There is no cross-database transaction or guess that a network timeout rolled back.

Review snapshots are preserved independently of the current proposal. The Files changed
view can reopen earlier rounds with their original branch heads, plan and
conflict resolutions. The API also returns the context captured with that round. These records contain no session credentials or
prepared write payloads. Snapshot creation and proposal updates share one database
transaction; a losing concurrent update cannot create a review round. Proposals
allow up to 499 review rounds, reserving one final round and event slots for a
rejected merge. Timeline pagination is independent of the review-round bound.

## Bounded web integration

The web merge planner currently supports up to 128 affected paths and sixteen
native delta pages. It follows one rename shape: a clean source-side move, where
the source branch recorded `A -> B` (with `scribe move`, edited afterwards or not)
and the target branch has neither changed nor removed `A` nor created `B` since
the merge base. The review shows `B` once, marked **Renamed** with its origin,
compares it against the target's `A`, and the merge submits exactly what the CLI
would: an add of `B` carrying `moved_from` paired with a delete of `A`, so
`scribe log B` continues into `A`'s history. Declining the change keeps `A`.
Every other rename shape — a target-side rename, a rename on both sides, an
origin the target edited or removed, a destination the target already has, or
an ambiguous ancestry — is rejected instead of being flattened into unrelated
add/delete operations. Resolve those changes with the CLI; CLI operation remains
unrestricted by web-review limits. Source assets are reused
by content identity and are not downloaded just to merge. Edited conflict text is
limited to 512 KiB per file and 2 MiB per review. Native lock policy remains final:
the web workflow does not acquire or steal another workspace's locks.

These bounds also apply when creating a proposal: unsupported integrations return
an explicit error before creating review metadata. Asset previews retain their
existing per-format byte limits. Text diffs are bounded to 600 lines per side and
100,000 combined characters, then fall back to ordinary file previews.

## REST interface

Generated documentation is available at `/developer/rest`, under **Proposals**:

- `GET /api/v1/proposals/activity?cursor=...`
- `GET /api/v1/repositories/:repo/proposals?state=open&cursor=...`
- `GET /api/v1/repositories/:repo/proposals/:id`
- `GET /api/v1/repositories/:repo/proposals/:id/rounds`
- `GET /api/v1/repositories/:repo/proposals/:id/rounds/:round`
- `GET /api/v1/repositories/:repo/proposals/:id/submits?round=...&cursor=...`
- `POST /api/v1/repositories/:repo/proposals` (session or `proposals:write` PAT)
- `POST /api/v1/repositories/:repo/proposals/:id/actions` (session or `proposals:write` PAT)

Actions are `edit`, `publish`, `resolve`, `comment`, `approve`, `request_changes`,
`close`, `reopen`, `merge`, `reviewers`, `assignees`, `labels`, `lock`, `unlock`,
and `archive`. Schemas document the exact request and response
shapes. `merge` on a pending proposal resumes the existing durable intent. MCP exposes
`proposal_activity`, `list_proposals`, `read_proposal`, `proposal_rounds`,
`proposal_submits`, `create_proposal`, and `proposal_action`. REST and MCP share
input schemas and the same application service.

## Design references

The design binds approval to an exact review round, learning from
[GitHub's stale-review and recent-push protections](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches)
and [Perforce Swarm's review states and blocking feedback](https://help.perforce.com/helix-core/helix-swarm/swarm/current/Content/Swarm/quickstart.review_page_overview.html).
Scribe keeps accepted history intact and treats binary assets as reviewable files,
without imposing a web dependency on native workflows.

### Inline diff comments

Select a line number in **Files changed**, then Shift-click a second line (or use
Shift+Arrow keys) to select a range. Mouse users can also drag along the line-number
gutter. **Add comment** opens the Markdown composer beneath that range. Comments
appear both beside the code and in Conversation, as one stored event. Conversation
links reopen the exact historical review and line range; later submits never
reattach comments to different code. Historical reviews remain read-only.

REST `POST /api/v1/repositories/{repo}/proposals/{id}/actions` and MCP
`proposal_action` use the same comment action:

```json
{
  "operationId": "2c08dc75-2218-42ca-adc4-f63dccf813a5",
  "version": 7,
  "action": "comment",
  "text": "Can we avoid allocating here?",
  "path": "src/player.ts",
  "references": [
    { "kind": "code", "path": "src/player.ts", "side": "result", "start": 12, "end": 18 }
  ]
}
```

Use `target` for the before side, `result` for the proposed merge result (including
saved custom resolutions), or `source` for a direct source-branch reference.
Ranges are inclusive and one-based: at most 200 lines within the first 5,000
lines of a UTF-8 text file up to 512 KiB. The server checks the actual pinned
bytes, current repository access, token scopes, conversation locks and proposal
version. Invalid/stale comments are rejected without rebinding them. Retries use
the same operation ID and payload. The event's `round` identifies the snapshot
returned by the existing review-round endpoint and MCP `read_proposal` round
parameter. No CLI or wire-protocol changes are required.


### Discussion API

`GET /api/v1/repositories/:repo/proposals/:id/events` returns `events` and a
`nextCursor`. Pass that cursor to read older events, or `thread=<root-comment-uuid>`
to page a thread. Detail returns the most recent page plus `eventCount` and
`eventsNextCursor`. The MCP tool is `proposal_events` with `params` and `query`.

The action endpoint and `proposal_action` support replies (`comment` with `replyTo`),
`edit_comment`, `delete_comment`, `resolve_thread`, and `reopen_thread` (the latter
four use `commentId`). All carry the expected proposal version and a persisted
operation ID. Replies remain pinned to the original round. Edits require the
comment's author; deletion and resolution also permit the proposal's author with
native write permission. Locked and archived conversations refuse mutations.
Deleting retains a tombstone, so replies and timeline links remain meaningful.
Every edit, deletion and resolution emits an auditable proposal timeline event.

`GET /api/v1/repositories/:repo/proposals/:id/diff?round=...&path=...` returns a
bounded, numbered comparison of target bytes with the exact proposed result,
including saved conflict resolutions. `cursor` pages 200 diff rows at a time.
Binary and oversized files return an explicit classification. The MCP tool is
`proposal_diff`; both transports enforce current repository visibility.
