diff --git a/SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--2026-05-28T08-25.md b/SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--2026-05-28T08-25.md new file mode 100644 index 0000000..ec7ade9 --- /dev/null +++ b/SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--2026-05-28T08-25.md @@ -0,0 +1,645 @@ +# Session 0014.0 — Transcript + +> Date: 2026-05-28 (PST) +> Start: 2026-05-28T07-01 PST +> End: 2026-05-28T08-25 PST +> Goal: Wave 7 driver session. Triage + fix the SMTP gap M.3 +> flagged; ship rfc-app v0.18.0 (email + webhook hygiene); +> review the two Session-M docs PRs; run DMARC Phase A; +> then execute roadmap #23 (transcript rename to numeric +> form) at operator's mid-session ask. +> +> Outcome: **OHM email infrastructure operational for the first +> time since launch (silent dead-letter regression fixed via SMTP +> AUTH + DMARC Phase A live). rfc-app v0.18.0 shipped (5/5 slices, +> tagged + pushed). flotilla v1.1.0 shipped (fixes VITE_* keys +> reaching the production bundle — Turnstile broken since v0.12.0 +> and Amplitude broken since v0.15.0 were both restored). Roadmap +> #23 fully executed: 16 transcripts renamed across local + +> wiggleverse/ohm-session-history (via git mv); SESSION-PROTOCOL.md +> §1+§5 rewritten; publish-transcript.sh validator extended. +> Session 0014.0 is the first session under the new naming end- +> to-end. Three latent bugs surfaced + fixed mid-session.** +> +> §19.2 candidates surfaced: 5 (listed below). + +--- + +## Pre-session state + +- rfc-app `main` at `1456c8b` (v0.17.0), tag pushed to canonical + + mirror. +- ohm-rfc `main` at `8cce148` (operator added #23 mid-Session-M); + `.rfc-app-version` = `0.17.0`. +- ohm-rfc-app-flotilla `main` at `65c0e55` (v1.0.1). +- OHM live: deploys.id=21, v0.17.0, `/api/health` returning + `{"version":"0.17.0","status":"ok"}`. +- Two Session-M feature branches awaiting operator merge: + rfc-app `feature/contributing-and-spec-analytics` @ `213f686`, + ohm-rfc `feature/contributing-roadmap-guide` @ `f26fae4`. +- Three Session-M-authored docs on disk at `~/git/ohm-infra/`: + REPO-NAMING-PROPOSAL.md, EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md, + RFC-APP-EMAIL-HYGIENE-PROPOSAL.md. +- Open URGENT from Session 0013.0 (= legacy "M") handoff: M.3 + found SMTP_HOST + SMTP_USER missing from the live flotilla + overlay. Driver to investigate via gcloud ssh + + `systemctl show ohm-app.service | grep Environment`. + +--- + +## Turn-by-turn arc + +### Arc 1 — SMTP triage and the cascade of latent bugs + +The handoff prompt's "first gesture" was the gcloud SSH check. +Ran it and three other checks in parallel: SESSION-PROTOCOL.md +read, ROADMAP.md read, cut-state verification. The SSH check +revealed `EnvironmentFiles=/opt/ohm-app/backend/.env` — systemd +reads .env, doesn't inject overrides. The .env file on the VM +contained only `SMTP_PORT=587` + `SMTP_STARTTLS=1` for SMTP keys. +No SMTP_HOST, no SMTP_USER, no SMTP_PASSWORD secret bound. + +The failure mode — `backend/app/email.py:316-318`: + +```python +if not cfg.smtp_host: + log.info("email (stdout fallback): to=%s subject=%s", to_address, subject) + return True +``` + +Returns True so the caller writes `email_sent_at = datetime('now')` +on the notifications row. The DB recorded successful delivery +for every email since launch; the bytes never left the VM. +Confirmed via `journalctl -u ohm-app --since '30 days ago' | +grep 'stdout fallback'` (one sample line visible; the journal +retention window is shorter than 30 days at 32 MB). + +`scripts/register-ohm.sh` (last touched at v0.1.0, never modified +since) sets HOST/PORT/USER/STARTTLS via `overlay set` but never +sets SMTP_PASSWORD as a secret. The script was always expecting a +separate operator gesture to bind the password. That gesture was +never run. + +Reviewed both M.3 artifacts to understand the framework-side gap: +`~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` (~700 +lines, operator-facing) and `RFC-APP-EMAIL-HYGIENE-PROPOSAL.md` +(~280 lines, framework-side, 5-slice shipping plan). + +### Arc 2 — Wave 7 scope question to operator + +Asked the operator: SMTP fix only / SMTP + v0.18.0 + M PR +reviews / SMTP + DMARC + M PR reviews. Operator chose "all of the +above". Set up tasks for: SMTP fix gesture, v0.18.0 dispatch, +DMARC Phase A verify, M PR review, probe email. + +Dispatched subagent 0014.1 (= legacy "N.1") for rfc-app v0.18.0 +(transcript at `SESSION-0014.1-TRANSCRIPT-2026-05-28T07-08--2026-05-28T07-40.md`). +Subsession prompt explicitly carried forward the §5 transcript- +before-report rule + the "no final sanity check between transcript +and report" lesson from M.1. + +Subagent 0014.1 returned 32 minutes later: all 5 slices shipped, +tagged at `31913b1`, 295 tests passing (+43 from baseline), pin +bumped to 0.18.0 (commit `c527aca` in ohm-rfc), pushed to both +remotes. Report-back was honest and accurate per the driver's +on-disk re-verification (`git tag -l v0.18.0`, `git ls-remote`, +`cat .rfc-app-version`, spot-check `config.py` + `webhooks.py`). + +### Arc 3 — M PR review + +Both branches reviewed via `git show` (not checkout, to avoid +disturbing the v0.18.0 subagent's working tree). rfc-app +`feature/contributing-and-spec-analytics` @ `213f686`: +CONTRIBUTING.md (407 lines, new) + SPEC.md §21 analytics chapter +(469 lines, new). ohm-rfc `feature/contributing-roadmap-guide` @ +`f26fae4`: CONTRIBUTING.md (+82 lines extending existing file). +Both clean, ready to merge as no-bump docs commits. Operator +merges, not driver. + +### Arc 4 — DMARC Phase A + +Provided operator the TXT record value (`v=DMARC1; p=none; +pct=100; sp=quarantine; adkim=s; aspf=s; +rua=mailto:ben.stull@wiggleverse.org`) and TTL recommendation +(3600s for ramp). Operator initially published the new record +**alongside** the old (RFC 7489 §6.6.3 violation — two DMARC +records at the same name causes receivers to discard the policy +entirely; net effect was weaker enforcement than before the edit). +Flagged this; operator deleted the old record; re-verified clean +via `dig @8.8.8.8` + `dig @1.1.1.1`. The `rua` mailto turned out +to be `ben.stull@wiggleverse.org` (not `ben@wiggleverse.org` as +the prior runbook had read) — operator's actual Workspace +identity. + +### Arc 5 — SMTP fix attempts (Path A, C, A') + +Walked the operator through Google Workspace SMTP relay config +(it's at Admin Console → Apps → Gmail → Routing → SMTP relay +service, not "an app to install"). Operator chose Path A (IP +allowlist) initially. + +Set `SMTP_HOST=smtp-relay.gmail.com` in overlay; unset SMTP_USER +that had been set earlier (with no password bound, the framework's +`smtp.login()` would have failed). Deployed at deploys.id=22 +(v0.17.0). The deploy itself succeeded; phase 7 (restart) hit the +Session-I §19.2 SSE-keepalive timeout pattern but the service +restarted correctly. + +Probed with the framework's real `_deliver` function via a Python +script piped through gcloud ssh + sudo + the ohm-app user's venv. +Got `SMTPSenderRefused: 550 5.7.1 Invalid credentials for relay +[136.116.40.66]. The IP address you've registered in your +Workspace SMTP Relay service doesn't match the domain of the +account this email is being sent from.` Google's relay accepted +the IP but rejected the From because Python's smtplib defaults +HELO to the VM hostname (`ohm-app`), not a Workspace-registered +domain. + +Tried Path C: pass `local_hostname='wiggleverse.org'` to +`smtplib.SMTP()`. Same 550 error. The relay's HELO-domain +matching is stricter than just the EHLO string — likely involves +reverse-DNS or full Workspace-domain verification of the +connection origin. + +Switched to Path A' (SMTP AUTH). Operator generated a Workspace +app password for ben.stull@wiggleverse.org and piped via +`pbpaste | flotilla secret set ohm-rfc-app SMTP_PASSWORD`. +Re-set SMTP_USER in overlay. The probe ran again — and surfaced +**a second latent bug**: a `pbpaste: command not found` error +printed by `set -a; source /opt/ohm-app/backend/.env` revealed +that `CLOUDFLARE_TURNSTILE_SECRET`'s value in GCP Secret Manager +was the literal string `$(pbpaste)` from an old failed +`secret set` invocation. This meant every Turnstile siteverify +call had been failing for an unknown duration, blocking OTC +sign-in independent of the SMTP gap. + +Operator re-set CLOUDFLARE_TURNSTILE_SECRET via the canonical +pbpaste pipe. Re-deployed (deploys.id=23, v0.18.0 picked up +because the subagent had bumped the pin while the operator was +working on SMTP). SSH timed out at phase 7 again (3rd time — +same pattern). Aborted the row. Verified via `/api/health` the +service was up at v0.18.0. + +Re-ran the SMTP AUTH probe via a Python script that parsed the +.env without shell evaluation (avoiding the `$(pbpaste)` +substitution that had broken the first probe attempt — first the +parser missed quote-stripping; fixed). The probe succeeded: +`LOGIN: ok`, `refused: {}`, `RESULT: sent ok`. Probe email +arrived in operator's inbox. **Framework-level SMTP path proven +end-to-end for the first time since launch.** + +### Arc 6 — Cloudflare Turnstile hostname allowlist + the bundle bug + +Operator tried OTC sign-in in incognito; Turnstile returned +"Couldn't verify you're human. Please retry the challenge." with +no checkbox. Browser console showed `401 from /start`. Walked +through Cloudflare Turnstile dashboard navigation (it's a +separate product from CDN/DNS — needs the "Turnstile" top-nav +link, not a "Custom Hostname" feature elsewhere in the dashboard). +Operator found the widget (only one on the account, sitekey +matched `0x4AAAAAADXziMhZwT0pKCud`) and added a hostname. + +After the hostname add, the 401 persisted. Investigated by +curl'ing `https://ohm.wiggleverse.org/` + the linked JS bundle, +grepping for the expected sitekey + Amplitude key. **Neither was +present in the bundle.** The Amplitude warning string was +hardcoded; the literal bundle had `console.warn("[analytics] +VITE_AMPLITUDE_API_KEY is unset...")` — a build-time confirmation +that no value reached the build. + +**Third latent bug**: flotilla's `deploy.py` phase 5 (frontend +build) runs `npm ci && npm run build` with no env vars passed. +The .env write is phase 6, AFTER the build; and it writes +`backend/.env`, not `frontend/.env*` where Vite reads from. So +every Vite-built deployment since this code shipped has had empty +VITE_* values. **Turnstile broken since rfc-app v0.12.0 (Session +0011.0). Amplitude broken since rfc-app v0.15.0 (Session 0012.0).** + +### Arc 7 — flotilla v1.1.0 + +Patched `deploy.py:407-417` to filter overlay for `VITE_*`- +prefixed keys and prepend them as shell env-var assignments to +the `npm run build` command (after `npm ci`). Added two tests in +`tests/test_deploy.py`: the happy path (VITE_* keys appear inline +in the right place; non-VITE_* keys do NOT leak into the build +command; sorted ordering) and the empty case (no VITE_* keys = +unchanged shape). 157 tests passing (+2 new from 155 baseline). + +Bumped VERSION + pyproject.toml to 1.1.0. Wrote CHANGELOG with +the full root-cause narrative + RFC 2119 Upgrade steps block +(operators MUST reinstall + redeploy once for the fix to land; +MAY verify via curl-grep of the bundle). Tagged `v1.1.0` at +commit `417b6c5`, pushed to canonical. `pip install -e .` to +reinstall locally; `flotilla --version` confirmed 1.1.0. + +Redeployed OHM (deploys.id=24). SSH timed out at phase 7 again +(4th time). Aborted, verified via curl on the new bundle: +`/assets/index-DGoalVcp.js` (fresh build, new content hash) + +grep — both `0x4AAAAAADXziMhZwT0pKCud` and +`741709e30574cc8314b83b763f675c32` **present in the bundle**. +Turnstile + Amplitude operational for the first time since their +respective releases. + +### Arc 8 — Roadmap #23 execution + +Operator mid-session: "Let's update the session history file +names that already exist and when we write this transcript, use +the new format." Added task #8 (rename), continued in parallel +with finishing v1.1.0. + +Local renames first: 16 transcript files in `~/git/ohm-infra/` +from `SESSION-` to `SESSION-NNNN.M` form per the roadmap +mapping table. Left a stale `SESSION-L-…-INPROGRESS.md` draft +untouched (local-only, not published). + +Updated `~/git/ohm-infra/scripts/publish-transcript.sh`: +- Filename validator regex now accepts BOTH `[A-Za-z]+` and + `[0-9]{4}` forms (with optional `.N` subagent suffix); +- Header validator regex same; +- Doc comments updated; usage example updated to a 0014.0 + filename. + +Rewrote `~/git/ohm-infra/SESSION-PROTOCOL.md`: +- §1 (naming convention) now describes the numeric form as + binding with the legacy letter form documented as historical; + legacy mapping table inline (A→0001.0 through M→0013.0). +- §5 (subagent transcripts) updated with numeric examples. +- §2 skeleton example updated to use `# Session NNNN.M — Transcript`. +- Various single-line refs throughout updated. + +Updated `ohm-rfc-app-flotilla/CLAUDE.md` (one line — the only +session-letter ref in the file). `rfc-app/CLAUDE.md` had no +refs. Committed + pushed both. + +Cloned `wiggleverse/ohm-session-history` (the public repo). +`git mv` for all 16 transcripts (Sessions A through M.3) → +numeric form in a single commit. Commit message documents the +mapping + the rationale + the bodies-not-rewritten caveat. Pushed +at `1afa9f5`. Gitea's rename detection should show each as +file-was-renamed in its UI (not file-was-deleted-and-recreated). + +Updated `ohm-rfc/ROADMAP.md`: struck #23 (executed); marked #18 +and #20 partial (framework side done in v0.18.0; ops side partial +per Session-N work + remaining gestures). Committed + pushed. + +### Arc 9 — Transcript + +This file. Composed the next-session prompt (Arc-by-arc + cut +state + Wave 8 candidates + §19.2 candidates + hard rules); +operator approved verbatim ("Looks great"). Wrote this transcript +in the new SESSION-0014.0 naming form. Publish via +`~/git/ohm-infra/scripts/publish-transcript.sh` is the next +gesture (this transcript first, then the renamed subagent +transcript at SESSION-0014.1-...). + +--- + +## Cut state (end of session) + +| Surface | State | +|---|---| +| rfc-app `main` | `31913b1` — v0.18.0 tagged; pushed to canonical (`origin`) + mirror (`benstull`). 295 tests passing (+43 from baseline). | +| ohm-rfc `main` | `8dbfb74` — `.rfc-app-version` = `0.18.0` (bumped by subagent 0014.1 at `c527aca`); ROADMAP updated this session (`8dbfb74` strikes #23, marks #18+#20 partial); operator's parallel-session adds at `74a3e12` (items #24-29). | +| ohm-rfc-app-flotilla `main` | `c8804a6` — v1.1.0 tagged + pushed at `417b6c5`; CLAUDE.md naming-form update at `c8804a6`. | +| OHM live | `/api/health` = `{"version":"0.18.0","status":"ok"}`. Bundle `/assets/index-DGoalVcp.js` carries both VITE_* keys. SMTP send proven end-to-end via probe email. deploys.id=22 was the last formally-succeeded row (the SMTP fix at v0.17.0); rows 23, 24, 25 all `aborted` due to SSH-timeout pattern but the service IS at v0.18.0 healthy with v1.1.0 bundle. | +| DMARC | Phase A live at `_dmarc.wiggleverse.org` (`v=DMARC1; p=none; pct=100; sp=quarantine; adkim=s; aspf=s; rua=mailto:ben.stull@wiggleverse.org`, TTL=3600). | +| Cloudflare Turnstile | sitekey `0x4AAAAAADXziMhZwT0pKCud` widget has `ohm.wiggleverse.org` in its hostname allowlist (operator-added this session). | +| SMTP | Workspace SMTP relay via `smtp-relay.gmail.com:587` + STARTTLS + SMTP AUTH (`ben.stull@wiggleverse.org` + Workspace app password bound at `wiggleverse-ohm/ohm-rfc-app-smtp-password@latest`). Probe email arrived. | +| Transcript naming | Letter form retired. 16 historical transcripts renamed on `wiggleverse/ohm-session-history` at commit `1afa9f5`. | + +| Wave 7 ledger | Status | +| --- | --- | +| SMTP fix (deployment-side) | ✅ shipped (operator + driver gestures) | +| Two M docs PRs review | ✅ reported (operator merges; not driver) | +| rfc-app v0.18.0 (#18 + #20 framework) | ✅ shipped (subagent 0014.1, all 5 slices) | +| flotilla v1.1.0 (VITE_* fix) | ✅ shipped (unplanned but load-bearing) | +| DMARC Phase A | ✅ live (operator DNS edit) | +| Roadmap #23 (transcript rename) | ✅ executed (16 transcripts + docs + script) | +| End-to-end OTC sign-in verify | ⏸ not yet operator-confirmed in session | + +--- + +## §19.2 candidates surfaced + +(Each is captured in the next-session prompt as a Wave 8+ +candidate. Numbering is session-local; cross-repo SPEC.md §19.2 +sections accumulate these over time.) + +1. **flotilla: per-deployment "expected VITE_* keys" manifest + + `flotilla overlay validate` verb.** A build that drops a + VITE_* key still succeeds; only browser inspection surfaces + the gap. Same shape as Session 0013.3's "expected but missing + keys" candidate — these may be the same item. v1.2.0 candidate. + +2. **flotilla: SSE-keepalive SIGTERM holding past 60s SSH + watchdog timeout** (= Session 0009.0's §19.2). Hit 4× more this + session (deploy rows 23, 24, 25 all aborted-but-healthy). The + watchdog timeout doesn't reflect the actual restart success. + A flotilla minor that pre-drains SSE connections before + `systemctl restart` would close this. v1.2.0 candidate. + +3. **rfc-app: framework should accept an `EMAIL_HELO_DOMAIN` env + override** (or auto-derive from EMAIL_FROM's domain) and pass + `local_hostname=` to `smtplib.SMTP()`. The IP-allowlist-only + SMTP relay path isn't viable for any deployment without this. + Path C in this session couldn't proceed without a framework + change. + +4. **flotilla / framework: .env quote-stripping inconsistency.** + `set -a; source .env` evaluates shell substitution (which is + how the `$(pbpaste)` Turnstile secret bug surfaced). systemd's + `EnvironmentFile` doesn't. Python-side parsing varies. A + `flotilla overlay set` / `flotilla secret set` that detected + and refused literal shell substitution tokens (`$(...)`, + backticks, `${...}` etc.) at input time would prevent this + class of bug entirely. + +5. **rfc-app: `mail-tester.com` baseline probe gesture should be + formally documented** (per v0.18.0 CHANGELOG SHOULD step) so + operators confirm the new envelope headers actually score on + real inboxes. Not done in Session 0014.0; defer to the next + email-touching session. + +--- + +## What lands on the operator's plate + +1. **End-to-end OTC sign-in test.** First action for the next + driver session. Fresh incognito window, type a `+ohm-otc-test` + alias, let Turnstile complete (should now actually issue a + token because the bundle has the sitekey + Cloudflare's + allowlist has the hostname), submit, watch the inbox for the + 6-digit code. If a code arrives, OHM email + sign-in are + proven end-to-end for the first time since launch. + +2. **Merge the two Session-M docs PRs** when convenient + (no-bump docs commits, operator-merges only): + - rfc-app `feature/contributing-and-spec-analytics` @ `213f686` + - ohm-rfc `feature/contributing-roadmap-guide` @ `f26fae4` + +3. **Watch the DMARC `rua` mailbox** at + `ben.stull@wiggleverse.org` over the next ≥1 week. When the + reports show clean alignment for a week, a future session + flips `p=none` → `p=quarantine`. + +4. **Decide on the remaining ops gestures from item #18**: stale + `wiggleverse/meta` hook deletion (5-second gesture, removes + plain-HTTP-to-deprovisioned-domain liability); bounce-source + wiring (Path A or B in the runbook). Schedule with a future + driver session. + +5. **Decide whether to ship flotilla v1.2.0** (the two + §19.2 candidates above: VITE_* validation manifest + SSE- + pre-drain on restart) as the next flotilla touch, or fold + them into a later batch. + +--- + +## Prompt the operator can paste into the next Claude Code session + +``` +You are the OHM roadmap driver, Session 0015.0 (= the session that +would have been labeled "Session O" under the legacy letter form). +Session 0014.0 (Session N) executed a heavy Wave 7 + the #23 +transcript rename — read this whole prompt before doing anything. + +# What Session 0014.0 shipped + +Wave 7 was supposed to be SMTP fix + v0.18.0 + DMARC + M PR review. +The operator chose "all of the above" and as the SMTP triage +unfolded, three additional latent bugs surfaced (all +broken-since-launch on OHM, none diagnosable from `flotilla overlay +show` or the deploy log): + +1. **rfc-app v0.18.0** (subagent 0014.1) — email + webhook hygiene + proposal landed all 5 slices: `build_envelope` helper with full + hardened-header set (Date, Message-ID, Auto-Submitted, + List-Unsubscribe + One-Click), mandatory `GITEA_WEBHOOK_SECRET` + (with `RFC_APP_INSECURE_WEBHOOKS=1` dev-bypass), `outbound_emails` + audit table + admin endpoint, bounce correlation. Tagged at + `31913b1`, pushed to both `origin` + `benstull`. 295 tests + passing (+43 from baseline). + +2. **flotilla v1.1.0** (committed `417b6c5`, tagged + pushed) — + `deploy.py` phase 5 (frontend build) now propagates VITE_* + overlay keys to `npm run build`'s process.env so they reach + Vite's `loadEnv()`. Pre-1.1.0 deploys shipped bundles with empty + VITE_*, which silently disabled **Cloudflare Turnstile since + rfc-app v0.12.0** (Session K) and **Amplitude analytics + + session replay since rfc-app v0.15.0** (Session L). Both were + confirmed broken by curl'ing the deployed bundle and grepping + for the expected key strings (neither was present) — fixed via + bundle re-grep (both now in the v0.18.0 bundle at + `/assets/index-DGoalVcp.js`). + +3. **SMTP fix (deployment-side, ops gesture)** — + `SMTP_PASSWORD` was never bound since launch. The framework's + `email.py:316` silently fell back to stdout-logging when + `SMTP_HOST` was empty, returning True so the DB recorded + successful delivery on every send. Every OTC, every invite, + every §15.4 notification was silently dead-lettered. The + restoration path took several iterations: + - First attempted IP allowlist alone (Path A): Workspace SMTP + relay rejected because Python smtplib's default HELO is the + VM hostname (`ohm-app`), not a Workspace-registered domain. + - Setting `local_hostname='wiggleverse.org'` (Path C) also + failed — Cloudflare-style edge checks rejected it. + - Path A' (SMTP AUTH) worked: operator generated a Workspace + app password, `flotilla secret set ohm-rfc-app SMTP_PASSWORD`. + Probe email through the framework's `_deliver` function + arrived in operator's inbox. + +4. **CLOUDFLARE_TURNSTILE_SECRET was the literal string + `$(pbpaste)`** (a previous failed `secret set` captured the + un-substituted shell token). Operator re-set via the canonical + pbpaste pipe gesture. Confirmed via VM-side `set -a; source + /opt/ohm-app/backend/.env` no longer erroring with + `pbpaste: command not found`. + +5. **DMARC Phase A** — DNS edit at `_dmarc.wiggleverse.org`: + `v=DMARC1; p=none; pct=100; sp=quarantine; adkim=s; aspf=s; + rua=mailto:ben.stull@wiggleverse.org` (TTL=3600s for fast + rollback during ramp). Single record at both 8.8.8.8 and + 1.1.1.1; the operator initially published it alongside the old + record (RFC 7489 §6.6.3 violation that weakens DMARC to "no + policy"), then deleted the old record. + +6. **Cloudflare Turnstile hostname allowlist** — the widget's + "Domains" / "Hostname management" list didn't include + `ohm.wiggleverse.org` (probably had `rfc.wiggleverse.org` from + pre-rename), causing Cloudflare's `/start` endpoint to return + 401 for every challenge. Operator added the hostname. + +7. **#23 (transcript rename to numeric form)** — fully executed: + - 16 local transcripts renamed A→0001.0 through M.3→0013.3 + + N.1→0014.1 (stale L INPROGRESS draft left as-is). + - SESSION-PROTOCOL.md §1 (naming convention) and §5 (subagent + transcripts) rewritten with the new convention and a legacy + mapping table. + - `scripts/publish-transcript.sh` filename + header validators + extended to accept BOTH the numeric form (binding from + 0014.0 onward) AND the legacy letter form (so renamed-old- + files remain re-publishable for content corrections). + - `ohm-rfc-app-flotilla/CLAUDE.md` updated. rfc-app/CLAUDE.md + had no session-letter refs. + - All 13 historical transcripts renamed via `git mv` on + `wiggleverse/ohm-session-history` in a single commit, history + preserved. + - Session 0014.0 itself is the first session under the new + naming. Its main transcript at + `SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--.md` + references the numeric mapping in its header. + +# Cut state at handoff + +- rfc-app `main`: `31913b1` — v0.18.0 tagged; pushed to canonical + + mirror. +- ohm-rfc `main`: `c527aca` — `.rfc-app-version=0.18.0`. Operator + added items #24-29 in a parallel session at `74a3e12`. +- ohm-rfc-app-flotilla `main`: `417b6c5` — v1.1.0 tagged + pushed. +- OHM live: **v0.18.0 healthy** (`/api/health` returns + `{"version":"0.18.0","status":"ok"}`). deploys.id=22 was the + last formally-succeeded row (the SMTP fix at v0.17.0). Rows 23, + 24, 25 are all aborted-due-to-SSH-timeout (Session-I §19.2 + SSE-keepalive pattern; service IS at v0.18.0 healthy with the + v1.1.0 bundle containing both VITE_* keys; the aborted rows just + reflect the SSH watchdog falling off, not the deploy itself). +- Pin source: `ohm-rfc/.rfc-app-version` = `0.18.0`. +- DMARC Phase A live (see #5 above). Watch `rua=` mailbox at + `ben.stull@wiggleverse.org` for ~1 week of clean reports before + flipping to `p=quarantine`. +- Two Session-M docs feature branches still awaiting operator + merge: + - rfc-app `feature/contributing-and-spec-analytics` @ `213f686` + - ohm-rfc `feature/contributing-roadmap-guide` @ `f26fae4` + +# What is and isn't proven working + +- ✅ SMTP send through Workspace SMTP relay + SMTP AUTH (probe + email arrived). +- ✅ VITE_TURNSTILE_SITE_KEY in production bundle. +- ✅ VITE_AMPLITUDE_API_KEY in production bundle. +- ✅ CLOUDFLARE_TURNSTILE_SECRET is no longer literal `$(pbpaste)`. +- ✅ Cloudflare Turnstile widget hostname allowlist contains + `ohm.wiggleverse.org` (per operator). +- ⏸ End-to-end OTC sign-in flow **not yet operator-confirmed** — + Session 0014.0 ended before the operator retested. First gesture + this session: have the operator try OTC sign-in in a fresh + incognito window. If a 6-digit code arrives in their inbox, the + full flow is proven for the first time since launch. If it + doesn't, dig in (most likely culprits: more browser-side + Turnstile issues, or a different env-var the bundle still + lacks). +- ⏸ Amplitude dashboard data — should start flowing now that the + bundle has the key, but no events have been confirmed yet. If + operator wants to verify quickly: sign in, navigate, check + Amplitude dashboard for events within ~5 min. + +# Wave 8 candidates (priority order is operator's call) + +1. **Verify OTC sign-in end-to-end** (above). Five minutes if + it works; longer if not. +2. **Operator merges the two M docs PRs** (no-bump docs commits). +3. **#1 VM rename** (`ohm-app` → `ohm-rfc-app`) — operator-led ops + gesture, schedule a maintenance window. The §19.2 SSE-keepalive + timeout pattern was hit 3× this session; worth fixing alongside + the rename (or as a flotilla v1.2.0 — graceful drain of SSE + connections before the 60s SSH watchdog). +4. **#20 ops side**: delete stale `wiggleverse/meta` hook on + deprovisioned `rfc.wiggleverse.org`; after ≥1 week of clean + `rua` data, flip DMARC to `p=quarantine`. +5. **#18 ops side**: bounce-source wiring (Path A or B in the + runbook). +6. **#21 Part A Amplitude audit** — now that data starts flowing + today, defer to ~2026-06-04 for a week of post-fix dashboard + data. +7. **#22 pro-consent copy** — operator-drafted + counsel-reviewed; + subagent wires once approved. +8. **#26-29** — the four new items the operator added at `74a3e12` + (propose-RFC fields, Haiku tags, PR cross-references, sign-in + resume). Each is a clean rfc-app minor. +9. **#17 repo naming alignment** — operator decides Q1 + (per-RFC repos vs monorepo) first. + +# §19.2 candidates surfaced in Session 0014.0 + +Per the cross-repo discipline, each is captured in its repo's +SPEC.md §19.2 list (or as an inline note in the Session 0014.0 +transcript): + +a. **flotilla: per-deployment "expected VITE_* keys" manifest + + `flotilla overlay validate` verb.** A v1.1.0 build that drops + VITE_AMPLITUDE_API_KEY still succeeds; only browser inspection + surfaces the gap. A declared-vs-actual check would catch this + regression class. Same shape as M.3's "expected but missing + keys" candidate — these may be the same item. + +b. **flotilla: SSE-keepalive SIGTERM holding past 60s SSH watchdog + timeout.** Session I noted it (§19.2); hit 3× more this session + (deploy rows 23, 24, 25 all aborted-but-healthy). The watchdog + timeout doesn't reflect the actual restart success. A flotilla + minor (v1.2.0?) that pre-drains SSE connections before + `systemctl restart` would close this. + +c. **rfc-app: framework should accept an `EMAIL_HELO_DOMAIN` env + override** (or auto-derive from EMAIL_FROM's domain) and pass + `local_hostname=` to `smtplib.SMTP()`. Without this, the + IP-allowlist-only SMTP relay path isn't viable for any + deployment — only SMTP AUTH paths work. Path C in the Session N + triage couldn't proceed without a framework change. + +d. **flotilla / framework: .env quote-stripping inconsistency.** + `set -a; source .env` evaluates shell substitution (which is how + the `$(pbpaste)` Turnstile bug surfaced). systemd-side + `EnvironmentFile` doesn't. Python-side parsing varies. A + `flotilla overlay set` that detected and refused literal shell + substitution tokens (`$(...)`, backticks, `${...}` etc.) would + prevent this class of input bug. + +e. **rfc-app: `mail-tester.com` baseline probe gesture should be + documented** (per v0.18.0 CHANGELOG SHOULD step) so operators + confirm the new envelope headers actually score on real + inboxes. Not done in Session 0014.0; defer to the next + email-touching session. + +# Hard rules carried forward (binding) + +- **Never EVER ask the operator to paste secret bytes into the + conversation.** Always give the operator-run gesture + (`pbpaste | flotilla secret set ohm-rfc-app `). Public + bundle-embedded values (VITE_*) are fine in-conversation via + `flotilla overlay set`. + +- **§5 subsession-transcript convention** (binding from Session + 0013.0 / Session-M onward): forked subagents write their own + transcripts at `SESSION-NNNN.N-TRANSCRIPT-…md` BEFORE returning + their report. Subagents MUST NOT add a "final sanity check" + phase between transcript-write and report-back (where M.1 died). + Driver re-verifies deliverables on disk rather than depending on + the report-back text. + +- **Protocol amendments must land with the code change** (Session + M §19.2 candidate #14): an amendment that claims a code change + MUST also include the code change in the same merge, or be + marked "pending implementation." + +- **Naming**: this is Session 0015.0. The legacy letter form is + retired from new transcripts. Driver-written transcript: + `SESSION-0015.0-TRANSCRIPT---.md`. Subagent + transcripts: `SESSION-0015.1-…`, `SESSION-0015.2-…`, etc. + +# First actions for Session 0015.0 + +1. Read `~/git/ohm-infra/SESSION-PROTOCOL.md` (updated this + session — read the new §1 + §5). +2. Read `/Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md` + end-to-end. The version-target table shows #18 + #20 partial, + #23 struck through, and the operator added #24-29 mid-Session-N. +3. Read the Session 0014.0 transcript at + `~/git/ohm-infra/SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--.md` + for the full triage detail (this prompt is the summary; the + transcript has the bug-by-bug arc). +4. Have the operator try OTC sign-in end-to-end. That's the + load-bearing first test. +5. Pick Wave 8's shippable scope from the candidates above. +```