add SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--2026-05-28T08-25.md
This commit is contained in:
@@ -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-<letter>` 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--<end>.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 <KEY>`). 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-<start>--<end>.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--<end>.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.
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user