# RFC App

A single-process FastAPI + SQLite + React + Vite + Tiptap app that
materializes the Wiggleverse RFC framework specified in
[`SPEC.md`](./SPEC.md). The framework's mission lives in
[`PHILOSOPHY.md`](./PHILOSOPHY.md); the spec is the binding contract;
this README is how to bring the app up against a local Gitea instance
and exercise the surface the build has shipped.

The v1 build is complete. Subsequent sessions pick from §19.2 by user
choice per §19.3's working agreement. See
[`docs/DEV.md`](./docs/DEV.md) for the build history and the work mode
that follows v1.

## What the app expects to talk to

- **A Gitea instance** at `GITEA_URL`. The instance hosts the meta
  repository and (eventually) one repository per graduated RFC.
- **A bot service account** in that Gitea, with a personal access
  token in `GITEA_BOT_TOKEN`. Per §1 the bot is the only writer in
  the system — every commit, branch, and PR the app produces flows
  through one wrapper that applies the §6.5 `On-behalf-of:` trailer
  and records a row in the `actions` audit log.
- **An OAuth2 application** registered against that Gitea, with the
  callback URL set to `{APP_URL}/auth/callback`. Real human users
  authenticate via Gitea OAuth (the §18 carryover); the app reads
  their Gitea profile, provisions a row in `users`, and layers §6's
  app-owned permission model on top.

## Local bring-up

The shortest path from a clean checkout to a working app is:

### 1. Stand up a local Gitea

Anything that exposes the Gitea REST API works. The fastest path is
Docker:

```sh
docker run -d --name gitea \
  -p 3000:3000 -p 222:22 \
  -v gitea-data:/data \
  gitea/gitea:1.21
```

Open `http://localhost:3000`, walk through the install wizard
(SQLite, default port), and create your owner-zero account.

### 2. Create the bot service account

In Gitea, sign in as your owner account and **Site Administration →
User Accounts → Create User Account**. Give it a name like `rfc-bot`
and an email. Then sign in as the bot, open **Settings → Applications
→ Generate New Token**, and grant it the `write:repository`,
`write:user`, and `write:admin` scopes (admin is needed because the
bot will create per-RFC repos on graduation; in v1 you can scope down
to `repo` and `org` if you want to defer admin until Slice 5).

Copy the token; you will paste it into `.env`.

### 3. Create the org that will host the meta repo

The seed script creates the meta repo *inside* an org. Create the org
(e.g. `wiggleverse`) in Gitea and add `rfc-bot` to it as an
**Owner**.

### 4. Register the OAuth2 application

In Gitea: **Site Administration → Integrations → OAuth2 Applications
→ Create**. Name it whatever you like, set the redirect URI to
`http://localhost:8000/auth/callback`. Copy the client id and client
secret — they go into `.env`.

### 5. Configure the app

```sh
cd backend
cp .env.example .env
$EDITOR .env    # fill in every variable
```

Required values:

| Variable                   | What it is                                                |
| -------------------------- | --------------------------------------------------------- |
| `GITEA_URL`                | Base URL of the Gitea instance (no trailing slash).       |
| `GITEA_BOT_USER`           | The bot account's login.                                  |
| `GITEA_BOT_TOKEN`          | The bot account's access token.                           |
| `GITEA_ORG`                | The org that owns the meta repo.                          |
| `META_REPO`                | The meta repo's name (default `meta`).                    |
| `OAUTH_CLIENT_ID`          | From the OAuth app you registered.                        |
| `OAUTH_CLIENT_SECRET`      | Likewise.                                                 |
| `APP_URL`                  | The URL the app is reachable at locally.                  |
| `SECRET_KEY`               | A long random string for cookie signing.                  |
| `OWNER_GITEA_LOGIN`        | Your owner-zero Gitea login — gets the owner role on first sign-in. |
| `GITEA_WEBHOOK_SECRET`     | A shared secret for the §4.1 webhook signature.           |

Optional values, picked up at process start:

| Variable                   | What it is                                                |
| -------------------------- | --------------------------------------------------------- |
| `ENABLED_MODELS`           | Comma-separated provider keys for §18 chat (e.g. `claude,gemini`). |
| `ANTHROPIC_API_KEY` etc.   | Per-provider keys; missing keys disable that provider.    |
| `SMTP_HOST` / `SMTP_PORT`  | §15.4 transactional-email adapter target. Empty falls back to logging the envelope to stdout — sufficient for dev and integration tests. |
| `SMTP_USER` / `SMTP_PASSWORD` | SMTP auth credentials. Optional alongside `SMTP_HOST`. |
| `SMTP_STARTTLS`            | `1` (default) to negotiate STARTTLS; `0` for plaintext.   |
| `EMAIL_FROM`               | Envelope From address for §15.4 mail. Defaults to a non-routable placeholder. |
| `EMAIL_FROM_NAME`          | Display name on the From header (default `Wiggleverse`).  |
| `EMAIL_ENABLED`            | `1` (default) to dispatch email; `0` to suppress all sends without disabling the inbox. |
| `EMAIL_BUNDLE_THRESHOLD`   | Held-during-quiet-hours threshold for the "Activity while you were away" bundle (default 5, §15.4). |
| `DIGEST_TICK_SECONDS`      | Cadence of the §15.5 digest scheduler's loop (default 3600). Tests drive ticks synchronously via `digest.run_tick`. |
| `HYGIENE_TICK_SECONDS`     | Cadence of the §12 hygiene scheduler's loop (default 3600). Tests drive ticks via `hygiene.run_tick(now=...)`. |
| `WEBHOOK_EMAIL_BOUNCE_SECRET` | When set, `/api/webhooks/email-bounce` requires the same value in the `X-Webhook-Secret` header. Unset leaves the webhook open for dev — the v1 contract. |

### 6. Install dependencies

Backend:

```sh
cd backend
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```

Frontend:

```sh
cd ../frontend
npm install
```

### 7. Seed the meta repo

The seed script creates `wiggleverse/meta` if it does not exist,
populates it with `PHILOSOPHY.md`, `README.md`, `CONTRIBUTING.md`,
the regenerate-index workflow placeholder, and an empty `rfcs/`
directory, and registers the Gitea webhook the app needs:

```sh
cd backend
.venv/bin/python ../scripts/seed_meta_repo.py
```

Re-running is safe — every step is upsert-shaped.

### 8. Run the app

In two terminals:

```sh
# Terminal 1 — backend
cd backend
.venv/bin/uvicorn app.main:app --reload --port 8000
```

```sh
# Terminal 2 — frontend
cd frontend
npm run dev
```

Open `http://localhost:5173`. Sign in with your owner-zero Gitea
account. The catalog should appear empty; the **+ Propose New RFC**
button at the bottom opens the propose modal.

## What the build lets you do

Slices 1–8 are shipped — v1 is complete. End-to-end paths the app
supports:

- **Propose → idea PR → merge → super-draft** (Slice 1, §9.1–§9.3).
- **Super-draft body editing** via meta-repo edit branches, with AI
  participation, the change-card panel, manual flushes, threads,
  flags, and DiffView (Slice 4, §9.5–§9.7 + §8 inherited).
- **The §8 active-RFC view** in full: per-branch chat, AI
  participation through the `<change>` protocol, accept / decline /
  edit, manual-edit flushes, sub-threads, flags, DiffView (Slice 2,
  §8 in full).
- **The §10 PR flow** against both per-RFC repos and meta-repo edit
  branches: open, AI-drafted title and description, the §10.3
  review page with the per-user seen-cursor, review threads,
  merge, withdraw, the §10.9 conflict-replay path (Slice 3 + Slice 4's
  routing-collapse extension, §10 in full).
- **§13 graduation** with the three-field dialog, the precondition
  popover for blocking body-edit PRs, the SSE-streamed five-step
  sequence, rollback on mid-sequence failure, and the §9.8
  pre-graduation history affordance on the new RFC view (Slice 5,
  §13 in full).
- **§13.1 ownership claim** as a meta-repo PR adding the claimant
  to the entry's `owners:` field; admin/owner merges the PR (Slice 5).
- **§15 notifications** end-to-end: a producer-side chokepoint in
  `notify.py` fans out from `actions` (and from chat-message
  inserts that don't go through the bot) into `notifications`
  rows under §15.1's routing rules; §15.6 watches auto-set on the
  first substantive gesture and decay after 90 days; the header
  badge and the `/inbox` overlay back the live counter via an SSE
  stream per §15.3; toasts fire for personal-direct events and for
  events landing on the view the user is currently watching;
  §15.4 email opts in per category with one-click unsubscribe and
  a global opt-out wired to the bounce webhook; §15.5 weekly /
  daily digest assembles eligible churn into a single mail; §15.7
  reconciles unread state when a scope cursor advances; §15.8
  quiet hours hold email and digest while letting the inbox row
  still land, and the per-user mute suppresses inbox rows
  produced by a specific actor (Slice 6).
- **§14 chrome and the settings / admin neighbourhoods** — the
  landing page with the three-item deck, the `/philosophy` route
  reading `PHILOSOPHY.md` from disk, the persistent About header
  link, `/settings/notifications` exposing the five §15-derived
  sub-sections, and `/admin` as the four-tab home base for role
  management, write-mute, audit-log, graduation-queue, and the §6.5
  permission-events read (Slice 7).
- **§12 branch hygiene + §10.7 post-merge deletion** — the
  30/90-day timers ride on a scheduler next to the digest, the bot
  is the only Git writer per §1, and the audit-log rows surface as
  "the app" actor per §15.9. The §19.2 candidates the hardening
  pass folded in: cache bootstrap against a meta repo the bot did
  not author, branch-name path routing via `{branch:path}`, in-app
  merge for `meta_metadata` PRs, the graduation-rollback branch
  cleanup, and the email-bounce webhook signing seam (Slice 8).

This exercises the §4 cache (webhook + reconciler), the §6
permission model in full, the §1 bot wrapper (every Git write goes
through it, every commit and PR carries the `On-behalf-of:`
trailer), the §17 routing-collapse rule that lets active and
super-draft surfaces share their endpoints, and three scheduled
jobs in the same shape (reconciler, digest, hygiene).

Out of scope for v1: every item under §16 ("What is deliberately
deferred"), and the §19.2 candidates the hardening pass left
queued. Subsequent sessions pick from §19.2 by user choice per
§19.3's working agreement.

## Verifying it worked

After bring-up:

- `http://localhost:8000/docs` lists the API routes the build session
  has wired so far.
- `sqlite3 backend/data/rfc-app.db .schema` shows the §5 schema.
- `gitea ls /api/v1/repos/wiggleverse/meta/contents/rfcs` after a
  proposal merges should show one new `<slug>.md`.

## Seeding an active RFC for §8 testing

With Slice 5 shipped, the `/graduate` flow in the app is the
canonical path from super-draft to active. The
[`scripts/seed_test_rfc.py`](./scripts/seed_test_rfc.py) shortcut is
still around for dev sessions that want an active RFC without
running the §9.1 propose flow and the §13 graduation dialog by
hand. Sign in once via OAuth so a `users` row exists, then:

```sh
cd backend && .venv/bin/python ../scripts/seed_test_rfc.py \
    --slug open-human-model \
    --title "Open Human Model"
```

The script creates `wiggleverse/rfc-NNNN-<slug>`, seeds `RFC.md` on
`main`, registers the webhook, and graduates the meta entry as a
bootstrap-only direct write. The §8 surface at `/rfc/<slug>` then
has something real to render.

## Troubleshooting

- **The catalog stays empty after a merge.** Check that the webhook
  is reaching the app: the reconciler runs every five minutes and
  will catch up, but a missing or misconfigured webhook is the most
  common reason for sub-second freshness to fail. The seed script
  registers the webhook for you; if you bring up Gitea on a different
  host (e.g. a Codespace, a tunnel), re-run the seed against the new
  `APP_URL`.
- **OAuth callback errors.** The redirect URI in Gitea has to match
  `APP_URL` exactly, including the protocol and port.
- **The bot can't merge.** The bot needs Maintainer or Owner on the
  meta repo (membership in `GITEA_ORG` as Owner gives it both).

## Where to read further

- [`SPEC.md`](./SPEC.md) — the binding contract. Every load-bearing
  decision is there.
- [`PHILOSOPHY.md`](./PHILOSOPHY.md) — why this framework exists.
  The spec's decisions answer to it.
- [`docs/DEV.md`](./docs/DEV.md) — the build's slicing plan, the
  current state, and the next slice's brief.
- [`deploy/RUNBOOK.md`](./deploy/RUNBOOK.md) — single-host production
  bring-up, day-2 operations (logs, database backup, secret rotation,
  the §12 hygiene cadence), and rollback shape.
