# Session 0012.0 — Transcript

> Date: 2026-05-28
> Goal: Execute Wave 5 of `ohm-rfc/ROADMAP.md` as the autonomous
> driver per the "Operating instructions for the next session"
> section. The operator's bedtime prompt asked for three items
> dispatched in parallel — **#13 Amplitude → v0.15.0** (Track A;
> originally wave-paused on operator-provided `AMPLITUDE_API_KEY`),
> **#12 Owner-only invite → v0.16.0** (Track B), and
> **#16 Admin-create user + invite email → v0.17.0** (Track C
> extension; layers on top of v0.9.0's `/admin/users` surface).
> Mid-session, the operator returned to the keyboard and added
> three roadmap items + a meaningful enlargement of #13's scope.
>
> Outcome: **All three Wave-5 features shipped to OHM live.**
>
> - **#13 Amplitude shipped as rfc-app v0.15.0 to OHM live**
>   (`deploys.id=19`, all 9 phases green, `/api/health` returns
>   `{"version":"0.15.0","status":"ok"}`). Vendor's recommended
>   `@amplitude/unified` package with `initAll(KEY, { analytics:
>   { autocapture: true }, sessionReplay: { sampleRate: 1 } })`.
>   Bound via `flotilla overlay set ohm-rfc-app
>   VITE_AMPLITUDE_API_KEY=…`, not `secret set` — Amplitude
>   browser keys are bundle-embedded (same nature as
>   `VITE_TURNSTILE_SITE_KEY`). The "wave pause for operator
>   secret-set" the bedtime prompt anticipated didn't materialize
>   in that shape; instead the operator returned mid-session,
>   provisioned the Amplitude project, surfaced the vendor's
>   installation prompt (which contained the public key inline),
>   and the binding settled as overlay. Conversation-layer hard
>   rule respected: the key value was operator-provided
>   voluntarily, not asked for.
> - **#12 Owner-only invite shipped as rfc-app v0.16.0 to OHM
>   live** (`deploys.id=20`, all 9 phases green, `/api/health`
>   returns `{"version":"0.16.0","status":"ok"}`). Per-RFC
>   `contributor` / `discussant` roles, transactional email,
>   migration `018_rfc_invitations.sql`.
> - **#16 Admin-create user + invite shipped as rfc-app v0.17.0
>   to OHM live** (`deploys.id=21`, all 9 phases green,
>   `/api/health` returns `{"version":"0.17.0","status":"ok"}`).
>   Admin-driven user provisioning with optional custom message,
>   opaque-token claim flow, migration `019_user_invite_tokens.sql`.
>
> Plus three new roadmap items captured mid-session
> (`ohm-rfc/ROADMAP.md` items #20, #21, #22 — see arc 7 below):
> email deliverability (DNS + SMTP + template hygiene), Amplitude
> audit + standing instrumentation discipline (with Part C
> identity-lifecycle work folded inline into all three v0.15.0 /
> v0.16.0 / v0.17.0 release commits), and pro-analytics-consent
> copy on the privacy/cookies opt-in (with legal caveats spelled
> out so the drafting accounts for 501(c)(3)-planned-not-granted
> status, "no external sharing" honesty about Amplitude as the
> processor, and consent-coercion risk).
>
> Side effect of the pin jumping from `0.12.0` → `0.15.0` → `0.16.0`
> → `0.17.0`: OHM now also runs v0.13.0's cookie-consent banner
> and v0.14.0's `/docs` route, which had been integrated on
> `main` since Session 0010.0 but were not currently deployed because
> the pin had rolled back during Session 0011.0's Wave-4 sequence.

---

## Pre-session state

- **rfc-app**: `main` at `b3f1b15` (Release 0.12.0 — Turnstile).
  `VERSION` = `0.12.0`. `frontend/package.json#version` = `0.12.0`.
  Tags shipped through v0.14.0; the v0.x lineage 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.9.0, v0.10.0, v0.11.0, v0.12.0, v0.13.0, v0.14.0`.
  Last migration on disk: `017_device_trust.sql` (slot 016
  reserved + skipped per Session 0011.0).
- **ohm-rfc**: `.rfc-app-version` = `0.12.0`. The pin was "below"
  v0.13.0 (cookie banner) and v0.14.0 (`/docs`) — those features
  were integrated on rfc-app `main` but not currently deployed
  to OHM. Any Wave-5 pin bump past `0.12.0` would pick up both
  side-effect features; flagged in arc 1 + reconfirmed at the
  arc 6 deploy. ROADMAP main was at `9654cbb` (post-Session-K
  ROADMAP: #10 v0.12.0 shipped row).
- **OHM live**: serving `v0.12.0` per
  `https://ohm.wiggleverse.org/api/health` returning
  `{"version":"0.12.0","status":"ok"}`. `flotilla deploy log
  ohm-rfc-app` showed last successful deploy as `deploys.id=18`
  (v0.12.0, succeeded, 11:18 UTC) — the post-`TURNSTILE_REQUIRED=true`
  re-deploy from Session 0011.0 that put abuse defense fail-closed.
- **ohm-rfc-app-flotilla**: `main` at `65c0e55` (CLAUDE.md
  secret-set gesture correction). No flotilla work expected in
  Wave 5; none performed.
- **ohm-infra**: most recent transcript is Session 0011.0
  (`SESSION-0011.0-TRANSCRIPT-2026-05-28T03-20--2026-05-28T03-55.md`).
  Next letter is L (Session 0011.0 = previous driver; Session 0010.0 = the
  parallel docs-feature / v0.14.0 session that ran during
  Session 0009.0's wind-down).
- **Hard rule baked in mid-Session-K**: never EVER ask the operator
  to paste secret bytes into the conversation. Stayed honored
  through this session — the Amplitude key the operator pasted is
  a public bundle-embedded value (same nature as
  `VITE_TURNSTILE_SITE_KEY`), which CLAUDE.md explicitly carves
  out as fine to receive in conversation. The driver did not ask
  for any value; the operator volunteered the vendor's
  installation prompt (which contained the key inline) +
  confirmed it. Reconciliation between the bedtime prompt's
  "wave-pauses on AMPLITUDE_API_KEY" framing and the actual
  vendor-recommended overlay binding is documented as the
  v0.15.0 CHANGELOG's "Caveat — overlay binding" section.

---

## Turn-by-turn arc

The session moves through nine arcs, in order.

### Arc 1 — Reconnaissance + dispatch

The driver read `~/git/ohm-infra/SESSION-PROTOCOL.md` and
`/Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md`
end-to-end, verified pre-session ground truth (rfc-app `main` at
`b3f1b15`, OHM serves v0.12.0 at `deploys.id=18`, ohm-rfc pin at
`0.12.0`), and dispatched three subagents in a single message
with parallel `Agent` tool uses, `subagent_type: general-purpose`,
running in the background. Each subagent created its own
`git worktree add` in `/Users/benstull/git/rfc-app-<target-version-
slug>` (the `Agent` tool's worktree isolation only covers the
flotilla CWD, not the rfc-app CWD subagents needed — Session 0011.0
lesson). Each prompt was self-contained: full roadmap text,
target version, reserved migration slot (018 for ν, 019 for ο,
none for ξ), descending-order CHANGELOG insertion point, 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.

Worktree base discovered via `ls -la /Users/benstull/git/`. One
ground-truth correction caught early: `~/git/ohm-infra` is NOT a
git repo locally — it's a working directory the publish script
clones the remote `wiggleverse/ohm-session-history` into. Session
K's prompt phrased it correctly; the driver's first `cd
~/git/ohm-infra && git log` failed and surfaced the right model.

### Arc 2 — Operator returns mid-session: Amplitude scope settled

While the three subagents were running in the background, the
operator returned to the keyboard and surfaced the Amplitude
provisioning:

1. "I provisioned an Amplitude instance."
2. The vendor's installation-wizard prompt (verbatim — it included
   the API key inline as a literal string argument:
   `amplitude.initAll('741709e30574cc8314b83b763f675c32', {
   "analytics": { "autocapture": true }, "sessionReplay":
   { "sampleRate": 1 } });` — and recommended the
   `@amplitude/unified` package).
3. "I have the amplitude API key… can set that secret when we're
   ready. Or, if it's not a secret and is really a public thing
   that's in JavaScript, can provide it here."

The driver settled three things in one response:

- **Secret vs. public**: the Amplitude browser key is bundle-
  embedded by design (visible in dev tools; vendor's own prompt
  shows it as a literal string argument). Same nature as
  `VITE_TURNSTILE_SITE_KEY` from v0.12.0, which is explicitly
  public per CLAUDE.md. The right binding is `flotilla overlay
  set`, not `flotilla secret set`. The roadmap row #13's "new
  secret: AMPLITUDE_API_KEY" wording predated vendor
  consultation; corrected at the v0.15.0 CHANGELOG + the
  shipped-row update.
- **Scope change**: the vendor recommends `@amplitude/unified`
  (analytics + session replay bundled) instead of the
  `@amplitude/analytics-browser` the dispatch brief specified.
  `sampleRate: 1` (100% session replay) is the vendor default
  — meaningfully larger privacy footprint than events-only. The
  driver surfaced three options to the operator via
  AskUserQuestion: events only / events + replay 10% / events
  + replay 100% (vendor default). The operator picked **events
  + session replay @ 100% (vendor default)**.
- **In-flight subagent**: ξ was already running under the
  pre-vendor brief; no `SendMessage` tool available in this
  harness to redirect it; chosen path is "let ξ finish, driver
  post-corrects on the branch" since the post-correction is
  tight and mechanical (package swap, init-call rewrite,
  CHANGELOG MUST-step rewrite). The wrapper structure, event
  taxonomy, and consent integration ξ would produce were
  brief-correct and would survive the post-correction
  unchanged.

### Arc 3 — Operator surfaces roadmap items #20, #21, #22

Across three messages while the subagents were running, the
operator added three new roadmap items:

- **#20 Email deliverability** — "do whatever we can to prevent
  invites from going to the spam folder." Captured during arc 5
  before any subagent had returned. Scope spelled out: DNS (SPF,
  DKIM, DMARC, BIMI follow-up), SMTP-provider posture (verified
  sender domain, warm-up), From / Reply-To hygiene (no
  `noreply@`), rfc-app template hygiene (plain-text alternative,
  `List-Unsubscribe` + `List-Unsubscribe-Post` per RFC 8058,
  bounce handling), engagement signals, monitoring (mail-tester,
  Postmaster Tools, DMARC `rua=`). Overlap with #18 (Secure SMTP
  + Gitea webhook) flagged explicitly; likely shipped together.
  Two-layer rule honored: rfc-app template hygiene is
  framework-wide, DNS + SMTP-provider config + BIMI logo are
  OHM-specific. Sequencing: don't block #12 / #16 on this; the
  real-world inbox/spam data from those first invite blasts
  informs the #20 sweep afterward. Committed to ohm-rfc at
  `2072663`.
- **#21 Amplitude audit + standing instrumentation discipline** —
  captured after the operator extended Amplitude scope in arc 2.
  Two-part structure: Part A (deep audit of v0.15.0's taxonomy +
  autocapture + replay quality once a week of real data is in)
  + Part B (standing discipline: SPEC.md analytics chapter,
  rfc-app CONTRIBUTING checklist, optional CI lint).
  Subsequently extended with **Part C** after the operator said:
  "ensure a new user in Amplitude is created when an invite is
  sent and we have a user with the OHM user ID set on that
  event"; "appropriate pattern for setting user properties on
  login + clearing on logout"; "implement Amplitude best
  practices from the very get-go". Part C captures the
  user-identity lifecycle contract (setUserId + identify
  properties on sign-in; setUserProperties on mid-session state
  change; reset on sign-out; identify-BEFORE-track on invite-
  claim so the Amplitude record is created with the OHM user_id
  from the first event rather than as an anonymous device that
  retroactively links). Part C explicitly names what "ships
  inline in Session 0012.0" vs. what waits for the data-informed
  audit. Committed to ohm-rfc at `f006d33` (Parts A + B), then
  `1c88f71` (Part C added).
- **#22 Pro-analytics-consent copy on the privacy/cookies
  opt-in** — captured later: "add language to the privacy /
  cookies opt-in that asks people to consider allowing analytics
  and we promise we won't share it externally — we're a
  non-profit and will only use it to make the experience better
  (if that language is legal)." Legal caveats spelled out in
  the item itself: 501(c)(3) is **planned not granted** per the
  project memory (copy MUST NOT claim "we are a 501(c)(3)"
  today; defensible alternatives are "being formed as", "non-
  commercial project", or neutral "operated by Wiggleverse Org"
  with an explainer); "never shared externally" is literally
  untrue because Amplitude IS an external service (defensible
  framing is "don't sell or trade" + "Amplitude holds it on our
  behalf"); purpose-limitation pledges are enforceable; GDPR /
  CCPA caution about coerced consent; withdrawal must stay easy.
  Drafting flow: operator drafts → counsel reviews → subagent
  wires approved text into banner + `/privacy` + `/cookies`
  (the wiring step is small and mechanical). Committed to
  ohm-rfc at `26b4684`.

### Arc 4 — Session ξ returns; driver post-corrects on its branch

ξ completed at `0fd8c52` on `feature/v0.15.0-amplitude` (origin +
benstull). 14 files touched; the nine-event taxonomy (Page
Viewed, RFC Viewed, User Signed In / Signed Out, RFC Proposed,
PR Opened, Comment Posted, Beta Access Requested, Admin
Permission Decision) wired into App.jsx, Login.jsx, ProposeModal,
PRModal, RFCView, RFCDiscussionPanel, PRView, Admin. Consent gate
read at `frontend/src/lib/consent.js:91` (getConsent) and `:102`
(onConsentChange). One env-var-naming flag from ξ that the
post-correction also resolves: ξ used `VITE_AMPLITUDE_API_KEY` in
code (Vite prefix convention) but the brief specified
`AMPLITUDE_API_KEY` in the CHANGELOG MUST-step; the overlay-
binding correction made the env-var-naming inconsistency moot.

Driver post-correction (on the same branch, as a follow-up commit
`6cfbf69` — no amend of pushed commits):
- Package: `npm uninstall @amplitude/analytics-browser && npm
  install @amplitude/unified` (resolved to `^1.1.9`).
- Init call: `mod.init(KEY, undefined, { defaultTracking: false
  }).promise` → `mod.initAll(KEY, { analytics: { autocapture:
  true }, sessionReplay: { sampleRate: 1 } })` with a defensive
  `if (ret && ret.promise) await ret.promise` for unified-build
  variance.
- CHANGELOG MUST step: secret-set → `flotilla overlay set
  ohm-rfc-app VITE_AMPLITUDE_API_KEY=<key>`. New Caveat sections
  spelled out the binding choice + the session-replay scope
  (with the separate-consent-category future-work flagged as
  §19.2).
- `frontend/.env.example`: VITE_AMPLITUDE_API_KEY block rewritten
  to describe the overlay binding.
- Wrapper header comment + warning text updated to reference
  unified + overlay.
- Wrapper structure, event taxonomy, queue + drain, consent
  integration, anonymize-on-sign-out: all unchanged from ξ's
  work.
- Frontend build verified green; pushed to origin + benstull
  at `6cfbf69`.

### Arc 5 — Sessions ο, then ν return

**ο** completed at `41b0c6a` on
`feature/v0.17.0-admin-create-user`. 33 backend tests total in
ο's surface (15 new in `test_admin_create_user_invite_vertical.py`).
Notable decisions: opaque DB token (256-bit CSPRNG, bcrypt-at-
rest, 7-day TTL constant `INVITE_TOKEN_TTL_DAYS = 7`); 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 (the brief floated `first_sign_in_at IS
NULL` as the discriminator, but the existing
`users.last_seen_at` is NOT NULL with `datetime('now')` default
+ no `first_sign_in_at` column existed — so the discriminator
became "active row in `user_invite_tokens` joined on
invited_user_id" instead, and `/api/admin/users` carries a
new `pending_invite` field via that join); admin self-grant
refusals (422 self-invite, 409 duplicate, 422 non-owner granting
owner); new `permission_events` event_kind `'user_invited'` for
the audit trail; `/api/invites/claim` lives in `main.py`'s
oauth_router (shares `_set_device_trust_cookie` helper with
`/auth/otc/verify`); inviter display name reads off the DB row
fresh (not from the session-cookie cache). Admin.jsx touched
additively only — button + modal + badge + listing-field
consumption; existing component shape preserved.

**ν** completed last (~30 min vs ο's ~19 and ξ's ~12) at
`a51beec` on `feature/v0.16.0-owner-invite`. 237 backend tests
total in ν's surface (18 new in `test_rfc_invitations_vertical.py`;
6 existing tests in test_pr_flow / test_graduation / test_e2e_smoke
opted into the new contract via a new `grant_rfc_collaborator`
helper). Notable: per-RFC roles are `contributor` (PR + discussion)
or `discussant` (discussion only); re-accept that would lower the
role is a no-op; the per-RFC gate falls through to the v0.6.0
platform-granted contract when an RFC has no frontmatter owners
yet (super-draft pre-§13.1 claim); accept endpoint requires the
accepting user's email to match the invitee's
`invitee_email` case-insensitively; platform-grant decision stays
the admin's call (accepting a per-RFC invite while pending
surfaces on `/api/admin/users` as the `rfc_invitations` array per
user — informing, not deciding, the platform grant); email
dispatch reuses `EmailConfig.from_env()` + `_SENT` like
`email_otc.py`; send failure does NOT roll back the invite row
(owner has the token on the listing surface for an out-of-band
share); regex email validation (`^[^\s@]+@[^\s@]+$`) instead of
pydantic `EmailStr` to avoid pulling in `email-validator`,
matching the v0.7.0 OTC body's shape. **ν did NOT touch
Admin.jsx directly** — the `/api/admin/users` response gained an
additive `rfc_invitations` array (forward-compat for the existing
UI), so the Admin.jsx-collision risk with ο that the bedtime
prompt flagged did not materialize. The conflict moved to
`backend/app/api_admin.py` (both ν and ο extended different
response fields + ο added new endpoints) — additive in different
scopes, hand-reconcilable.

### Arc 6 — Integration, with #21 Part C wiring folded inline

The driver squash-merged each branch into rfc-app `main` in
version order, hand-resolving conflicts and adding inline
Amplitude wiring per #21 Part C "ships inline in Session 0012.0":

**v0.15.0 integration** — fast-forward squash from `b3f1b15`
(no conflicts: main was at ξ's base). Driver-side extensions
folded in BEFORE the release commit:

- `analytics.js`: `identify(...)` signature extended to
  `{ user_id, properties? }` — properties apply via a new
  internal `applyProperties(props)` that builds an Amplitude
  `Identify` event with `.set(k, v)` semantics by default and
  `.setOnce(k, v)` semantics when the value is wrapped as
  `['__setOnce__', v]` (the sentinel pattern). New public
  `setUserProperties(properties)` API exposed the same
  property-apply path for mid-session state changes.
  `anonymize()` now clears `_pendingProperties` too so a
  fresh sign-in doesn't carry-over the previous user's
  property cache.
- `App.jsx`: identify call extended to pass a viewer-derived
  property bag — `role` / `permission_state` / `passcode_set` /
  `device_trusted` (mutable `.set()`), `first_sign_in_at` /
  `account_created_at` (immutable `.setOnce()`). PII discipline
  preserved: no email, no display_name, no gitea_login.
- CHANGELOG opening summary, Added wrapper bullet, and Added
  user-binding bullet rewritten to reflect the extensions.
- Committed as `72f8457` "Release 0.15.0: Amplitude Analytics
  + Session Replay (with #21 Part C identity lifecycle)";
  tag `v0.15.0` pushed origin + benstull.

**v0.16.0 integration** — conflicts in VERSION, package.json,
CHANGELOG. CHANGELOG resolved by reordering to strict-descending
(0.16.0 above 0.15.0 above 0.14.0) via a small Python script
that read the conflict markers and emitted the incoming-block-
first variant. Inline Amplitude wiring per #21 Part C:

- EVENTS taxonomy in analytics.js extended with
  `INVITATION_SENT: 'Invitation Sent'` + `INVITATION_ACCEPTED:
  'Invitation Accepted'` (plus `USER_INVITED` + `INVITE_CLAIMED`
  pre-added for v0.17.0).
- `InvitationsModal.jsx`: fires
  `track(EVENTS.INVITATION_SENT, { rfc_slug, role_in_rfc })`
  on send success.
- `AcceptInvitation.jsx`: fires
  `identify({ user_id, properties: { invited_at (setOnce),
  last_invited_to_rfc, last_invite_role_in_rfc, claim_method:
  'rfc-invite' } })` BEFORE
  `track(EVENTS.INVITATION_ACCEPTED, { rfc_slug, role_in_rfc })`
  on accept success.
- CHANGELOG Added section extended with an "Amplitude wiring"
  bullet documenting the calls + the no-PII discipline.
- Committed as `ee4925b` "Release 0.16.0: owner-only invite for
  per-RFC contribution + discussion (+ #21 Part C Amplitude
  wiring)"; tag `v0.16.0` pushed.

**v0.17.0 integration** — conflicts in VERSION, package.json,
CHANGELOG, App.jsx (two regions — both imports + both routes
kept), `backend/app/api_admin.py` (two regions — both per-user
additive fields + ο's new endpoints all kept, resolved by a
small Python script that emits head-block then incoming-block).
Full backend pytest run post-merge: **252 tests pass** (matches
the union of ν's 237 + ο's 15 new). Inline Amplitude wiring per
#21 Part C:

- `Admin.jsx` `CreateUserInviteModal`: fires
  `track(EVENTS.USER_INVITED, { target_user_id, initial_role,
  custom_message_chars })` on `POST /api/admin/users` success.
  `custom_message_chars` is a coarse admin-effort signal
  (0 = template-only, 1+ = personalized).
- `InviteClaim.jsx`: fires
  `identify({ user_id, properties: { claim_method:
  'admin-invite', invited_at (setOnce), invited_by_admin_id
  (setOnce), initial_role (setOnce) } })` BEFORE
  `track(EVENTS.INVITE_CLAIMED, { invited_by_admin_id,
  initial_role, needs_passcode, trust_device })` on
  `POST /api/invites/claim` success — so the Amplitude user
  record is created with the OHM user_id from the very first
  event the invitee fires, never as an anonymous device that
  retroactively links.
- Committed as `1456c8b` "Release 0.17.0: admin-create user +
  invite email (+ #21 Part C Amplitude wiring)"; tag `v0.17.0`
  pushed.

### Arc 7 — Three serialized deploys to OHM

Per the §8.3 single-in-flight-deploy rule and Session 0011.0's
serialization lesson, each version deployed in turn:

- **Overlay set**: `flotilla overlay set ohm-rfc-app
  VITE_AMPLITUDE_API_KEY=…` ran first. Confirmed via
  `flotilla overlay show ohm-rfc-app` — the key joined
  `VITE_TURNSTILE_SITE_KEY` and the other non-secret overlay
  values; all five Secret-Manager refs unchanged.
- **v0.15.0**: pin bumped, committed `c784107`, pushed.
  `flotilla deploy run ohm-rfc-app` → `deploys.id=19`, all 9
  phases green, verify 6.9s. `/api/health` returns
  `{"version":"0.15.0","status":"ok"}`. Side-effect features
  v0.13.0 (cookie banner) + v0.14.0 (`/docs` route) now also
  on production for the first time since the pin had rolled
  back during Session 0011.0's Wave-4 sequence.
- **v0.16.0**: pin bumped, committed `1c2bb26`, pushed.
  `flotilla deploy run ohm-rfc-app` → `deploys.id=20`, all 9
  phases green, verify 2.4s. `/api/health` returns
  `{"version":"0.16.0","status":"ok"}`.
- **v0.17.0**: pin bumped, committed `dc7a572`, pushed.
  `flotilla deploy run ohm-rfc-app` → `deploys.id=21`, all 9
  phases green, verify 2.4s. `/api/health` returns
  `{"version":"0.17.0","status":"ok"}`.

No deploy aborts, no rollbacks, no surprises. The "stop on first
deploy failure" Wave constraint never engaged.

### Arc 8 — ROADMAP strikethroughs

`ohm-rfc/ROADMAP.md` rows for #12, #13, #16 struck through with
release-tag links + `deploys.id` references. #13's note records
the mid-session binding correction ("originally said `secret
set AMPLITUDE_API_KEY`; settled as `overlay set
VITE_AMPLITUDE_API_KEY`") so the historical record is honest
about the pre-vendor wording. Committed at `47cb2f9`, pushed.

### Arc 9 — Protocol amendment: subagent transcripts (Session-L-introduced)

After the driver had written what it thought was the final
Session 0012.0 transcript, the operator surfaced a series of follow-up
asks:

1. "Prompt for next session pls?" + "and update transcript" —
   straightforward; the next-session prompt was already at the
   bottom of the transcript file, and the driver surfaced it as
   a copy-pasteable block in the conversation.
2. "Do the forked agents get transcripts captured? If not, let's
   make sure they write their own transcripts out before
   returning to the main session. They can be e.g. SESSION-L.1
   etc so it's clear they're a subsession."
3. "And the call to the agent should tell it what its transcript
   ID is."
4. "And then update the transcript again — always do that as a
   final step of a session."

The first two are a protocol amendment. The current
SESSION-PROTOCOL.md §3 said "Driver-only writes" — subagents
returned report-backs that the driver folded into the single
session transcript. That shape captures the high-level shape of
each subagent's work but flattens the detail (mid-flight
decisions, dead ends, schema choices, micro-corrections,
test-suite outcomes). The amendment introduces subsession
transcripts:

- **Naming**: `SESSION-<letter>.<n>-TRANSCRIPT-<start>--<end>.md`
  (e.g. `SESSION-0013.1-TRANSCRIPT-…md` for the first subagent in
  Session 0013.0). Nested ordinals (`.1.1`) supported if a forked
  subagent itself dispatches another.
- **Dispatch contract**: the driver pre-assigns subsession IDs
  in dispatch order, names the exact filename + the
  before-return write requirement + the transcript skeleton in
  each subagent's prompt.
- **Driver responsibility**: main transcript references each
  subsession by filename in the cut-state table; driver
  publishes its own + each subsession's transcript via the
  publish script. Stub-write on subagent's behalf if the
  subagent crashed without producing one.
- **Applies from Session 0013.0 onward.** Session 0012.0 itself ran under
  the prior "driver-only" rule because the amendment landed
  mid-session after L's three subagents had already returned;
  no `SESSION-L.1` / `.2` / `.3` transcript files exist or will
  be created retroactively.

Changes landed:

- `~/git/ohm-infra/SESSION-PROTOCOL.md` — new §5 "Subagent
  transcripts (Session-L amendment)" inserted before what was
  §5 (now renumbered §6 "When the protocol is unclear").
- `~/git/ohm-infra/scripts/publish-transcript.sh` — filename
  validator regex extended from `[A-Za-z]+` to
  `[A-Za-z]+(\.[0-9]+)*` so subsession filenames pass.
  Dry-run validation confirmed: the parent filename still
  validates, and a hypothetical `SESSION-0013.1-TRANSCRIPT-…md`
  would also pass.
- Session 0012.0's next-session prompt (below) updated with the new
  dispatch shape so Session 0013.0's driver inherits it.

The fourth ask — "update the transcript again — always do that
as a final step of a session" — was captured as a feedback
memory at
`~/.claude/projects/.../memory/feedback_transcript_is_final_step.md`
so future driver sessions honor the "transcript is the LAST
thing before publish" discipline. Memory index updated.

### Arc 10 — First transcript pass + publish

This file's first "final" pass folded in arcs 1–9 + arc 10;
cut-state reflected the protocol amendment + the publish-script
edit + the memory captured. Published via
`~/git/ohm-infra/scripts/publish-transcript.sh` to
`wiggleverse/ohm-session-history` at commit `b983b7d`.

### Arc 11 — Operator reinforced the final-step discipline; re-finalize + re-publish

Immediately after the first publish, the operator repeated the
instruction: "and then update the transcript again — always do
that as a final step of a session." This is precisely the
pattern the new feedback memory
(`feedback_transcript_is_final_step.md`) anticipates: a "finalized"
transcript that gets new material added after it (in this case,
the arc-9 protocol-amendment exchange itself + the driver's
closing summary that surfaced the next-session prompt) is not
yet final — the discipline is to apply the new material AND
finalize AGAIN AND re-publish.

So this arc 11 captures: the operator reinforced the discipline,
the driver re-finalized the transcript with this arc included,
and re-published. The publish script's idempotent-on-identical /
update-on-changed behavior means the re-publish lands as an
"Update SESSION-0012.0-TRANSCRIPT-…md" commit on
`wiggleverse/ohm-session-history`, not a duplicate file. The
filename's `--<end>` timestamp stays the original
`2026-05-28T05-17` (the time of the first finalization); the
re-finalized content acknowledges that the actual session end
crept a few minutes later (~05-25 PST) as the discipline-
reinforcement exchange happened.

Future driver sessions should expect this pattern to recur:
"finalization" is not a one-shot gesture. Any operator
instruction landing after the first publish triggers another
finalize + re-publish cycle. The discipline is captured for
Session 0013.0 onward in the next-session prompt's closing block
+ the feedback memory.

### Arc 12 — Roadmap item #23 surfaced: rename transcripts to numeric form

Right after arc 11's re-publish, the operator surfaced another
ask: "Add renaming transcripts from e.g. SESSION-A to SESSION-0001.0
etc to the roadmap." This is roadmap item #23 (Track Ω, operator-
led, one-session operational gesture):

- **New convention**: `SESSION-NNNN.M-TRANSCRIPT-<start>--<end>.md`
  where `NNNN` is a zero-padded 4-digit session number and `M` is
  the subagent ordinal (`.0` = main driver, `.1+` = subagents).
  Nested as `.M.N` for sub-subagents.
- **Mapping**: A → `SESSION-0001.0`, B → `SESSION-0002.0`, …,
  L → `SESSION-0012.0`. Twelve existing transcripts in
  `wiggleverse/ohm-session-history` to rename, plus the local
  `~/git/ohm-infra/` copies.
- **Surfaces touched** (enumerated in the item): the public repo
  (git mv per file for history preservation), local working dir,
  SESSION-PROTOCOL.md §1 + §5 + worked examples, publish-
  transcript.sh filename validator, flotilla CLAUDE.md
  references, ohm-rfc/ROADMAP.md shipped-row references in the
  table, cross-transcript body links (Session-K-to-Session-I
  etc.).
- **Open questions enumerated in the item**: git-mv vs.
  add+delete (preserve history); redirect tombstones vs. 404
  (URL breakage for external references); sequencing relative
  to the next-letter-after-L session (most-elegant timing is
  Session 0013.0 as the rename session itself, so `SESSION-0013.0`
  debuts as the new shape from the start). Decide at session
  time.

Committed to ohm-rfc at `8cce148` with a 130-line item
description + a single-row addition to the version-targets table.

### Arc 13 — Re-finalize + re-publish (again)

Per the same discipline arc 11 named: the operator's #23 ask
landed after the first re-publish, so this triggers another
finalize-and-republish cycle. This arc 12+13 added; cut-state
table updated to reflect ohm-rfc at `8cce148` and the third
finalization pass; re-published.

If a fourth operator instruction lands after this publish, the
cycle continues. The discipline holds open-ended.

---

## Cut state (end of session)

| | |
| --- | --- |
| rfc-app | `main` at `1456c8b` (Release 0.17.0). Tags through v0.17.0. Last migration on disk: `019_user_invite_tokens.sql`. 252 backend tests pass. |
| ohm-rfc | `main` at `8cce148` (ROADMAP strike + pin at 0.17.0 + #23 rename-transcripts item added in arc 12). `.rfc-app-version` = `0.17.0`. |
| OHM live | `deploys.id=21`, v0.17.0, healthy. `/api/health` = `{"version":"0.17.0","status":"ok"}`. Cookie banner (v0.13.0), `/docs` route (v0.14.0), Amplitude analytics + session replay (v0.15.0), per-RFC owner invites (v0.16.0), admin-create user + invite (v0.17.0) all live. |
| flotilla overlay | `VITE_AMPLITUDE_API_KEY` bound (public). All other overlay / secret bindings unchanged. |
| ohm-rfc-app-flotilla | `main` at `65c0e55`. Unchanged this session. |
| ohm-infra (local) | `SESSION-PROTOCOL.md` amended (new §5 "Subagent transcripts"; §5 → §6); `scripts/publish-transcript.sh` regex extended to accept `SESSION-<letter>.<n>-TRANSCRIPT-…md` subsession form. |
| Auto-memory | New feedback memory: `feedback_transcript_is_final_step.md` (transcript is the LAST step before publish). MEMORY.md index updated. |

| Wave 5 ledger | Status |
| --- | --- |
| #13 Amplitude (v0.15.0) | ✅ shipped, `deploys.id=19` |
| #12 Owner invite (v0.16.0) | ✅ shipped, `deploys.id=20` |
| #16 Admin-create user + invite (v0.17.0) | ✅ shipped, `deploys.id=21` |

| New roadmap items captured (Session 0012.0) | Status |
| --- | --- |
| #20 Email deliverability | 📝 captured, awaiting session |
| #21 Amplitude audit + standing discipline | 📝 captured. Part C identity-lifecycle work shipped inline in Session 0012.0's v0.15.0 + v0.16.0 + v0.17.0 commits. Part A audit waits for ~1 week of real Amplitude data; Part B discipline can start anytime. |
| #22 Pro-analytics-consent copy on privacy/cookies opt-in | 📝 captured. Waiting on operator-drafted + counsel-reviewed copy. |
| #23 Rename transcripts `SESSION-<letter>` → `SESSION-NNNN.M` | 📝 captured. One Track-Ω session, operator-led. Mapping A→0001.0 through L→0012.0 spelled out in the item. Surfaces enumerated (12 transcripts + protocol + publish-script + cross-doc refs). |

---

## §19.2 candidates surfaced

1. **`@amplitude/unified` bundle size + lazy import effectiveness**
   — the unified package adds ~150 KB gzipped (vs. ~30 KB for
   analytics-browser only); the wrapper's consent-gated `await
   import('@amplitude/unified')` keeps it off the initial bundle
   for users who haven't opted in, but the post-consent init
   path hasn't been measured for jank. Part of #21 Part A's
   bundle + performance check.
2. **Session-replay-specific consent category** — v0.13.0's
   cookie banner has a single "analytics" toggle that gates both
   events and full-DOM session recording. Recording has a
   meaningfully larger privacy footprint than event counters;
   splitting the consent into "analytics" vs "session replay"
   categories is the cleaner shape. Captured in #21's Part A +
   §19.2 follow-up.
3. **CHANGELOG `### Migration` block has no machine-readable
   shape** — three releases in this session said "auto-applied
   on deploy by the existing migration runner" with prose;
   nothing pins which slot was claimed or surfaces a conflict
   if two parallel features claim the same slot. The slot
   coordination this session (016 reserved, 018 → #12, 019 →
   #16) was done by the driver's brief, not enforced by tooling.
   Worth a small lint or CI step.
4. **Bedtime prompt drift vs. live state** — the bedtime prompt
   said "OHM serves v0.12.0" which was accurate, but did NOT
   mention that v0.13.0 and v0.14.0 features (cookie banner,
   `/docs`) were integrated on `main` but absent from the live
   deploy because the pin had rolled back during Session 0011.0.
   Any Wave-5 deploy was always going to bring those features
   back; the driver flagged this in arc 1 + handled it
   transparently, but a session-bootstrap script that emits
   "what is on main but not deployed?" would catch this kind of
   drift before it surprises a future driver. (This is adjacent
   to the Session-I §19.2 candidate around deploy-snapshot
   surfacing.)
5. **Vendor-prompt-as-truth pattern** — Amplitude's installation
   wizard is an LLM-targeted prompt the vendor publishes for
   their customers to drop into Cursor / Claude Code / etc.
   When the operator pastes it mid-session, the prompt's
   recommendations override the roadmap row's pre-vendor
   guesses (package name, init shape, secret-vs-overlay
   framing). For future vendor integrations (Stripe? Linear?
   Sentry?), a discipline is worth naming: vendor-prompt
   guidance is authoritative for SDK-shape decisions, but
   integration points (consent, identity, PII) stay project-
   sovereign. Could become a §19.2 candidate spec section.
6. **Subagent re-direction without `SendMessage`** — when the
   operator's mid-session input invalidated ξ's pre-vendor
   brief, the driver had no `SendMessage` tool available to
   redirect the in-flight subagent. The chosen path
   (let-finish, driver-post-corrects) worked here because the
   correction was small and on a fresh-cut branch; in a larger
   correction it would waste subagent work or force a
   `TaskStop` + re-dispatch. Worth exploring whether the
   harness can expose `SendMessage` for background agents, or
   whether the dispatch pattern should pre-commit to "wait for
   green-light from the operator" before doing the more
   invasive work.

---

## What lands on the operator's plate

1. **Amplitude dashboard verification** — vendor's installation
   prompt says "tell the user to start the application and fire
   an event to verify it is being sent to Amplitude." The
   deploy + `/api/health` verification confirmed the version is
   on the box; the **Amplitude-side confirmation that events
   actually arrive** is the operator's gesture (you have the
   dashboard login; the driver does not). Easiest probe per the
   v0.15.0 CHANGELOG: open `https://ohm.wiggleverse.org` in an
   Incognito window, accept analytics on the cookie banner,
   navigate to an RFC, watch the project's live event stream +
   session-replay panel. Expected events: `Page Viewed` (path),
   `RFC Viewed` (rfc_slug, rfc_id), the autocapture-generated
   click events on header links.
2. **Send a real invite (#16) end-to-end** — the live
   `/admin/users` surface now has the "Create user + invite"
   affordance. A real-world deliverability test is part of the
   #20 input data: send to a gmail address, a yahoo address, a
   protonmail address, watch which ones land in inbox vs. spam.
3. **Send a real per-RFC invitation (#12)** — same shape; pick
   an RFC, hit "Invitations" in the header strip, send to your
   alternate email.
4. **Roadmap items #20 / #22 are operator-led**:
   - **#20** needs DNS edits on `wiggleverse.org` (SPF / DKIM /
     DMARC) and SMTP-provider dashboard work. Subagent can
     inventory + propose; the executions are operator-only.
   - **#22** needs operator-drafted copy + counsel review
     before a subagent wires it. The roadmap item spells out
     the legal caveats (501(c)(3) is planned-not-granted, "no
     external sharing" honesty about Amplitude as the
     processor, purpose-limitation enforceability,
     GDPR/CCPA coerced-consent risk).
5. **§19.2-candidate #6 (subagent re-direction)** is a
   harness-feature request worth raising with the Claude Code
   team if it bites again.
6. **Item #1 VM rename + #17 inventory + #19 PR coordination
   + #23 transcript-rename** stay operator territory. #23
   adds one decision-point: do you want Session 0013.0 to be the
   rename session (so it debuts as `SESSION-0013.0`), or run
   a few more letter-form sessions first and rename in a
   later session?

---

## Updated next-session prompt

```
You are the OHM roadmap driver. The previous session (Session 0012.0)
shipped rfc-app v0.15.0 (Amplitude Analytics + Session Replay,
#13, deploys.id=19), v0.16.0 (per-RFC owner invites, #12,
deploys.id=20), and v0.17.0 (admin-create user + invite email,
#16, deploys.id=21) to OHM. All of Wave 5 is closed. OHM serves
v0.17.0 with Amplitude analytics + session replay live (gated by
the v0.13.0 cookie banner), v0.13.0 cookie banner itself, v0.14.0
/docs route, per-RFC owner invites, and admin-create user invites
all in production for the first time. Session 0012.0 also folded #21
Part C identity-lifecycle work inline across all three release
commits — identify({ user_id, properties }) + setUserProperties()
+ amplitude.reset on sign-out + identify-BEFORE-track on every
invite-claim path.

Read `~/git/ohm-infra/SESSION-PROTOCOL.md` and
`/Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md`
end-to-end. The next session letter is M.

**New protocol amendment from Session 0012.0**: subagents write their
own transcripts (subsession files like
`SESSION-0013.1-TRANSCRIPT-<start>--<end>.md`) before returning to
the driver. The driver pre-assigns each subagent's subsession ID
in the dispatch prompt. The driver's own Session-M transcript
stays load-bearing for cross-subagent synthesis + cut-state,
with cross-references to each subsession. See
SESSION-PROTOCOL.md §5 for the binding form. The publish
script's filename validator was extended in Session 0012.0 to accept
`SESSION-<letter>.<n>-TRANSCRIPT-…md`.

**Hard rule (Session 0011.0 + Session 0012.0): never EVER ask the operator
to paste secret bytes into the conversation.** Session 0012.0
confirmed the corollary: public bundle-embedded values
(`VITE_AMPLITUDE_API_KEY`, `VITE_TURNSTILE_SITE_KEY`) are fine
to receive in conversation and set via `flotilla overlay set` —
but never the secret-half of a public/secret pair, and never
something whose nature you're unsure about. When in doubt,
default to the operator-run gesture (`pbpaste | flotilla secret
set …` for secrets; `flotilla overlay set DEPLOYMENT KEY=VALUE`
for non-secrets).

Wave 6 candidates (per Session 0012.0's roadmap state):
- **#20 Email deliverability** (Track Ω + rfc-app template
  hygiene) — high priority now that two invite paths ship and
  the recipient's first contact with OHM is an email; spam
  foldering is silent invite failure. Likely one combined ops
  session for DNS + SMTP-provider posture + template hygiene
  (note: overlaps with #18; ship together).
- **#22 Pro-analytics-consent copy on the privacy/cookies
  opt-in** — small frontend-only rfc-app minor; depends on
  operator-drafted + counsel-reviewed copy in hand. Legal
  caveats spelled out in the item (501(c)(3) is planned, not
  granted; "no external sharing" honesty about Amplitude as the
  processor; purpose-limitation pledges are enforceable;
  GDPR/CCPA coerced-consent risk; withdrawal must stay easy).
- **#21 Part A audit** — waits for ~1 week of real Amplitude
  data, then a deep audit pass on taxonomy, autocapture quality
  (DOM hygiene), session-replay masking, bundle/performance.
  Ships as a rfc-app minor.
- **#21 Part B discipline** — SPEC.md analytics chapter +
  rfc-app CONTRIBUTING checklist + optional CI lint. Can start
  anytime; ships with #19's CONTRIBUTING PRs.
- **#18 Secure SMTP relay + Gitea webhook** — likely bundled
  with #20.
- **#17 Repo naming + location alignment** — operator-led; a
  subagent can inventory + propose, operator executes.
- **#19 CONTRIBUTING guides + transcript-linked onboarding** —
  Track Ω docs PR; subagent can draft, operator approves +
  merges.
- **#1 VM rename** — operator-led ops gesture.

Dispatch shape (updated for Session 0013.0 per the Session-L
amendment):
1. For each subagent, pre-assign a subsession ID: M.1, M.2, …
2. In the dispatch prompt, name the subsession ID + tell the
   subagent it MUST write `~/git/ohm-infra/SESSION-M.<n>-TRANSCRIPT-<start>--<end>.md`
   BEFORE returning a report.
3. The subagent's transcript follows the same shape as a main
   transcript (pre-state, arcs, cut-state, what-the-driver-
   needs). The driver's report-back is a tight summary; the full
   detail lives in the subagent's transcript file.
4. 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.
5. Driver integrates serially.
6. Driver publishes its own transcript AND each subsession
   transcript via `~/git/ohm-infra/scripts/publish-transcript.sh`
   (the regex now accepts `SESSION-<letter>.<n>-TRANSCRIPT-…md`).

Session-L lessons to apply automatically (in addition to all of
Session 0011.0's):
- **Vendor prompts can override the roadmap's pre-vendor
  guesses.** When the operator pastes a vendor installation
  prompt mid-session (Amplitude this time; could be Stripe,
  Linear, Sentry next time), the prompt's SDK-shape
  recommendations are authoritative for the SDK choices —
  package name, init signature, sample rates, etc. The roadmap
  row's "new secret: FOO" or "uses bar library" wording was
  best-effort pre-vendor and gets superseded. Integration
  points (consent gates, identity lifecycle, PII discipline)
  stay project-sovereign — the vendor doesn't get to decide
  those. Document the override in the release CHANGELOG +
  strike the ROADMAP row with the corrected framing.
- **No `SendMessage` available in this harness.** When the
  operator's mid-session input invalidates an in-flight
  subagent's brief, the choices are (a) let it finish and
  driver-post-correct, (b) `TaskStop` and re-dispatch. Pick (a)
  when the correction is small and on a fresh-cut branch; (b)
  when the structural decisions would be wrong. The
  driver-post-correction is a new commit on the same branch
  (no amend, no force-push), pushed to both remotes.
- **Squash-merge integration with hand-resolved CHANGELOG**
  works well for parallel-feature waves. Use a small Python
  script for the conflict resolution when the block is large
  (~150+ lines) — git's conflict markers are accurate but
  manual editing is error-prone at scale.
- **#21 Part C is a standing pattern**: every new
  identity-meaningful surface MUST call `identify({ user_id,
  properties })` (not just `setUserId`), and any new claim/
  sign-in/invite-accept path MUST call identify BEFORE the
  first track event so the Amplitude record is created with
  the OHM user_id from the first event rather than as an
  anonymous device that retroactively links. SPEC chapter to
  codify is part of #21 Part B.
- **Pin-jump side effects**: when the pin bumps past versions
  that were integrated-but-undeployed (in Session 0012.0: v0.13.0
  cookie banner + v0.14.0 /docs both went live as a side
  effect of the v0.15.0 → v0.17.0 sequence), flag this to the
  operator at the deploy step. Not a blocker; just transparency.
- **CHANGELOG slot 016 stays reserved + skipped**. v0.15.0
  used no schema migration; #12 took slot 018, #16 took 019.
  Next free: 016, 020, 021, ….
- **Login.jsx remains contested**: Session 0012.0's v0.15.0
  Amplitude wiring added `User Signed In` calls at the three
  verify paths (otc / passcode / trust-device). Future
  auth-touching releases need to preserve v0.11.0 + v0.12.0 +
  v0.15.0 injections.
- **Admin.jsx contested too**: v0.15.0 added Admin Permission
  Decision firing; v0.17.0 added Create-user-invite modal +
  pending-invite badge. Future admin-page work layers on top
  additively.
- **api_admin.py contested**: v0.16.0 added `rfc_invitations[]`
  per-user; v0.17.0 added `pending_invite` per-user + new
  endpoints. Both additive; the per-user dict has two new
  fields side-by-side. Future admin-API work should follow
  the same additive shape and not restructure the response
  envelope.
- **Roadmap items added mid-session**: #20 email deliverability,
  #21 Amplitude audit + standing discipline (Parts A + B + C),
  #22 pro-analytics-consent copy on the privacy/cookies opt-in.
  All three pushed to ohm-rfc as their own commits during the
  session. Don't batch roadmap captures to end-of-session — push
  promptly so the operator sees them on the gitea side without
  waiting.

End-of-session: write `~/git/ohm-infra/SESSION-0013.0-TRANSCRIPT-…md`
(your main transcript) AND ensure each subagent's subsession
transcript was written. Per Session-L feedback: **transcript is
the LAST step** — finish all other work first, then finalize the
transcript, then publish all of them via
`~/git/ohm-infra/scripts/publish-transcript.sh`. If the operator
surfaces new instructions after the transcript is "finalized,"
it isn't yet final — apply the new work, finalize again, then
publish.
```
