diff --git a/IMPLEMENTATION-PROMPT.md b/IMPLEMENTATION-PROMPT.md deleted file mode 100644 index 0feaa8a..0000000 --- a/IMPLEMENTATION-PROMPT.md +++ /dev/null @@ -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 §§1–15 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 `` / `` / `` / `` 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 `` 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. diff --git a/SPEC.md b/SPEC.md index e1f6d4b..5512260 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,24 +1,23 @@ # RFC App — Specification -This app hosts the **Open Human Model (OHM)**, the corpus of RFCs the -Wiggleverse framework produces. Each RFC defines one word; the first -defines *human*. OHM is English-first by design: the markdown bodies -are canonical, and the OpenXML APIs and UX surfaces a downstream -system needs to actually let humans and machines interact are derived -from that English source, not authored alongside it. The framework's -*why* 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 Wiggleverse RFC Contributor app — +the host for the **Open Human Model (OHM)**, the corpus of RFCs the +framework produces. Each RFC defines one word; the first defines +*human*. OHM is English-first by design: the markdown bodies are +canonical, and the OpenXML APIs and UX surfaces a downstream system +needs to actually let humans and machines interact are derived from +that English source, not authored alongside it. The framework's *why* +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 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 -spec plus the existing prototype as context. +spec as context. -The technical stack is unchanged from the prototype: FastAPI with SSE streaming -on the backend, React + Vite + Tiptap (ProseMirror) on the frontend, Gitea as -the Git host, multiple LLM providers (Anthropic, Google, OpenAI / GitHub -Copilot). The code base is a clean rewrite; the existing app is reference only. +The technical stack: FastAPI with SSE streaming on the backend, React + Vite + +Tiptap (ProseMirror) on the frontend, Gitea as the Git host, multiple LLM +providers (Anthropic, Google, OpenAI / GitHub Copilot). --- @@ -112,9 +111,9 @@ conflict-free even with concurrent proposers. Integer `RFC-NNNN` IDs are assigned at graduation time as `max(existing 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 -concurrent submissions can try to claim the same number) and means -proposing an idea costs nothing in identifier space. +is the slug. This avoids the race condition where two concurrent +submissions could try to claim the same number, and means proposing +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. - `changes` — structured proposed edits to a branch's document. Each change is either AI-proposed (from a `` block in an assistant - message, per the protocol carried over from the prototype, §18) or - manually authored (typed directly into the editor). Columns: `id`, + message, per the §18 protocol) or manually authored (typed directly + into the editor). Columns: `id`, `rfc_slug`, `branch_name`, `thread_id` (nullable; null for direct manual edits not tied to a thread), `source_message_id` (nullable; 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`, `commit_sha` (nullable; the bot commit that materialized the acceptance, see §8.6). The `` / `` / `` - / `` parsing protocol is unchanged from the prototype. + / `` parsing protocol is specified in §18. - `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`, `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 §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 -used. Per-message picker grain inside a chat thread (the §18 -prototype carryover) is preserved: each message can name a different +used. Per-message picker grain inside a chat thread (per §18) is +preserved: each message can name a different model from the resolved list, and the picker's currently-selected entry persists across messages within a session. @@ -632,7 +631,7 @@ between branches. ### 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. - **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) -The prototype's discuss-vs-contribute distinction survives, but is -scoped to the current branch rather than global. +The discuss-vs-contribute distinction is scoped to the current +branch rather than global. - **Discuss mode** is the default on any branch (including main). 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. 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 accepted change in branch history, and renders the result in-place 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 demand. -The chat's model picker (the §18 prototype carryover) draws its -option list from the resolved per-RFC set per §6.6 — the +The chat's model picker (§18) draws its option list from the +resolved per-RFC set per §6.6 — the intersection of the entry's optional `models:` frontmatter (or the operator universe when absent) with the operator's currently 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 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 entry point); the review-mode toggle and `DiffView` for inspecting 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 rich markdown, headings, links, code blocks. - **The per-RFC and per-branch chat UX.** Threading model, AI - participation, the discuss-vs-contribute mode distinction from the - prototype, the selection tooltip, the prompt bar, the model picker - chrome (its option-list source is settled in §6.6 / §8.12; the - visual treatment and per-thread persistence remain). + participation, the discuss-vs-contribute mode distinction, the + selection tooltip, the prompt bar, the model picker chrome (its + option-list source is settled in §6.6 / §8.12; the visual treatment + and per-thread persistence remain). - **The revision flow.** How proposed changes from AI or contributors appear, the change-card panel, accept/decline/edit, tracked 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 per §15.4; sets the recipient's global email opt-out. -Plus all the chat / streaming / model-picker endpoints inherited from -the prototype, scoped to per-RFC and per-branch threads. +Plus all the chat / streaming / model-picker endpoints, scoped to +per-RFC and per-branch threads. The `branches//...` endpoint family covers both per-RFC-repo 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 -preserved as-is in the rewrite unless a downstream decision changes -them: +The stack the app is built on: - FastAPI + SSE for streaming chat. - React + Vite + Tiptap (ProseMirror) for the editor. diff --git a/backend/app/api.py b/backend/app/api.py index dab2d29..6bb607c 100644 --- a/backend/app/api.py +++ b/backend/app/api.py @@ -94,8 +94,7 @@ def make_router( return {"body": payload["body"]} # --------------------------------------------------------------- - # Auth surface — extends the prototype's pattern but reads role - # from our users table per §6. + # Auth surface — reads role from our users table per §6. # --------------------------------------------------------------- @router.get("/api/auth/me") diff --git a/backend/app/chat.py b/backend/app/chat.py index 3751668..13538af 100644 --- a/backend/app/chat.py +++ b/backend/app/chat.py @@ -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` -keyed by a session_id. Here, history is the durable list of -`thread_messages` rows on a `threads` row, scoped to one branch (or -to a sub-thread anchored to a range or paragraph within it). The -streaming response is parsed for `` blocks per §18; each -`` becomes a `changes` row with `state='pending'` per §8.14 -the moment it is parsed, regardless of mode. +Conversation history is the durable list of `thread_messages` rows on +a `threads` row, scoped to one branch (or to a sub-thread anchored to +a range or paragraph within it). The streaming response is parsed for +`` blocks per §18; each `` becomes a `changes` row +with `state='pending'` per §8.14 the moment it is parsed, regardless +of mode. This module exposes two seams: - `build_history` and `build_system_prompt` — pure functions a caller @@ -35,9 +34,8 @@ from .providers import BaseProvider log = logging.getLogger(__name__) -# The §18 system prompt, adapted from the prototype. The prototype's -# version assumed one RFC document loaded as context; here the document -# is the branch's RFC.md at its current tip. The selection-quote shape +# The §18 system prompt. The document loaded as context 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 # 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}". @@ -316,9 +314,8 @@ async def stream_assistant_turn( Provider streaming is synchronous (an `Iterator[str]`) per §18; we drain it eagerly into chunks and yield them as async strings. This - is sufficient at single-process scale (§4.2) and the streaming - impl is what the prototype shipped — re-wrapping it in a worker - thread for a future deployment shape is a one-liner if it + is sufficient at single-process scale (§4.2); re-wrapping it in a + worker thread for a future deployment shape is a one-liner if it matters. """ full_text_chunks: list[str] = [] diff --git a/backend/app/providers.py b/backend/app/providers.py index 2a43fd5..03af245 100644 --- a/backend/app/providers.py +++ b/backend/app/providers.py @@ -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` — so the chat layer in `chat.py` is provider-agnostic. Enabled providers -and their API keys are configured via env per the prototype's -`ENABLED_MODELS` contract; per §16 / §19.2, per-RFC model availability -and credential delegation are deferred until the topic is settled. +and their API keys are configured via env via the `ENABLED_MODELS` +contract; per §16 / §19.2, per-RFC model availability and credential +delegation are deferred until the topic is settled. """ 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]] = { @@ -149,7 +149,7 @@ _GEMINI_VARIANTS: dict[str, tuple[str, str]] = { 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()] providers: dict[str, BaseProvider] = {} diff --git a/deploy/DEPLOY-NEW-SESSION-PROMPT.md b/deploy/DEPLOY-NEW-SESSION-PROMPT.md new file mode 100644 index 0000000..62e871a --- /dev/null +++ b/deploy/DEPLOY-NEW-SESSION-PROMPT.md @@ -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= +GITEA_ORG=wiggleverse +META_REPO=meta + +# OAuth +OAUTH_CLIENT_ID= +OAUTH_CLIENT_SECRET= + +# App +APP_URL=https://rfc.wiggleverse.org +SECRET_KEY= +DATABASE_PATH=/opt/rfc-app/backend/data/rfc-app.db +OWNER_GITEA_LOGIN=ben.stull +GITEA_WEBHOOK_SECRET= + +# LLM +ENABLED_MODELS=claude +ANTHROPIC_API_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= +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 +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"]. diff --git a/deploy/RUNBOOK.md b/deploy/RUNBOOK.md index ea6ed62..6ef34a5 100644 --- a/deploy/RUNBOOK.md +++ b/deploy/RUNBOOK.md @@ -20,9 +20,10 @@ recover from a partial install is safe. `git.wiggleverse.org` over HTTPS. - DNS: an `A` record for `rfc.wiggleverse.org` pointing at the same IP as `git.wiggleverse.org`. -- Python 3.13 available system-wide. Node 20+ available (for `npm run - build` once; the build output is what runs in production — Node isn't - needed at runtime). +- Python 3.11+ available system-wide (the project has no `requires-python` + pin; the current production VM runs 3.11 on Debian bookworm). Node 20+ + 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. --- diff --git a/docs/DEV.md b/docs/DEV.md index a0f5e85..566a106 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -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 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 +stack landed as `backend/app/providers.py` (the multi-provider +abstraction) 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`, +`api.make_router`. On the frontend, `RFCView.jsx` is 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. +`DiffView.jsx`, `ModelPicker.jsx`, and `modelStyles.js` make up the +center column. The §18 stack: 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: diff --git a/frontend/src/components/DiffView.jsx b/frontend/src/components/DiffView.jsx index d6319e9..1240f62 100644 --- a/frontend/src/components/DiffView.jsx +++ b/frontend/src/components/DiffView.jsx @@ -4,8 +4,7 @@ // view. We reconstruct the markup for every accepted change in branch // history by reading the `changes` table (passed in as `changes`) plus // the current rendered HTML; hovering any tracked span surfaces a -// tooltip with the change's type/model/prompt/reason context. Carryover -// from the prototype. +// tooltip with the change's type/model/prompt/reason context. import { useState, useCallback } from 'react' import { MODEL_STYLES } from '../modelStyles' diff --git a/frontend/src/components/PromptBar.jsx b/frontend/src/components/PromptBar.jsx index 38b0706..901b5b8 100644 --- a/frontend/src/components/PromptBar.jsx +++ b/frontend/src/components/PromptBar.jsx @@ -1,10 +1,9 @@ // PromptBar.jsx — the §8.1 prompt-bar at the bottom of the center column. // -// Carryover from the prototype. In discuss mode the contributor types -// to talk; in contribute mode the model is told to lean toward concrete -// edits. The selection-quote machinery is preserved — a passage -// highlighted in the editor surfaces here as a "scoped to selection" -// badge and travels to the backend with the message. +// In discuss mode the contributor types to talk; in contribute mode the +// model is told to lean toward concrete edits. A passage highlighted in +// the editor surfaces here as a "scoped to selection" badge and travels +// to the backend with the message. import { useState } from 'react' import ModelPicker from './ModelPicker.jsx' diff --git a/frontend/src/components/SelectionTooltip.jsx b/frontend/src/components/SelectionTooltip.jsx index 7f86a55..de41bee 100644 --- a/frontend/src/components/SelectionTooltip.jsx +++ b/frontend/src/components/SelectionTooltip.jsx @@ -1,9 +1,9 @@ // SelectionTooltip.jsx — the §8.12 selection-anchored entry point. // -// Carryover from the prototype. The contributor selects a passage in -// the editor; this floating panel appears anchored to that selection. -// Submitting creates a new range-anchored chat thread (or invokes the -// AI on the current branch chat with the selection as `quote`). +// The contributor selects a passage in the editor; this floating +// panel appears anchored to that selection. Submitting creates a new +// range-anchored chat thread (or invokes the AI on the current branch +// chat with the selection as `quote`). // // 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 diff --git a/frontend/src/modelStyles.js b/frontend/src/modelStyles.js index 2997b5c..bc8beb5 100644 --- a/frontend/src/modelStyles.js +++ b/frontend/src/modelStyles.js @@ -1,6 +1,6 @@ // modelStyles.js — colors and display labels for each LLM provider. -// §18 carryover from the prototype. Per §19.2's per-RFC-model topic, -// future per-RFC overrides land on top of this map without replacing it. +// Per §19.2's per-RFC-model topic, future per-RFC overrides land on +// top of this map without replacing it. export const MODEL_STYLES = { // Claude variants — shades of purple