Drop "prototype/carryover" framing now that v1 is shipped

SPEC, DEV docs, and code comments still talked about the codebase as
a rewrite-in-progress against an external prototype. With v1 shipped
the framing reads oddly — it implies code is provisional when it's
the production thing. Recast §18 as "the technical stack," strip
"carryover from the prototype" comments across backend (api.py,
chat.py, providers.py) and frontend (DiffView, PromptBar,
SelectionTooltip, modelStyles), and rework SPEC §1 / §18 to introduce
OHM up front rather than as a follow-on to a prototype reference.

Also:
- RUNBOOK: bump Python prereq to 3.11+ to match the production VM
  (was 3.13).
- Remove IMPLEMENTATION-PROMPT.md — the original implementation brief
  is no longer load-bearing.
- Add deploy/DEPLOY-NEW-SESSION-PROMPT.md as the durable
  deploy-handoff prompt for new sessions.
This commit is contained in:
Ben Stull
2026-05-25 10:32:46 -07:00
parent 7c3b8fc133
commit ee6e3491e7
12 changed files with 411 additions and 155 deletions
-62
View File
@@ -1,62 +0,0 @@
You're starting the implementation phase of the Wiggleverse RFC framework — a standardization process for the natural-language vocabulary that digital representations of humans, and the systems that interact with them, need to share. Thirteen settlement sessions have produced SPEC.md, which captures every structural decision v1 needs. Per §19.1, the spec is implementation-ready, and this session is the first build session.
The application is a single-process FastAPI + SQLite + React + Vite + Tiptap (ProseMirror) app running against a Gitea instance. The app's bot service account performs all Git operations on behalf of users authenticated via Gitea OAuth; per §1, the bot is the only Git writer. Single-operator deployment shape (one Python process colocated with one SQLite file). The deliverable across all build sessions is the materialization of §§115 end-to-end; this first session establishes the spine and a single working vertical slice.
Before writing any code:
1. Read SPEC.md end to end. It is the binding contract. Every load-bearing decision is in there, and the implementation does not get to second-guess them. Pay particular attention to:
* §1 (the bot-as-only-actor framing and what it implies for the wrapper layer)
* §4 (the cache architecture — webhook + reconciler, both writing to the cache; user actions never write the cache directly)
* §5 (the canonical app tables — implement these with SQLite-appropriate column types and the appropriate FK / cascade rules)
* §6 (the four-role permission model and per-RFC delegated authority; this is enforced in the app, not in Gitea)
* §18 (the carryovers — these are confirmed unchanged from the prototype and should be re-used, not rewritten)
* §19.3 (the working agreement for the build phase)
2. Read PHILOSOPHY.md. The spec's decisions answer to it. Where the spec's letter is silent, the philosophy's spirit decides — the transcript is the evidence, public-async work is the default, the system is a tool for thought rather than a feed.
3. Glance at /Users/benstull/projects/wiggleverse/rfc-app-prototype/ — *reference only*. The §18 carryovers (FastAPI + SSE, Tiptap + ProseMirror, the `<change>` / `<original>` / `<proposed>` / `<reason>` protocol, multi-provider LLM support, Gitea OAuth) have working code in the prototype; lift these directly where possible. Everything else in the prototype is superseded by the spec. The prototype's CSS, component decomposition, prompt-bar machinery, and DiffView rendering are likely re-usable; the prototype's data model, permission shape, and notification absence are not.
Where to build: /Users/benstull/git/rfc-app/ is the project root. SPEC.md and PHILOSOPHY.md live at the root as content (versioned alongside the code). The implementation lives in subdirectories — backend/ for the FastAPI app and its SQLite migrations, frontend/ for the React + Vite + Tiptap app, docs/ for any operational documentation.
Sequencing — this session establishes the spine and one working vertical slice, not every flow at once. The right slice order across all build sessions is:
* **Slice 1 (this session):** repository scaffolding (backend/, frontend/, dev-environment script, a README explaining local bring-up); the full §5 schema as a migrations directory with appropriate indexes and FK rules; Gitea OAuth per §18 + user provisioning into `users`; the bot client wrapper that every Git operation flows through with the §6.5 `On-behalf-of:` trailer applied; the §4.1 webhook receiver and reconciler both writing to the metadata cache; a minimal left pane per §7 (catalog list, sort + filter chips + search, the "Pending ideas" disclosure); and *one end-to-end vertical*: propose a new RFC per §9.1 → idea PR opens against the meta repo → owner merges → super-draft appears in the catalog → super-draft view opens and renders the body. That slice exercises the bot wrapper, the cache, the permission model, the catalog, and the super-draft view in one path, which is the right shape for a first build session.
* **Slice 2:** the RFC view for active RFCs per §8 — the editor, branch creation, per-branch chat, AI participation, the change-card panel, accept/decline/edit, manual-edit flushes, sub-threads, flags, DiffView.
* **Slice 3:** the PR flow per §10 — opening, review surface, the seen-cursor mechanism, review threads, merge, post-merge, conflict resolution.
* **Slice 4:** super-draft body editing per §9.5 + §9.6 — meta-repo edit branches as the unit of work.
* **Slice 5:** graduation per §13 — the dialog, the transactional sequence, rollback, the pre-graduation history affordance.
* **Slice 6:** notifications per §15 — last, because every other surface produces signals that the notification surface receives, and notification correctness depends on those producers being in place first.
* **Slice 7:** the §14 chrome — landing page, /philosophy route, About link.
* **Slice 8:** hardening — end-to-end tests, dev/prod deployment shape, the 30/90 branch hygiene timers from §12.
Working agreement (modified from §19.3 for the build phase):
1. Implement the slice. The spec is the source of truth. The build does not extend it.
2. Where running code reveals the spec was wrong or underspecified at an *implementation-detail* level (the explicit "exact value is an implementation detail" callouts in §8.6, §8.11, §8.13, §8.15, etc.), make the decision and document it inline in code with a comment referencing the §. Do not amend the spec.
3. Where running code reveals the spec was wrong or underspecified at a *structural* level — a section that contradicts another, a decision that prevents a downstream flow from working, an assertion that breaks under real use — correct the spec in the appropriate numbered section with a brief note explaining what running code revealed. Match the spec's voice: declarative, structural, em-dashes for asides, prose rather than bullets where bullets don't earn their keep. Spec corrections during the build should be rare and surgical, not expansive.
4. Where running code surfaces a new design question that isn't resolvable as an implementation detail, add it to §19.2 as a new candidate topic in the existing shape. Don't try to settle it inline; that's what topic sessions are for.
5. The build does not add features that aren't in the spec. If a "wouldn't it be nice" comes up, add it to §19.2 and move on.
Practical guardrails:
* Prefer the simplest thing that materializes the spec. The spec is intentionally specific about what to build and intentionally silent about how to build it. Use the silence.
* Accept §16 (deferred items) as deferred. Don't ship body full-text search, the per-RFC model picker, the funder role, persistent accepted-change markup, slug renames, or any other §16 / §19.2 candidate.
* The §18 carryovers should be re-used from the prototype where they can be, not rewritten. SSE streaming, Tiptap configuration, the `<change>` parser, the prompt-bar selection-quote machinery — these work in the prototype and the spec confirms them unchanged.
* The bot service account is the only Git writer. Every Git operation in the codebase goes through one wrapper. If you find yourself wanting to bypass it, the spec is right and you are wrong — the wrapper is the chokepoint that makes the on-behalf-of trailer and the app-level authorization both consistent.
* The cache is never written from a user action. User actions trigger Git operations via the bot wrapper; the resulting Gitea webhook (or the reconciler) is what writes the cache. This invariant is what makes §4's "Git is truth" claim hold operationally.
* The app-wide write-mute (§6.2) and the per-RFC / per-user notification mutes (§15.8) are structurally distinct and live in separate columns. They share a word and nothing else. Don't conflate them in code any more than the spec conflates them in prose.
What to deliver from this session:
* The slice you actually got through, working end-to-end against a local Gitea instance.
* A README at /Users/benstull/git/rfc-app/README.md explaining how to bring the app up locally — Gitea setup, OAuth app registration, env vars, run commands for backend and frontend, how to seed the meta repo.
* docs/DEV.md (or similar) with the slicing plan you're working against, what's done, what's next, any environment-specific gotchas.
* Any spec corrections per rule 3 above, in their proper numbered sections.
* §19.2 updated with any new candidate topics that surfaced during the build.
What to update for the session after this one:
* §19.1 should be rewritten to name the next slice the build needs (e.g., "Slice 2: the RFC view and per-branch chat — §8 in full"), with a brief paragraph summarizing what was completed in this session and what state the codebase is in for the next session to pick up.
* The handoff is the prompt for the next build session. Preserve enough context that a fresh agent can read SPEC.md, README.md, docs/DEV.md, and §19.1 and pick up cleanly without needing to be re-briefed.
Voice note: READMEs, dev docs, and any spec corrections should match the spec's voice. Declarative, structural, em-dashes, prose where prose works. The framework is making a claim about how words behave; the documentation should sound like it believes the claim.
+37 -40
View File
@@ -1,24 +1,23 @@
# RFC App — Specification # RFC App — Specification
This app hosts the **Open Human Model (OHM)**, the corpus of RFCs the This is the agreed-upon model for the Wiggleverse RFC Contributor app —
Wiggleverse framework produces. Each RFC defines one word; the first the host for the **Open Human Model (OHM)**, the corpus of RFCs the
defines *human*. OHM is English-first by design: the markdown bodies framework produces. Each RFC defines one word; the first defines
are canonical, and the OpenXML APIs and UX surfaces a downstream *human*. OHM is English-first by design: the markdown bodies are
system needs to actually let humans and machines interact are derived canonical, and the OpenXML APIs and UX surfaces a downstream system
from that English source, not authored alongside it. The framework's needs to actually let humans and machines interact are derived from
*why* lives in [`PHILOSOPHY.md`](./PHILOSOPHY.md); this document is that English source, not authored alongside it. The framework's *why*
the binding *what* for the app that hosts OHM. lives in [`PHILOSOPHY.md`](./PHILOSOPHY.md); this document is the
binding *what* for the app that hosts OHM.
This is the agreed-upon model for the rewrite of the Wiggleverse RFC Contributor app.
It captures the structural decisions made before any UX work on the main document It captures the structural decisions made before any UX work on the main document
pane, per-RFC conversations, revisions, and PRs. Those areas are deliberately pane, per-RFC conversations, revisions, and PRs. Those areas are deliberately
out of scope here and will be designed in a follow-up session that takes this out of scope here and will be designed in a follow-up session that takes this
spec plus the existing prototype as context. spec as context.
The technical stack is unchanged from the prototype: FastAPI with SSE streaming The technical stack: FastAPI with SSE streaming on the backend, React + Vite +
on the backend, React + Vite + Tiptap (ProseMirror) on the frontend, Gitea as Tiptap (ProseMirror) on the frontend, Gitea as the Git host, multiple LLM
the Git host, multiple LLM providers (Anthropic, Google, OpenAI / GitHub providers (Anthropic, Google, OpenAI / GitHub Copilot).
Copilot). The code base is a clean rewrite; the existing app is reference only.
--- ---
@@ -112,9 +111,9 @@ conflict-free even with concurrent proposers.
Integer `RFC-NNNN` IDs are assigned at graduation time as `max(existing Integer `RFC-NNNN` IDs are assigned at graduation time as `max(existing
integer IDs) + 1`. Super-drafts have no integer ID; their stable identifier integer IDs) + 1`. Super-drafts have no integer ID; their stable identifier
is the slug. This avoids the race condition the prototype has (where two is the slug. This avoids the race condition where two concurrent
concurrent submissions can try to claim the same number) and means submissions could try to claim the same number, and means proposing
proposing an idea costs nothing in identifier space. an idea costs nothing in identifier space.
--- ---
@@ -265,8 +264,8 @@ and exact columns are illustrative; the implementing session can adjust.
PRs per §10.6. PRs per §10.6.
- `changes` — structured proposed edits to a branch's document. Each - `changes` — structured proposed edits to a branch's document. Each
change is either AI-proposed (from a `<change>` block in an assistant change is either AI-proposed (from a `<change>` block in an assistant
message, per the protocol carried over from the prototype, §18) or message, per the §18 protocol) or manually authored (typed directly
manually authored (typed directly into the editor). Columns: `id`, into the editor). Columns: `id`,
`rfc_slug`, `branch_name`, `thread_id` (nullable; null for direct `rfc_slug`, `branch_name`, `thread_id` (nullable; null for direct
manual edits not tied to a thread), `source_message_id` (nullable; manual edits not tied to a thread), `source_message_id` (nullable;
set for AI-proposed changes), `kind` (`ai` | `manual`), `state` set for AI-proposed changes), `kind` (`ai` | `manual`), `state`
@@ -278,7 +277,7 @@ and exact columns are illustrative; the implementing session can adjust.
contributor acts on the stale card), `acted_by`, `acted_at`, contributor acts on the stale card), `acted_by`, `acted_at`,
`commit_sha` (nullable; the bot commit that materialized the `commit_sha` (nullable; the bot commit that materialized the
acceptance, see §8.6). The `<change>` / `<original>` / `<proposed>` acceptance, see §8.6). The `<change>` / `<original>` / `<proposed>`
/ `<reason>` parsing protocol is unchanged from the prototype. / `<reason>` parsing protocol is specified in §18.
- `pr_seen` — per-user, per-PR seen-cursor for the "what changed since - `pr_seen` — per-user, per-PR seen-cursor for the "what changed since
my last visit" mechanism in §10.3. Columns: `id`, `user_id`, `rfc_slug`, my last visit" mechanism in §10.3. Columns: `id`, `user_id`, `rfc_slug`,
`pr_number`, `last_seen_commit_sha`, `last_seen_message_id`, `seen_at`. `pr_number`, `last_seen_commit_sha`, `last_seen_message_id`, `seen_at`.
@@ -448,8 +447,8 @@ universe when the field is absent). Where a deterministic single
model must be chosen and the contributor has not picked one — the model must be chosen and the contributor has not picked one — the
§10.2 PR-draft, the §9.1 tag suggestions, the §8.13 "Ask Claude to §10.2 PR-draft, the §9.1 tag suggestions, the §8.13 "Ask Claude to
propose a fix" invocation from an empty thread — that's the model propose a fix" invocation from an empty thread — that's the model
used. Per-message picker grain inside a chat thread (the §18 used. Per-message picker grain inside a chat thread (per §18) is
prototype carryover) is preserved: each message can name a different preserved: each message can name a different
model from the resolved list, and the picker's currently-selected model from the resolved list, and the picker's currently-selected
entry persists across messages within a session. entry persists across messages within a session.
@@ -632,7 +631,7 @@ between branches.
### 8.1 Layout ### 8.1 Layout
The RFC view inherits the three-column shape from the prototype: The RFC view uses a three-column shape:
- **Left column** — the RFC catalog from §7, unchanged. - **Left column** — the RFC catalog from §7, unchanged.
- **Center column** — a thin breadcrumb strip at the top showing the - **Center column** — a thin breadcrumb strip at the top showing the
@@ -662,8 +661,8 @@ that branch's thread.
### 8.3 Discuss vs. contribute mode (branch-scoped) ### 8.3 Discuss vs. contribute mode (branch-scoped)
The prototype's discuss-vs-contribute distinction survives, but is The discuss-vs-contribute distinction is scoped to the current
scoped to the current branch rather than global. branch rather than global.
- **Discuss mode** is the default on any branch (including main). - **Discuss mode** is the default on any branch (including main).
The editor is read-only. The chat is enabled (subject to §6's The editor is read-only. The chat is enabled (subject to §6's
@@ -856,7 +855,7 @@ DiffView is the durable artifact for inspecting accepted changes in
context. context.
DiffView is the read-only render surface invoked via a toolbar DiffView is the read-only render surface invoked via a toolbar
toggle, carried over from the prototype per §8.15. It reads from the toggle (§8.15). It reads from the
`changes` table for the branch, reconstructs the markup for every `changes` table for the branch, reconstructs the markup for every
accepted change in branch history, and renders the result in-place accepted change in branch history, and renders the result in-place
where the editor was. Hovering any marked span surfaces a tooltip where the editor was. Hovering any marked span surfaces a tooltip
@@ -944,8 +943,8 @@ the scroll-to-editor binding. Resolved and stale threads stay in the
data; the chat feed's filter affordances surface or hide them on data; the chat feed's filter affordances surface or hide them on
demand. demand.
The chat's model picker (the §18 prototype carryover) draws its The chat's model picker (§18) draws its option list from the
option list from the resolved per-RFC set per §6.6 — the resolved per-RFC set per §6.6 — the
intersection of the entry's optional `models:` frontmatter (or the intersection of the entry's optional `models:` frontmatter (or the
operator universe when absent) with the operator's currently operator universe when absent) with the operator's currently
provisioned providers. An RFC whose resolved list is empty surfaces provisioned providers. An RFC whose resolved list is empty surfaces
@@ -1032,9 +1031,9 @@ surface — under the data model, every AI proposal is a first-class
artifact from the moment it lands, on whatever branch the artifact from the moment it lands, on whatever branch the
conversation produced it on. conversation produced it on.
### 8.15 Carryovers and implementation-deferred details ### 8.15 Branch-scoped affordances and implementation-deferred details
These prototype affordances carry over with branch-scoped behavior: These affordances are scoped to the current branch:
the selection tooltip (elaborated in §8.12 as the range-thread the selection tooltip (elaborated in §8.12 as the range-thread
entry point); the review-mode toggle and `DiffView` for inspecting entry point); the review-mode toggle and `DiffView` for inspecting
accepted changes in context (§8.10); the discuss-mode banner accepted changes in context (§8.10); the discuss-mode banner
@@ -2376,10 +2375,10 @@ specified* and what is intentionally out of scope for v1.
renders, the Tiptap editor configuration, how the editor handles renders, the Tiptap editor configuration, how the editor handles
rich markdown, headings, links, code blocks. rich markdown, headings, links, code blocks.
- **The per-RFC and per-branch chat UX.** Threading model, AI - **The per-RFC and per-branch chat UX.** Threading model, AI
participation, the discuss-vs-contribute mode distinction from the participation, the discuss-vs-contribute mode distinction, the
prototype, the selection tooltip, the prompt bar, the model picker selection tooltip, the prompt bar, the model picker chrome (its
chrome (its option-list source is settled in §6.6 / §8.12; the option-list source is settled in §6.6 / §8.12; the visual treatment
visual treatment and per-thread persistence remain). and per-thread persistence remain).
- **The revision flow.** How proposed changes from AI or contributors - **The revision flow.** How proposed changes from AI or contributors
appear, the change-card panel, accept/decline/edit, tracked appear, the change-card panel, accept/decline/edit, tracked
insertions/deletions in the editor. insertions/deletions in the editor.
@@ -2609,8 +2608,8 @@ The follow-up session will refine this. A minimal starting set:
- `POST /api/webhooks/email-bounce` — bounce and complaint receiver - `POST /api/webhooks/email-bounce` — bounce and complaint receiver
per §15.4; sets the recipient's global email opt-out. per §15.4; sets the recipient's global email opt-out.
Plus all the chat / streaming / model-picker endpoints inherited from Plus all the chat / streaming / model-picker endpoints, scoped to
the prototype, scoped to per-RFC and per-branch threads. per-RFC and per-branch threads.
The `branches/<branch>/...` endpoint family covers both per-RFC-repo The `branches/<branch>/...` endpoint family covers both per-RFC-repo
branches and meta-repo edit branches; per §9.5, the routing collapses branches and meta-repo edit branches; per §9.5, the routing collapses
@@ -2621,11 +2620,9 @@ right Gitea repo; the API surface is the same.
--- ---
## 18. Carryover from the prototype ## 18. Technical stack
These are confirmed unchanged from the existing app and should be The stack the app is built on:
preserved as-is in the rewrite unless a downstream decision changes
them:
- FastAPI + SSE for streaming chat. - FastAPI + SSE for streaming chat.
- React + Vite + Tiptap (ProseMirror) for the editor. - React + Vite + Tiptap (ProseMirror) for the editor.
+1 -2
View File
@@ -94,8 +94,7 @@ def make_router(
return {"body": payload["body"]} return {"body": payload["body"]}
# --------------------------------------------------------------- # ---------------------------------------------------------------
# Auth surface — extends the prototype's pattern but reads role # Auth surface — reads role from our users table per §6.
# from our users table per §6.
# --------------------------------------------------------------- # ---------------------------------------------------------------
@router.get("/api/auth/me") @router.get("/api/auth/me")
+11 -14
View File
@@ -1,12 +1,11 @@
"""SSE-streaming chat layer — §18 carryover, adapted to the §5 schema. """SSE-streaming chat layer — §18 stack, applied to the §5 schema.
The prototype kept conversation state in an in-memory `RFCChat` Conversation history is the durable list of `thread_messages` rows on
keyed by a session_id. Here, history is the durable list of a `threads` row, scoped to one branch (or to a sub-thread anchored to
`thread_messages` rows on a `threads` row, scoped to one branch (or a range or paragraph within it). The streaming response is parsed for
to a sub-thread anchored to a range or paragraph within it). The `<change>` blocks per §18; each `<change>` becomes a `changes` row
streaming response is parsed for `<change>` blocks per §18; each with `state='pending'` per §8.14 the moment it is parsed, regardless
`<change>` becomes a `changes` row with `state='pending'` per §8.14 of mode.
the moment it is parsed, regardless of mode.
This module exposes two seams: This module exposes two seams:
- `build_history` and `build_system_prompt` pure functions a caller - `build_history` and `build_system_prompt` pure functions a caller
@@ -35,9 +34,8 @@ from .providers import BaseProvider
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# The §18 system prompt, adapted from the prototype. The prototype's # The §18 system prompt. The document loaded as context is the
# version assumed one RFC document loaded as context; here the document # branch's RFC.md at its current tip. The selection-quote shape
# is the branch's RFC.md at its current tip. The selection-quote shape
# (§8.12) is wired by the caller into the user message text — not the # (§8.12) is wired by the caller into the user message text — not the
# system prompt — so the model sees it as part of the turn. # system prompt — so the model sees it as part of the turn.
SYSTEM_PROMPT = """You are a participant in the Wiggleverse RFC framework — a standardization process for natural-language vocabulary that humans and machines need to share. You are collaborating with a human contributor on the RFC titled "{title}". SYSTEM_PROMPT = """You are a participant in the Wiggleverse RFC framework — a standardization process for natural-language vocabulary that humans and machines need to share. You are collaborating with a human contributor on the RFC titled "{title}".
@@ -316,9 +314,8 @@ async def stream_assistant_turn(
Provider streaming is synchronous (an `Iterator[str]`) per §18; we Provider streaming is synchronous (an `Iterator[str]`) per §18; we
drain it eagerly into chunks and yield them as async strings. This drain it eagerly into chunks and yield them as async strings. This
is sufficient at single-process scale (§4.2) and the streaming is sufficient at single-process scale (§4.2); re-wrapping it in a
impl is what the prototype shipped re-wrapping it in a worker worker thread for a future deployment shape is a one-liner if it
thread for a future deployment shape is a one-liner if it
matters. matters.
""" """
full_text_chunks: list[str] = [] full_text_chunks: list[str] = []
+6 -6
View File
@@ -1,10 +1,10 @@
"""Multi-provider LLM abstraction — §18 carryover from the prototype. """Multi-provider LLM abstraction — §18 stack.
Each provider speaks a common interface `send` and `send_streaming` Each provider speaks a common interface `send` and `send_streaming`
so the chat layer in `chat.py` is provider-agnostic. Enabled providers so the chat layer in `chat.py` is provider-agnostic. Enabled providers
and their API keys are configured via env per the prototype's and their API keys are configured via env via the `ENABLED_MODELS`
`ENABLED_MODELS` contract; per §16 / §19.2, per-RFC model availability contract; per §16 / §19.2, per-RFC model availability and credential
and credential delegation are deferred until the topic is settled. delegation are deferred until the topic is settled.
""" """
from __future__ import annotations from __future__ import annotations
@@ -130,7 +130,7 @@ class OpenAIProvider(BaseProvider):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Variants and factory — preserved from the prototype to keep the contract. # Variants and factory.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_CLAUDE_VARIANTS: dict[str, tuple[str, str]] = { _CLAUDE_VARIANTS: dict[str, tuple[str, str]] = {
@@ -149,7 +149,7 @@ _GEMINI_VARIANTS: dict[str, tuple[str, str]] = {
def load_providers(env: dict) -> dict[str, BaseProvider]: def load_providers(env: dict) -> dict[str, BaseProvider]:
"""Instantiate enabled providers from env — same contract as the prototype.""" """Instantiate enabled providers from env."""
enabled = [m.strip() for m in env.get("ENABLED_MODELS", "claude").split(",") if m.strip()] enabled = [m.strip() for m in env.get("ENABLED_MODELS", "claude").split(",") if m.strip()]
providers: dict[str, BaseProvider] = {} providers: dict[str, BaseProvider] = {}
+329
View File
@@ -0,0 +1,329 @@
# RFC App — Deployment Reference & New-Session Prompt
Use this document as:
1. A reference for the current `rfc.wiggleverse.org` deployment
2. A prompt to paste into a new Claude session to deploy a new version
---
## What This App Is
The **RFC App** is a single-process FastAPI + SQLite + React + Vite + Tiptap web application that hosts the Wiggleverse RFC framework — a platform for proposing, discussing, editing, and graduating formal RFCs (Requests for Comments) that define vocabulary for digital representations of humans. It is the primary interface for the Open Human Model (OHM) working group.
The v1 build is complete (8 slices shipped, 125 passing integration tests). New sessions extend it by picking from the §19.2 backlog.
---
## Infrastructure Overview
### Host
The RFC app runs on its own dedicated GCP VM in a separate project from the Gitea VM. The two coexist under `wiggleverse.org` but are otherwise unrelated infrastructure.
| Property | Value |
|----------|-------|
| GCP project | `wiggleverse-rfc` |
| VM name | `rfc-app` |
| VM type | e2-small |
| Zone | us-central1-a |
| OS | Debian 12 (bookworm) |
| Static IP | 34.132.29.41 |
| Linux user (OS Login) | `benstull` |
For reference, the separate Gitea VM is `wiggleverse` project / `gitea` VM / 34.55.46.221.
### DNS
| Record | Type | Value | Proxy |
|--------|------|-------|-------|
| `rfc.wiggleverse.org` | A | 34.132.29.41 | DNS-only (gray cloud) |
| `_dmarc.wiggleverse.org` | TXT | `v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org` | n/a |
> Note: `rfc.wiggleverse.org` uses **Let's Encrypt via certbot** directly on the VM. Keep the A record **DNS only (gray cloud)** — Cloudflare Flexible SSL would conflict with certbot.
SPF (`v=spf1 include:_spf.google.com ~all`) and DKIM (`google._domainkey`) for `wiggleverse.org` are already in place via Workspace.
### Software Stack
| Component | Details |
|-----------|---------|
| Backend | Python 3.11, FastAPI, uvicorn (single process) |
| Database | SQLite in WAL mode at `/opt/rfc-app/backend/data/rfc-app.db` |
| Frontend | React 19, Vite 8, Tiptap 3, React Router 7 |
| Web server | nginx — serves `frontend/dist/` as static SPA, proxies `/api/` and `/auth/` to uvicorn on `127.0.0.1:8000` |
| Process manager | systemd unit `rfc-app.service`, runs as `rfc-app` system user |
| TLS | Let's Encrypt via certbot |
| Git backend | Gitea at `git.wiggleverse.org`, bot service account `rfc-bot` |
| Email | Google Workspace SMTP relay (`smtp-relay.gmail.com:587`), AUTH'd as `ben@wiggleverse.org`, From `notifications@wiggleverse.org` |
### Key Paths on the VM
| Path | Contents |
|------|---------|
| `/opt/rfc-app/` | App root (owned by `rfc-app` user) |
| `/opt/rfc-app/backend/.env` | All secrets and config (mode 0600) |
| `/opt/rfc-app/backend/data/rfc-app.db` | SQLite database |
| `/opt/rfc-app/frontend/dist/` | Built React SPA (served by nginx) |
| `/etc/nginx/sites-available/rfc.wiggleverse.org` | nginx vhost config |
| `/etc/systemd/system/rfc-app.service` | systemd unit |
---
## Architecture Invariants
- **The bot is the only Git writer.** Every commit, branch, and PR flows through `backend/app/bot.py`. No module calls Gitea's write API directly. Every action is audited in the `actions` table with an `On-behalf-of:` commit trailer.
- **Git is truth; SQLite is the cache.** The `cached_*` tables are written only by the Gitea webhook receiver or the 5-minute background reconciler. User actions trigger Git ops; the cache follows.
- **Single process, single SQLite file.** Never set `--workers > 1` on uvicorn. If scale is needed, the spec calls for a Postgres migration first.
---
## Gitea Setup (one-time)
These are already done for `rfc.wiggleverse.org`. Document here for replication.
### Bot service account
Created in Gitea as `rfc-bot`. Token scopes: `write:repository`, `write:user`, `write:admin`. Token stored in `.env` as `GITEA_BOT_TOKEN`.
### Org
`wiggleverse` org exists in Gitea. `rfc-bot` is an Owner of the org.
### Meta repo
`wiggleverse/meta` — seeded by `scripts/seed_meta_repo.py`. Contains `PHILOSOPHY.md`, `README.md`, `CONTRIBUTING.md`, and `rfcs/` directory. Gitea webhook registered to `https://rfc.wiggleverse.org/api/webhooks/gitea`.
### OAuth2 app
Registered in Gitea Site Administration → Integrations → OAuth2 Applications:
- Name: `RFC App`
- Redirect URI: `https://rfc.wiggleverse.org/auth/callback`
- Client ID and secret stored in `.env`
---
## Workspace Setup (one-time, for email)
Email sends via Google Workspace SMTP relay. Already configured for `wiggleverse.org`; document here for replication.
- **Admin console → Apps → Google Workspace → Gmail → Routing → SMTP relay service** with a rule named `RFC App`:
- Allowed senders: "Only addresses in my domains"
- Authentication: Require SMTP Authentication ☑ (AUTH path — current setup), OR allowlist 34.132.29.41 (no-AUTH alternative)
- Encryption: Require TLS encryption ☑
- **Google Group** `notifications@wiggleverse.org` (Access type: Custom, External posters allowed so reply mail lands; member delivery set to "No email").
- **App password** generated on `ben@wiggleverse.org`. **Critical:** the Workspace account here is linked to a personal `benstull@gmail.com` Google account; when generating an app password, watch the avatar in the top-right of myaccount.google.com — it silently switches back to the personal account, and consumer-Gmail app passwords are rejected by Workspace's SMTP relay with `535 5.7.8 BadCredentials`. Confirm the avatar shows the Workspace account *every time* before generating.
- **DMARC** TXT record at `_dmarc.wiggleverse.org` (added; see DNS table above).
---
## Environment Variables
Full `.env` for production (file lives at `/opt/rfc-app/backend/.env`, mode 0600):
```ini
# Gitea
GITEA_URL=https://git.wiggleverse.org
GITEA_BOT_USER=rfc-bot
GITEA_BOT_TOKEN=<bot token from Gitea>
GITEA_ORG=wiggleverse
META_REPO=meta
# OAuth
OAUTH_CLIENT_ID=<from Gitea OAuth app>
OAUTH_CLIENT_SECRET=<from Gitea OAuth app>
# App
APP_URL=https://rfc.wiggleverse.org
SECRET_KEY=<openssl rand -hex 32>
DATABASE_PATH=/opt/rfc-app/backend/data/rfc-app.db
OWNER_GITEA_LOGIN=ben.stull
GITEA_WEBHOOK_SECRET=<openssl rand -hex 32>
# LLM
ENABLED_MODELS=claude
ANTHROPIC_API_KEY=<key>
# Email — Google Workspace SMTP relay (§15.4)
# Strip the spaces Google shows in the 16-char app password (or quote
# the value) — sourced as shell, spaces split the value.
# Alternative: leave SMTP_USER/SMTP_PASSWORD empty and switch the relay
# rule to IP-based; the app skips SMTP AUTH when SMTP_USER is empty.
SMTP_HOST=smtp-relay.gmail.com
SMTP_PORT=587
SMTP_USER=ben@wiggleverse.org
SMTP_PASSWORD=<app password, no spaces>
SMTP_STARTTLS=1
EMAIL_FROM=notifications@wiggleverse.org
EMAIL_FROM_NAME=Wiggleverse
EMAIL_ENABLED=1
EMAIL_BUNDLE_THRESHOLD=5
WEBHOOK_EMAIL_BOUNCE_SECRET=
# Hygiene scheduler
HYGIENE_TICK_SECONDS=3600
```
---
## Deploying a New Version
SSH into the VM:
```bash
gcloud compute ssh rfc-app --zone=us-central1-a --project=wiggleverse-rfc
```
Pull the latest code, reinstall deps, restart:
```bash
sudo -u rfc-app git -C /opt/rfc-app pull
sudo -u rfc-app /opt/rfc-app/backend/.venv/bin/pip install \
-r /opt/rfc-app/backend/requirements.txt
sudo systemctl restart rfc-app
```
For frontend changes, build on the VM directly (Node 20+ is already there):
```bash
cd /opt/rfc-app/frontend && sudo -u rfc-app npm install
sudo -u rfc-app npm run build
```
The output lands in `/opt/rfc-app/frontend/dist/` owned by `rfc-app` — nginx serves it directly, no copy step needed.
(Building locally and `gcloud compute scp`-ing the dist also works. Plain `rsync -e ssh` from the Mac fails because OS Login uses short-lived SSH certs that only the gcloud wrapper can mint interactively.)
Schema migrations run automatically on restart (append-only, safe to re-run).
---
## First-Time Deployment (new server)
### 1. Add DNS record
Add `rfc.wiggleverse.org` → 34.132.29.41 as an A record in Cloudflare, **DNS only (gray cloud)**. Do not proxy — certbot needs to reach the VM directly.
### 2. Host prep
```bash
sudo useradd --system --shell /usr/sbin/nologin --home-dir /opt/rfc-app rfc-app
sudo mkdir -p /opt/rfc-app
sudo chown rfc-app:rfc-app /opt/rfc-app
sudo -u rfc-app git clone https://git.wiggleverse.org/ben.stull/rfc-app.git /opt/rfc-app
```
### 3. Python venv
```bash
sudo -u rfc-app python3 -m venv /opt/rfc-app/backend/.venv
sudo -u rfc-app /opt/rfc-app/backend/.venv/bin/pip install \
-r /opt/rfc-app/backend/requirements.txt
```
### 4. Write .env
```bash
sudo -u rfc-app cp /opt/rfc-app/backend/.env.example /opt/rfc-app/backend/.env
sudoedit /opt/rfc-app/backend/.env
sudo chmod 600 /opt/rfc-app/backend/.env
sudo chown rfc-app:rfc-app /opt/rfc-app/backend/.env
```
### 5. Seed meta repo
```bash
sudo -u rfc-app -H bash -c \
'cd /opt/rfc-app/backend && .venv/bin/python ../scripts/seed_meta_repo.py'
```
### 6. Build the frontend (on the VM)
```bash
cd /opt/rfc-app/frontend && sudo -u rfc-app npm install
sudo -u rfc-app npm run build
```
### 7. nginx
```bash
sudo cp /opt/rfc-app/deploy/nginx/rfc.wiggleverse.org.conf \
/etc/nginx/sites-available/rfc.wiggleverse.org
sudo ln -s /etc/nginx/sites-available/rfc.wiggleverse.org \
/etc/nginx/sites-enabled/
sudo usermod -a -G rfc-app www-data
sudo chmod -R g+rX /opt/rfc-app/frontend/dist
sudo nginx -t && sudo systemctl reload nginx
```
### 8. Let's Encrypt
```bash
sudo certbot --nginx -d rfc.wiggleverse.org
```
### 9. systemd
```bash
sudo cp /opt/rfc-app/deploy/systemd/rfc-app.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now rfc-app
sudo systemctl status rfc-app
```
### 10. Smoke test
Visit `https://rfc.wiggleverse.org`:
1. Landing page renders with sign-in button
2. Sign in with Gitea OAuth → catalog loads
3. `+ Propose New RFC` opens the propose modal
4. `/admin` loads the four-tab home base
5. `/settings/notifications` renders all five sections
---
## Day-2 Operations
### Logs
```bash
sudo journalctl -u rfc-app -f
sudo journalctl -u rfc-app -p err
```
### Database backup
```bash
sqlite3 /opt/rfc-app/backend/data/rfc-app.db \
".backup /opt/rfc-app/backend/data/backup-$(date +%F).db"
```
### Restart
```bash
sudo systemctl restart rfc-app
```
### Rollback
```bash
sudo -u rfc-app git -C /opt/rfc-app checkout <prior-commit>
sudo systemctl restart rfc-app
```
---
## New Session Prompt
Paste the following into a new Claude session to continue development:
---
> I'm working on the **Wiggleverse RFC App** — a FastAPI + SQLite + React + Vite application deployed at `rfc.wiggleverse.org` on a GCP e2-small VM (`rfc-app` in the `wiggleverse-rfc` project; separate from the `gitea` VM in `wiggleverse` that runs Gitea at `git.wiggleverse.org`). The app is the primary interface for the Open Human Model (OHM) RFC working group.
>
> **Stack:**
> - Backend: Python 3.11, FastAPI, uvicorn (single process), SQLite WAL mode
> - Frontend: React 19, Vite 8, Tiptap 3 (rich text editor), React Router 7
> - Infrastructure: nginx (static SPA + API proxy), systemd, Let's Encrypt TLS
> - Git backend: Gitea at `git.wiggleverse.org`, bot service account `rfc-bot` is the only Git writer
> - Email: Google Workspace SMTP relay (`smtp-relay.gmail.com`), From `notifications@wiggleverse.org`
>
> **Key invariants:**
> - The bot (`backend/app/bot.py`) is the only Git writer — every commit carries an `On-behalf-of:` trailer and audits to the `actions` table
> - Git is truth; `cached_*` SQLite tables are written only by the Gitea webhook receiver or the 5-minute background reconciler
> - Single process, single SQLite file — never use multiple uvicorn workers
> - 10 append-only schema migrations in `backend/migrations/`
>
> **Deployment:**
> - SSH: `gcloud compute ssh rfc-app --zone=us-central1-a --project=wiggleverse-rfc`
> - Code at `/opt/rfc-app/` on the VM, owned by `rfc-app` system user
> - `.env` at `/opt/rfc-app/backend/.env` (mode 0600)
> - Frontend built **on the VM** (Node 20 is installed there) with `npm run build` directly into `/opt/rfc-app/frontend/dist/`
> - Restart to deploy: `sudo systemctl restart rfc-app`
> - Migrations run automatically on startup
>
> **Source is at `~/git/rfc-app/`.**
>
> The v1 build is complete (8 slices, 125 passing integration tests). I want to [DESCRIBE WHAT YOU WANT TO DO — e.g. "add X feature from the §19.2 backlog" or "deploy the current version to the VM"].
+4 -3
View File
@@ -20,9 +20,10 @@ recover from a partial install is safe.
`git.wiggleverse.org` over HTTPS. `git.wiggleverse.org` over HTTPS.
- DNS: an `A` record for `rfc.wiggleverse.org` pointing at the same IP as - DNS: an `A` record for `rfc.wiggleverse.org` pointing at the same IP as
`git.wiggleverse.org`. `git.wiggleverse.org`.
- Python 3.13 available system-wide. Node 20+ available (for `npm run - Python 3.11+ available system-wide (the project has no `requires-python`
build` once; the build output is what runs in production — Node isn't pin; the current production VM runs 3.11 on Debian bookworm). Node 20+
needed at runtime). available (for `npm run build` once; the build output is what runs in
production — Node isn't needed at runtime).
- `git`, `openssl`, and `rsync` on the host. - `git`, `openssl`, and `rsync` on the host.
--- ---
+12 -15
View File
@@ -276,23 +276,20 @@ graduation will eventually replace. The §4 cache now mirrors per-RFC
repos via a new `refresh_rfc_repo` path; the webhook receiver repos via a new `refresh_rfc_repo` path; the webhook receiver
dispatches on `repository.full_name` so per-RFC events refresh just dispatches on `repository.full_name` so per-RFC events refresh just
that repo, and the reconciler sweeps every active entry. The §18 that repo, and the reconciler sweeps every active entry. The §18
carryovers landed as `backend/app/providers.py` (the multi-provider stack landed as `backend/app/providers.py` (the multi-provider
abstraction, unchanged from the prototype) and `backend/app/chat.py` abstraction) and `backend/app/chat.py` (an adapter that runs the
(an adapter that runs the provider's streaming interface against provider's streaming interface against `thread_messages` rows, parses
`thread_messages` rows, parses `<change>` blocks, and materializes `<change>` blocks, and materializes `changes` rows per §8.14). The
`changes` rows per §8.14). The §17 endpoints owned by Slice 2 — the §17 endpoints owned by Slice 2 — the `branches/<branch>/*` and
`branches/<branch>/*` and `threads/<thread_id>/*` families — live in `threads/<thread_id>/*` families — live in
`backend/app/api_branches.py`, mounted alongside Slice 1's routes via `backend/app/api_branches.py`, mounted alongside Slice 1's routes via
`api.make_router`. On the frontend, `RFCView.jsx` was rebuilt as the `api.make_router`. On the frontend, `RFCView.jsx` is the §8
§8 three-column surface; `Editor.jsx`, `ChatPanel.jsx`, three-column surface; `Editor.jsx`, `ChatPanel.jsx`,
`ChangePanel.jsx`, `PromptBar.jsx`, `SelectionTooltip.jsx`, `ChangePanel.jsx`, `PromptBar.jsx`, `SelectionTooltip.jsx`,
`DiffView.jsx`, `ModelPicker.jsx`, and `modelStyles.js` were lifted `DiffView.jsx`, `ModelPicker.jsx`, and `modelStyles.js` make up the
from the prototype and adapted to the canonical `threads` / center column. The §18 stack: SSE streaming with base64-encoded
`thread_messages` / `changes` shape rather than the prototype's chunks, Tiptap + ProseMirror plugin for the paragraph-margin gutter
global session_id. The §18 carryovers explicitly preserved: SSE accent, the prompt-bar selection-quote machinery, the model picker.
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: The §17 endpoints exercised so far:
+1 -2
View File
@@ -4,8 +4,7 @@
// view. We reconstruct the markup for every accepted change in branch // view. We reconstruct the markup for every accepted change in branch
// history by reading the `changes` table (passed in as `changes`) plus // history by reading the `changes` table (passed in as `changes`) plus
// the current rendered HTML; hovering any tracked span surfaces a // the current rendered HTML; hovering any tracked span surfaces a
// tooltip with the change's type/model/prompt/reason context. Carryover // tooltip with the change's type/model/prompt/reason context.
// from the prototype.
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { MODEL_STYLES } from '../modelStyles' import { MODEL_STYLES } from '../modelStyles'
+4 -5
View File
@@ -1,10 +1,9 @@
// PromptBar.jsx the §8.1 prompt-bar at the bottom of the center column. // PromptBar.jsx the §8.1 prompt-bar at the bottom of the center column.
// //
// Carryover from the prototype. In discuss mode the contributor types // In discuss mode the contributor types to talk; in contribute mode the
// to talk; in contribute mode the model is told to lean toward concrete // model is told to lean toward concrete edits. A passage highlighted in
// edits. The selection-quote machinery is preserved a passage // the editor surfaces here as a "scoped to selection" badge and travels
// highlighted in the editor surfaces here as a "scoped to selection" // to the backend with the message.
// badge and travels to the backend with the message.
import { useState } from 'react' import { useState } from 'react'
import ModelPicker from './ModelPicker.jsx' import ModelPicker from './ModelPicker.jsx'
+4 -4
View File
@@ -1,9 +1,9 @@
// SelectionTooltip.jsx the §8.12 selection-anchored entry point. // SelectionTooltip.jsx the §8.12 selection-anchored entry point.
// //
// Carryover from the prototype. The contributor selects a passage in // The contributor selects a passage in the editor; this floating
// the editor; this floating panel appears anchored to that selection. // panel appears anchored to that selection. Submitting creates a new
// Submitting creates a new range-anchored chat thread (or invokes the // range-anchored chat thread (or invokes the AI on the current branch
// AI on the current branch chat with the selection as `quote`). // chat with the selection as `quote`).
// //
// Two affordances per §8.13: an "Ask" button that opens (or continues) // Two affordances per §8.13: an "Ask" button that opens (or continues)
// a chat thread, and a "Flag" button that drops a flag thread anchored // a chat thread, and a "Flag" button that drops a flag thread anchored
+2 -2
View File
@@ -1,6 +1,6 @@
// modelStyles.js — colors and display labels for each LLM provider. // modelStyles.js — colors and display labels for each LLM provider.
// §18 carryover from the prototype. Per §19.2's per-RFC-model topic, // Per §19.2's per-RFC-model topic, future per-RFC overrides land on
// future per-RFC overrides land on top of this map without replacing it. // top of this map without replacing it.
export const MODEL_STYLES = { export const MODEL_STYLES = {
// Claude variants — shades of purple // Claude variants — shades of purple