# Build notes The slicing plan for the v1 build, the current state of the codebase, and the next slice's brief. ## The slicing plan Eight slices carry §§1–15 of [`SPEC.md`](../SPEC.md) end-to-end. The build does not extend the spec; spec corrections during the build are rare and surgical and live in the appropriate numbered section per §19.3's working agreement. 1. **Repository scaffolding + propose-to-super-draft vertical.** The chokepoint that every Git operation flows through (§1 bot wrapper), the §4 cache machinery (webhook + reconciler), the §5 schema, Gitea OAuth + user provisioning, the minimal §7 catalog, and one end-to-end vertical: propose → idea PR → merge → super-draft view. 2. **The active-RFC view per §8 in full.** Editor, branch creation, per-branch chat with AI participation (the §18 `` protocol), the change-card panel, accept/decline/edit, manual-edit flushes, sub-threads, flags, DiffView. 3. **The PR flow per §10.** Open, review surface (diff + compressed chat), the §10.3 seen-cursor, §10.4 review threads, merge, post-merge, §10.9 conflict resolution. 4. **Super-draft body editing per §9.5 + §9.6.** Meta-repo edit branches as the unit of work; everything from §8 inherits. 5. **Graduation per §13.** The dialog, the five-step transactional sequence, rollback, the pre-graduation history affordance. 6. **Notifications per §15.** Last, because every other surface produces signals the inbox receives — notification correctness depends on the producers being in place first. 7. **The §14 chrome + the settings and admin neighborhoods.** Landing page polish, the `/philosophy` route, the persistent About link; the `/settings/notifications` surface that exposes Slice 6's preferences/quiet-hours/mute/watches endpoints; the `/admin` home base that consolidates role management, the §6.2 write-mute, the audit-log viewer, and the §13.2 graduation- readiness queue. 8. **Hardening.** End-to-end tests, dev/prod deployment shape, the §12 30/90 branch-hygiene timers, the §19.2 candidates that cluster with deployment (branch-name path routing, cache bootstrap from a pre-existing meta repo, in-app metadata-PR merges, graduation rollback's branch cleanup). ## State of the codebase ### Slice 1 — shipped The repository scaffolding (`backend/`, `frontend/`, `scripts/`, `docs/`), the §5 schema as numbered migrations under `backend/migrations/`, the §1 bot wrapper (`app/bot.py`) that is the single chokepoint every Git write flows through, Gitea OAuth and the §6.1 user-provisioning row in `users`, the §4.1 webhook receiver and the §4.1 periodic reconciler (both writing to the cache; user actions never do), the §7 left pane (catalog list, search, sort, state-filter chips, pending-ideas disclosure), and one end-to-end vertical: propose → idea PR opens → owner merges → super-draft appears in the catalog → super-draft view renders the body. ### Slice 2 — shipped The §8 active-RFC view in full. The bot wrapper grew per-RFC-repo write operations — branch cut from main, accept-change commit with the structured `original`/`proposed`/`reason` body and trailers, manual-edit flush, and a `ensure_rfc_repo_seed` seam Slice 5's graduation will eventually replace. The §4 cache now mirrors per-RFC repos via a new `refresh_rfc_repo` path; the webhook receiver dispatches on `repository.full_name` so per-RFC events refresh just that repo, and the reconciler sweeps every active entry. The §18 carryovers landed as `backend/app/providers.py` (the multi-provider abstraction, unchanged from the prototype) and `backend/app/chat.py` (an adapter that runs the provider's streaming interface against `thread_messages` rows, parses `` blocks, and materializes `changes` rows per §8.14). The §17 endpoints owned by Slice 2 — the `branches//*` and `threads//*` families — live in `backend/app/api_branches.py`, mounted alongside Slice 1's routes via `api.make_router`. On the frontend, `RFCView.jsx` was rebuilt as the §8 three-column surface; `Editor.jsx`, `ChatPanel.jsx`, `ChangePanel.jsx`, `PromptBar.jsx`, `SelectionTooltip.jsx`, `DiffView.jsx`, `ModelPicker.jsx`, and `modelStyles.js` were lifted from the prototype and adapted to the canonical `threads` / `thread_messages` / `changes` shape rather than the prototype's global session_id. The §18 carryovers explicitly preserved: SSE streaming with base64-encoded chunks, Tiptap + ProseMirror plugin for the paragraph-margin gutter accent, the prompt-bar selection-quote machinery, the model picker. The §17 endpoints exercised so far: | Method | Path | § | | ------ | -------------------------------------- | ------- | | GET | `/api/auth/me` | §6 | | GET | `/api/rfcs` | §7, §17 | | GET | `/api/rfcs/{slug}` | §17 | | GET | `/api/proposals` | §17 | | GET | `/api/proposals/{pr_number}` | §17 | | POST | `/api/rfcs/propose` | §9.1 | | POST | `/api/proposals/{pr_number}/merge` | §9.3 | | POST | `/api/proposals/{pr_number}/decline` | §9.3 | | POST | `/api/proposals/{pr_number}/withdraw` | §9.3 | | POST | `/api/webhooks/gitea` | §4.1 | | GET | `/auth/login` / `/auth/callback` / `/auth/logout` | §18 | | GET | `/api/models` | §18 | | GET | `/api/rfcs/{slug}/main` | §8.1, §8.2, §17 | | GET | `/api/rfcs/{slug}/branches/{branch}` | §8.4, §17 | | POST | `/api/rfcs/{slug}/branches/main/promote-to-branch` | §8.14, §17 | | POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/accept` | §8.9, §17 | | POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/decline` | §8.9, §17 | | POST | `/api/rfcs/{slug}/branches/{branch}/changes/{id}/reask` | §8.11, §17 | | POST | `/api/rfcs/{slug}/branches/{branch}/manual-flush` | §8.11, §17 | | POST | `/api/rfcs/{slug}/branches/{branch}/visibility` | §11.1, §17 | | POST | `/api/rfcs/{slug}/branches/{branch}/grants` | §6.4, §17 | | DELETE | `/api/rfcs/{slug}/branches/{branch}/grants/{login}` | §6.4 | | GET | `/api/rfcs/{slug}/branches/{branch}/threads` | §8.12, §17 | | POST | `/api/rfcs/{slug}/branches/{branch}/threads` | §8.12, §8.13 | | GET | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/messages` | §8.12 | | POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/messages` | §8.12 | | POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/resolve` | §8.12 | | POST | `/api/rfcs/{slug}/branches/{branch}/threads/{id}/chat` | §18 | Slice 2 ships covered by `backend/tests/test_rfc_view_vertical.py` — the FakeGitea simulator from Slice 1 grew per-RFC-repo support (PUT contents, POST `orgs/{org}/repos`, `seed_rfc_repo`), and a new test file walks the §8 vertical end-to-end: main-view read, promote-to- branch, accept (with and without edit-before-accept), decline, manual flush + system message, flag creation, visibility flip, anonymous read-but-no-contribute, stale-change refusal, and the chat-streaming path with a fake provider injected. ### Slice 3 — shipped The §10 PR flow in full. The bot wrapper grew per-RFC-repo PR operations — `open_branch_pr` (with the §10.9 `Supersedes:` trailer hook), `merge_branch_pr` (no-fast-forward via Gitea's `style='merge'`, the `On-behalf-of:` trailer carrying the merging user per §6.5), `withdraw_branch_pr`, `cut_resolution_branch`, and `commit_replay_change` for the §10.9 per-accept replay onto fresh main. The §4 cache learned about per-RFC PRs via the existing `refresh_rfc_repo` sweep, plus a `_parse_supersedes` pass that bumps an original PR's state to closed and records the supersession the moment the resolution PR's merge arrives — whether via webhook or the reconciler. The §17 endpoints owned by Slice 3 — the `branches//{pr-draft,open-pr}` and the `prs//*` family — live in `backend/app/api_prs.py`, mounted alongside Slices 1 and 2's routes via `api.make_router`. The migration in `007_pr_flow.sql` adds `superseded_by_pr_number` and `merge_commit_sha` columns to `cached_prs` plus the `pr_resolution_branches` join table that records resolution-branch parentage so the cache can supersede the original on the resolution PR's merge. On the frontend, the `Open PR` affordance landed on `RFCView.jsx`'s branch view (gated on the branch having commits ahead of main and no already-open PR), opening a new `PRModal.jsx` that fetches the AI draft via `/pr-draft`, lets the contributor edit, and surfaces the §11.3 universal-public confirmation inline when the source branch is private. The `PRView.jsx` sibling to `RFCView.jsx` is mounted at `/rfc/:slug/pr/:prNumber` and renders the §10.3 three-column shape: catalog left (App chrome), a unified/split diff in the center computed from main and branch RFC.md bodies, and a compressed conversation surface on the right that interleaves chat / flag / review threads with visual distinction per §10.4. The per-user seen-cursor advances on every visit; new commits and new messages since the cursor surface with an accent. The merge button is arbiter-gated per §6.3; withdraw is contributor-or-arbiter per §10.8; the §10.9 `Start resolution branch` affordance fires from the conflict banner when the live Gitea pull reports the PR as unmergeable, and the new resolution branch opens in the §8 editor for the contributor to re-anchor stale changes before opening the resolution PR. The §17 endpoints exercised in Slice 3: | Method | Path | § | | ------ | ----------------------------------------------- | ------- | | POST | `/api/rfcs/{slug}/branches/{branch}/pr-draft` | §10.2 | | POST | `/api/rfcs/{slug}/branches/{branch}/open-pr` | §10.1 | | GET | `/api/rfcs/{slug}/prs/{n}` | §10.3 | | POST | `/api/rfcs/{slug}/prs/{n}/seen` | §10.3 | | POST | `/api/rfcs/{slug}/prs/{n}/review` | §10.4 | | POST | `/api/rfcs/{slug}/prs/{n}/merge` | §10.5 | | POST | `/api/rfcs/{slug}/prs/{n}/withdraw` | §10.8 | | POST | `/api/rfcs/{slug}/prs/{n}/description` | §10.2 | | POST | `/api/rfcs/{slug}/prs/{n}/resolution-branch` | §10.9 | Slice 3 ships covered by `backend/tests/test_pr_flow_vertical.py` — nine integration tests against an extended FakeGitea that grew PR mergeability via base-snapshot tracking, no-fast-forward merge behavior, and a `mergeable` field on PR responses. The tests cover opening (with the §11.3 visibility flip and the §10.9 one-PR-per- branch refusal), the AI draft, the three-column payload shape, seen-cursor advance with stale-tab protection, review-thread posting, arbiter-only merge, contributor withdraw with the `withdrawn` state distinct from generic `closed`, anonymous read of a public PR, and the full §10.9 conflict-replay path including the auto-close of the original PR on the resolution PR's merge. ### Slice 6 — shipped Notifications per §15 in full, end-to-end against the local Gitea. The producer-side chokepoint lives in [`backend/app/notify.py`](../backend/app/notify.py). Every bot `_log` call drops into `notify.fan_out_from_action`, which upserts the actor's `watches` row per §15.6's substantive-gesture rule and runs the §15.1 routing table to insert zero-or-more `notifications` rows. Chat-message inserts (the second writer surface, since chat doesn't flow through the bot) call `notify.fan_out_chat_message` from inside `chat.append_user_message` — same chokepoint shape, one place to read the routing. The graduation orchestrator's `_audit` helper folds into the same fan-out so `graduate_start` / `graduate_complete` ride the chokepoint too. §15.4 email lives in [`backend/app/email.py`](../backend/app/email.py). The SMTP adapter wraps Python's `smtplib`; when `SMTP_HOST` is unset it falls back to logging the envelope (and appending it to an in-memory `_SENT` buffer the integration tests read from). The per-category dispatch consults the recipient's toggles, holds during §15.8 quiet hours, and on quiet-hours window-end the `flush_pending` pass bundles into a single "Activity while you were away" mail when more than `EMAIL_BUNDLE_THRESHOLD` accumulated. One-click unsubscribe is a signed token over `(user_id, category)`; the bounce webhook flips `email_opt_out_all` on the user (new column added by migration 008). §15.5 digest lives in [`backend/app/digest.py`](../backend/app/digest.py) as a `DigestScheduler` mirroring `cache.Reconciler`'s shape. The `run_tick` function is the test seam — integration tests drive ticks synchronously, production runs the loop on a `DIGEST_TICK_SECONDS` cadence (default 3600s). Each tick releases held emails, decays §15.6 `watching` rows whose `last_participation_at` is >90 days old, and assembles digests for users whose cadence window has rolled over per `notification_digests.period_end`. §15.2 / §15.3 / §15.7 / §15.8 surface as the twelve endpoints in [`backend/app/api_notifications.py`](../backend/app/api_notifications.py) plus the §15.7 chat-seen advance on `api_branches` and the PR seen-cursor advance on `api_prs` — both extended to call `notify.reconcile_seen_advance` so visit-advances-cursor closes the inbox-read loop per §15.7. The `/api/notifications/stream` SSE handler holds a per-user subscriber queue keyed by user_id; one event per browser tab, all subscribers for a user receive every event so the badge counter stays in lockstep across tabs. | Method | Path | § | | ------ | ------------------------------------------------- | ------- | | GET | `/api/notifications` | §15.2 | | POST | `/api/notifications/{id}/read` | §15.2 | | POST | `/api/notifications/read` | §15.2 | | GET | `/api/notifications/stream` | §15.3 | | GET | `/api/watches` | §15.6 | | POST | `/api/rfcs/{slug}/watch` | §15.6 | | POST | `/api/rfcs/{slug}/branches/{branch}/chat-seen` | §15.7 | | GET | `/api/users/me/notification-preferences` | §15.4/5 | | POST | `/api/users/me/notification-preferences` | §15.4/5 | | GET | `/api/users/me/quiet-hours` | §15.8 | | POST | `/api/users/me/quiet-hours` | §15.8 | | POST | `/api/users/{id}/notification-mute` | §15.8 | | DELETE | `/api/users/{id}/notification-mute` | §15.8 | | GET | `/api/email/unsubscribe` | §15.4 | | POST | `/api/webhooks/email-bounce` | §15.4 | On the frontend, `App.jsx` grew a header badge button (`📮` glyph with a 99+-capped unread count) that opens the inbox overlay. The overlay is `Inbox.jsx` — three filter chips (Unread only, RFC, Category) plus a Bundle toggle and a "Mark all read (under filter)" action. The badge subscribes to the SSE stream alongside the overlay so they share a counter. `ToastHost.jsx` renders personal- direct toasts and live-view toasts (an event firing on the slug the URL points at), capped at four visible at a time with auto- dismiss after a short interval. Slice 6 ships covered by [`backend/tests/test_notifications_vertical.py`](../backend/tests/test_notifications_vertical.py) — seventeen integration tests covering the producer-side fan-out for the propose/merge/decline chain, §15.6 auto-watch on first interaction, the §15.2 inbox listing with filter chips, the §15.7 chat-seen reconciler, the §15.8 per-user mute and the per-RFC mute, the §15.4 email-bounce webhook, the `/email/unsubscribe` signed-URL path, the §15.8 quiet-hours hold, the §15.5 digest's emit-then-skip behavior across two consecutive ticks, preferences and quiet-hours round-trips, the explicit-watch override that prevents auto-downgrade, and the SSE subscriber/broadcast substrate. The full Slices 1–6 test suite is 62/62 green. The schema needed one small migration — [`008_email_opt_out.sql`](../backend/migrations/008_email_opt_out.sql) adds the `email_opt_out_all` column to `users` for the bounce webhook. Topic 13 settled the rest of the §5 surface before the build started, so no further migrations were needed. ### Slice 7 — shipped The §14 chrome, the §15 settings neighborhood, and the §6/§17 admin home base — three surfaces over existing infrastructure. §14.1's pre-login landing carries the title, the subtitle, the short-form pitch from [`PHILOSOPHY.md`](../PHILOSOPHY.md), the sign-in affordance, and a three-item deck that names what the framework is. §14.2's `/philosophy` route reads `PHILOSOPHY.md` through [`backend/app/philosophy.py`](../backend/app/philosophy.py) (disk-backed, configurable via `PHILOSOPHY_PATH`; defaults to the file at the project root) and renders with `marked`. §14.3's persistent About link sits in the header alongside Settings (open to everyone) and Admin (owner/admin only); the chrome's visual budget stays tight per §14.4. The notification-settings surface lives at `/settings/notifications` ([`NotificationSettings.jsx`](../frontend/src/components/NotificationSettings.jsx)) and is what the §15.4 email footer's `Manage all preferences` link resolves to. Five sub-sections: the §15.4 per-category toggles (with the `email_watched_churn` toggle permanently disabled and the §15.4 refusal tooltip inline — naming the refusal is what keeps the contract honest), the §15.5 digest cadence dropdown, the §15.8 quiet-hours editor (three inputs against `Intl.supportedValuesOf('timeZone')` with all-three-or-clear validation server-side), the §15.6 watches overview, and the §15.8 per-user mute list with a typeahead add. Owners and admins see the "cannot mute" prose inline per §15.8. The admin home base lives at `/admin` ([`Admin.jsx`](../frontend/src/components/Admin.jsx)) as a four- tab left-rail: Users (role management + §6.2 write-mute), Graduation queue (§13.2-ready partition), Audit log (paged `actions` with filter chips), and Permission events (paged `permission_events`). Role-grant constraints land server-side in [`backend/app/api_admin.py`](../backend/app/api_admin.py) per §6.1 — only owners may grant `owner`; owners cannot self-demote on the role endpoint (the explicit succession path is a §19.2 candidate). The §6.2 write-mute applies only to contributors; admins and owners are not write-mutable. Every role and mute change writes a `permission_events` row joined to `users` for the surface. | Method | Path | § | | ------ | --------------------------------------------- | ------- | | GET | `/api/philosophy` | §14.2 | | GET | `/api/admin/users` | §6.1 | | POST | `/api/admin/users/{id}/role` | §6.1 | | POST | `/api/admin/users/{id}/mute` | §6.2 | | GET | `/api/admin/audit` | §6.5 | | GET | `/api/admin/permission-events` | §6.5 | | GET | `/api/admin/graduation-queue` | §13.2 | | GET | `/api/users/me/notification-mutes` | §15.8 | | GET | `/api/users/search` | §15.8 | Slice 7 ships covered by [`backend/tests/test_chrome_vertical.py`](../backend/tests/test_chrome_vertical.py) — thirteen integration tests against the FakeGitea, covering the philosophy route for anonymous and authenticated callers, the §15.4 / §15.5 / §15.8 preferences round-trip (including the permanent `email_watched_churn` refusal), the quiet-hours all-or-nothing validation, the §15.8 mute add/list/unmute round-trip, the user-search typeahead, the admin role and write-mute round-trips with their `permission_events` audit, the §6.1 refusal of owner-grant by non-owners, the audit-log filter chips, the graduation-queue partition under both preconditions, and the permission-events listing. The full Slices 1–7 test suite is 75/75 green. No schema migrations. The two surface-facing spec corrections — §14.2 (PHILOSOPHY.md source is a deployment-time decision; v1 reads from the app repo) and the §17 admin block (extended to name the seven new endpoints) — are the only places the slice's running code asked the spec to be more honest than it was. ### Slice 5 — shipped Graduation per §13 in full. The §13.3 five-step transactional sequence flips a super-draft to active: create the per-RFC repo, seed `RFC.md` / `README.md` / `.rfc/metadata.yaml`, open a meta-repo PR that strips the entry's body and fills the graduation frontmatter (`state: active`, `id: RFC-NNNN`, `repo`, `graduated_at`, `graduated_by`), auto-merge that PR with the admin as merge actor, refresh the cache so the catalog row and the new RFC view reflect `active` immediately. Each step goes through a new bot primitive — `create_rfc_repo_for_graduation`, `seed_graduated_rfc`, `open_graduation_pr`, `merge_graduation_pr` — that records its own row in `actions`, bracketed by `graduate_start` and `graduate_complete` for the linkable sequence the §13.3 audit shape calls for. The orchestrator in [`backend/app/api_graduation.py`](../backend/app/api_graduation.py) runs the sequence as an asyncio task fed by an in-memory queue; the §17 SSE endpoint subscribes to that queue and emits one event per step transition, plus the trailing rollback step's events if any earlier step fails. Rollback is per-step and runs in reverse. Each forward step has a paired undo registered in `_UNDO_BY_STEP`: `create_repo` → delete the repo, `seed_files` → folded into the repo deletion (the seed commits live inside the same repo), `open_pr` → close the graduation PR. There is no `merge_pr` undo by design — once the meta-repo merge has landed, graduation is irreversible per §13.5; the path forward is `withdraw` via §3. The rollback also records `graduate_rollback` in `actions` with the failed-at step name, the error, and the list of undone steps, so the failure surface in the dialog and the `actions` log carry the same record. The §9.8 precondition gate — open body-edit PRs against `rfcs/.md` would attempt to re-introduce a body to a frontmatter-only entry after step 3 — is enforced before the bot starts the sequence, so the §13.3 rollback complexity does not grow. The check runs both client-side as the dialog probes `GET /api/rfcs//blocking-prs` and server-side at the top of `POST .../graduate` as an atomic re-check. §13.4 chat migration is a database semantic no-op. The whole-doc main thread on the super-draft (`rfc_slug=`, `branch_name='main'`) is the same row interpreted as the super-draft's canonical-body thread before graduation and as the new RFC's main thread after — the slug is the canonical key per §2.3, the branch_name 'main' now points at the per-RFC repo's main, no data movement is needed. Range and paragraph sub-threads on the canonical-body view migrate the same way per §9.8. Edit-branch chats stay attached to their original `branch_name` on the meta repo per §9.8's "no data movement" framing; the §9.8 pre-graduation history affordance on the new RFC view surfaces them as a distinct disclosure in the breadcrumb dropdown. The §13.1 claim flow landed alongside graduation since claiming is the prerequisite for non-admin graduation. The bot grew `open_claim_pr`; the existing `api_prs` merge endpoint broadened to accept `pr_kind='meta_claim'` so the merge surface inherits structurally from §10. Until §13.1's claim runs, the dialog refuses the start when `owners=[]` and the popover surfaces "Claim ownership yourself" as a remediation affordance — admins are contributors per §6.1 and can claim solo if they intend to graduate without further ceremony. The five §17 routes Slice 5 added: | Method | Path | § | | ------ | ----------------------------------------------- | ------- | | POST | `/api/rfcs/{slug}/claim` | §13.1 | | GET | `/api/rfcs/{slug}/blocking-prs` | §13.2 | | GET | `/api/rfcs/{slug}/graduate/check` | §13.2 | | POST | `/api/rfcs/{slug}/graduate` | §13.3 | | GET | `/api/rfcs/{slug}/graduate/progress` | §13.3 | On the frontend, `RFCView.jsx`'s breadcrumb actions grew a `Graduate to RFC repo` button (admins/owners and entry owners) and a `Claim ownership` button (signed-in non-owners). `GraduateDialog.jsx` owns the three-field surface with debounced `/check` polling, the precondition popover backed by `/blocking-prs`, and the live step stack fed by an `EventSource` on the progress SSE. The `BranchDropdown` gains a `Pre-graduation history (N)` disclosure that surfaces edit-branch threads on the new RFC view per §9.8. Slice 5 ships covered by [`backend/tests/test_graduation_vertical.py`](../backend/tests/test_graduation_vertical.py) — ten integration tests against the FakeGitea (extended with `DELETE /repos/{owner}/{repo}` for the rollback inverse). The tests cover the dialog validator's per-field checks, the no-owners refusal, the §9.8 open-body-edit-PR precondition refusing the start, the §13.3 happy path end-to-end (with audit-log verification), mid-sequence rollback at step 2 (seed) and step 3 (PR open), the concurrent-graduation refusal, §13.4's chat-row-survives-without- data-movement contract, the §9.8 pre-graduation history surface, and the §13.1 claim PR cycle. The full Slices 1–5 test suite is 45/45 green. The orchestrator's `?_sync=1` test seam on `POST .../graduate` awaits the sequence inline so integration tests can assert post-conditions without driving the SSE. Production clients use the spec-described shape — POST returns immediately and the client subscribes to the progress SSE. ### Slice 4 — shipped Super-draft body editing per §9.5 + §9.6 + §9.7. The §17 routing-collapse rule landed in `backend/app/api_branches.py` and `backend/app/api_prs.py` — every `branches//...` and `prs//...` route now dispatches on the entry's state to pick the right Gitea repo, and the body extracted from the entry's frontmatter envelope is what the editor and the diff see. The bot wrapper grew `open_metadata_pr`; the rest of the bot's methods already accepted owner/repo arguments and worked against the meta repo without change. The §4 cache learned about meta-repo edit branches via a new `refresh_meta_branches` pass that mirrors `edit--<6hex>` branches into `cached_branches` and synthesizes a per-slug `main` row so the §10.1 has-commits-ahead check works uniformly across active and super-draft surfaces. The §5 schema needed no migration — the super-draft scoping note already settled that the existing tables carry both cases. The two §17 routes Slice 4 added: | Method | Path | § | | ------ | -------------------------------------- | ------- | | POST | `/api/rfcs/{slug}/start-edit-branch` | §9.5 | | POST | `/api/rfcs/{slug}/metadata` | §9.5 | Everything else from the §8 vertical (chat, accept, decline, manual flush, threads, flags, visibility, grants, the SSE chat stream) and the §10 PR flow (open, draft, review, merge, withdraw, conflict-replay) reaches super-drafts through the same routes Slices 2 and 3 shipped — no per-state forks at the API surface. The branch-naming choice: §9.5 names the structural shape `edit//`, but FastAPI's default `{branch}` path matcher refuses slashes (the §19.2 path-routing candidate). Slice 4 picked `edit--<6hex>` — same dash-separated shape Slice 2 used for `-draft-<6hex>`. Metadata-pane PRs use the parallel `metadata--<6hex>` form. The cache parsers in `app/cache.py` recognize both the dashed and slashed prefixes so a future routing-fix slice can flip back without a data migration. On the frontend, `RFCView.jsx`'s super-draft placeholder was replaced by the full editor surface — same component, dispatched on `entry.state`. The `BranchDropdown` renders `canonical body` as the first position when the entry is a super-draft, per §9.4. A new `MetadataPaneModal` opens from the breadcrumb actions when the viewer holds super-draft edit authority per §9.5 (until §13.1's claim runs, that's app admins/owners only). Slice 4 ships covered by `backend/tests/test_super_draft_vertical.py` — ten integration tests against the FakeGitea, covering main-view read, start-edit-branch, body extraction from the envelope on read, accept preserving the frontmatter on write, manual flush through the envelope, the body-edit PR's `pr_kind='meta_body_edit'` shape, the full cut-accept-open-merge loop with the §9.5 unclaimed-merge gate (admin/owner only), the metadata pane PR cycle, the canonical-body branch (`main` for super-drafts) being read-only, and the metadata pane permission gate. ### What's deferred from Slice 2 These were in the §8 spec but lean on infrastructure later slices build, so they were scoped out of this slice without altering the spec: - **Super-draft body editing on the meta repo (§9.5).** The `branches/` machinery is structurally general enough that meta-repo edit branches fall out of it once Slice 4 wires the super-draft view's "Start Contributing" gesture to cut against the meta repo. The Slice 2 RFCView renders a placeholder for super-draft entries pointing at Slice 4. - **The §10.4 review threads on PRs.** `thread_kind='review'` is in the schema and the threads endpoints honor it generically, but the PR-page surface where review threads anchor to diff hunks lands with Slice 3. - **DiffView's full reconstruction from `changes` history.** Slice 2 renders the editor's current HTML (which carries the session-local tracked-change markup from the accepts that happened in this session) into DiffView; rebuilding the full accepted-change markup from `changes` for a returning contributor needs a render pipeline DiffView doesn't yet own. The current behavior matches §8.10's "session-local" framing exactly; the §19.2 "persistent accepted-change markup" topic is the durable extension when evidence demands it. - **The §10.6 PR-side commit / chat reconciliation.** Manual-edit flushes drop a system-author message into branch chat per §10.6 in Slice 2, but the PR-side seen-cursor that uses the marker ships with Slice 3. - **Branch-name path conversion for slashes.** The auto-generated branch name in Slice 2 is `-draft-` (no slash) so the FastAPI `{branch}` path segment matches without `{branch:path}`. Users can still rename to a slashed name, but the routes will 404 on read; the proper fix is `{branch:path}` everywhere, which lands cleanly when Slice 3 makes the same change to the PR routes (PR numbers don't have this problem, but resolving the routing shape once across both surfaces is the right hop). ## Environment notes - **Python 3.13.** Earlier 3.11+ should also work; 3.13 is what the build session ran on. - **Node 20+** for the frontend. - **Local Gitea on port 3000.** Anything that exposes the Gitea v1 REST API works. If you tunnel Gitea elsewhere (e.g. a container, a Codespace), re-run `scripts/seed_meta_repo.py` so the webhook re-registers against the right `APP_URL`. ## Conventions - **Bot writes only via `app/bot.py`.** If a module wants to call `app/gitea.py`'s write methods directly, the spec is right and the module is wrong — the wrapper is the chokepoint that makes the §6.5 `On-behalf-of:` trailer and the §6 authorization both consistent. - **Cache writes only from `app/cache.py`.** User actions trigger Git operations via the bot; the cache learns about them when the webhook arrives (or the next reconciler sweep), and never before. This invariant is what makes §4's "Git is truth" claim hold operationally. - **Spec corrections during the build are rare and surgical.** When running code reveals the spec was wrong at a structural level (per §19.3's working agreement), the correction lands in the appropriate numbered section with a brief note explaining what running code revealed. Spec extensions during the build are not in scope — they accumulate in §19.2. - **§16 stays deferred.** Body full-text search, per-RFC model picker, funder role, persistent accepted-change markup, slug renames — these are not shipped in any slice. They earn their own topic sessions when use surfaces evidence they matter. ## Next slice **Slice 8: hardening — the last slice of the v1 build.** With Slice 7 shipped, every structural beat the spec commits to is live and every surface the framework exposes has chrome around it. What remains is the hardening pass that lets a single-operator deployment actually run end-to-end without hand-holding. Three pieces hang together: - **The §12 30/90 branch-hygiene timers.** §11.5 names the branch lifecycle (open → merged → 30d read-only → 90d deleted-by-bot, with the per-user message-cursor preservation contract); §12 formalizes the policy. The wiring is a scheduled task next to the existing `DigestScheduler` — same `run_tick` test-seam shape. The §10.7 90-day deletion timer Slice 3 left explicitly deferred lives here too. Touches `cache.Reconciler` (the natural place to fire the hygiene sweep), `bot.delete_branch` (the §12 actuator, not yet exercised), and the §19.2 cache-bootstrap topic if the hygiene sweep also rebuilds branch state from Gitea after a cache wipe. - **An end-to-end smoke pass** over the working surfaces. Propose → super-draft → branch → PR → merge → graduate → active-RFC PR → notification → inbox → email — one or two `test_e2e_smoke.py` cases that exercise the seams a per-slice test wouldn't. Plus the §19.2 follow-ons the hardening pass is the natural place to fold in: branch-name path routing (`{branch:path}` everywhere with route-ordering discipline), cache bootstrap from a pre-existing meta repo (the audit-log-first attribution shape exercised against history the bot did not author), in-app merge for metadata PRs, the graduation rollback's branch cleanup, and the small Slice-2-onward follow-ons that are deferred until the hardening pass demands them. - **The dev/prod deployment shape.** `deploy/` already carries an nginx vhost, a systemd unit, and a runbook stub. Slice 8 proves the bring-up against a fresh host, settles the secret-material handling (the existing `.env.example` plus the §15.4 SMTP wiring), wires the §6 / §15.4 SMTP credentials, and lands the README updates that take a new operator from `git clone` to a signed-in browser. What Slice 8 does NOT own: - New surfaces. The v1 surface is complete; the hardening pass is about making what's there resilient, observable, and operable. - The §16 deferred items. - The §19.2 candidate set as a whole — the hardening pass folds in the candidates that naturally cluster with hygiene timers, cache rebuild, and deployment; the rest stay queued for post-v1 sessions. The carryovers Slice 8 inherits — the full §11.5 / §12 spec text, the existing `cache.Reconciler` and `DigestScheduler` shape, the deploy/ infrastructure, and the 75/75 green test suite. The next build session should read `SPEC.md`, `README.md`, `docs/DEV.md`, and `SPEC.md`'s §19.1 and pick up Slice 8 cleanly without re-briefing. The working agreement in §19.3 continues to apply: implement the slice, correct the spec only where running code reveals it was wrong at a structural level, accumulate new candidate topics in §19.2, do not extend the spec beyond what the slice requires.