# Session K — Transcript > Date: 2026-05-28 > Goal: Execute Wave 4 of `ohm-rfc/ROADMAP.md` as the autonomous > driver per the "Operating instructions for the next session" > section. The operator is awake-ish but stepped away mid-session; > the driver dispatches each shippable item per wave as a forked > subagent in a single message, serializes the deploys, verifies > each, and writes this transcript at end-of-session. > Wave 4 targets: #7 Admin user-management page (Track C1, v0.9.0), > #9 Trust device 30d (Track C2, v0.11.0), #10 CloudFlare Turnstile > (Track C, v0.12.0 — required to pause on operator-provided > `CLOUDFLARE_TURNSTILE_SECRET`). > > Outcome: **Two of three deployed to OHM live; third tagged and > paused for the operator gesture, as designed.** > > - **#7 Admin user-management page shipped as rfc-app v0.9.0 to OHM > live** (`deploys.id=15`, all 9 phases green, `/api/health` returns > `{"version":"0.9.0","status":"ok"}`, `/admin/users` 200s). > - **#9 Trust device 30d shipped as rfc-app v0.11.0 to OHM live** > (`deploys.id=16`, all 9 phases green, `/api/health` returns > `{"version":"0.11.0","status":"ok"}`). > - **#10 CloudFlare Turnstile tagged as rfc-app v0.12.0** at > `b3f1b15` on `origin` + `benstull`. The OHM deploy is paused — > `flotilla deploy` not yet invoked — until the operator runs the > two MUST gestures from the v0.12.0 CHANGELOG (see operator-plate > §1 below). `ohm-rfc/.rfc-app-version` stays at `0.11.0` for now; > OHM continues to serve v0.11.0 with abuse defense **off**. > > Four new roadmap items captured mid-session from the operator > (added to `ohm-rfc/ROADMAP.md` as items #16, #17, #18, #19 — > see arc 5 below): admin-create user with role + invite email, > repo naming/location alignment, SMTP relay + Gitea webhook > hardening, and CONTRIBUTING guides for rfc-app + ohm-rfc that > link to the public session transcripts. > > **Session-letter correction logged:** the driver's prior commits > in this session (`64f5344`, `54cadd8` on `ohm-rfc`) labeled this > as Session J. The operator corrected mid-session: Session J was > the parallel docs-feature / v0.14.0 session that ran during > Session I's wind-down; this driver session is Session K. The > correction landed in commit `cfc0c6a` on `ohm-rfc` (which fixed > the two prior strikethrough rows). The mislabeled commits stay > as-written — no force-push, no `--amend` — per session-protocol > discipline. --- ## Pre-session state - **rfc-app**: `main` at `de28272` (Release 0.14.0). `VERSION` = `0.14.0`. Tags shipped through v0.14.0; the v0.x lineage so far is `v0.2.0, .1, .2, .3, v0.3.0, v0.4.0, v0.5.0, v0.6.0, v0.7.0, v0.8.0, v0.10.0, v0.13.0, v0.14.0` — note `v0.9.0`, `v0.11.0`, and `v0.12.0` were unclaimed because items #7, #9, #10 hadn't shipped yet. Last migration on disk: `015_passcode.sql`. - **ohm-rfc**: `main` at `d6af91f` (Pin rfc-app to 0.14.0). `.rfc-app-version` = `0.14.0`. - **OHM live**: serving `v0.14.0` per `https://ohm.wiggleverse.org/api/health`. `flotilla deploy log ohm-rfc-app` shows last successful deploy as `deploys.id=14` (`v0.14.0`, succeeded). Two preceding `failed` rows (id=12, 13) were from the parallel docs-feature session retrying. - **ohm-rfc-app-flotilla**: `main` at `a1588c1` (v1.0.1 + CLAUDE.md bake-in). No work needed in this repo for Wave 4. - **ohm-infra**: most recent transcript was Session I, published. The convention says the next letter is J, but the parallel docs-feature session that shipped v0.14.0 between Session I's wind-down and this session's start claimed J — the operator confirmed mid-session that this driver session is Session K rather than J. - **Driver brief**: the operator's bedtime prompt (the one that opens this session) said Wave 4 lineup is #7 v0.9.0 + #9 v0.11.0 + #10 v0.12.0, with #10 pausing on `CLOUDFLARE_TURNSTILE_SECRET`. It also pre-allocated migration slots 016, 017, 018. --- ## Turn-by-turn arc The session moves through five arcs, in order. ### Arc 1 — Reconnaissance + dispatch The driver read `~/git/ohm-infra/SESSION-PROTOCOL.md` (the binding cross-repo session shape) and `ohm-rfc/ROADMAP.md` (the product order). Confirmed pre-session state: rfc-app main at de28272 (v0.14.0), ohm-rfc pin at 0.14.0, OHM live on v0.14.0, last migration 015. The bedtime prompt's claim that "OHM is currently serving v0.10.0" was stale — the parallel docs-feature session shipped v0.14.0 and successfully deployed it as `deploys.id=14` before this session opened. Folded that into the state model and proceeded. The flotilla CLI is invoked as `/Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla/.venv/bin/ohm-rfc-app-flotilla` (the operator's transcript convention writes `flotilla` as shorthand; there's no global `flotilla` binary). Dispatched three subagents in a single `Agent` message with three parallel tool uses, `subagent_type: general-purpose`, no `isolation: worktree` (each subagent creates its own `git worktree add` in `/Users/benstull/git/rfc-app` per Session-I lesson — the `Agent` tool's worktree isolation only covers the flotilla CWD, not the rfc-app CWD subagents need). Each prompt was self-contained: full roadmap text, target version, reserved migration slot, descending-order CHANGELOG insertion point, the hard constraints (do NOT tag, do NOT touch the ohm-rfc pin, do NOT run `flotilla deploy`, no force-push, no `--amend`, no secret bytes, no `git config` edits), and a structured reporting brief. ### Arc 2 — Session κ ships #7 → v0.9.0 Subagent κ branched at `feature/v0.9.0-admin-users` off `origin/main`. Built the `/admin/users` surface (state-filter chips, permission badge, Grant / Revoke, sign-up reason blockquote under pending rows, decided-by/decided-at audit trail). Wired `POST /api/admin/users/{id}/permission` with self-flip-refusal (422) and non-admin gate (403). Added the `new_beta_request` notification event_kind (category `admin-actionable`), fired by `notify.fan_out_new_beta_request` on first complete capture submission. Widened `GET /api/admin/users` payload. Updated BetaPending copy to be honest about cadence (no SLA invented). **No new migration.** The v0.8.0 `014_beta_access.sql` already provides every column the page consumes. Slot 016 stays reserved. **Allowlist decision:** `/admin/allowlist` stays as a sibling sub-tab, not merged. The two surfaces key on different things (allowlist by email pre-sign-up, user list by user_id post-sign-up). Pushed `feature/v0.9.0-admin-users` at `7872b92` to `origin` + `benstull`. Tests: 200 passed (10 new in `test_admin_users_vertical.py`). Frontend build clean. Driver integration: clean fast-forward of `main` onto `7872b92` (both v0.9.0 and v0.11.0 / v0.12.0 branched from de28272, but v0.9.0 is the first to merge so it FFs trivially). Tagged `v0.9.0` on the FF tip; pushed `main` + tag to `origin` + `benstull`. Bumped `ohm-rfc/.rfc-app-version` from `0.14.0` → `0.9.0` (the pin tracks the *latest deployed framework version*, not the highest semver). Ran `flotilla deploy ohm-rfc-app`: opened `deploys.id=15`, all 9 phases green, verify took 4.8s, `/api/health` returned `{"version":"0.9.0","status":"ok"}`. The §8.2 stale-lockfile fault that bit Session I's v0.5.0 deploy did NOT recur — subagent κ ran `npm install` in `frontend/` after bumping `package.json`, the lockfile diff is in the commit, the VM's `git checkout` had nothing to clobber. ### Arc 3 — Session λ ships #9 → v0.11.0 Subagent λ branched at `feature/v0.11.0-device-trust` off `origin/main` (same merge base as κ — de28272). Built the trust- device gesture: checkbox on OTC + passcode verify steps, opaque token + `rfc_device_trust` cookie (HttpOnly + Secure + SameSite=Lax + 30-day Max-Age), `POST /auth/device-trust/start` to re-establish session on returning visit, `/settings/ notifications → Trusted devices` revoke list with per-row + revoke-all controls, three new endpoints (`GET /api/auth/me/ devices`, `DELETE` for one and all). Migration `017_device_trust.sql`: new `device_trust` table — `id`, `user_id` (FK CASCADE), `device_token_hash` (bcrypt, unique idx), `created_at`, `expires_at` (= created + 30 days), `user_agent`, `last_seen_at`, `revoked_at` NULL. Secondary idx on `(user_id, revoked_at)` for fast active-row lookup. Security shape: raw token never logged or stored, only the bcrypt hash; lookup walks the candidate set with `bcrypt.checkpw` for constant-time per row. A handled detail: on a miss, `device_trust_start` returns a hand-built `JSONResponse` (not `HTTPException`) so the cookie-clear `Set-Cookie` header rides on the 401 — FastAPI's exception path drops `set_cookie` calls. Pushed `feature/v0.11.0-device-trust` at `abd3626`. Tests: 204 passed (14 new in `test_device_trust_vertical.py`, covering cookie shape, hash≠raw-token, expired/revoked/forged clears, single + cross-user + revoke-all). Frontend build clean. Driver integration was the first non-FF cherry-pick of the session — both v0.9.0 and v0.11.0 branched from de28272, so once v0.9.0 was on main at 7872b92, v0.11.0's commit had to be cherry-picked. Conflicts: `VERSION`, `frontend/package.json`, `frontend/package-lock.json` (all three on the version-bump line; resolved by taking the v0.11.0 value), `CHANGELOG.md` (auto-merge succeeded — v0.11.0's entry slotted between 0.13.0 and 0.10.0, v0.9.0's entry between 0.10.0 and 0.8.0, no overlap), `SPEC.md` (auto-merge succeeded, both releases appended to §19.2 candidates in separate spots), `backend/app/api.py` (auto-merge succeeded). The post-cherry- pick tree at `6fb68a9` ran 214 tests green (= 190 base + 10 κ + 14 λ); tagged `v0.11.0`, pushed `main` + tag to `origin` + `benstull`. Bumped pin `0.9.0` → `0.11.0`; ran `flotilla deploy ohm-rfc-app`: opened `deploys.id=16`, all 9 phases green, verify 2.4s, `/api/health` returned `{"version":"0.11.0","status":"ok"}`. A snag worth naming: the shared backend `.venv` at `/Users/benstull/git/rfc-app/backend/.venv` does NOT have `app/` on its import path by default — running pytest from that directory requires `PYTHONPATH=.`. The subagents had worked in their own worktrees (where they presumably set up their own pytest config or used `pyproject.toml` adjacency); the driver's verification pytest had to be run with `PYTHONPATH=. .venv/bin/pytest -q` from `backend/`. Not a release fault — just a re-discovery of the test-runner ergonomic. §19.2 candidate: a `backend/conftest.py` or a `backend/pyproject.toml` `[tool.pytest.ini_options] pythonpath = ["."]` that makes pytest invocable without the `PYTHONPATH` rune. ### Arc 4 — Session μ tags #10 → v0.12.0, deploy paused Subagent μ branched at `feature/v0.12.0-turnstile` off `origin/main` (also from de28272). Built the Turnstile path: - `backend/app/turnstile.py` with `VerifyOutcome` enum (`ok`, `skipped`, `misconfigured`, `missing-token`, `failed`, `network`). - `OtcRequestBody.turnstile_token` field; `/auth/otc/request` calls `verify_token` before bcrypt + SMTP so a failed challenge spends no rate budget. - `TurnstileWidget.jsx` React wrapper over the official CloudFlare JS API (dynamic script load, cleanup on unmount). - Widget rendered on email-entry step + on passcode-verify step (for the "Use a code instead" fallback dispatch, which calls the same `/auth/otc/request` endpoint). **No migration.** Slot 018 stays reserved. **`TURNSTILE_REQUIRED` default: `false`.** Rationale: the operator may temporarily forget to set the secret; soft-fail-on-missing- secret keeps `/login` working during the wave-pause. The CHANGELOG MAY-step tells the operator to flip to `true` once they've confirmed a real sign-in works end-to-end so a future config drift fails loud (HTTP 500 "auth misconfigured") instead of silently disabling abuse defense. Pushed `feature/v0.12.0-turnstile` at `f7228d2`. Tests: 195 passed (5 new in `test_turnstile_vertical.py`; `httpx.post` mocked at the `app.turnstile` boundary). Frontend build clean with and without `VITE_TURNSTILE_SITE_KEY` set. Driver integration was the most-conflicted cherry-pick — v0.11.0 and v0.12.0 share the `Login.jsx` verify step (both wanted to inject a child element there) and share `SPEC.md` §19.2 (v0.11.0 moved the "Device trust" candidate to "Settled" + added three new candidates; v0.12.0 moved the "Cloudflare Turnstile" candidate to "Settled"). Resolutions: - `Login.jsx`: keep both — v0.11.0's trust-device checkbox renders first, v0.12.0's `` renders after it. They are orthogonal UI elements on the passcode-verify step. - `SPEC.md` §19.2: keep v0.11.0's "Settled" device-trust entry + all three new v0.11.0-surfaced candidates (cross-device session revocation, password-equivalent change invalidates trust, device-trust window tunables) + v0.12.0's "Settled" Turnstile entry. Dropped v0.11.0's old "still-open Turnstile" candidate text and v0.12.0's old "still-open Device trust" candidate text (both superseded by the Settled-in-this-release entries on the other side of the merge). - `CHANGELOG.md`: the auto-merger combined-but-overlapped the v0.11.0 and v0.12.0 entries (each tried to slot between 0.13.0 and 0.10.0). Resolved by hand-assembling the file into strict descending order: 0.12.0 above 0.11.0 above 0.10.0. The two entries are unaffected internally; the merge was purely about their relative position. - `VERSION`, `frontend/package.json`, `frontend/package-lock.json`: resolved to v0.12.0 (the target). Post-cherry-pick tree at `b3f1b15` ran 219 tests green (= 214 prior + 5 μ). Tagged `v0.12.0`; pushed `main` + tag to `origin` + `benstull`. **Did NOT bump pin.** Did NOT run `flotilla deploy`. The wave is paused on the operator gesture spelled out in the v0.12.0 CHANGELOG `MUST` block: ``` flotilla secret set ohm-rfc-app CLOUDFLARE_TURNSTILE_SECRET flotilla overlay set ohm-rfc-app VITE_TURNSTILE_SITE_KEY # Optional after end-to-end verification: flotilla overlay set ohm-rfc-app TURNSTILE_REQUIRED true ``` Once the operator confirms both env values are set, they (or the next driver session) bump the pin `0.11.0` → `0.12.0` and run `flotilla deploy ohm-rfc-app`. With `TURNSTILE_REQUIRED=false`, deploying without the secret is non-fatal — `/login` still serves but abuse defense is silently off; the pause exists for *rollout correctness*, not crash-prevention. ### Arc 5 — Operator adds #16, #17, #18, #19 mid-session While the driver was integrating v0.9.0, v0.11.0, and v0.12.0, the operator sent four asynchronous roadmap-add messages. Captured and added to `ohm-rfc/ROADMAP.md`: - **#16 Admin-create user + invite email (custom message).** rfc-app, Track C, depends on #5 + #7. The new affordance lives on the v0.9.0 `/admin/users` surface: admin types first/last/ email, picks a role, optionally adds a custom message text, clicks Create — the system sends an invite email with a claim link. Distinguishes from #12 (per-RFC contribution invite) by being platform-level access provisioning rather than per-RFC membership. - **#17 Repo naming + location alignment across `git.wiggleverse.org` + `git.benstull.org`.** Track Ω, no rfc-app version. Inventory + propose + execute the moves (personal-namespace `rfc-app` → `wiggleverse/`, mirror convention, per-deployment-flotilla naming rule documentation), with tombstone redirects + cross-repo reference updates. Likely a multi-session arc. - **#18 Secure the SMTP relay + (Gitea) webhook.** Partly Track Ω (SMTP creds, TLS, SPF/DKIM/DMARC, ingress restriction, bounce surfacing), partly Track C (webhook signature verification, replay-resistance, ingress). One session to inventory + fix easy wins; follow-ups for anything that turns into a framework release. - **#19 CONTRIBUTING guides + transcript-linked onboarding.** Two `CONTRIBUTING.md` files (rfc-app + ohm-rfc) that point outside contributors at the public session transcripts at `wiggleverse/ohm-session-history` as the authoritative development arc, plus the explicit boundary: contributors open PRs; the **operator alone** approves, merges, bumps the pin, sets secrets, and runs `flotilla deploy`. Operator's phrasing: "I'm the only one that gets to yolo." All four added as prose sections after #14 and as rows in the version-target table. Items #16 and #17 landed in commit `64f5344` on `ohm-rfc` (alongside the v0.9.0 pin bump); #18 landed in `cfc0c6a` (alongside the v0.12.0 status row); #19 landed in `cce9fe3` as a standalone roadmap commit at the very end of the session. The mid-session correction — "Not session J — session K" — landed in `cfc0c6a` too: it fixed the two prior strikethrough rows (`#7` v0.9.0 and `#9` v0.11.0) to attribute Session K, not J. The two prior commits with the wrong attribution (`64f5344`, `54cadd8`) stay as written, with the correction documented in this transcript per session-protocol §3 (no curation, no amend-after-push). --- ## Cut state (end of session) | Repo | State | | --- | --- | | rfc-app | `b3f1b15` tag `v0.12.0` (latest). Tags pushed to `origin` + `benstull` through v0.14.0, v0.13.0, v0.12.0, v0.11.0, v0.10.0, v0.9.0, v0.8.0, v0.7.0, v0.6.0, v0.5.0, v0.4.0, v0.3.0, v0.2.x. CHANGELOG strict-descending; SPEC.md §19.2 carries five new candidates from this session (see below). | | OHM live | `deploys.id=16`, `v0.11.0`, healthy. `/api/health` returns `{"version":"0.11.0","status":"ok"}`. `/admin/users` 200s. `/settings/notifications` 200s. v0.12.0 deploy is paused on operator secret. | | ohm-rfc | `cce9fe3`. `.rfc-app-version` = `0.11.0`. ROADMAP reflects #7 + #9 shipped, #10 tagged-but-paused, #16 + #17 + #18 + #19 captured. | | ohm-rfc-app-flotilla | `a1588c1` (unchanged — no flotilla work in Wave 4). | | ohm-infra | this transcript at `SESSION-K-TRANSCRIPT-2026-05-28T03-20--2026-05-28T03-55.md`; publish pending. | | Wave 4 ledger | Status | | --- | --- | | #7 Admin user-management page (v0.9.0) | ✅ shipped — deploys.id=15 | | #9 Trust device 30d (v0.11.0) | ✅ shipped — deploys.id=16 | | #10 CloudFlare Turnstile (v0.12.0) | 🟡 tagged at b3f1b15, deploy paused on operator gesture | --- ## §19.2 candidates surfaced Numbered list of architectural / process / spec deferrals this session uncovered. 1. **Cross-device session revocation surface** (v0.11.0). `/settings/notifications → Trusted devices` revokes device-trust rows. It does NOT revoke active session cookies in another browser, or v0.10.0's passcode-failure counter, or future credential shapes. The natural next step is one "active sessions and devices" page that lists every credential authenticating as this user. Earns its session when a second cross-cutting concern lands — most likely WebAuthn / Yubikey support. 2. **Password-equivalent change invalidates device trust** (v0.11.0). v0.11.0 intentionally leaves device-trust rows live across passcode set / change / clear. The argument is structural; the argument against is conventional. Earns its session on either a security-review finding or user feedback. 3. **Device-trust window tunables via env** (v0.11.0). The 30-day window is hard-coded as `TRUST_DURATION_DAYS = 30` in `backend/app/device_trust.py`. Surface as `DEVICE_TRUST_DURATION_DAYS` env var when a deployment asks. 4. **Grant/revoke user-facing notification** (v0.9.0). The `personal_change_affecting_me` enum already exists; v0.9.0 doesn't fire it on grant/revoke. Small follow-up. 5. **Decline-with-reason on Revoke** (v0.9.0). Free-text into `permission_events.details` JSON; no schema change. Small follow-up. 6. **Allowlist deprecation** (v0.9.0). The `/admin/allowlist` surface stays as a sibling sub-tab in v0.9.0; the question of whether to retire the `allowed_emails` table once `/admin/users` handles every pending-user case is deferred to post-v0.9.0 operator experience. 7. **backend pytest `PYTHONPATH=.` rune** (driver discovery during v0.11.0 integration). The shared `backend/.venv` doesn't put `app/` on the import path; `pytest` from `backend/` requires `PYTHONPATH=.`. A `backend/conftest.py` or a `backend/pyproject.toml` `[tool.pytest.ini_options]\npythonpath = ["."]` fixes it. Trivial; flag for a future cleanup session. (The §3-invariant-1 hygiene is unchanged: no secret bytes anywhere in any of the commits or this transcript. `CLOUDFLARE_TURNSTILE_SECRET` is referenced by name; its bytes live only in the operator's hand.) --- ## What lands on the operator's plate 1. **Run the v0.12.0 wave-pause gesture and re-enter the wave.** The CloudFlare Turnstile release is tagged but not deployed. The operator must: ``` /Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla/.venv/bin/ohm-rfc-app-flotilla \ secret set ohm-rfc-app CLOUDFLARE_TURNSTILE_SECRET /Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla/.venv/bin/ohm-rfc-app-flotilla \ overlay set ohm-rfc-app VITE_TURNSTILE_SITE_KEY ``` then (in `ohm-rfc/`): ``` echo 0.12.0 > .rfc-app-version git add .rfc-app-version git commit -m "Pin rfc-app 0.12.0 (Turnstile)" git push origin main ``` then back in the flotilla CWD: ``` /Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla/.venv/bin/ohm-rfc-app-flotilla \ deploy ohm-rfc-app ``` After verification with a real sign-in, flip the production- locked shape: ``` /Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla/.venv/bin/ohm-rfc-app-flotilla \ overlay set ohm-rfc-app TURNSTILE_REQUIRED true ``` The next driver session will strikethrough `#10` in `ohm-rfc/ROADMAP.md` once the deploy completes successfully. 2. **Decide phasing for items #16, #17, #18, #19.** Four new roadmap items captured this session; pick which wave each rides. #16 (admin-create user + invite) is naturally a Wave-5 candidate alongside #12. #17 (repo naming) is operator-led; the inventory step can be a Track-Ω subagent but the rename gestures are live moves. #18 (SMTP + webhook hardening) is similar — partly subagent-doable, partly operator-led. #19 (CONTRIBUTING guides) is a Track-Ω docs PR that can ride alongside any other item; a subagent can draft it and the operator approves + merges as a normal PR. 3. **Cleanup**: there are no leftover worktrees on `/Users/benstull/git/rfc-app` — all three subagent worktrees were removed at the end of the session. 4. **§19.2-list follow-up.** Seven candidates surfaced this session (see above). None blocks the next wave. The pytest-PYTHONPATH one is trivial enough to be folded into the next session's warm-up. 5. **Session-letter correction documentation**: this transcript records the J→K correction explicitly. If the parallel docs-feature session (the real Session J) hasn't yet published its transcript, that's its own session's responsibility — not this one's. --- ## Prompt the operator can paste into the next Claude Code session ``` You are the OHM roadmap driver. The previous session (Session K) shipped rfc-app v0.9.0 (admin user-management page, item #7, deploys.id=15) and v0.11.0 (trust device 30d, item #9, deploys.id=16) to OHM. v0.12.0 (CloudFlare Turnstile, item #10) is tagged at `b3f1b15` on origin + benstull but the OHM deploy is paused on `CLOUDFLARE_TURNSTILE_SECRET` + `VITE_TURNSTILE_SITE_KEY` (see SESSION-K transcript §"operator plate"). OHM serves v0.11.0 until the operator clears the pause. Four new roadmap items landed mid-Session-K from the operator: - #16 admin-create user + invite email (with optional custom message), rfc-app, depends on #5 + #7. - #17 repo naming + location alignment across `git.wiggleverse.org` + `git.benstull.org`, ohm-infra + cross- repo, Track Ω. - #18 secure the SMTP relay + Gitea webhook, partly ohm-infra, partly rfc-app framework hardening. - #19 CONTRIBUTING guides for rfc-app + ohm-rfc that link to the public session transcripts at `wiggleverse/ohm-session-history`, with the explicit boundary that contributors open PRs and the operator alone bumps pins + sets secrets + runs flotilla deploy. Read `~/git/ohm-infra/SESSION-PROTOCOL.md` and `/Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md` end-to-end. Pick a path: A. **Finish Wave 4** by clearing the v0.12.0 pause: if the operator has set `CLOUDFLARE_TURNSTILE_SECRET` and `VITE_TURNSTILE_SITE_KEY`, bump pin to 0.12.0 and deploy. Verify `/api/health` returns `{"version":"0.12.0","status":"ok"}` and that a `/login` page renders the Turnstile widget. Then strikethrough `#10` in ROADMAP. Then ask the operator before flipping `TURNSTILE_REQUIRED=true`. B. **Start Wave 5** if Wave 4 is clear and the operator wants to advance: - Session ν (Track B): #12 Owner invite → v0.16.0 (slot 14 was claimed by docs). - Session ξ (Track A): #13 Amplitude → v0.15.0 (operator must provide `AMPLITUDE_API_KEY` — same wave-pause pattern as #10). - Optionally session ρ: #16 Admin-create user + invite → next available slot. Lives on the v0.9.0 surface; subagent should read `frontend/src/components/Admin.jsx` and `backend/app/api_admin.py` for the create-user affordance, plus `backend/app/email.py` for the invite-email template. Wave-5 dispatch should reuse Session K's subagent pattern: each subagent creates its own `git worktree add` in `/Users/benstull/git/rfc-app`, pushes a feature branch, does NOT tag, does NOT touch the ohm-rfc pin, does NOT run flotilla deploy — the driver integrates serially. The cherry-pick conflicts are all in the same usual places: `VERSION`, `frontend/package.json`, `frontend/package-lock.json`, `CHANGELOG.md` (strict descending order), `SPEC.md` §19.2, occasionally `Login.jsx`. Session-K lessons to apply automatically: - Subagents push feature branches only. Driver tags, bumps the pin, deploys. - Migration slots: 016 + 018 stayed reserved (no schema change needed for #7 or #10). 017 was used by #9. Next free is 016 or 018 depending on what's next. - CHANGELOG inserts go in version-descending order. After v0.12.0 the file is: 0.14.0 → 0.13.0 → 0.12.0 → 0.11.0 → 0.10.0 → 0.9.0 → 0.8.0 → ... — subagents inserting a new entry slot it at the right position; cherry-pick conflicts get hand-assembled if two features ship at adjacent slots. - backend pytest needs `PYTHONPATH=.` from `backend/` unless someone fixes the §19.2 candidate. - `Login.jsx` is now contested territory — two consecutive releases (v0.11.0 + v0.12.0) edited the same component on the verify step. Expect a conflict any time #16's admin-create-user invite-claim flow shares the path. - Item #1 VM rename and operator-provided secrets stay operator territory. End-of-session: write `~/git/ohm-infra/SESSION-L-TRANSCRIPT-…md` and publish via `~/git/ohm-infra/scripts/publish-transcript.sh`. ```