Restores the §8.10 paragraph-margin marker layer Phase 1 dropped when
Tiptap left, this time against the CM6 raw pane on the left half of
the Contribute split. The matching inline tracked-insert/tracked-
delete overlay Phase 3 shipped lives on the preview pane to the
right; the two visual layers answer different questions on the two
halves of the split — "did anything change in this region?" (gutter,
amber, scannable) vs. "what changed here?" (inline, green/red,
precise) — and are deliberately separate signals.
New file: frontend/src/components/diffGutterExtension.js. The
extension exposes a `setBaseline` StateEffect and a gutter that marks
every line whose text differs from the same-indexed line of the
baseline (the last server-confirmed branch body). Per-line, not
paragraph-block — CM6's natural unit; collapsing to paragraph ranges
is more spec-faithful but adds code, and the per-line stance is the
pre-fancy default. A TODO is left for a future paragraph-collapse
pass if the result reads noisy.
MarkdownSourceEditor.jsx changes:
- Install the gutter extension in the editor's extension list.
- Seed the baseline to `initialDoc` at construct time.
- In the existing `initialDoc`-watching effect, dispatch the doc
replacement AND a `setBaseline` effect in the SAME transaction so
there's no one-frame window where the new doc reads as "divergent"
against the old baseline. This carries through every server-
confirmed branch refresh that RFCView already wires (accept,
decline, manual flush, branch switch); no RFCView changes needed
because all four paths already re-push `initialDoc` after pulling
fresh state.
Design calls per the Phase 4 prompt's open list:
• Per-line marks, not paragraph-block ranges. Pre-fancy stance.
• Amber (#f59e0b) thin 3px vertical bar, distinct from the
preview pane's green-on-light / red-on-light tracked-change
inline overlay. Reads as "in-flight / not yet on the server."
• Baseline reset on every branchView refresh (accept / decline /
manual flush / branch switch), matching RFCView's existing
originalSourceLinesRef discipline. Gutter then reads as "what's
in the buffer but not on the server."
• No hover / no click. The inline overlay already carries the
click-to-card binding; the gutter is scannable only.
Known caveats kept deliberately:
• Insert/delete shifts. Adding a line in the middle shifts every
subsequent line's index and the naive compare lights up
everything below — tolerated as the honest "you've touched
stuff below this point" cue. A future LCS-anchored pass could
fix it; Phase 4 doesn't need to.
SPEC §8.10:
- First paragraph rewritten to name the CM6 raw pane as the gutter's
home and replace the stale "ProseMirror plugin" wording with the
CodeMirror gutter framing. Mirrors how Phase 3 named the preview
pane as the inline overlay's home.
- DiffView retirement paragraph adjusted: gutter and inline overlay
together (across the split) cover its review affordance, not
"both layers in the same surface" — the layers are deliberately on
different surfaces.
Verification:
- Vite preview sandbox eval — standalone CM6 mount, dispatch tests
across construct (no marks on identity), per-line edit (mark on
exactly the touched line, confirmed by Y-coordinate matching the
line-number gutter element), baseline reset clears, atomic
doc+baseline dispatch leaves zero marks (the RFCView accept-flow
path), insert-in-middle exhibits the expected cascade, and
computed-style proof for the amber bar (`#f59e0b`, 3px wide,
positioned left of the line-numbers gutter at x=21 vs x=24).
- Screenshot captures the bar on the touched line only.
- 125 backend integration tests still green.
- Live RFCView accept → branch refresh → gutter clear flow against a
real branch was not driven (backend not running this session,
carrying forward Phase 2/3's verification gap). The sandbox-level
proof covers the atomic dispatch correctness; the next session
with a backend up should drive that golden path before piling
Phase 5 work on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RFC App
A single-process FastAPI + SQLite + React + Vite + Tiptap app that
materializes the Wiggleverse RFC framework specified in
SPEC.md. The framework's mission lives in
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 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.5On-behalf-of:trailer and records a row in theactionsaudit 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 inusers, 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:
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
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:
cd backend
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
Frontend:
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:
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:
# Terminal 1 — backend
cd backend
.venv/bin/uvicorn app.main:app --reload --port 8000
# 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.pyfans out fromactions(and from chat-message inserts that don't go through the bot) intonotificationsrows 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/inboxoverlay 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
/philosophyroute readingPHILOSOPHY.mdfrom disk, the persistent About header link,/settings/notificationsexposing the five §15-derived sub-sections, and/adminas 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 formeta_metadataPRs, 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/docslists the API routes the build session has wired so far.sqlite3 backend/data/rfc-app.db .schemashows the §5 schema.gitea ls /api/v1/repos/wiggleverse/meta/contents/rfcsafter 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 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:
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_URLexactly, including the protocol and port. - The bot can't merge. The bot needs Maintainer or Owner on the
meta repo (membership in
GITEA_ORGas Owner gives it both).
Where to read further
SPEC.md— the binding contract. Every load-bearing decision is there.PHILOSOPHY.md— why this framework exists. The spec's decisions answer to it.docs/DEV.md— the build's slicing plan, the current state, and the next slice's brief.deploy/RUNBOOK.md— single-host production bring-up, day-2 operations (logs, database backup, secret rotation, the §12 hygiene cadence), and rollback shape.