refresh_meta_pulls / refresh_rfc_repo recover a PR's slug from its
Gitea head.ref, which collapses to the refs/pull/<N>/head sentinel
once a merged PR's branch is deleted. The slug then parsed to None,
the row was skipped, and cached_prs.state froze at 'open' — so the
entry showed as both a super-draft and a pending idea. Recover the
real branch name from the stored cached_prs row when Gitea reports an
empty or sentinel ref.
Surfaced via the ROADMAP #35 operator authoring lane (CLI merge with
--delete-branch); the web UX leaves branches in place so it never hit
this. Regression test added; full suite 375 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documentation-only minor. The guide had drifted since it was first
written; brought back in sync with v0.7.0–v0.29.0:
- "Signing in" rewritten: email + one-time-code, optional passcode,
trust-device 30d, optional Turnstile, and the beta-request → pending
→ admin-grant gate, plus admin-create + invite-claim. The vestigial
email allowlist is no longer described as the gate.
- "Proposing a new RFC": four → five fields (optional use-case #26) +
AI tag-suggestion disclosure (#27).
- "Roles & permissions": documents the pending state.
- New "Invitations, cross-references, and contribution requests"
section (#12 owner invites; #28 auto-link / create-RFC / ask-to-
contribute).
- New "Privacy and cookies" section (#11/#13).
No code/schema/API/config/overlay/secret change — DOCS.md is served
verbatim by /api/docs. VERSION + frontend/package.json bumped to 0.30.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the v0.26.0 (#28 Part 1) read-time scanner into three buckets in
one pass — active link (Part 1), pending-RFC contribute offer (Part 3),
create-RFC offer (Part 2) — precedence active > pending > candidate. The
backend still emits only structured segments (never HTML), so the surface
stays XSS-safe by construction.
Part 2 — create-RFC offers: a multi-word tag from the #27 taxonomy with no
defining RFC renders, for a create-rights viewer, as an inline "+ create
RFC" affordance that opens the propose modal pre-filled (?propose=<term>;
ProposeModal gained initialTitle). Conservative multi-word gate; broader
heuristics + the Haiku path are deferred.
Part 3 — contribute-to-pending offers: a term matching a super-draft
renders, for a signed-in non-owner, an "ask to contribute" affordance with
the owner's display name. It opens a 3-field request form (who/why/optional
use-case); submitting lands a contribution_requests row (migration 024) and
one actionable §15 notification per owner (new kind
contribution_request_on_pending_rfc, personal-direct). The owner's inbox
shows who/why/use-case inline with Accept/Decline. Accept fires #12's
owner-invite flow with the requester as invitee and echoes a notification
back; decline notifies the requester. Pre-merge idea PRs are out of scope.
New endpoints: GET /api/rfcs/{slug}/contribution-target,
POST /api/rfcs/{slug}/contribution-requests,
.../{id}/accept, .../{id}/decline. The invite issue path was refactored
into one reusable api_invitations.issue_invitation(...) chokepoint shared
by the manual invite endpoint and Part 3's accept.
Tests: 9 new (3 scanner-bucket unit + 6 e2e). Full suite 374 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two informational findings from the Session-0026 audit, both
framework-internal defense-in-depth. No operator action: no migration,
no schema/config/overlay change, no deployment-facing surface.
- I3: guard the dead text/html branch in email_envelope.build_envelope.
No send path passes body_html; the unused branch would emit HTML built
from possibly-unescaped user content (C1 stored-XSS class in the mail
channel). Passing body_html now raises NotImplementedError; the arg is
kept for documented future symmetry, enabling HTML mail becomes a
deliberate escape-then-unguard change.
- I4: make turnstile.verify_token async. The sync httpx.post ran inside
the async /auth/otc/request handler, blocking the event loop up to the
10s timeout on a slow CloudFlare call. It now awaits httpx.AsyncClient
via a narrow _siteverify_post seam (tests patch the seam, not the
shared AsyncClient). The sole caller (main.py) now awaits it.
Tests: full backend suite 365 passed. Added a coroutine-contract unit
test for verify_token and flipped the email_envelope HTML test to assert
the guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remediates the rfc-app application + deploy-config findings from the
Session 0026 security audit. Cut as the "v0.25.0-security-hardening"
branch (from v0.24.0); reversioned to 0.27.0 on rebase onto main since
v0.26.0 (#28) shipped while this was in flight.
- C1 (Critical): single sanitizeHtml.js chokepoint (DOMPurify) for every
marked→innerHTML / dangerouslySetInnerHTML sink (MarkdownPreview,
ProposalView x2, Editor); rel=noopener hook on target=_blank links.
- H1: per-account OTC-verify lockout (migration 023, auto-applied) +
per-IP throttle via new ratelimit.py; wired on otc verify/request +
passcode check/verify.
- M1: device_trust.lookup() single indexed-row read — cookie value is now
"<row_id>.<raw_token>"; bcrypt-checks one row, not a global table scan.
(Behavior change: existing device-trust cookies re-prompt once.)
- M2: HTTP security headers (CSP/HSTS/XFO/XCTO/Referrer-Policy) at nginx.
- M4: session cookie Secure-by-default (SESSION_COOKIE_SECURE opt-out).
- M5: bounce webhook fails CLOSED (503) when secret unset, instead of open;
RFC_APP_INSECURE_BOUNCE_WEBHOOK=1 dev opt-in.
- L2/L3: per-IP cooldown + check-endpoint throttle.
- L4: systemd sandbox knobs. L8/I1: nginx server_tokens off + TLS1.0/1.1 out.
VERSION + frontend/package.json → 0.27.0; CHANGELOG documents the upgrade
steps (incl. the out-of-band nginx + systemd apply, which the flotilla
deploy gesture does not perform).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part 1 of item #28: references to existing accepted RFCs inside PR
descriptions and comment text now render as inline links to the
referenced RFC. Parts 2 (offer-to-create) and 3 (offer-to-contribute-
to-pending) are deliberately deferred — Part 1 ships first as the easy
win, per the roadmap row.
Shipped in parallel with the v0.25.0 security-hardening session; this
took the next free version slot (0.26.0) per the roadmap's
"claims the next available version number" rule. Expect a top-of-file
CHANGELOG/VERSION merge with 0.25.0 — distinct concerns, trivial to
resolve.
Backend:
- rfc_links.py (new): builds a term index from the live accepted
(state='active') RFC corpus and segments plain text into text /
rfc-link segments. Conservative matching — links only rfc_id tokens
(RFC-0001), multi-word titles (Open Human Model), and hyphenated
slugs (open-human-model); a single common-word title/slug is NOT
linked (would turn every prose "human" into a link). Case-insensitive,
word-boundary-anchored, longest-match-wins, self-reference suppressed.
- api_prs.py get_pr(): enriches the PR description (description_segments)
and every PR comment (text_segments).
- api_discussion.py: enriches PR-less discussion comments (text_segments).
Read-time, not submit-time: the roadmap says "at submit time" but the
intent it names is "not as live compose preview", which read-time
honors. Chosen for correctness (links track the live active set —
newly-accepted RFCs start linking, withdrawn ones stop), zero migration,
and cheapness (small cache-resident corpus). Recorded as a §19.3-rule-2
note in the session transcript.
Frontend:
- LinkedText.jsx (new): maps backend segments onto React text nodes +
anchors. No dangerouslySetInnerHTML — XSS-safe by construction,
independent of any HTML-sanitization layer. Falls back to raw text
when segments are absent.
- PRView.jsx: description + PR conversation comment bodies render via
LinkedText.
- RFCDiscussionPanel.jsx: discussion comment bodies render via LinkedText.
- App.css: .rfc-autolink (subtle accent + dotted underline, tokenized).
Tests: 12 new (test_rfc_links_vertical.py) — 9 scanner units + 3
end-to-end (PR description / review comment / discussion comment all
surface *_segments; self-reference suppression). Full suite 363 green;
frontend builds clean.
No upgrade steps: additive, no migration, no secret, no config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wave 9 follow-up to roadmap item #30 (Session 0017.0 shipped #30 as v0.19.0;
v0.20.0 lands the operator-feedback follow-ups on top).
1. Specs on /docs/specs/<name> (backend docs_specs.py + frontend DocsSpec.jsx
+ DocsSpecsIndex.jsx). Configured via OHM_DOCS_SPECS; framework default
carries OHM's two specs (rfc-app/SPEC.md + flotilla SPEC.md). Runtime
fetch from gitea raw with 5-min TTL cache, mirroring docs_sessions.py.
2. Nested flyout nav hierarchy (DocsLayout.jsx). Sessions render as a tree
with transcripts nested under each session row (labeled by .N ordinal).
New Specs section between User Guide and Sessions.
3. /docs/sessions/<NNNN> body-list removed (DocsSessionIndex.jsx). Body
becomes a session-overview card; navigation lives in the left nav.
19 new pytest cases for docs_specs (332 backend total green). Frontend
build clean. Sync frontend/package-lock.json version drift (0.15.0 → 0.20.0)
alongside the VERSION + package.json bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorganizes the /docs surface from a single DOCS.md route into a hub
with a left-side flyout nav and three new sub-routes for the on-site
sessions browser (roadmap item #30):
/docs → redirect to /docs/user-guide
/docs/user-guide → existing DOCS.md content
/docs/sessions → redirect to /docs/sessions/about
/docs/sessions/about → README.md of ohm-session-history
/docs/sessions/<NNNN> → per-session transcript index
/docs/sessions/<NNNN>/<f> → per-transcript view
The flyout is a persistent left sidebar on desktop and a slide-out
drawer on mobile (toggled by a ☰ button in the docs header). Its
session list is driven by the /api/docs/sessions/manifest fetch —
loading shows a skeleton; 502 shows an inline retry; empty manifest
shows only the "About" row.
Each sub-route owns its own empty-state / error handling:
- 404 transcripts render "This transcript isn't published yet"
with a link back to the parent session index, no JS crash.
- 502 (gitea unreachable) renders a retry button.
- Manifest 404 is mapped to {} server-side so the flyout renders
cleanly with no error banner when no sessions are published yet.
Analytics (per SPEC §21):
- new EVENTS.DOC_VIEWED ("Doc Viewed") fires on each sub-route
mount with `section`: 'user-guide' | 'sessions/about' |
'sessions/<NNNN>' | 'sessions/<NNNN>/<filename>'.
- every interactive nav element carries aria-label +
data-amp-track-name so autocapture rows are readable.
The existing v0.14.0 Docs.jsx component is dropped — its content
moved verbatim into DocsUserGuide.jsx; the new layout subsumes the
back-button + signed-out home affordances it used to carry. All
four new sub-routes reuse the existing MarkdownPreview renderer
(marked + mermaid lazy-load) so no second markdown library lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VERSION + frontend/package.json -> 0.18.0. CHANGELOG entry with
the binding Upgrade-steps block per SPEC.md §20.4.
This release lands all five slices of the v0.18.0 proposal at
~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md:
Slice 1: build_envelope helper + unit tests.
Slice 2: migrate send paths; add POST /api/email/unsubscribe
for RFC 8058 one-click.
Slice 3: mandatory GITEA_WEBHOOK_SECRET (+ dev-bypass) +
unknown-repo logging.
Slice 4: outbound_emails audit table + admin endpoint.
Slice 5: bounce correlation via Message-ID.
Full suite: 295 passed (was 252 pre-release).
Upgrade-steps (RFC 2119) block in CHANGELOG covers:
* MUST: GITEA_WEBHOOK_SECRET non-empty at startup.
* MAY: RFC_APP_INSECURE_WEBHOOKS=1 for local dev only.
* MAY: EMAIL_UNSUBSCRIBE_MAILTO to route opt-out courtesy mail
to a different mailbox than EMAIL_FROM.
* MUST: apply migration 020_outbound_emails.sql (auto-applied
on next start; no operator action).
* MUST: rebuild frontend + restart backend.
* SHOULD: run mail-tester.com probe post-upgrade.
Wave 5. Roadmap item #16. Folds in #21 Part C Amplitude wiring inline.
From the v0.9.0 /admin/users surface, an admin can create a fresh
users row, assign a role (admin/owner/granted-beta-user), include
an optional custom message, and dispatch an invite email carrying
a single-use opaque claim token (256-bit CSPRNG, bcrypt-at-rest,
7-day TTL). The invitee clicks through, lands at /invites/claim,
optionally checks "trust this device for 30 days", and is signed
in without going through OTC (the token in the email is itself
proof of email control).
Backend: migration 019_user_invite_tokens.sql (auto-applied);
backend/app/invites.py (create + claim + list module);
backend/app/email_invite.py (sibling of email_otc); POST
/api/admin/users + GET /api/admin/users/invites in api_admin;
POST /api/invites/claim in main.py's oauth_router (shares
trust-device cookie helper with /auth/otc/verify). 15 new tests in
test_admin_create_user_invite_vertical.py. permission_events row
with event_kind='user_invited' for the audit trail.
Frontend: Admin.jsx Create-user-invite modal + (pending invite)
badge in UserRow; InviteClaim.jsx /invites/claim landing page.
App.jsx route registration. api.js helpers.
Design choices documented in the CHANGELOG: immediate-send (no
admin-review queue); no bulk-invite (deferred); OTC skipped on
first sign-in (token = proof of email control); no users table
changes (discriminator is active user_invite_tokens row, not a
new first_sign_in_at column); refusal cases (self-invite 422,
duplicate email 409, non-owner admin granting owner 422).
Amplitude wiring (inline, #21 Part C):
- USER_INVITED from CreateUserInviteModal { target_user_id,
initial_role, custom_message_chars }
- INVITE_CLAIMED from InviteClaim { invited_by_admin_id,
initial_role, needs_passcode, trust_device }
- identify() BEFORE the claim event with properties:
claim_method 'admin-invite', invited_at (setOnce),
invited_by_admin_id (setOnce), initial_role (setOnce) —
so the Amplitude user record is created with the OHM
user_id from the very first event the invitee fires
Subagent ο shipped the feature on feature/v0.17.0-admin-create-user
(41b0c6a). Driver-side integration squash-merged into main,
hand-resolved 5 files (VERSION, package.json, CHANGELOG —
strict-descending to 0.17.0 → 0.16.0 → 0.15.0; App.jsx — both
new routes kept; api_admin.py — both per-user additive fields
+ pending-invite query both kept). Added inline Amplitude wiring
in CreateUserInviteModal + InviteClaim. 252 backend tests pass
(33 new across #12 + #16 surfaces). Frontend build verified green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 5 / Track B. Roadmap item #12. Folds in #21 Part C Amplitude
wiring inline (operator ask: "best practices from the very get-go").
RFC owners can invite specific users to one of two per-RFC roles:
contributor (open PRs + join discussion) or discussant (discussion
only). Non-invited users keep the v0.6.0 anonymous-read contract.
The per-RFC write gate layers on top of the existing
require_contributor gate; a super-draft with no owners yet falls
through to the platform-granted contract, preserving the
v0.6.0/v0.7.0/v0.8.0 contracts in their domains.
Backend: migration 018_rfc_invitations.sql (auto-applied — two
tables: rfc_invitations + rfc_collaborators); api_invitations.py
with five endpoints + transactional email; auth.py helpers
(is_rfc_owner / is_rfc_collaborator / can_discuss_rfc /
can_contribute_to_rfc / can_invite_to_rfc); api_discussion +
api_branches + api_prs gate composition; api_admin.py additive
rfc_invitations[] per user. 237 backend tests pass (18 new in
test_rfc_invitations_vertical.py).
Frontend: InvitationsModal.jsx (owner surface), AcceptInvitation.jsx
(/invitations/accept route), api.js helpers, RFCView.jsx
Invitations button, App.jsx route registration.
Amplitude wiring (inline, #21 Part C):
- INVITATION_SENT from InvitationsModal { rfc_slug, role_in_rfc }
- INVITATION_ACCEPTED from AcceptInvitation { rfc_slug, role_in_rfc }
- identify() BEFORE the accept event with properties: invited_at
(setOnce), last_invited_to_rfc, last_invite_role_in_rfc,
claim_method: 'rfc-invite'
- EVENTS taxonomy extended with INVITATION_SENT + INVITATION_ACCEPTED
No new secrets, no new overlay keys, no operator gesture beyond the
v0.15.0 overlay-set + restart. Frontend build verified green.
Subagent ν shipped the feature on feature/v0.16.0-owner-invite
(a51beec). Driver-side integration squash-merged into main,
hand-resolved VERSION + package.json + CHANGELOG (strict-descending
0.16.0 → 0.15.0), and added the inline Amplitude wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 5 / Track A. Roadmap item #13. Folds in #21 Part C user-identity-
lifecycle work inline rather than as a follow-up release (operator
ask: "implement Amplitude best practices from the very get-go").
Wraps @amplitude/unified with:
- consent-gated lazy init (v0.13.0 cookie banner; SDK never loads
without explicit analytics opt-in)
- amplitude.initAll(KEY, { analytics: { autocapture: true },
sessionReplay: { sampleRate: 1 } }) — vendor-recommended shape
- identify({ user_id, properties? }) with set vs setOnce semantics
- setUserProperties(properties) for mid-session state changes
- anonymize() clears both user_id binding AND property cache
Wired into App.jsx (identify on me.user with role / permission_state /
passcode_set / device_trusted as set; first_sign_in_at / account_created_at
as setOnce; Page Viewed on route change; Sign-out fires User Signed
Out + anonymize), Login.jsx (User Signed In + Beta Access Requested),
and the per-feature surfaces (ProposeModal, PRModal, RFCView,
RFCDiscussionPanel, PRView, Admin).
Binding: VITE_AMPLITUDE_API_KEY via `flotilla overlay set`
(bundle-embedded public key like VITE_TURNSTILE_SITE_KEY — not
secret). Mid-Session-L correction: original dispatch brief specified
secret-set; vendor guidance + bundle visibility settled it as overlay.
PII discipline: no email, no display_name, no gitea_login, no free-text
fields ever sent. Properties are opaque ids + enums + timestamps +
booleans only.
Subagent ξ shipped the wrapper structure + nine-event taxonomy on
feature/v0.15.0-amplitude (0fd8c52 + 6cfbf69 post-correction).
Driver-side integration extended the wrapper with setUserProperties
+ identify properties for the #21 Part C identity-lifecycle work.
Frontend build verified green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships DOCS.md and the /docs route — a plain-prose translation of
SPEC.md for users, distinct from the binding spec. Originating need
was the admin-vs-owner role distinction on /admin/users (§6.1);
response is a single guide that covers the framework's user-facing
surfaces end-to-end (roles, proposing, branches, PRs, graduation,
notifications). Mirrors /philosophy: markdown file at repo root +
sibling backend loader + MarkdownPreview render. No schema migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After a successful OTC sign-in, a contributor can set a passcode
(4-20 characters, bcrypt-hashed) and use email + passcode for
subsequent sign-ins. OTC remains the structural fallback: five
consecutive failed verifies lock the passcode path for 15 minutes
(HTTP 423), and a forgotten passcode is recovered by requesting a
fresh code. Migration 015_passcode.sql adds four nullable columns
to the users table; existing rows pass through as OTC-only and can
opt into a passcode from a new Sign-in tab in /settings/notifications.
The /login surface is extended to a five-step flow (email → either
passcode or OTC code → optional post-OTC passcode offer → optional
set-passcode). SPEC corrections per §19.3 rule 2: §6 names the
three auth paths, §14.1 documents the stepped login flow, §17 lists
the four new /auth/passcode/* endpoints, §19.2 surfaces four new
candidates and refreshes the cross-refs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the v0.3.0 / v0.7.0 allowed_emails admission gate with an
admin-grant flow (roadmap item #6, SPEC §6.1 / §6.2 / §14.1 / §17).
Any valid email can sign in via OTC; a fresh user lands in
permission_state='pending' with a captured first/last/why profile,
and an admin grant flips them to 'granted' before write endpoints
accept them. Grandfathered users pass through the migration with
the column default 'granted' so existing contributors are unaffected.
The allowed_emails table stays in the schema as a fast-path bypass
pending v0.9.0's admin user-management page (item #7).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roadmap item #11. Ships the non-modal bottom-of-page cookie consent
banner, the default /privacy and /cookies policy pages, the
`cookie_consent` table + two §17 endpoints for server-side persistence,
the localStorage fallback for anonymous viewers, the /settings
"Privacy & cookies" tab for revisiting the choice, and the
`frontend/src/lib/consent.js` helper that roadmap item #13's analytics
SDK (v0.15.0) will gate against. No analytics SDK ships in this release
— the consent infrastructure goes in first so the gate is already in
place. Adds SPEC §14.5 / §14.6, lists two new endpoints in §17, names
the new table in §5, and surfaces four §19.2 candidates (content-repo
file vs env-var policy, GPC / DNT headers, i18n, item-#13 dependency).
Two new optional env vars (`VITE_PRIVACY_POLICY_URL`,
`VITE_COOKIES_POLICY_URL`) — defaults render the framework's stub
pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the Gitea OAuth gesture as the primary human-auth path
(roadmap item #5, SPEC §6.2). Users sign in by entering their email,
receiving a six-digit code via the existing SMTP layer, and entering
the code on a two-step /login surface. The Gitea OAuth callback
remains functional during migration — the new UI links to it as a
fallback for users with active OAuth sessions or older invite paths
— and is scheduled for removal in a future release once OTC adoption
is universal. Existing users are linked by email on first OTC sign-
in (gitea_id preserved); new users are provisioned with NULL
gitea_id and rely on email as the identity key. The migration
introduces backend/migrations/012_otc.sql (otc_codes table + users
schema rebuild for nullable gitea_id and a partial unique index on
email), two new endpoints (POST /auth/otc/request, POST /auth/otc/verify),
bcrypt as a new backend dependency for code hashing, and 11 new
tests in test_otc_vertical.py covering the happy path, expired and
consumed and wrong codes, the per-email rate limit, the allowlist
gate, the OAuth-era link path, fresh provisioning, and prior-code
invalidation on re-request. No new secrets are required — the
existing SECRET_KEY signs sessions and bcrypt's per-row salt covers
the code hashes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep-the-edges hardening release (roadmap item #4). Audits every
write-shaped backend endpoint to confirm each one enforces an explicit
auth.require_contributor (or stricter) gate before doing state-changing
work; adds a regression test net (test_anon_offlimits_vertical.py, 12
tests, 66 assertions) so future endpoints can't quietly ship without a
gate. The only behaviour change: GET /api/rfcs/<slug>/graduate/progress
now requires auth.require_user since the step detail (repo name, PR
number, rollback steps) isn't part of the v0.3.0 anonymous-read contract.
No schema, no env, no dependency changes; operator action is rebuild +
restart per CHANGELOG §0.6.0 upgrade steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roadmap item #3. An RFC's main view now carries a discussion surface
distinct from PR comments and from branch chat. The substrate is the
existing threads/thread_messages tables — rows with branch_name IS NULL
scope to the RFC's main view; the schema already permitted that shape,
v0.5.0 is the first build to write it. Five new endpoints under
/api/rfcs/<slug>/discussion/..., a new RFCDiscussionPanel right-column
component used when branchParam === main, SPEC §10.10 settling
discussion-vs-contribution, and §17 listing the new routes. Notification
routing reuses the existing chat_message_in_participated_thread /
chat_reply_to_my_message event kinds with branch_name=null on the
fan-out row; a distinct event_kind is a §19.2 candidate. Anonymous
viewers can read; writes require contributor — v0.6.0's item #4 will
harden adjacent gates. No schema migration; minor bump, no operator
action required beyond rebuild and restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an email allowlist (toggleable per deployment) that restricts
OAuth sign-in to listed emails while keeping read paths public.
Anonymous visitors now see the full app shell in read-only mode
instead of the §14.1 landing wall. Empty allowlist = gate off, so
deployments that don't enable it behave exactly as 0.2.3.
Also fixes single-finger scroll on /philosophy and other .chrome-pane
views on iOS Safari (.app: 100vh → 100dvh).
Renames deploy/nginx/rfc.wiggleverse.org.conf →
ohm.wiggleverse.org.conf to match the deployed-domain rename
(rfc.wiggleverse.org deprovisioned 2026-05-27).
See CHANGELOG.md for full details + upgrade steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an unauthenticated GET /api/health endpoint returning JSON
{version, status} for ops tooling. version is the running framework
version (read from VERSION at process start, cached as a module-level
constant in backend/app/health.py); status is "ok" with HTTP 200 in
v1, with the "degraded" / 503 path reserved in the response shape for
future degradation conditions.
The structural value is the version-match check: a deploy control
panel (flotilla, in particular) polls the endpoint after
`systemctl restart` reports active and verifies the returned version
equals the tag just deployed, catching the failure mode where a
restart did not pick up the new code.
Patch-shaped per SPEC.md §20.2 — no operator action required, no env
vars, no schema changes. The CHANGELOG carries one MAY-language step
naming the optional monitoring affordance.
SPEC.md §17 now lists the endpoint; the §19.2 candidate-topic entry
records the topic's settlement (version source = VERSION file at
import time; degraded = reserved v1 scaffold; CHANGELOG = patch shape
with MAY-language).
Tests: backend/tests/test_health.py (3 tests — 200/payload shape,
unauthenticated, version-matches-VERSION). Full suite 128/128 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Philosophy.jsx parsed PHILOSOPHY.md with bare marked + dangerouslySet
InnerHTML, so ```mermaid fences fell through as <pre> blocks. Swapped
to MarkdownPreview, which already carries the lazy mermaid loader +
SVG render path used by the RFC body view. Pre-existing gap, surfaced
when an OHM deployment authored a mermaid block in its PHILOSOPHY_PATH
override.
PATCH per SPEC §20.2 — additive, no operator action required beyond
the routine rebuild + restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PRView.jsx declared its threadsByKind useMemo after the early-return
guard for the loading state. First mount: pr is null, early return
fires, 14 hooks called. Second render: pr populated, execution
reaches the useMemo, 15 hooks — React #310, page goes blank.
Move the useMemo above the early returns and null-safe the
pr?.threads access so the hook count stays stable across renders.
Pre-existing bug in the post-Contribute-rewrite PR view; surfaced
in production by 0.2.0's graduation merge race fix enabling the
workflow that opens this code path. Patch per §20.2 — no operator
action required beyond rebuilding the frontend and restarting.
- Fix §13.3 step 4 race against Gitea's async mergeability
computation: gitea.wait_for_mergeable polls the mergeable
field before calling merge, and bot._merge_with_retry rides
out the transient 'Please try again later' response.
- Frontend now requires VITE_APP_NAME at build time
(frontend/vite.config.js fails the build if unset). Header
brand, landing H1, and browser title read the deployment's
configured name.
- Establish framework versioning: VERSION (canonical) +
frontend/package.json#version mirror; CHANGELOG.md as the
release log with RFC 2119 / 8174 normative-language
upgrade-steps; SPEC.md §20 as the binding policy; CLAUDE.md
capturing the separation-of-concerns rule; docs/DEPLOYMENTS.md
as the downstream operating manual; docs/QUICKSTART-VISION.md
staking the future one-command-deploy capability.
- Surgical fix to SPEC.md opening so the spec no longer asserts
OHM is the corpus the framework produces.
- SPEC.md §19.2 gains the 'Deployment-supplied subject framing'
candidate that drives the 0.3.0 release.
Add a rendered preview pane and split the Contribute-mode center
column. Discuss mode is now a single rendered preview (no more
read-only Tiptap mount); Contribute mode renders the CM6 raw editor
on the left and a live-updating preview on the right at 50/50, with
the existing chat + change-card panels untouched on the right.
The new MarkdownPreview component renders markdown via marked and
lazy-loads mermaid on the first encounter of a ```mermaid fence in
any rendered doc. Mermaid (~200 KB gzipped, plus dagre/graphlib
subchunks) stays out of the main bundle and is fetched only when a
diagram actually appears in the rendered content. The marked
renderer is scoped via a per-component Marked instance so the
mermaid-fence behavior does not leak to Editor.jsx / DiffView.
XSS hardening on mermaid: securityLevel: 'strict' is set explicitly
in mermaid.initialize. Verified end-to-end via a sandbox eval — a
hostile `<script>window.X=true</script>` payload inside a mermaid
fence is neutralized (no script tags in the rendered SVG, no global
side-effect, diagram still renders with the offending node label
empty).
The §8.12 selection tooltip is now sourced from window.getSelection()
inside the preview surface rather than Tiptap PM positions. The
SelectionTooltip's existing `{text, coords}` contract is unchanged;
coords come from the selection range's bounding rect. Anchor payloads
for flag threads now carry just the quote text — the PM-position
from/to fields that the Tiptap-era code attached are dropped (they
were never meaningful as durable anchors anyway; quote is what §8.12
and §8.13 specify).
Design decisions confirmed before coding (all defaults):
- PromptBar spans both panes at the bottom of the center column; it
operates on the document, not on either pane individually.
- Start-Contributing is a hard cut — no transition animation. Phase
6's chat-drawer collapse will redo the layout machinery anyway.
- Mermaid lazy-loads on first ```mermaid fence detected, not on
Contribute-mode entry. Keeps the gzipped cost off any doc that has
no diagrams.
- Below 1280px viewport, a narrow-viewport banner surfaces in
Contribute mode. The drawer collapse that fixes this properly is
Phase 6.
Mermaid integration is kept loose for the future authoring tool: the
placeholder DOM node carries the source as a data attribute, the
renderer takes (source) → SVG via mermaid.render, and per-block
memoization keys on source. A future authoring pane can intercept
the placeholder before SVG render without restructuring.
SPEC §8.3 updated to reflect the split-pane Contribute layout — the
smallest edit that captures the new shape.
Verification notes:
- Vite preview on :5180 — sandbox mount of MarkdownPreview confirms
rendering, mermaid SVG output, securityLevel:'strict' XSS
neutralization, and the window.getSelection → {text, coords}
bridge end to end.
- Network log confirms mermaid + sub-chunks are absent from initial
load and fetched only after first ```mermaid encounter.
- Backend was not running this session, so the manual-debounce
save-now / accept / decline / live-preview-mirror pathways were
not driven against a real branch; they remain verified by
inspection. Phase 1's verification gap therefore carries forward.
- 125 backend integration tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the Contribute-mode editing surface from Tiptap WYSIWYG to a
CodeMirror 6 markdown source editor. Discuss mode and any read-only
viewing continues to render through Tiptap.
The §8.11 manual-edit debounce now reads the raw markdown source via
CodeMirror's doc, eliminating the lossy `editor.getText()` round-trip
that RFCView.jsx flagged as a §19.2 candidate; what the contributor
typed is exactly what gets POSTed to manual flush.
The §8.10 paragraph-margin gutter accent on Tiptap is dropped — it was
dead in read-only Discuss anyway, and Phase 4 of the Contribute
rewrite adds a change-anchored gutter against the CM6 raw pane.
The Tiptap-side accept-time injection of <span class="tracked-*">
markup also drops out — CM6 can't render those spans, and Phase 3's
preview pane is the proper home for tracked changes. The reviewMode /
DiffView toggle still works against marked-rendered HTML in the
interim until Phase 7 retires it.
Notes:
- Bundle gzipped grows ~50KB for CM6 modules (state/view/commands/
language/lang-markdown). Mermaid in Phase 2 will be the larger lift
and should lazy-load.
- Verified the CM6 editor mounts cleanly via Vite dev preview: ref
handle (`view/getDoc/setDoc`) is wired, `onUpdate` fires on doc
changes, gutter / line-numbers / active-line all render, and the
source round-trips verbatim. Could not drive the full RFCView
Contribute flow end-to-end without a running backend; the manual
countdown + save-now + accept/decline pathways are verified by
inspection only.
- 125 backend integration tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the
§5 schema (six numbered migrations), Gitea OAuth + §6 user
provisioning, the §7 catalog left pane, and the propose-to-merge
vertical: propose modal opens an idea PR against the meta repo, an
owner merges from the pending-idea view, the cache picks it up via
webhook or reconciler sweep, and the catalog renders the new
super-draft.
Per §1 the bot is the only Git writer; every commit, branch
creation, and PR merge carries the §6.5 On-behalf-of: trailer and
an `actions` audit row. Per §4 the cache is never written from a
user action — it's webhook+reconciler only.
Covered by `backend/tests/test_propose_vertical.py` against an
in-process Gitea simulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>