Files
session-history/SESSION-K-TRANSCRIPT-2026-05-28T03-20--2026-05-28T03-55.md
T

26 KiB
Raw Blame History

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.00.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.00.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 <TurnstileWidget> 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 <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.00.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-appwiggleverse/, 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 <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`.