Proposals: review a branch integration
Documentation/Web interface
Proposals: review a branch integration14 min read

Proposals: review a branch integration

On this page

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 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 and Perforce Swarm's review states and blocking feedback. 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.

Source docs/proposals.mdSnapshot 2b8dcccb