Commit Graph

36 Commits

Author SHA1 Message Date
Ben Stull 1456c8b73f Release 0.17.0: admin-create user + invite email (+ #21 Part C Amplitude wiring)
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>
2026-05-28 05:10:52 -07:00
Ben Stull ee4925b6ac Release 0.16.0: owner-only invite for per-RFC contribution + discussion (+ #21 Part C Amplitude wiring)
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>
2026-05-28 05:06:55 -07:00
Ben Stull 72f8457933 Release 0.15.0: Amplitude Analytics + Session Replay (with #21 Part C identity lifecycle)
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>
2026-05-28 05:04:10 -07:00
Ben Stull b3f1b15f65 Release 0.12.0: CloudFlare Turnstile on OTC email-entry 2026-05-28 03:52:45 -07:00
Ben Stull 6fb68a95c7 Release 0.11.0: trust device for 30 days 2026-05-28 03:46:24 -07:00
Ben Stull 7872b921ed Release 0.9.0: admin user-management page + new-request notifications 2026-05-28 03:39:25 -07:00
Ben Stull de28272914 Release 0.14.0: public /docs user guide (DOCS.md served at /docs)
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>
2026-05-28 03:08:28 -07:00
Ben Stull 55beba5c0a Release 0.10.0: user-set passcodes (OTC stays as fallback)
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>
2026-05-28 02:39:27 -07:00
Ben Stull ca8ba69acb Release 0.8.0: open beta-access request flow (first/last/why)
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>
2026-05-28 02:25:59 -07:00
Ben Stull 8aa65014b4 Release 0.13.0: cookie/privacy consent banner + policy pages (instrumentation prep)
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>
2026-05-28 01:08:23 -07:00
Ben Stull f8e797ab09 Release 0.7.0: email/OTC sign-in (Gitea OAuth retained as fallback)
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>
2026-05-28 01:03:18 -07:00
Ben Stull 21743a08b1 Release 0.6.0: anonymous-write gates audit + hardening
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>
2026-05-28 00:47:49 -07:00
Ben Stull c92730a737 Release 0.5.0: PR-less per-RFC discussion (contribution still requires PR)
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>
2026-05-27 23:04:52 -07:00
Ben Stull 0f8b318afa Release 0.4.0: auto-set RFC owner = proposer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:53:09 -07:00
Ben Stull 21fcbc92d4 Release 0.3.0: private-beta gate + anonymous read mode
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>
2026-05-27 20:58:58 -07:00
Ben Stull 1a9374aa52 Release 0.2.3: /api/health framework dependency for flotilla
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>
2026-05-26 19:40:40 -07:00
Ben Stull 018e323ed4 Release 0.2.2: Philosophy.jsx now renders mermaid (uses MarkdownPreview)
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>
2026-05-26 13:31:49 -07:00
Ben Stull 0e1805b8ce Release 0.2.1: PRView Rules-of-Hooks fix (React #310 blank page)
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.
2026-05-26 08:50:48 -07:00
Ben Stull 29c96ea300 Release 0.2.0: graduation merge race fix + VITE_APP_NAME + versioning infrastructure
- 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.
2026-05-26 06:37:48 -07:00
Ben Stull 46049d296b Contribute rewrite Phase 7: retire DiffView
The Phase 3 preview-pane inline overlay and Phase 4 raw-pane gutter
accent together cover the review affordance DiffView was the interim
home for, so the toolbar toggle collapses and the surface goes.

- frontend/src/components/DiffView.jsx deleted.
- RFCView.jsx: drop the DiffView + marked imports, the reviewMode /
  reviewHTML state, the toggleReviewMode callback, the "Review changes"
  toolbar button, and the reviewMode ternary in render. editorEditable
  simplifies to mode === 'contribute' && canContribute. File-header
  docstring updated: "DiffView" → "preview-pane tracked-change overlay
  (§8.10)".
- App.css: remove the .diff-view-wrapper and .diff-view-empty rules;
  relabel the DiffView section header as ChangeTooltip (which lives on
  as the Phase 3 overlay's hover affordance). Drop the dead .editor-
  content .tiptap rules — Tiptap had no remaining consumers after
  DiffView's deletion. The Contribute toolbar still renders the
  accepted/pending hint + Phase 6 Chat toggle; only the Review-changes
  button is removed.
- MarkdownPreview.jsx: comment updated to drop the DiffView mention
  from the scoped-Marked-instance rationale.
- SPEC.md: §8.10 retitled "Tracked-change markup" (was "...and the
  review-mode toggle"); the legacy-DiffView paragraph rewritten as a
  one-sentence retirement note. §8.1, §8.15, §9.4, §9.5 references to
  DiffView / review-mode cleaned up. §19.2's persistent-accepted-change
  candidate updated to point at the preview-pane overlay rather than
  DiffView.

No DiffView orphans remain (grep confirms .btn-review-toggle is still
the Discuss-mode "Show tracked changes" toggle, ChangeTooltip and
trackedOverlay carry historical comments only, PRView's §19.2 comment
is unrelated). Frontend reload shows no console errors.

Backend integration suite: 125 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:33:14 -07:00
Ben Stull 49e91f8829 Contribute rewrite Phase 6: narrow-viewport right-panel drawer
Below 1280px the right-panel (chat + change-card panel) collapses into
an off-canvas drawer toggled from a Chat button in the editor toolbar.
Above 1280px the inline three-column layout is unchanged. Replaces the
Phase 2 narrow-viewport-banner stopgap.

- drawerOpen useState in RFCView; per-session, default closed; tied to
  a .rfc-body.drawer-open class plus .right-panel.drawer-open and the
  backdrop's data-open attribute.
- ChatDrawerToggleButton in each editor-toolbar branch (Contribute,
  Discuss-with-accepted-changes, and a narrow-only fallback toolbar
  for the default state). Badge mirrors change-panel-header: pending
  AI changes + the buffered manual card. Hidden above 1280px via CSS.
- .right-panel becomes position: fixed; top: 48px; right: 0; width:
  min(420px, 92vw); transform: translateX(100%) by default; slides in
  with .drawer-open. Backdrop sibling with opacity transition handles
  click-to-close.
- Escape closes the drawer when open (useEffect listener gated on
  drawerOpen, so it's a no-op when closed).
- Toolbar gets padding-right shifted to drawer-width when the drawer
  is open at narrow widths so the toggle stays clickable to close
  (otherwise it'd sit under the open drawer).
- @media (min-width: 1280px) restores .right-panel to inline flex
  (360px, position: static), hides the backdrop, hides the toggle
  button, and hides the narrow-only toolbar fallback.
- Removes the .narrow-viewport-banner JSX + its CSS block + its
  @media display:none rule.

SPEC.md §8.1 gains a paragraph noting the narrow-viewport drawer
collapse — explicit acknowledgment that the three-column shape adapts
below ~1280px without re-architecting around responsive primitives.

Verified in Vite preview sandbox at 1024px (drawer offscreen,
backdrop opacity 0, toggle visible) and 1440px (inline three-column,
drawer fixed-positioning reset, toggle hidden, backdrop display:none).
Backend was down this session (proxy 502 on /api/me) so the cumulative
Phase 2–5 end-to-end verification gap is still open; sandbox proof
only, as called out in the phase prompt.

Backend integration suite: 125 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:02:52 -07:00
Ben Stull f1d126e991 Rename landing page to Open Human Model with Wiggleverse RFC attribution
The 5dbcac8 reframe established OHM as the corpus and Wiggleverse RFC
as the substrate. The landing page and the global header still read
"Wiggleverse RFCs," which puts the substrate's name in front of a
visitor when what they're actually visiting is OHM. Flip both surfaces.

- App header brand and Landing's H1 → "Open Human Model"
- Subtitle becomes "An open dictionary of the words humans and
  machines need to agree on" — names the corpus, not the process tool
- Pitch paragraph reworded to lead with "the Open Human Model is the
  corpus this framework produces," with first RFC defining "human"
- New .landing-attribution block at the foot of the deck: "OHM is run
  by Ben on Wiggleverse RFC — the open-source standardization-process
  software that powers it. Anyone can run their own collaboration on
  the same substrate." Reader can now tell instance-vs-substrate at
  a glance
2026-05-25 11:37:27 -07:00
Ben Stull 4afb018bb0 Contribute rewrite Phase 5: inline word-diff in manual pending card
The §8.11 manual-edit card was a stub — "{N} paragraphs edited
directly" plus a countdown and a Save-now button. The spec says the
card should grow one inline word-diff per touched paragraph as the
contributor types, in the same green/red register the Phase 3 accepted
overlay uses. Phase 5 lands that.

In RFCView.jsx, the 800ms-debounced handleEditorUpdate now computes a
per-paragraph token diff alongside the existing paragraph count and
stores both on manualPending. Baseline is the same Phase 4 gutter
source — originalSourceLinesRef.current, the last server-confirmed
body split on blank lines — so the card resets to empty the same
moment the gutter clears (accept/decline/manualFlush/branch-switch).

In ChangePanel.jsx, diffWords is exported (it was already the AI
card's inline-diff engine — token-level LCS over a whitespace-
preserving /(\\s+)/ split, ~30 lines, no runtime dep). The manual
card consumes the same tokens. Wholly-inserted paragraphs render as
all-insert blocks; wholly-deleted paragraphs as all-delete blocks.

Visual register is intentionally shared with Phase 3's preview
overlay: same #dcfce7/#166534 inserts, same #fee2e2/#991b1b
strike-through deletes. Selectors are scoped under .change-manual-diff
rather than reusing .markdown-preview .tracked-* since the card lives
outside the preview surface.

Pre-fancy stance, matching Phase 4's gutter: the diff is index-aligned
against the baseline, so adding a paragraph in the middle lights up
the rest of the doc. Tolerated as the "you've touched stuff below this
point" cue. An LCS-anchored future pass can fix it.

Verification gap matching Phases 2/3/4: backend was not running this
session, so the live RFCView → real-branch flow wasn't exercised.
Drove the Vite preview sandbox instead — mounted ChangePanel with a
hand-built diffs payload, confirmed three blocks render (mixed edit,
wholly-inserted, wholly-deleted), inserts/deletes carry the expected
computed colors, no console errors. Backend integration suite still
green (125 passed).
2026-05-25 11:29:03 -07:00
Ben Stull 886bbf5512 Contribute rewrite Phase 4: §8.10 gutter accent in CM6 raw pane
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>
2026-05-25 11:00:45 -07:00
Ben Stull 2a7c099a33 Contribute rewrite Phase 3: tracked-change overlay in MarkdownPreview
Reintroduces the §8.10 inline tracked-change layer Phase 1 dropped when
Tiptap left, this time anchored to the rendered preview rather than a
writable editor. Each accepted change on the branch decorates the
preview DOM with a `<span class="tracked-insert">` at the proposed text
and a `<span class="tracked-delete">` strikethrough for the original;
hover surfaces the same ChangeTooltip DiffView uses (badge + prompt +
quote + reason), now extracted to its own file so both surfaces share
the affordance.

Design calls per the Phase 3 prompt's open list:
  • Contribute pane: default-on. The pane exists for editorial review
    of your own work; the overlay reinforces that.
  • Discuss pane: opt-in via a new "Show tracked changes" toolbar
    toggle on non-main branches. Clean prose stays the default.
  • DiffView's "Review changes" toolbar is untouched — DiffView dies in
    Phase 7 and overloading its toggle now creates surface to unwind.

Pre-fancy stance on the known overlay subtleties:
  • Drift — if `proposed` no longer appears verbatim (later manual
    edit touched it) we skip the decoration rather than mis-anchor.
  • Overlap — decorate in `acted_at` ascending order; later spans win
    on whatever text is still un-wrapped.
  • Mermaid — text nodes inside `.mermaid-block` are skipped; a tracked
    span that intersects diagram source drops silently. Flagged as a
    known limitation rather than worked around.
  • Code — `<pre>`/`<code>` parents skipped too; matching inside
    verbatim code produced noisy false positives in fixtures.

Backend tests: 125 passed. Helper verified via Vite preview sandbox
eval across eight cases (single-paragraph match, distinct-original
strike, no-match skip, mermaid skip, two-change overlap ordering,
declined/pending ignored, whitespace-normalized cross-newline match,
code-block skip), plus computed-style proof for the new
`.markdown-preview .tracked-insert` / `.tracked-delete` rules. The
end-to-end CM6 → accept → overlay flow against a live branch wasn't
exercised (backend not running this session); the simpler unit-level
verification looked clean, but a future session with the backend up
should drive that golden path before Phase 4 piles on top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:44:09 -07:00
Ben Stull ee6e3491e7 Drop "prototype/carryover" framing now that v1 is shipped
SPEC, DEV docs, and code comments still talked about the codebase as
a rewrite-in-progress against an external prototype. With v1 shipped
the framing reads oddly — it implies code is provisional when it's
the production thing. Recast §18 as "the technical stack," strip
"carryover from the prototype" comments across backend (api.py,
chat.py, providers.py) and frontend (DiffView, PromptBar,
SelectionTooltip, modelStyles), and rework SPEC §1 / §18 to introduce
OHM up front rather than as a follow-on to a prototype reference.

Also:
- RUNBOOK: bump Python prereq to 3.11+ to match the production VM
  (was 3.13).
- Remove IMPLEMENTATION-PROMPT.md — the original implementation brief
  is no longer load-bearing.
- Add deploy/DEPLOY-NEW-SESSION-PROMPT.md as the durable
  deploy-handoff prompt for new sessions.
2026-05-25 10:32:46 -07:00
Ben Stull 7c3b8fc133 Contribute rewrite Phase 2: split-pane preview with mermaid
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>
2026-05-25 10:24:19 -07:00
Ben Stull 13d59b5d26 Contribute rewrite Phase 1: CM6 markdown source editor
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>
2026-05-25 09:49:17 -07:00
Ben Stull a255429e57 Post-v1: per-RFC model availability (UX half) folded into §6.6
First §19.2 candidate settled after v1. The heavier per-RFC-model
topic subdivided into UX (this) and credential delegation + funder
role (still §19.2). New §6.6 carries the rule: an optional `models:`
frontmatter field on the meta-repo RFC entry; absent inherits the
operator universe, populated narrows the picker to the intersection
with provisioned providers, `[]` opts the RFC out of AI entirely.
The first resolved entry is the RFC default. §18's ENABLED_MODELS is
reframed as the operator universe.

Code: migration 009 adds nullable cached_rfcs.models_json (NULL ≠ []
is load-bearing); entry.py grows the optional field with absent-vs-
empty round-tripping in parse/serialize; new models_resolver module
holds the rule; api_branches replaces /api/models with the slug-aware
/api/rfcs/{slug}/models and threads the chat + reask paths through
the resolver; api_prs §10.2 uses the resolver and extends the stub
fallback to the opt-out case; frontend passes slug to listModels.

Tests 106/106 green (96 prior + 10 in test_per_rfc_models.py). No
behavioral change for entries without `models:` — operator universe
preserved as default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:42:15 -07:00
Ben Stull 060fa408a2 Slice 7: §14 chrome + settings and admin neighborhoods
§14.1 richer landing, §14.2 /philosophy route (disk-backed), §14.3
persistent About link. /settings/notifications surfaces Slice 6's
preferences/quiet-hours/mute/watches endpoints. /admin home base
consolidates role management, the §6.2 write-mute, the audit-log
viewer, the permission-events log, and the §13.2 graduation queue.

Backend: backend/app/philosophy.py, backend/app/api_admin.py (seven
admin endpoints + user-search), GET /api/users/me/notification-mutes.
Frontend: Landing.jsx (deck), Philosophy.jsx, NotificationSettings.jsx,
Admin.jsx, App.jsx routing for the chrome surfaces.

Tests: backend/tests/test_chrome_vertical.py — 13 cases. Full suite
75/75 green.

Spec corrections: §14.2 (PHILOSOPHY.md source is a deployment-time
decision), §17 (admin block extended to name the seven new endpoints
+ user-search and notification-mutes read). §19.1 rewritten for
Slice 8 hardening; §19.2 grew four candidates (owner succession,
mute-from-actor, the "Following since <date>" disclosure, audit-log
row prose).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:40:49 -07:00
Ben Stull f67d0aa0db Slice 6: notifications per §15
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:09:04 -07:00
Ben Stull 1b0968a9a2 Slice 5: graduation per §13
The §13.3 transactional sequence flips a super-draft to active —
five steps with paired undoes, an in-process orchestrator fed by
an asyncio.Queue, the §17 SSE endpoint streaming step transitions
to the dialog. Each step is a new bot primitive that logs an
`actions` row, bracketed by `graduate_start` / `graduate_complete`
for the linkable audit sequence. Rollback runs the undoes in
reverse from the last completed step; merge_pr has no undo by
design per §13.5.

The §9.8 precondition gate is enforced server-side at the top of
POST /graduate so the §13.3 rollback complexity does not grow.
The §13.4 chat migration is a database semantic no-op — the
(slug, branch_name='main') threads keep their identity, only the
interpretation changes. The §9.8 pre-graduation history surfaces
via a new _is_meta_target(rfc, branch) dispatch helper and lands
as pre_graduation_history on /main.

§13.1 claim flow landed alongside since it's the prerequisite for
non-admin graduation — bot.open_claim_pr plus broadening
api_prs._require_pr to accept meta_claim.

45/45 tests green; ten new integration tests cover the validator,
the §9.8 precondition refusal, happy path with audit verification,
mid-sequence rollback at steps 2 and 3, concurrent refusal,
chat-survives-without-data-movement, pre-graduation history, and
the §13.1 claim PR cycle.

SPEC.md §19.1 rewritten for Slice 6 (notifications); §19.2 grew
four candidates surfaced during the slice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:52:29 -07:00
Ben Stull 4565a6cb95 Slice 4: super-draft body editing per §9.5 + §9.6
The §17 routing-collapse rule lands in api_branches.py and
api_prs.py — every branches/<branch>/... and prs/<n>/... route
dispatches on the entry's state to pick the right Gitea repo, and
the body extracted from the entry's frontmatter envelope is what
the editor and the diff see. The bot grows open_metadata_pr;
cache grows refresh_meta_branches. Two §17 routes added:
start-edit-branch and metadata. The §9.4 super-draft view replaces
RFCView.jsx's Slice 2 placeholder; a metadata pane modal opens
from the breadcrumb. Branch naming uses edit-<slug>-<6hex> to
dodge the §19.2 path-routing candidate while preserving §9.5's
structural shape.

Covered by tests/test_super_draft_vertical.py (10 tests). The
full Slices 1-4 suite is 35/35 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:43:21 -07:00
Ben Stull a2bf89e90b Slice 3: the PR flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:37:54 -07:00
Ben Stull 3bc8fe92af Slice 2: the §8 active-RFC view in full
Per the §19.1 brief: the three-column shape (§8.1) opens on main
in discuss mode (§8.2), supports the §8.3 discuss-vs-contribute
flip on non-main branches, hosts §8.4's per-branch chat with AI
participation (§18's <change> protocol → §8.14 changes rows), the
§8.8 change-card panel with §8.9 accept/decline/edit-before-accept,
the §8.10 tracked-change markup + DiffView toggle, the §8.11
manual-edit flushes with the stale-change mechanic, the §8.12
range and paragraph sub-threads, the §8.13 flag affordance, and
the §8.14 discuss-mode buffer.

Backend: bot.py grew per-RFC-repo write ops (cut_branch_from_main,
commit_accepted_change with the structured original/proposed/reason
body and Change-Id + Source-Message-Id + On-behalf-of trailers,
commit_manual_flush, ensure_rfc_repo_seed). cache.py grew
refresh_rfc_repo and the webhook dispatches on repository.full_name.
providers.py and chat.py port the §18 carryovers — multi-provider
LLM abstraction and SSE-streaming chat against the §5 threads /
thread_messages / changes schema. api_branches.py mounts the §17
branches/<branch>/* and threads/<thread_id>/* routes with the §6
/ §11 permission checks inline.

Frontend: RFCView.jsx rebuilt as the §8 surface; Editor.jsx,
ChatPanel.jsx, ChangePanel.jsx, PromptBar.jsx, SelectionTooltip.jsx,
DiffView.jsx, ModelPicker.jsx, modelStyles.js lifted from the
prototype and adapted to the canonical schema.

Covered by `backend/tests/test_rfc_view_vertical.py` — eleven new
integration tests against an extended FakeGitea (PUT contents,
POST orgs/{org}/repos, seed_rfc_repo): main-view read,
promote-to-branch, accept (with and without edit-before-accept),
decline, manual flush + system message, flag creation, visibility
flip, anonymous read-but-no-contribute, stale-change refusal, and
the chat-streaming path with a fake provider injected. The 5
Slice 1 tests continue to pass alongside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:35:14 -07:00
Ben Stull 779ba6db59 Slice 1: scaffolding + propose-to-super-draft vertical
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>
2026-05-24 04:31:11 -07:00