36635049c7
- Five new integration test files raise the suite from 75 to 96 green: test_hygiene_vertical (7), test_branch_path_routing (4), test_metadata_pr_merge (3), test_cache_bootstrap (4), test_e2e_smoke (3). The smoke test walks propose → super-draft → edit branch → body-edit PR → graduate → active-RFC PR → merge → notification → hygiene-sweep deletion end-to-end. - deploy/RUNBOOK.md replaces the prior DEPLOY.md stub as a real runbook: prerequisites, first-time bring-up, day-2 ops (logs, DB backup, secret rotation, the §12 hygiene cadence), rollback shape, troubleshooting table. - backend/.env.example grows the SMTP block, HYGIENE_TICK_SECONDS, and WEBHOOK_EMAIL_BOUNCE_SECRET with inline commentary. - README points to RUNBOOK.md; the "what the build lets you do" section adds Slices 7 and 8. - docs/DEV.md gets a Slice 8 — shipped section; the "Next slice" footer becomes the v1-complete epitaph. - SPEC corrections per the §19.3 working agreement: §10.7 names the shared §12 sweep; §12 names the bot as actuator and the per-user branch_chat_seen preservation contract; §19.1 marks v1 complete and records Slice 8; the five §19.2 candidates Slice 8 folded in are marked settled with pointers at the resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
289 lines
13 KiB
Markdown
289 lines
13 KiB
Markdown
# 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.
|