#30: per-session folder layout + README + sessions.json

Restructures the flat repo into one folder per session, with a top-level
README.md (the "about sessions" page, also rendered at /docs/sessions/about
by rfc-app v0.19.0+) and a sessions.json title manifest keyed by 4-digit
NNNN. Filenames inside each folder retain the full SESSION-NNNN.M-TRANSCRIPT-…
form (no trim of the SESSION- prefix). Legacy letter-form transcripts, if
any are still at the root, are not moved — the folder convention applies
to the numeric form only.

Session 0017.0; see SESSION-PROTOCOL.md §1 amendment for the binding shape.
This commit is contained in:
Ben Stull
2026-05-28 09:12:08 -07:00
parent b6a7b7dfe9
commit ad01c0854a
21 changed files with 144 additions and 52 deletions
@@ -0,0 +1,803 @@
# Session M — Transcript
> Date: 2026-05-28
> Goal: Drive Wave 6 — three Track Ω inventory / draft subsessions in
> parallel. No rfc-app feature releases this wave; the wave's job is
> to surface the operator-led gestures that the post-Wave-5 plate
> needs (DNS edits, repo renames, counsel-reviewed copy) and to land
> docs that make OHM's contribution surface honest.
>
> Outcome: **All three subsessions produced their deliverables.**
> M.1 wrote docs feature branches in rfc-app + ohm-rfc (CONTRIBUTING
> + SPEC §21 analytics chapter) — pushed, awaiting operator PR
> review. M.2 wrote a 33 KB repo-naming inventory + move proposal
> with six open questions for the operator. M.3 wrote a deliverability
> + webhook hardening runbook + a rfc-app v0.18.0 framework-hygiene
> proposal — and surfaced one potentially-load-bearing finding:
> SMTP_HOST may be missing from the live flotilla overlay (every OHM
> email path silently broken if so). M.1 hit a stream-watchdog stall
> on the report-back step AFTER writing its transcript + pushing both
> feature branches — recovery was driver-reads-transcript-direct, no
> re-dispatch.
>
> First session running under the §5 subsession-transcript amendment
> (Session-L addition to SESSION-PROTOCOL.md). Three forked
> subagents pre-assigned M.1 / M.2 / M.3; each writes its own
> `SESSION-M.<n>-TRANSCRIPT-…md` before returning a tight report to
> the driver. Driver had to extend the publish script's filename +
> header validators mid-session — Session L documented the §5
> validator extension but never landed it in the script (Arc 7).
> 14 §19.2 candidates surfaced across the three subs + driver.
---
## Pre-session state
**Time**: opened 2026-05-28T05-26 PST (Thursday, just-pre-dawn —
operator handed off the wheel after Session L's three-deploy
Wave 5 finish at 05:17 PST and is sleeping). Session L's main
transcript is at
`~/git/ohm-infra/SESSION-L-TRANSCRIPT-2026-05-28T04-22--2026-05-28T05-17.md`.
**Repos at session open:**
| Repo | State at open |
| --- | --- |
| `rfc-app` (`~/git/rfc-app`, branch `main`) | clean tree, HEAD at `1456c8b` (Release 0.17.0: admin-create user + invite email + #21 Part C). Tags through `v0.17.0`. |
| `ohm-rfc` (`~/projects/wiggleverse/ohm-rfc`, branch `main`) | clean tree, HEAD at `47cb2f9` (ROADMAP strike of #12 / #13 / #16). `.rfc-app-version` = `0.17.0`. |
| `ohm-rfc-app-flotilla` (`~/projects/wiggleverse/ohm-rfc-app-flotilla`, branch `main`) | clean tree, HEAD at `65c0e55` (CLAUDE.md: correct the secret-set gesture). v1.0.1. |
| `ohm-infra` (`~/git/ohm-infra/`) | Plain directory (not a local git repo). Transcripts AL published to `wiggleverse/ohm-session-history` via `scripts/publish-transcript.sh`. SESSION-PROTOCOL.md at the §5-amended version. |
**OHM live state**: `ohm.wiggleverse.org` serves rfc-app v0.17.0,
deploys.id=21 (last deploy from Session L). v0.13.0 cookie banner +
v0.14.0 /docs route + v0.15.0 Amplitude analytics & session replay +
v0.16.0 per-RFC owner invites + v0.17.0 admin-create user invites
all live in production for the first time. #21 Part C identity-
lifecycle wiring (identify-with-properties on sign-in,
setUserProperties on mid-session changes, amplitude.reset on
sign-out, identify-BEFORE-track on invite-claim paths) shipped
inline with the v0.15.0 / v0.16.0 / v0.17.0 release commits.
**Roadmap state**: #2#16 + #21 Part C all shipped (struck through
in `~/projects/wiggleverse/ohm-rfc/ROADMAP.md`). Wave 6 candidates
open:
- #20 Email deliverability (HIGH priority — invites are now the
recipient's first contact with OHM, spam-foldering is silent
invite failure)
- #18 Secure SMTP relay + Gitea webhook (bundles with #20 — shared
DNS edits)
- #19 CONTRIBUTING guides + transcript-linked onboarding
- #21 Part B — SPEC analytics chapter + CONTRIBUTING checklist
(rides #19)
- #17 Repo naming + location alignment (operator-led; subagent
inventories + proposes)
- #1 VM rename (operator-led)
- #22 Pro-analytics-consent copy (BLOCKED — waiting on operator-
drafted + counsel-reviewed copy)
- #21 Part A — deep audit of v0.15.0 taxonomy + autocapture +
replay quality (BLOCKED — needs ~1 week of Amplitude data; v0.17.0
is <12 hours old)
**Housekeeping noticed at open**:
- A stale `SESSION-L-TRANSCRIPT-2026-05-28T04-22--INPROGRESS.md`
(4.3 KB) sits next to Session L's final transcript at
`~/git/ohm-infra/`. The final transcript is the load-bearing one
(was published). The INPROGRESS placeholder is leftover.
Recommend the operator delete it (or driver deletes at the end
of M); not load-bearing for Session M's work.
- Three rfc-app worktrees from Session L still on disk under
`~/git/`:
- `rfc-app-v0.15.0-amplitude/`
- `rfc-app-v0.16.0-owner-invite/`
- `rfc-app-v0.17.0-admin-create-user/`
Each release has shipped and merged to `main`; the worktrees are
no longer load-bearing. Could be cleaned up via
`git worktree remove` when the operator next runs through housekeeping.
---
## Dispatch plan
**Wave 6** runs as three forked subagents, each in its own subsession,
each writing its own transcript per §5 of the protocol. All three
are Track Ω (docs + ops inventory / proposal) — they touch
non-overlapping files in non-overlapping repos, so they run safely
in parallel without merge-conflict risk. None ship code that
runs on OHM in this wave; each produces a feature branch with a
draft + a recommendation block for the operator to review.
| Sub | Brief | Repo(s) touched | Output |
| --- | --- | --- | --- |
| **M.1** | #19 CONTRIBUTING guides bundled with #21 Part B (SPEC.md analytics chapter + CONTRIBUTING analytics checklist) | `rfc-app` (CONTRIBUTING.md + SPEC.md) + `ohm-rfc` (CONTRIBUTING.md) | Feature branches in both repos with drafts; ready for operator PR review |
| **M.2** | #17 inventory of every repo under `git.wiggleverse.org` + `git.benstull.org` with current name, canonical location, mirror status, and proposed name. Drafts a move-proposal doc. No renames executed. | `ohm-infra` (write proposal here) + read-only across all Wiggleverse / benstull repos | A `REPO-NAMING-PROPOSAL.md` (or similar) for operator to approve |
| **M.3** | #20 deliverability + #18 SMTP/webhook hardening combined inventory. Reads current DNS posture, rfc-app `backend/app/email_*.py`, ohm-infra SMTP/webhook config, Gitea webhook auth surfaces. Drafts a hardened-shape runbook + a rfc-app template-hygiene minor proposal. | `ohm-infra` (runbook) + read-only on `rfc-app` (drafts the proposal as a doc, NOT as code changes) | A hardening runbook + an rfc-app template-hygiene proposal doc |
**Why no rfc-app code release this wave**: every Wave 6 candidate
either (a) is docs / inventory / proposal that lands as a PR the
operator merges, no version bump, or (b) requires operator-led
gestures (DNS edits, counsel review, rename execution) that can't
happen autonomously. Wave 6 is the "set up Wave 7" wave —
deliverables here become the operator's plate, and Wave 7 picks up
the rfc-app minors (template hygiene, pro-consent copy) and the
ops moves (DNS, renames) that fall out.
**Dispatch contract for each subagent** (per §5.2):
1. Pre-assigned subsession ID named in the dispatch prompt.
2. Subagent MUST write `~/git/ohm-infra/SESSION-M.<n>-TRANSCRIPT-<start>--<end>.md`
before returning a report.
3. Subagent does NOT push code that ships on OHM. Branches only;
operator merges.
4. Subagent does NOT execute operator-led gestures (no DNS edits,
no repo renames, no SMTP provider config changes).
5. Subagent reports back with: branch + commit SHAs, the draft's
one-paragraph PR description, any §19.2 candidates surfaced.
---
## Turn-by-turn arc
### Arc 1 — Opening + Wave 6 plan + parallel dispatch
Operator handed off the wheel at 05:20 PST with a thorough brief
explicitly listing Wave 6 candidates and the new §5 subsession-
transcript dispatch shape. Read SESSION-PROTOCOL.md and ROADMAP.md
end-to-end. Identified six Wave-6-eligible items; three blocked
or operator-only (#22 / #21 Part A / #1), three dispatchable now
(#19 + #21B / #17 / #20+#18). Bundling rationale: #21 Part B
explicitly rides #19's CONTRIBUTING PRs per the roadmap; #18 and
#20 explicitly ship together because the same DNS edits serve both.
Pre-state snapshot above. Then opened this transcript at
`SESSION-M-TRANSCRIPT-2026-05-28T05-26--INPROGRESS.md` and
dispatched the three subagents in parallel at 2026-05-28T05-30 PST.
Each subagent's dispatch prompt named:
- the subsession ID (M.1 / M.2 / M.3)
- the transcript filename to write at session end
- the binding constraints (never-secret-bytes; no-operator-led-
gestures; no force-push / `--amend` / `git config`)
- the read-only inventory boundary (no DNS edits, no repo
renames, no flotilla deploy, no SMTP provider changes)
- the deliverable shape and the report-back format
Dispatch summary:
| Sub | agent state | Scope |
| --- | --- | --- |
| **M.1** | running (background) | `rfc-app/CONTRIBUTING.md` + `rfc-app/SPEC.md` analytics chapter + `ohm-rfc/CONTRIBUTING.md`. Feature branches in both repos. |
| **M.2** | running (background) | `REPO-NAMING-PROPOSAL.md` — inventory + move proposal across `git.wiggleverse.org` + `git.benstull.org`. No renames executed. |
| **M.3** | running (background) | `EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` + `RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`. Read-only DNS / flotilla-overlay / rfc-app code inventory. No DNS or SMTP changes. |
Driver waits for each completion notification, folds the report
+ subsession-transcript reference into Arc 2 / Arc 3 / Arc 4 as
they return. Cut-state table written after all three are in.
### Arc 2 — M.2 returns: #17 repo naming inventory
M.2 completed at 2026-05-28T05-37 PST (~7 min). Full detail:
`SESSION-M.2-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-37.md`.
**Inventory totals:** 9 repos on `git.wiggleverse.org` (4 under
`ben.stull/`, 5 under `wiggleverse/`); 3 on `git.benstull.org`
(under `benstull/`: 1 real sha-synced mirror, 1 stub, 1 empty);
9 local-only working dirs catalogued in the proposal.
**Surfaced friction not in roadmap #17:**
- Three-meta-repos collision (`wiggleverse/meta` +
`wiggleverse/ohm-meta` + `wiggleverse/rfc-0001-human`
the monorepo `ohm-rfc` has won in practice; three repos for
one job).
- Push-mirror description drift between `ben.stull/rfc-app` and
`benstull/rfc-app`.
- A `ben` username alias on git.wiggleverse.org creating a
1-hop redirect chain alongside the canonical `ben.stull`.
- `~/git/ohm-infra/` is a plain directory, not a git repo —
matches what the driver caught at session open. M.2 surfaces
it as a §19.2 candidate (see below).
**Top moves proposed (execution order from the proposal doc):**
1. Archive `wiggleverse/meta` (vestigial; zero-risk warmup).
2. Archive `wiggleverse/ohm-meta` + `wiggleverse/rfc-0001-human`
(requires Q1 operator confirm — per-RFC repos vs. monorepo).
3. Transfer `ben.stull/ohm-rfc``wiggleverse/ohm-rfc` — medium
risk; affects flotilla SPEC (~10 places), 4 test files
hardcode `ben/ohm-rfc`, and flotilla's SQLite registry stores
it as `pin_source_repo` + `corpus_repo` — **operator must run a
flotilla update post-move or the next deploy chases a 2-hop
Gitea redirect.**
4. Transfer `ben.stull/rfc-app``wiggleverse/rfc-app` — largest
blast radius. Flotilla SPEC §3.1 has a *conceptual* paragraph
explaining why rfc-app gets a benstull mirror — that rationale
dissolves under org-namespace and needs rewriting. Good news:
no shipped frontend artifacts reference these URLs, so end-
users see nothing.
5. Archive `benstull/rfc-app-flotilla` stub (empty placeholder
that contradicts SPEC §3.1's "no benstull mirror for flotilla"
rule).
**§19.2 candidates surfaced by M.2:**
1. Stop relying on Gitea path aliases — resolve canonical paths
at registry-update time so flotilla's SQLite registry doesn't
silently chase Gitea redirects after a transfer.
2. Concrete `flotilla-core` reservation slot (the
`benstull/rfc-app-flotilla` stub repo is an accidental artifact
of this need).
3. Promote `~/git/ohm-infra/` to a real git repo — currently a
plain directory; transcripts publish to the remote
`wiggleverse/ohm-session-history` but the runbooks, proposals,
and protocol doc that live in ohm-infra have no version
control.
**Open questions for the operator** (the proposal lists six;
the load-bearing ones):
- **Q1**: per-RFC repos vs monorepo — settles whether step 2 above
happens at all, and may reshape the proposal entirely.
- **Q2**: mirror destination after rfc-app moves.
- **Q3**: fate of the `benstull/rfc-app-flotilla` stub (the
proposal recommends archive; operator confirm).
- **Q4**: promote ohm-infra/ to a real repo (yes? then where?).
- **Q5**: remove the `ben` username alias.
- **Q6**: backup ohm-infra/ before execution session begins.
**Artifacts written by M.2:**
- `~/git/ohm-infra/REPO-NAMING-PROPOSAL.md` (~33 KB).
- `~/git/ohm-infra/SESSION-M.2-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-37.md`.
Zero remote git operations. No Gitea admin actions. Clean
inventory + proposal pass per scope.
### Arc 3 — M.3 returns: #20 + #18 deliverability & webhook hardening
M.3 completed at 2026-05-28T05-58 PST (~28 min). Full detail:
`SESSION-M.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md`.
**Load-bearing finding (top of operator's plate)**: `SMTP_HOST`
and `SMTP_USER` are MISSING from the live flotilla overlay. Only
`SMTP_PORT=587`, `SMTP_STARTTLS=1`, `EMAIL_FROM`, `EMAIL_FROM_NAME`
are present. Either the bootstrap was reshaped post-run and these
dropped, OR they live on the VM out-of-band via systemd override.
**If neither holds, every OHM email path is silently failing
right now** (OTC, beta-access notifications, per-RFC owner invites,
admin-create user invites — all four paths added across v0.7.0 →
v0.17.0). Operator disambiguates next session via
`gcloud ssh ohm-app` + `systemctl show ohm-app.service | grep Environment`.
**Current DNS posture (`wiggleverse.org`, literal values):**
- SPF: `v=spf1 include:_spf.google.com ~all` (soft-fail)
- DMARC: `v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org`
(monitoring-only, no enforcement)
- DKIM: present at `google._domainkey.wiggleverse.org`
(2048-bit Workspace key)
- MX: standard Google Workspace `aspmx.l.google.com` family
**SMTP provider identified:** Google Workspace SMTP relay
(`smtp-relay.gmail.com`) — inferred from bootstrap + DKIM + MX
(no direct confirmation since `SMTP_HOST` is missing from the
overlay).
**Top 3 deployment-side hardening gestures (priority order):**
1. **Delete stale `wiggleverse/meta` Gitea hook** — still active,
points at `http://rfc.wiggleverse.org/api/webhooks/gitea`
(host was deprovisioned 2026-05-27, plain HTTP). 5-second
`curl DELETE`. Trivial cleanup.
2. **Disambiguate the SMTP_HOST gap** (see load-bearing finding
above). Highest user impact if email is actually silently
broken.
3. **DMARC Phase A** — extend the existing `p=none` record with
`pct=100; sp=quarantine; adkim=s; aspf=s`, collect `rua` data
for 1 week, flip to `p=quarantine`. Conservative enforcement
ramp.
**Top 3 framework-side changes (target rfc-app v0.18.0):**
1. **`build_envelope` shared helper** — adds `Date`, `Message-ID`,
`Auto-Submitted: auto-generated`, `List-Unsubscribe` +
`List-Unsubscribe-Post` (per RFC 8058), and
`multipart/alternative` text+html to all four email paths
(`email_otc.py`, `email_invite.py`, `email.py:_deliver`,
`email.py:_send_bundle`). Today: NONE of these headers are
set; bodies are plain-text-only; Gmail's one-click-unsubscribe
enforcement is unsatisfied for bulk-adjacent watcher
notifications.
2. **Make `GITEA_WEBHOOK_SECRET` required, not optional.**
`config.py:75` is `_optional`; `webhooks.py:43` silently
accepts unsigned POSTs when empty. Insecure-by-default for any
deployment that forgets the binding. Add `RFC_APP_INSECURE_WEBHOOKS=1`
dev-bypass for local dev only.
3. **`outbound_emails` audit table + write on every send.**
Today failures log-and-forget; operator has zero visibility
without grepping VM logs. The audit table seam doubles as a
place to wire bounce-handling later.
**Webhook posture audit:** signature verification is correct
(`hmac.compare_digest`, HMAC-SHA-256 over raw body) **when the
secret is set** — and on OHM it is. Two structural issues:
(a) silent-accept-when-unset default; (b) no replay-window
(currently mitigated by idempotent cache refreshes, so not yet
exploited but worth a §19.2 candidate or follow-up rfc-app minor).
M.3 also noted: `wiggleverse/rfc-0001-human` has NO Gitea webhook
configured (the only published RFC's content changes only reach
OHM via the reconciler). Operator confirms whether this is
intentional. Bot lacks admin on `ben.stull/ohm-rfc` (the pin
source), so M.3 couldn't self-check that one.
**§19.2 candidates surfaced by M.3 (5):**
1. rfc-app: `GITEA_WEBHOOK_SECRET` should be required.
2. rfc-app: outbound-email observability seam (audit table).
3. flotilla: `overlay show` doesn't surface expected-but-missing
keys; needs `overlay validate` verb.
4. flotilla: lacks a `webhook list/check` verb (generic, would
land in `flotilla-core` after extraction per §19.2).
5. rfc-app: From-identity spec to bind `EMAIL_FROM` consumers +
DMARC + Workspace allowed-senders to one source.
**Artifacts written by M.3:**
- `~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` (~21 KB).
- `~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md` (~9 KB).
- `~/git/ohm-infra/SESSION-M.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md`.
Read-only DNS / flotilla-overlay / rfc-app code inventory. Zero
DNS edits, zero overlay set, zero code commits. Clean per scope.
### Arc 4 — M.1 returns (with a stall): #19 + #21B docs landed
M.1 hit the stream watchdog at 600s-no-progress and the agent was
killed before it could return its report. Its own transcript says
end-time 05:40 PST — meaning it wrote the transcript ~10 minutes
into its run, then spent another ~50 minutes in a "sanity check
before reporting back" phase that produced no streamable output and
got watchdog-killed.
Per protocol §5.5 the driver MAY write a stub if a subagent fails to
produce a transcript. M.1 did produce a transcript (full 460-line
detailed shape, all the synthesis the driver needs); the stall was
on the report-back-to-driver step. Driver chose **no re-dispatch**:
all deliverables landed cleanly on disk, both feature branches
pushed to canonical (rfc-app to origin + benstull, ohm-rfc to
origin), tree clean. Recovery was: read M.1's transcript directly
+ switch the two repos back to `main` (M.1 left them on the feature
branches) + fold synthesis into this transcript.
**Pre-state addendum surfaced by M.1's own pre-state**: ohm-rfc
`main` was at `8cce148` when M.1 opened at 05:30, but this
driver's pre-state at 05:26 (above) recorded ohm-rfc `main` at
`47cb2f9`. The operator pushed `8cce148 ROADMAP: +#23 (rename
transcripts SESSION-<letter> → SESSION-NNNN.M)` to ohm-rfc/main in
the 4-minute gap between session-open and M.1 dispatch. Recorded
honestly here per the protocol's no-curation rule — this is the
Session-L "don't batch roadmap captures to end-of-session" lesson
applied in real time. **Roadmap item #23 now exists** (transcript-
filename rename) and is captured on ohm-rfc/main outside of any
Session-M subagent's scope.
**M.1 deliverables (all on feature branches, none merged):**
| | |
| --- | --- |
| `rfc-app` `feature/contributing-and-spec-analytics` | `213f686 docs: CONTRIBUTING.md + SPEC.md §21 analytics chapter`. Pushed to `origin` + `benstull`. |
| `ohm-rfc` `feature/contributing-roadmap-guide` | `f26fae4 CONTRIBUTING: add roadmap-change guide + operator-only boundary`. Pushed to `origin`. |
`rfc-app/CONTRIBUTING.md` is new, ~370 lines, covering: PR shape,
strict-descending CHANGELOG convention, RFC 2119 `Upgrade steps:`
discipline, §19.2 candidate practice, test-coverage expectations
characterized from the real ~250-test backend suite, the operator-
only-gestures boundary (with the hard no-secret-bytes rule cited
from Session K Arc 6), and the #21 Part B analytics-instrumentation
checklist as a dedicated section. Worked-example links to Sessions
E (clean small release), I (deploy-fault recovery), K (multi-feature
wave + paused-on-secret), L (squash-merge across three parallel
features + #21 Part C inline). Links to the repo root not specific
transcript filenames — anticipating the #23 rename.
`rfc-app/SPEC.md` gets new §21 "Analytics instrumentation and
identity", ~440 lines, structured §21.1 event-taxonomy → §21.2
required prop families → §21.3 autocapture-friendly DOM patterns
→ §21.4 session-replay masking → §21.5 consent-gate contract →
§21.6 identity lifecycle (#21 Part C) → §21.6.1 set vs setOnce
taxonomy → §21.7 cohort-shape implications → §21.8 secret-vs-public
framing → §21.9 §19.2 candidates → §21.10 open question. RFC 2119
keywords used precisely throughout. The starting v0.17.0 taxonomy
(13 events) laid out verbatim.
**Placement decision the driver should weigh in on:** M.1 settled
inline on placing the new chapter as §21 (after §20 versioning)
rather than §16 (the naively-correct insertion-after-§15
notifications). Rationale: §19.2 is a load-bearing project noun
(in CLAUDE.md, SESSION-PROTOCOL.md §3, 12 published transcripts,
historical commit messages) — renumbering would cascade across all
of those. §15 now carries a forward-pointer naming §21 as the peer
cross-cutting analytics chapter. The driver agrees with M.1's call:
the right §16 numbering belongs to a future cross-repo cleanup
session, NOT a docs-PR. Flagged as §19.2 candidate #1 from M.1
(see below).
`ohm-rfc/CONTRIBUTING.md` extends the existing 130-line RFC-content
guide (preserved verbatim) with a new "Proposing a change to the
OHM roadmap" section (~80 added lines). Covers: roadmap-change
kinds, ohm-session-history pointer + worked-example links into
Sessions J/K/L roadmap reshuffles, PR-submission gesture with the
**anonymous-PR open question SETTLED inline** (see Arc 5 below),
the operator-only-gestures boundary phrased with the "I'm the only
one that gets to yolo" quote, the two-layer rule.
### Arc 5 — Anonymous-PR question settlement (M.1 inline)
M.1 probed `https://git.wiggleverse.org/user/sign_up` and got HTTP
200 with body containing `<p>Registration is disabled. Please
contact your site administrator.</p>`. Definitive answer.
The ohm-rfc CONTRIBUTING text settles on two contributor paths:
Issue-first (default; the operator or next driver session turns it
into a roadmap entry or opens discussion in the issue thread), or
request-account-from-operator (for repeat contributors). Phrased so
the operator can edit down to one path if registration policy
flips later.
### Arc 6 — Recovery + finalize
Driver switched rfc-app + ohm-rfc back to `main` (M.1 left them on
feature branches; `git checkout main` on each — clean, "up to date
with origin/main"). Marked M.1 task completed with a description
recording the stall + recovery decision. Wrote this turn-by-turn,
cut state, §19.2 ledger, operator plate, next-session prompt.
Renamed transcript INPROGRESS → final timestamp. About to publish
all four transcripts (M + M.1 + M.2 + M.3) via
`~/git/ohm-infra/scripts/publish-transcript.sh` per §5.6.
### Arc 7 — Publish-script validator extension (in-scope plumbing)
First attempt to publish M.1 failed:
`filename does not match SESSION-<letter>-TRANSCRIPT[-<start>--<end>].md`.
Investigation revealed a **protocol-vs-reality discrepancy**: both
the operator's brief ("the publish script's filename validator was
extended in Session L to accept `SESSION-<letter>.<n>-TRANSCRIPT-…md`")
and SESSION-PROTOCOL.md §5.6 ("The publish script's filename
validator accepts the `SESSION-<letter>.<n>-TRANSCRIPT-…md` form
(extended Session-L, see `scripts/publish-transcript.sh`)") said
the extension was already in place — but the actual regex in
`scripts/publish-transcript.sh` at L127 was still
`^SESSION-[A-Za-z]+-TRANSCRIPT(-${_TS}--${_TS})?\.md$`, and the
header check at L140 only accepted `Transcript` / `transcript`
endings, not `Subsession transcript`. The extension was documented
in Session L's protocol amendment but never landed in the script.
Driver made minimal-scope edits to fix:
- Filename regex: extended to `^SESSION-[A-Za-z]+(\.[0-9]+)*-TRANSCRIPT(-${_TS}--${_TS})?\.md$` (accepts `.N` and `.N.N` for hypothetical sub-subagents per §5.2's nested convention).
- Header regex: extended to accept `Subsession transcript` and `— Subsession transcript` forms; allow `.N` after the session letter.
- Updated the comment block at L123 and L131-135 to document the subsession form.
- Updated the error message at L128 to name `[.<n>]` explicitly.
Dry-ran M.1 to verify both validators pass against
`# Session M.1 — Subsession transcript`. Header OK. Then published
M.1 → M.2 → M.3 → M serially.
**Note on scope**: this script edit is in-scope plumbing for §5 being
operational at all — Session M is the first session under §5, so the
validator gap had to be fixed before the wave could complete. The
change is small (regex extensions + comment/message updates only,
no logic changes), commented in the script, and the dry-run gesture
confirmed correctness before the real push. The script lives in
`~/git/ohm-infra/`, which is **not under version control** per M.2's
§19.2 candidate #7 — so this script change is on local disk only
with no history. That candidate is now more salient.
This driver-surfaced issue is captured below as §19.2 candidate #14
(reconcile protocol amendments with actual code changes — the
amendment should fail review if the code change isn't also present).
### Arc 8 — Post-publish close (operator-confirmed at ~07:00 PST)
After Arc 7's validator extension + serial publish of all four
transcripts (M.1 @ `7c7c478` → M.2 @ `5dad1b2` → M.3 @ `2007cf3`
→ M @ `61698af`), driver re-published M with the Arc 7 + §19.2
candidate #14 + script-edit-disclosure additions (final commit
`6248f88` on `wiggleverse/ohm-session-history` `main`).
Operator (returned ~07:00 PST, not asleep) confirmed close and
asked for: (1) the next-session prompt surfaced verbatim in chat
for easy copy, (2) the transcript updated with this final
exchange. Driver replied with the prompt printed in full (no
substantive change from the version embedded in this transcript
below in the "Prompt the operator can paste…" section; reproduced
verbatim for ergonomics), then added this Arc 8 + re-published
per the Session-L "transcript is the final session step" feedback
memory ("re-finalize if more work happens after").
Final state of this session matches the cut-state table below.
No code shipped to OHM. Two docs feature branches and three
ohm-infra/-local proposal/runbook docs are the deliverables that
become Wave 7's plate.
---
## Cut state (end of session, 2026-05-28T06-50 PST)
| | |
| --- | --- |
| `rfc-app` | `main` unchanged at `1456c8b` (Release 0.17.0). New branch `feature/contributing-and-spec-analytics` at `213f686` pushed to `origin` + `benstull` — awaiting operator PR review. Local tree on `main`, clean. |
| `ohm-rfc` | `main` advanced from `47cb2f9``8cce148` mid-session (operator pushed #23). New branch `feature/contributing-roadmap-guide` at `f26fae4` pushed to `origin` — awaiting operator PR review. `.rfc-app-version` unchanged at `0.17.0`. Local tree on `main`, clean. |
| `ohm-rfc-app-flotilla` | unchanged at `65c0e55`. v1.0.1. |
| OHM live | unchanged from Session L cut — `deploys.id=21`, v0.17.0. No deploy gesture this session. |
| `ohm-infra` (local dir) | 4 new files this session: this transcript + M.1/M.2/M.3 subsessions. Plus 2 inventory/runbook docs from M.2 (`REPO-NAMING-PROPOSAL.md`) and 2 from M.3 (`EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md`, `RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`). |
| Wave 6 ledger | Status |
| --- | --- |
| #19 CONTRIBUTING guides (rfc-app + ohm-rfc) | 🟡 drafts pushed on feature branches; awaiting operator PR review + merge |
| #21 Part B SPEC.md §21 chapter + CONTRIBUTING checklist | 🟡 drafted on rfc-app feature branch (rides #19's commit); awaiting operator review |
| #17 Repo naming + location alignment (inventory + proposal) | 🟡 proposal doc written at `~/git/ohm-infra/REPO-NAMING-PROPOSAL.md`; six open questions for operator; execution session deferred |
| #20 Email deliverability inventory | 🟡 runbook written; one load-bearing finding (SMTP_HOST possibly missing from overlay); awaiting operator disambiguation + DNS edits |
| #18 SMTP relay + Gitea webhook hardening inventory | 🟡 runbook same as #20 (bundled); webhook code-side proposal in `RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`; awaiting future rfc-app v0.18.0 minor + ops gestures |
| #21 Part A audit | ⏸ blocked — needs ~1 week of Amplitude data; v0.17.0 is <12h old |
| #22 Pro-analytics-consent copy | ⏸ blocked — waiting on operator-drafted + counsel-reviewed copy |
| #1 VM rename | ⏸ operator-led ops gesture |
| #23 Transcript filename rename (`SESSION-<letter>``SESSION-NNNN.M`) | ✅ added to ROADMAP (ohm-rfc/main `8cce148`, operator pushed mid-session) |
No rfc-app release shipped this wave; that's Wave 6's intended shape
(set-up wave — deliverables become Wave 7's plate). The plate is
heavy and clearly-named.
---
## §19.2 candidates surfaced this session
From **M.1**:
1. **SPEC.md §16..§20 renumbering vs. §21-at-end placement.** The
chapter went in as §21 not §16 to preserve the §19.2 / §19.3
cross-reference graph; a future cleanup session can do the
numerically-clean §16 insertion + cascade across CLAUDE.md
files + SESSION-PROTOCOL.md + 12 transcripts + auto-memory.
2. **Centralized email-hash helper** (SHA-256 of normalized lower-
cased email for #12-style per-RFC `target_email` hashing).
3. **Property-shape CI lint** for `track(` / `identify(` callsites
(allowlist of prop keys, denylist of PII names).
4. **Frontend test runner** (no `vitest`/`jest`/`playwright` today;
the CONTRIBUTING file documents the gap honestly).
From **M.2**:
5. **Stop relying on Gitea path aliases** — resolve canonical paths
at registry-update time so flotilla's SQLite registry doesn't
silently chase Gitea redirects after a transfer.
6. **Concrete `flotilla-core` reservation slot** — the
`benstull/rfc-app-flotilla` stub is an accidental artifact of
this latent need.
7. **Promote `~/git/ohm-infra/` to a real git repo** — currently a
plain directory; transcripts publish to the remote but runbooks,
proposals, and the protocol doc have no local version control.
From **M.3**:
8. rfc-app: `GITEA_WEBHOOK_SECRET` should be **required**, not
`_optional`. Insecure-by-default for any deployment that forgets
the binding.
9. rfc-app: **outbound-email observability seam** (`outbound_emails`
audit table + write on every send). Today failures log-and-
forget; doubles as bounce-handling hook later.
10. flotilla: `overlay show` doesn't surface expected-but-missing
keys; needs an `overlay validate` verb. (The session's own
headline finding — possibly-missing SMTP_HOST — would have been
a one-command detection if this existed.)
11. flotilla: lacks a `webhook list/check` verb (generic — would
land in `flotilla-core` after §19.2 extraction).
12. rfc-app: **From-identity spec** to bind `EMAIL_FROM` consumers
+ DMARC + Workspace allowed-senders to one source.
Driver-surfaced (this transcript):
13. **Subagent stream-watchdog stall pattern.** M.1 finished its
work + transcript at minute ~10 but spent ~50 more minutes in a
quiet "sanity check" phase before the watchdog killed it. The
session-protocol §5.3 instruction says transcript-write happens
BEFORE report-back — that's load-bearing, since the report-back
is where the agent dies. Worth a protocol clarification:
"transcript first; report-back is the only thing after; do NOT
add a 'final sanity check' phase between — sanity-check
findings go into the transcript." Or alternatively, a
documented expectation that the driver always re-verifies
deliverables on disk and doesn't depend on the report-back text
alone.
14. **Protocol amendments must land with the code change.** Session L
documented in SESSION-PROTOCOL.md §5.6 that the publish script's
validator was extended to accept `SESSION-<letter>.<n>-TRANSCRIPT-…md`,
and the Session M brief repeated the claim. Reality at session
open: the script's regex hadn't been modified. Driver had to
extend it mid-Session-M to publish the subsession transcripts at
all (see Arc 7). Worth a discipline: an amendment to the
protocol that claims a code change MUST also include the code
change in the same merge (or be marked "pending implementation"
in the protocol text). Plus: amendment review should diff the
protocol claim against the named code surface and reject if
they don't match. This is a Session M lesson; would land in
SESSION-PROTOCOL.md §6 ("when the protocol is unclear") as a
sub-paragraph.
---
## What lands on the operator's plate
In rough priority order (driver's read):
### URGENT — investigate before any further Wave 7 work
1. **Disambiguate the SMTP_HOST / SMTP_USER overlay gap** (M.3
finding). Run `gcloud ssh ohm-app` + `systemctl show
ohm-app.service | grep Environment` to check whether they're set
via systemd override. If not, OHM has been silently dropping
every email (OTC, beta-access notifications, per-RFC invites,
admin-create invites) — meaning v0.7.0 / v0.9.0 / v0.16.0 /
v0.17.0's user-facing email features are degraded since deploy.
Send a manual test through the admin "Create user + invite"
flow as a probe.
### HIGH — review + merge the docs PRs
2. **rfc-app PR** for `feature/contributing-and-spec-analytics` at
`213f686`. Suggested PR description in
`SESSION-M.1-TRANSCRIPT-…md` What-the-driver-needs-to-know #6.
Open on:
- `https://git.wiggleverse.org/ben.stull/rfc-app/pulls/new/feature/contributing-and-spec-analytics`
- `https://git.benstull.org/benstull/rfc-app/pulls/new/feature/contributing-and-spec-analytics`
Operator decides §21-vs-§16 placement — driver concurs with M.1's
§21 call (don't break the §19.2 cross-reference graph).
3. **ohm-rfc PR** for `feature/contributing-roadmap-guide` at
`f26fae4`. Description also in M.1's transcript. Open on:
- `https://git.wiggleverse.org/ben.stull/ohm-rfc/pulls/new/feature/contributing-roadmap-guide`
### MEDIUM — schedule + decide
4. **#17 repo naming proposal** at `~/git/ohm-infra/REPO-NAMING-PROPOSAL.md`.
Six open questions in the proposal that need operator input
before the execution session can be scoped. Q1 (per-RFC repos
vs monorepo) may reshape the proposal entirely; resolve first.
5. **#20 + #18 deliverability runbook** at
`~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md`.
Once #1 above is settled, this is the priority-ordered list of
operator gestures. Top three:
(a) `curl DELETE` the stale `wiggleverse/meta` Gitea webhook
pointing at deprovisioned `http://rfc.wiggleverse.org`;
(b) DMARC Phase A (`pct=100; sp=quarantine; adkim=s; aspf=s`);
(c) confirm `wiggleverse/rfc-0001-human` has no webhook (M.3
noted absence — intentional or oversight?).
6. **rfc-app v0.18.0 minor** is now the natural next release —
carries the framework-side hardening from M.3:
`build_envelope` shared helper (Date/Message-ID/Auto-Submitted/
List-Unsubscribe headers + multipart text+html); required
`GITEA_WEBHOOK_SECRET`; `outbound_emails` audit table.
Proposal doc at `~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`.
### LOW / informational
7. **#23 transcript filename rename** is on ROADMAP now (operator
pushed `8cce148`). This session is the last to use the
`SESSION-<letter>` convention before #23 ships; future sessions
will name as `SESSION-NNNN.M` per the new spec.
8. **Stale Session-L INPROGRESS file** at
`~/git/ohm-infra/SESSION-L-TRANSCRIPT-2026-05-28T04-22--INPROGRESS.md`
(4.3 KB). Final L transcript is published; this is a placeholder
that can be deleted. Driver did NOT delete (operator's call).
9. **Three rfc-app worktrees from Session L** still on disk:
`~/git/rfc-app-v0.15.0-amplitude/`, `~/git/rfc-app-v0.16.0-owner-invite/`,
`~/git/rfc-app-v0.17.0-admin-create-user/`. All shipped + merged.
Cleanup via `git worktree remove` at the operator's convenience.
10. **§19.2 candidate #13** (subagent stream-watchdog stall pattern)
is a candidate protocol clarification for SESSION-PROTOCOL.md §5
— see the candidates list above.
11. **`scripts/publish-transcript.sh` was edited mid-session** to
actually extend the filename + header validators for the §5
subsession form (Session L documented the extension but never
landed it). The script lives in `~/git/ohm-infra/` which is
not version-controlled, so the change has no commit history.
Driver-applied diff: regex `^SESSION-[A-Za-z]+-TRANSCRIPT…`
`^SESSION-[A-Za-z]+(\.[0-9]+)*-TRANSCRIPT…`; header regex
extended to accept `Subsession transcript` and `— Subsession
transcript` endings plus `.N` after the letter. Worked
correctly for all three subsession transcripts. Captured as
§19.2 candidate #14 (protocol amendments must land with the
code change). M.2's §19.2 candidate #7 (promote ohm-infra/ to
a real git repo) becomes more salient — this script change is
not under version control.
---
## Prompt the operator can paste into the next Claude Code session
```
You are the OHM roadmap driver. The previous session (Session M)
ran Wave 6 as three Track Ω inventory/draft subsessions in parallel
(M.1 / M.2 / M.3) under the new §5 subsession-transcript convention.
**What Session M produced:**
- Two docs feature branches awaiting your PR review:
- rfc-app `feature/contributing-and-spec-analytics` at `213f686`
(CONTRIBUTING.md + SPEC.md §21 analytics chapter, lands #19 +
#21 Part B; placed §21 not §16 to preserve the §19.2 cross-
reference graph — driver concurs with M.1's call)
- ohm-rfc `feature/contributing-roadmap-guide` at `f26fae4`
(CONTRIBUTING extension covering roadmap changes + operator-
only boundary)
- Three new docs on disk at `~/git/ohm-infra/`:
- `REPO-NAMING-PROPOSAL.md` (#17 inventory + proposal; six open
questions need your input)
- `EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` (#18 + #20 deployment-
side gestures, prioritized)
- `RFC-APP-EMAIL-HYGIENE-PROPOSAL.md` (#18 + #20 framework-side
minor proposal — target rfc-app v0.18.0)
**URGENT to investigate FIRST**: M.3 found `SMTP_HOST` + `SMTP_USER`
missing from the live flotilla overlay. If they aren't being set
via systemd override on the VM, every OHM email path (OTC,
beta-access notifications, per-RFC invites, admin-create invites
— v0.7.0/v0.9.0/v0.16.0/v0.17.0) has been silently dropping mail
since the relevant deploy. First gesture: `gcloud ssh ohm-app` +
`systemctl show ohm-app.service | grep Environment` to check. If
broken, send a probe through the "admin create user + invite"
flow.
**Wave 7 candidates (priority order):**
1. **Investigate + fix SMTP gap** (above) — possibly an ops-only
gesture if systemd override is sufficient; possibly an rfc-app
minor if the email-send code needs a defensive change.
2. **Operator reviews + merges the two docs PRs from Session M**
(no version bump for either; lands as no-bump docs commits).
3. **rfc-app v0.18.0** — framework-side hardening from
`RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`: `build_envelope` shared
helper (Date/Message-ID/Auto-Submitted/List-Unsubscribe headers
+ multipart text+html for all four email paths); make
`GITEA_WEBHOOK_SECRET` required (no longer `_optional`);
`outbound_emails` audit table. Add `RFC_APP_INSECURE_WEBHOOKS=1`
dev-bypass for local dev only.
4. **#17 execution session** — needs Q1 (per-RFC repos vs monorepo)
answered first; then operator runs the moves per
`REPO-NAMING-PROPOSAL.md`.
5. **#20 deployment-side gestures** — DMARC Phase A (`pct=100;
sp=quarantine; adkim=s; aspf=s`), then ramp to `p=quarantine`
after a week of `rua` data; delete stale `wiggleverse/meta`
Gitea hook pointing at deprovisioned rfc.wiggleverse.org.
6. **#22** — operator drafts + counsel-reviews the pro-analytics-
consent copy; subagent wires it in once approved.
7. **#21 Part A** — earliest reasonable target ~2026-06-04 (one
week of Amplitude data post-v0.17.0). Defer until then.
8. **#1 VM rename** — operator-led ops gesture, schedule a
maintenance window.
9. **#23 transcript filename rename** — on ROADMAP at `8cce148`;
pick a session to execute the rename across all 13 published
transcripts + the publish script's filename validator.
**Hard rules carried forward from Sessions K + L + M:**
- Never EVER ask the operator to paste secret bytes into the
conversation. Always give the operator-run gesture (`pbpaste |
flotilla secret set <deployment> <KEY>`). Public bundle-embedded
values (VITE_AMPLITUDE_API_KEY, VITE_TURNSTILE_SITE_KEY) are fine
in-conversation via `flotilla overlay set`.
- Subagent stream-watchdog stalls (Session M new): subagents
MUST write their transcript BEFORE returning a report (already
in §5.3) — and SHOULD NOT add a "final sanity check" phase
between transcript-write and report-back (where M.1 died).
Driver should always re-verify deliverables on disk rather
than depend on the report-back text alone.
- §5 subsession-transcript convention is binding from Session M
onward.
**Cut state at handoff:**
- rfc-app `main` at `1456c8b` (v0.17.0)
- ohm-rfc `main` at `8cce148` (#23 added by operator mid-Session-M)
- ohm-rfc-app-flotilla `main` at `65c0e55` (v1.0.1)
- ohm-rfc `.rfc-app-version` = `0.17.0`
- OHM live: deploys.id=21, v0.17.0
- Two feature branches awaiting your PR review (above)
- Next session letter: **N**
Read `~/git/ohm-infra/SESSION-PROTOCOL.md` and
`/Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md`
end-to-end. Then pick Wave 7's shippable scope.
```
---
(Cut state, §19.2 candidates, operator plate, and next-session
prompt all written at session-end.)
@@ -0,0 +1,460 @@
# Session M.1 — Subsession transcript
> Parent: `SESSION-M-TRANSCRIPT-2026-05-28T05-26--<end>.md`
> Date: 2026-05-28
> Goal: Draft `rfc-app/CONTRIBUTING.md`, the `rfc-app/SPEC.md` analytics chapter,
> and the `ohm-rfc/CONTRIBUTING.md` roadmap-guide extension — landing roadmap
> items #19 (CONTRIBUTING guides + transcript-linked onboarding) and #21 Part B
> (standing analytics instrumentation discipline). Push feature branches; do
> NOT tag, do NOT bump VERSION, do NOT move the pin, do NOT deploy.
>
> Outcome: **All three deliverables shipped on feature branches; both branches
> pushed to canonical remotes; one open question settled inline.**
>
> - `rfc-app feature/contributing-and-spec-analytics` at `213f686` —
> pushed to `origin` (git.wiggleverse.org) + `benstull`
> (git.benstull.org). One commit: CONTRIBUTING.md (new, ~370 lines) +
> SPEC.md §21 (new chapter, ~470 lines including the new §15 forward-
> pointer to §21).
> - `ohm-rfc feature/contributing-roadmap-guide` at `f26fae4` — pushed
> to `origin` (git.wiggleverse.org, the only remote). One commit:
> CONTRIBUTING.md extended with a new "Proposing a change to the OHM
> roadmap" section (~80 added lines on top of the existing 130-line
> RFC-content guide).
> - Open question on Gitea anonymous-PR posture: **settled inline** —
> `https://git.wiggleverse.org/user/sign_up` returns "Registration is
> disabled. Please contact your site administrator." The
> ohm-rfc CONTRIBUTING text reflects this with two paths (Issue-first
> default + request-account for repeat contributors) and is phrased so
> the operator can update if registration policy changes.
---
## Pre-subsession state
- **rfc-app**: `main` at `1456c8b` (Release 0.17.0). Tags through
v0.17.0. No CONTRIBUTING.md existed. `SPEC.md` 4302 lines, chapter
sequence §1..§20.
- **ohm-rfc**: `main` at `8cce148`. `.rfc-app-version` = `0.17.0`.
`CONTRIBUTING.md` existed (130 lines, RFC-content guide); no
roadmap-change section.
- **ohm-infra (local)**: most recent transcripts Session L
(`SESSION-L-TRANSCRIPT-2026-05-28T04-22--2026-05-28T05-17.md`) +
Session M's main transcript in-progress
(`SESSION-M-TRANSCRIPT-2026-05-28T05-26--INPROGRESS.md`).
- **Branches at start**: both repos on `main`, both clean working trees.
---
## Turn-by-turn arc
### Arc 1 — Reconnaissance
Read in order: `ohm-rfc/ROADMAP.md` (#19 at L405-456, #21 at L549-731,
particularly Part B at L625-655 and Part C at L657-731);
`ohm-rfc/CONTRIBUTING.md` (existing 130-line file — RFC-content
oriented, no roadmap-change section); `ohm-infra/SESSION-PROTOCOL.md`
(§1 naming, §2 transcript shape, §5 subagent-transcript amendment —
binding for this subsession);
`ohm-infra/SESSION-K-TRANSCRIPT-…md` (whole file — clean-small-
release + multi-feature-wave + paused-on-secret patterns; the
Arc-6 hard-rule incident around secret bytes);
`ohm-infra/SESSION-L-TRANSCRIPT-…md` (whole file — squash-merge
integration with #21 Part C identity-lifecycle wiring folded inline
across three releases; the addendum cycles around "transcript is the
LAST step" discipline);
`rfc-app/SPEC.md` selectively (top, §15 around L2259-2732, §16 + §17
boundaries, §19 around L3183, §20 ending around L4132-4302); the §15
→ §16 transition was the candidate insertion-point for the new
analytics chapter;
`rfc-app/CHANGELOG.md` opening + the v0.17.0 / v0.16.0 / v0.15.0
entries (lines 1-413) for the strict-descending convention and
RFC 2119 keyword usage examples;
`rfc-app/frontend/src/lib/analytics.js` end-to-end (the v0.15.0
wrapper extended in Session L with `{ user_id, properties }` identify
signature, `applyProperties` with `__setOnce__` sentinel,
`setUserProperties`, anonymize-clears-cache);
`rfc-app/frontend/src/App.jsx` lines 40-90 (the App-level identify
call with viewer-derived `set` + `setOnce` property bag);
`rfc-app/frontend/src/components/InviteClaim.jsx` lines 40-90 (the
v0.17.0 identify-BEFORE-track pattern for the admin-create invite
claim).
### Arc 2 — Placement decision for the SPEC chapter (settled inline)
The roadmap (and the dispatch brief) said "Place AFTER §15
notifications" and "numbering becomes §16 or whatever's next
available." The straightforward read is "insert as §16 and renumber
existing §16..§20 to §17..§21." A grep for `§16` through `§20`
references in SPEC.md surfaced 50+ cross-references across the file,
many of them to `§19.2` and `§19.3` specifically.
§19.2 specifically is a load-bearing project noun: it's the
defer-decisions queue, named that way in CLAUDE.md, in
`SESSION-PROTOCOL.md` §3, in every session transcript ("§19.2
candidates surfaced"), and in commit messages going back to Slice 5.
Renumbering §19 → §20 would invalidate every external reference and
require coordinated edits across at least: rfc-app CLAUDE.md,
ohm-rfc-app-flotilla CLAUDE.md, ohm-infra SESSION-PROTOCOL.md, all
12 published session transcripts, the auto-memory files. None of
those are within the scope of this subsession's docs-PR.
The "or whatever's next available" hedge in the brief covers this
case. **Settled inline:** place the new chapter as §21 (after §20
versioning), with a forward-pointer added at the end of §15's preamble
naming §21 as the peer cross-cutting analytics chapter. The new
chapter's opening explicitly states the placement reasoning so a
future reader knows why §21 isn't between §15 and §16. This is a
deliberate deferral of the "right" numbering to a future cleanup
session that scopes the cross-repo renumber properly — flagged in the
report-back to the driver.
### Arc 3 — `rfc-app/CONTRIBUTING.md` draft
Branched `rfc-app` at `feature/contributing-and-spec-analytics` off
`origin/main` at `1456c8b`. Drafted CONTRIBUTING.md (~370 lines)
covering, in order:
- Project framing — what rfc-app is, what a deployment is, the
difference between framework-PR and content-PR.
- "How the project actually evolves" with explicit
wiggleverse/ohm-session-history pointer + worked-example links
into Sessions E (clean small release), I (recovery from deploy
fault), K (multi-feature wave + paused-on-secret), L (squash-merge
across three parallel features + #21 Part C wiring inline). The
session links are to the repo root rather than specific
transcript filenames so they don't break when transcripts get
renamed via roadmap #23.
- Contribution flow — Issue-first for structural changes, branch
naming convention (matched against the driver session's
`feature/v<target>-<slug>` shape), explicit "do not bump VERSION,
do not tag, do not deploy, do not touch any .rfc-app-version pin."
- CHANGELOG strict-descending convention with a worked entry
template (mirroring v0.15.0/v0.16.0/v0.17.0 actual shapes) and
the operator-fills-version-at-merge-time note.
- RFC 2119 keyword discipline per SPEC §20.4 — MUST / MUST NOT /
SHOULD / SHOULD NOT / MAY — with the cross-version-upgrade
composition rule cited.
- §19.2 candidate discipline — explicit "don't scope-creep, name
the deferral." Cited v0.11.0's three new candidates (Session K)
and v0.15.0's two (Session L) as worked examples.
- Test-coverage expectations characterized from the actual
~250-test backend suite. Named specific vertical files
(`test_admin_create_user_invite_vertical.py`, etc.) and the
`PYTHONPATH=. .venv/bin/pytest -q` ergonomic (still §19.2). Noted
that there is no frontend test runner today — the discipline is
build-clean + backend HTTP contract coverage.
- Analytics instrumentation checklist (the #21 Part B CONTRIBUTING
artifact bullet): (1) named events, (2) autocapture-friendly DOM,
(3) replay masking, (4) PR description discipline. Plus a
pointer to SPEC §21.6 identity-lifecycle contract for any new
claim/sign-in/invite path.
- Operator-only gestures explicitly enumerated, with the hard
no-secret-bytes-in-conversation rule named (Session K Arc 6
origin cited).
- "When in doubt" closer.
The CONTRIBUTING file uses MIT License as the rfc-app `LICENSE`
shape; checked `ls rfc-app/LICENSE` showed it exists.
### Arc 4 — `rfc-app/SPEC.md` §21 chapter draft
Appended new §21 "Analytics instrumentation and identity" (~440
lines) at the end of SPEC.md (after §20.7), structured as:
- §21 preamble — what this chapter codifies, framework-neutral
posture, the placement-as-§21 reasoning (the cross-ref-preservation
rationale from Arc 2).
- §21.1 event-taxonomy conventions — Title Case "Subject Verb",
stability + dashboard-name-match, opaque-ids-only-in-props,
casing consistency. The starting v0.17.0 taxonomy (13 events)
laid out verbatim.
- §21.2 required prop families — navigation / auth-state / authored-
action / admin-action / invite-side / claim-side. The hashed
target_email convention (SHA-256 of normalized lowercased email)
named for the #12 per-RFC invite case where the invitee isn't
yet a user.
- §21.3 autocapture-friendly DOM patterns — stable visible text,
aria-label on icon-only buttons, `data-amp-track-*` for repeated
rows, `data-amp-track-suppress` for noise surfaces, with a worked
HTML example.
- §21.4 session-replay masking — credentials MUST mask, PII SHOULD
mask, privacy-policy alignment MUST hold, selective redaction
preferred over whole-form. The `.amp-mask` / `data-amp-mask`
convention named with the qualifier "whichever the wrapper's SDK
version uses" (acknowledging that the Amplitude SDK's masking
attribute name has varied across versions; the wrapper's bootstrap
comment is the live source of truth).
- §21.5 consent-gate contract — five MUSTs: pre-consent no init/no
network/no recording; denied→granted init at consent moment;
granted→denied setOptOut(true) within one tick; no double-init on
re-grant; build-time env-var resolution with one-warn no-op when
unset.
- §21.6 identity lifecycle (per #21 Part C) — sign-in /
state-change / sign-out / invite-claim / inviter-side
identification, each with MUST language. The User-Signed-Out
track-BEFORE-anonymize ordering called out as load-bearing.
- §21.6.1 set vs setOnce taxonomy — explicit classification rule
with worked examples on each side (role / permission_state /
passcode_set / device_trusted = set; first_sign_in_at /
account_created_at / invited_by_admin_id / invited_at /
claim_method = setOnce).
- §21.7 cohort-shape implications (informative) — four worked
questions the dashboard can now answer with the conventions:
admin-invite vs OTC funnel by week; passcode-set rate in first
session by claim path; per-RFC owner-invited-contributor counts;
invite-send-to-claim gap by inviter.
- §21.8 secret-vs-public framing — overlay binding for the public
Amplitude key, secret binding for the Turnstile siteverify
secret half. The pbpaste-pipe canonical gesture for OHM-shape
deployments named.
- §21.9 §19.2 candidates surfaced — four (session-replay-specific
consent category; bundle-size budget measurement; property-shape
CI lint; centralized email-hash helper).
- §21.10 open question — consent-category split timing tied to the
next privacy-policy revision.
Then added a forward-pointer paragraph at the end of §15's
preamble (the "section commits the surface..." paragraph) noting
§21 as the peer cross-cutting analytics chapter and explicitly
saying the two surfaces don't overlap (notifications =
signals-of-others-acting; analytics = observability of use).
The chapter uses RFC 2119 keywords precisely throughout. No
"should probably" / "might want to" weasel-words inside MUST/
SHOULD steps. Cited frontend/src/lib/analytics.js, App.jsx,
InviteClaim.jsx, AcceptInvitation.jsx as worked-example
references.
### Arc 5 — `ohm-rfc/CONTRIBUTING.md` extension
Branched `ohm-rfc` at `feature/contributing-roadmap-guide` off
`origin/main` at `8cce148`. Strategy: extend the existing
130-line RFC-content guide with a new section rather than create a
competing file. The existing sections (RFC discussions, RFC PR
format, RFC numbering, values, license) are preserved verbatim;
the roadmap-change section appends after the License block as a new
"Proposing a change to the OHM roadmap" heading + subsections.
Section structure:
- What kinds of roadmap changes are welcome (5 bullets — add,
reshape, claim a version slot, strike-through on ship,
§19.2 candidate at the OHM layer).
- How OHM actually evolves — wiggleverse/ohm-session-history
pointer + worked-example links into Sessions J (mid-build
version-slot reshuffle when docs-feature took v0.14.0),
K (mid-session capture of #16/#17/#18/#19), L (mid-build
capture of #20/#21/#22 + Part C wiring + #23).
- How to submit a roadmap PR — including the now-settled Gitea
registration-disabled question.
- The operator-only gestures — phrased with the operator's "I'm
the only one that gets to yolo" quote. Six bullets: flotilla
deploy, .rfc-app-version pin bump, secret-set, overlay-set on
OHM live, repo rename, Gitea account provisioning, PR merge.
Hard no-secret-bytes-in-conversation rule named.
- The two-layer rule — framework change → upstream rfc-app PR;
OHM-specific change → here.
- "When you're not sure" closer.
### Arc 6 — Open question on Gitea registration posture, settled
Per the dispatch brief, probed git.wiggleverse.org:
- `https://git.wiggleverse.org/api/v1/version` returned
`{"version":"1.26.2"}` — confirming the instance is up.
- `https://git.wiggleverse.org/user/sign_up` returned HTTP 200 but
the body contained `<p>Registration is disabled. Please contact
your site administrator.</p>` — definitive answer.
Registration is **disabled**, meaning outside contributors without a
Gitea account cannot directly open PRs. The ohm-rfc CONTRIBUTING
text settles on two paths: Issue-first (default; the operator or
next driver session turns it into a roadmap entry or opens
discussion in the issue thread), or request-account-from-operator
(for repeat contributors). The text is phrased so the operator can
edit it down to one path if they choose to flip registration policy
later.
The rfc-app CONTRIBUTING file doesn't carry this question
explicitly — its operator-gestures section already covers
"contributors do not have direct push access," and the Issue-first
path is the default mode for PRs the operator hasn't expected.
### Arc 7 — Commits + push
`rfc-app`:
- Staged CONTRIBUTING.md (new) + SPEC.md (modified).
- Committed `213f686` "docs: CONTRIBUTING.md + SPEC.md §21 analytics
chapter" with a HEREDOC commit body naming the deliverables and
the placement-as-§21 reasoning. Identity defaulted to
`Ben Stull <benstull@mac.lan>` (matches the operator's prior
commits on this repo; no git config edit). Pushed to both remotes:
`origin` (git.wiggleverse.org) and `benstull` (git.benstull.org)
with `-u` flag; both succeeded.
`ohm-rfc`:
- Staged CONTRIBUTING.md (modified).
- Committed `f26fae4` "CONTRIBUTING: add roadmap-change guide +
operator-only boundary" with a HEREDOC commit body. Same
operator identity. Pushed to `origin` (the only remote on this
repo) with `-u`; succeeded.
No tags, no VERSION bump, no `.rfc-app-version` pin move, no
`flotilla deploy`. All operator-only gestures untouched.
### Arc 8 — Transcript
Wrote this subsession transcript at
`~/git/ohm-infra/SESSION-M.1-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-40.md`
per the §5 subagent-transcript convention. Parent reference in the
header. The driver publishes this alongside its own Session-M
transcript via `~/git/ohm-infra/scripts/publish-transcript.sh`.
---
## Cut state (end of subsession)
| | |
| --- | --- |
| `rfc-app` | `feature/contributing-and-spec-analytics` at `213f686`; pushed to `origin` + `benstull`. `main` unchanged at `1456c8b`. Nothing tagged. |
| `ohm-rfc` | `feature/contributing-roadmap-guide` at `f26fae4`; pushed to `origin`. `main` unchanged at `8cce148`. `.rfc-app-version` unchanged at `0.17.0`. |
| OHM live | unchanged — `deploys.id=21`, v0.17.0. No deploy gesture performed. |
| `ohm-rfc-app-flotilla` | unchanged at `65c0e55`. |
| Operator-only gestures | none touched (no tag, no version bump, no pin move, no deploy, no secret-set). |
| Subsession M.1 ledger | Status |
| --- | --- |
| #19 rfc-app CONTRIBUTING.md | drafted on feature branch, pushed (origin + benstull) |
| #19 ohm-rfc CONTRIBUTING.md (extension) | drafted on feature branch, pushed (origin) |
| #21 Part B SPEC.md §21 analytics chapter | drafted on feature branch, pushed (origin + benstull); rides the rfc-app CONTRIBUTING commit |
| #19 anonymous-PR open question | settled — registration disabled on the Gitea instance; documented inline with two contributor paths |
---
## §19.2 candidates surfaced
1. **SPEC.md §16..§20 renumbering vs. §21-at-end placement.**
The roadmap brief's "place AFTER §15" naturally implies insert-
as-§16-and-shift-§16..§20-down, but renumbering would cascade
through 50+ cross-references, including `§19.2` (the project's
defer-decisions noun, used in CLAUDE.md, the session protocol,
12 published transcripts, and historical commit messages). This
subsession chose the lower-risk placement-as-§21 with a §15
forward-pointer, and documented the choice in the new chapter's
preamble. A future cleanup session that wants to do the
numerically-clean §16 placement needs to scope: rfc-app SPEC
renumber + rfc-app CLAUDE.md update + ohm-rfc-app-flotilla
CLAUDE.md cross-ref update + ohm-infra SESSION-PROTOCOL.md
reference sweep + all 12 published transcript header notes (in
wiggleverse/ohm-session-history) + the auto-memory
feedback files. None of those are within this subsession's
docs-PR scope; flagged for operator's call.
2. **Centralized email-hash helper.** §21.2 specifies SHA-256 of
the normalized lower-cased email as the hashing function for
the per-RFC `target_email` fingerprint, but no helper exists in
the framework today. A small `frontend/src/lib/hash.js` or
`backend/app/hash.py` that centralizes normalization + hashing
would make the contract enforceable rather than relying on
per-callsite re-implementation. Captured in SPEC §21.9 as well.
3. **Property-shape CI lint.** The conventions in SPEC §21.1 /
§21.2 are review-enforced today. A small lint (grep `track(` /
`identify(` callsites with a property-key allowlist + a PII-name
denylist) would catch drift mechanically. Captured in SPEC
§21.9.
4. **Frontend test runner.** rfc-app/frontend has no test runner
today (no `vitest`, no `jest`, no `playwright` — only `dev` /
`build` / `preview` scripts). The discipline that the
CONTRIBUTING file documents (backend vertical tests carry the
regression boundary; frontend ships build-clean) is honest about
the current state but is itself a §19.2 candidate worth flagging
for a future session — a small `vitest` setup + a few smoke
tests against the wrapper modules (analytics.js, consent.js, api.js)
would catch the kind of structural breakage a frontend-only
release would otherwise discover at deploy time.
---
## What the driver needs to know
1. **Both branches are pushed and ready for PR.** The driver/operator can
open PRs by visiting:
- `https://git.wiggleverse.org/ben.stull/rfc-app/pulls/new/feature/contributing-and-spec-analytics`
- `https://git.benstull.org/benstull/rfc-app/pulls/new/feature/contributing-and-spec-analytics`
(the mirror; merge here too if the operator's release flow
normally syncs both — Session K/L precedent is that operator
pushes tags to both but PR-opens on one and merges the merge-
commit to both. The operator's call.)
- `https://git.wiggleverse.org/ben.stull/ohm-rfc/pulls/new/feature/contributing-roadmap-guide`
2. **The SPEC chapter is §21, not §16.** This is a deliberate
docs-PR-scope-only choice (Arc 2 / §19.2 candidate #1 above).
The new chapter's preamble explains the placement to readers.
If the operator prefers the §16 insertion with full renumber,
that's a separate larger PR that touches CLAUDE.md files and
transcript headers across multiple repos — too large for this
subsession.
3. **Anonymous-PR question settled inline.** Registration is
disabled on git.wiggleverse.org (confirmed via the sign-up
page). The ohm-rfc CONTRIBUTING text is phrased to update
easily if the operator changes that posture later.
4. **No operator gestures were performed.** No tag, no VERSION
bump, no pin move, no deploy, no secret-set, no overlay-set.
Both branches are drafts ready for operator review + merge at
the operator's chosen cadence.
5. **No secret bytes in either commit body or in this transcript.**
The hard rule was honored throughout. The SPEC chapter and both
CONTRIBUTING files reference secrets by name only
(`SMTP_PASSWORD`, `AMPLITUDE_API_KEY`, `CLOUDFLARE_TURNSTILE_SECRET`).
6. **Suggested PR descriptions** (operator can paste verbatim into
the new-PR form):
- `rfc-app` PR: "Lands roadmap item #19 (CONTRIBUTING +
transcript-linked onboarding) and #21 Part B (standing analytics
instrumentation discipline) as a single docs commit. Adds
CONTRIBUTING.md (~370 lines) covering PR shape, CHANGELOG
strict-descending convention, RFC 2119 upgrade-steps discipline,
§19.2 candidate practice, test-coverage expectations from the
real ~250-test backend suite, the operator-only-gestures
boundary, and the #21 Part B analytics-instrumentation checklist
(named events / autocapture-friendly DOM / replay masking /
PR-description discipline). Adds SPEC.md §21 'Analytics
instrumentation and identity' (~440 lines) codifying event
taxonomy, required prop families, autocapture DOM patterns,
session-replay masking conventions, the consent-gate contract,
the #21 Part C identity lifecycle (identify-with-properties on
sign-in; setUserProperties on state change; reset on sign-out;
identify-BEFORE-track on invite-claim), the set-vs-setOnce
taxonomy with classification rule, cohort-shape implications,
and four new §19.2 candidates. Placed as §21 rather than §16 to
preserve the load-bearing §19.2 / §19.3 cross-reference graph;
reasoning in the chapter preamble. §15 carries a new forward-
pointer naming §21 as the peer cross-cutting analytics chapter.
No code change; no VERSION bump; rides a future release or
lands as a no-bump docs commit at operator discretion."
- `ohm-rfc` PR: "Lands roadmap item #19's OHM-side artifact.
Extends the existing 130-line RFC-content CONTRIBUTING.md with
a new 'Proposing a change to the OHM roadmap' section (~80
lines) covering roadmap-change kinds, the
wiggleverse/ohm-session-history pointer with worked-example
links into Sessions J/K/L, the PR-submission gesture (with the
open question on Gitea registration settled inline:
git.wiggleverse.org currently has registration disabled, so
contributors choose between Issue-first and request-account
paths), the explicit operator-only-gestures boundary with the
'I'm the only one that gets to yolo' quote, and the two-layer
rule. No version target."
---
## Files written
- `/Users/benstull/git/rfc-app/CONTRIBUTING.md` (new)
- `/Users/benstull/git/rfc-app/SPEC.md` (modified — new §21 chapter
+ §15 forward-pointer paragraph)
- `/Users/benstull/projects/wiggleverse/ohm-rfc/CONTRIBUTING.md`
(modified — new "Proposing a change to the OHM roadmap"
section appended after the License block)
- `/Users/benstull/git/ohm-infra/SESSION-M.1-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-40.md`
(this file)
@@ -0,0 +1,276 @@
# Session M.2 — Transcript (subsession)
> Parent: SESSION-M-TRANSCRIPT-2026-05-28T05-26--INPROGRESS.md
> Date: 2026-05-28
> Goal: Roadmap item #17 step 1 (inventory) + step 2 (propose) — survey every repo on `git.wiggleverse.org` and `git.benstull.org`, draft the move proposal as a single document for the operator to review. **Step 3 (execute) is explicitly out of scope.**
>
> Outcome: **`REPO-NAMING-PROPOSAL.md` written to `~/git/ohm-infra/`. 12 repos inventoried across the two hosts plus 9 local-only working dirs documented. 8 moves proposed (3 archives, 2 namespace transfers, 3 operator-decision items). 6 open questions surfaced. Zero git operations performed against any remote.**
This is a subsession of Session M (the OHM driver session). The driver
dispatched M.2 with a tight read-only-inventory + single-deliverable
brief. Subsession boundaries follow the OHM session protocol §1
(letter+number scheme — M.2 is the second subsession of M).
---
## Pre-session state
The driver's hand-off named:
- 9 known wiggleverse.org repos (driver named 3; M.2 inventoried the
rest via Gitea explore endpoint).
- 3 known benstull.org repos (same).
- Roadmap item #17 ("Repo naming + location alignment") names the
framing: rfc-app should be in `wiggleverse/`, not personal namespace;
mirror convention undocumented; the `ohm-app` legacy VM (tracked
separately as roadmap #1, out of scope for M.2).
- Hard constraint: subsession publishes its transcript to
`wiggleverse/ohm-session-history` (public). No secret bytes.
- Hard constraint: no Gitea admin actions, no transfers, no edits to
anything but the proposal document itself.
Operator's working tree (parent session M's pre-state): clean.
Active OHM deployment: v0.17.0 (deploys.id=21). Pin: 0.17.0. Healthy.
---
## Turn-by-turn arc
### Arc 1 — Read roadmap #17 + scout local checkouts
Read `~/projects/wiggleverse/ohm-rfc/ROADMAP.md` lines 314+ for
item #17's full statement. The session-shape it names is:
inventory → propose → execute. M.2 owns the first two.
Listed `~/git/` and `~/projects/wiggleverse/` to map local checkouts:
- `~/git/`: ohm-infra (not a git repo!), ohm-meta (empty), rfc-app
(canonical clone), rfc-app-v0.15/v0.16/v0.17 (per-release worktrees),
wiggleverse-meta (clone of `wiggleverse/meta`), benstull-infra
(not a repo, just two old transcript MDs).
- `~/projects/wiggleverse/`: ohm-rfc (clone), ohm-rfc-app-flotilla
(clone), rfc-app-prototype (clone), org/ (not inspected).
Already a discovered-friction item: `~/git/ohm-infra/` not being a
git repo is structurally weird. Filed as §3.4 in the proposal.
### Arc 2 — Inventory both Gitea hosts
`curl https://git.wiggleverse.org/explore/repos` and the equivalent
on benstull.org returned HTTP 200 HTML for both — anonymous explore
works. Parsed out `href="/<owner>/<name>"` links.
Wiggleverse.org reported 9 repos:
- `ben.stull/{benstull,ohm-rfc,rfc-app,rfc-app-prototype}`
- `wiggleverse/{meta,ohm-meta,ohm-rfc-app-flotilla,ohm-session-history,rfc-0001-human}`
Benstull.org reported 3:
- `benstull/{docs,rfc-app,rfc-app-flotilla}`
For each, hit `/api/v1/repos/<owner>/<name>` for metadata (description,
created/updated, default branch, size, mirror flag, private flag).
All API calls anonymous; all returned 200. No auth needed for this
inventory.
Also hit `/api/v1/repos/<owner>/<name>/commits?limit=1` per repo for
the actual last-commit timestamp and message — `updated_at` from the
repo metadata wasn't always commit-tied (could be a description edit).
### Arc 3 — Confirm the push-mirror mechanism
`ben.stull/rfc-app` and `benstull/rfc-app` both reported commit
`1456c8b73f70` at `2026-05-28T05:10:52-07:00` — same sha, same
timestamp within seconds. But neither was flagged `mirror: True`
in the API. Tried `/api/v1/repos/.../push_mirrors` — returned
"user should be an owner or a collaborator with admin write," so
the push-mirror config is admin-only-visible.
Conclusion: the sync mechanism is a push-mirror configured by the
operator on the canonical side, invisible to the public read API.
Worth a note in the proposal — operator needs to reauthorize it
after Move 3.
Also noticed: the **descriptions** of the canonical and mirror have
drifted apart ("The Wiggleverse RFC framework app — see PHILOSOPHY.md
and SPEC.md." vs "Open-source RFC platform software"). Filed under
§3.3.
### Arc 4 — Probe URL-redirect handling on Gitea
The flotilla CHANGELOG L282283 has a note about `ben/ohm-rfc` being
a redirect alias to `ben.stull/ohm-rfc`. Tested:
- `/api/v1/repos/ben/ohm-rfc` → HTTP 307
- `/api/v1/repos/ben.stull/ohm-rfc` → HTTP 200
- Same `307` for `ben/rfc-app`, `ben/benstull`, `ben/rfc-app-prototype`.
Conclusion: `ben` is an old username that aliases to `ben.stull` via
Gitea's user-rename redirect feature. This matters because if Moves
3+4 land, the alias chain becomes two hops: `ben → ben.stull →
wiggleverse`. Filed as risk in Move 4 and as Q5.
### Arc 5 — Find URL refs in load-bearing docs
`grep -nE 'git\.(wiggleverse|benstull)\.org'` against:
- `~/git/rfc-app/CLAUDE.md` and `SPEC.md` → zero hits. rfc-app source
doesn't reference the hosting URLs. Good — moves don't break
shipped framework code.
- `~/git/rfc-app/frontend/` → zero hits. The running frontend doesn't
embed the canonical URL. Confirms zero shipped-artifact breakage.
- `~/projects/wiggleverse/ohm-rfc-app-flotilla/CLAUDE.md`, `SPEC.md`,
`README.md`, `CHANGELOG.md`, `docs/operator-guide.md`, `scripts/`,
`tests/`**many hits**. Catalogued line-by-line in the proposal's
Moves 3 and 4 sections. Test files hardcode `ben/ohm-rfc` four
places. The SPEC has it ~10 places, plus a whole framing paragraph
in §3.1 about why rfc-app gets a benstull mirror that **needs
conceptual rewriting**, not just URL swapping.
- `~/projects/wiggleverse/ohm-rfc/ROADMAP.md` → many hits including
the entire §16 release-table (rows 213) with tag URLs. Note in
proposal: those should probably stay (they record historical state)
but Gitea's redirect will keep them resolving.
- `~/git/ohm-infra/SESSION-*-TRANSCRIPT-*.md` → many hits. Per session
protocol these are immutable. Gitea's redirect handles them. Filed
explicitly in the proposal's "do NOT edit historical transcripts"
callout.
### Arc 6 — Inspect overlapping meta-repo content
`wiggleverse/meta`, `wiggleverse/ohm-meta`, and
`wiggleverse/rfc-0001-human` all had READMEs and `rfcs/` dirs.
Fetched contents via API.
- `wiggleverse/meta`: `PHILOSOPHY.md` (12 KB), `CONTRIBUTING.md`
(626 B), `rfcs/.gitkeep`, generic README.
- `wiggleverse/ohm-meta`: `PHILOSOPHY.md` (12 KB), `CONTRIBUTING.md`
(626 B), `rfcs/human.md` (292 B **stub**), README.
- `wiggleverse/rfc-0001-human`: `RFC.md` (22 KB, the full body),
`.rfc` marker, README that says "canonical text lives here;
meta-repo entry is `rfcs/human.md` in ohm-meta."
- `ben.stull/ohm-rfc`: `PHILOSOPHY.md` (13.5 KB — superset),
`CONTRIBUTING.md` (4.5 KB — superset), `RFC-0001-human.md` (22 KB
**same body** as `rfc-0001-human/RFC.md`), `ROADMAP.md` (61 KB),
`CLAUDE.md`, `LICENSE`, `.rfc-app-version`.
The proposal's §2 Move 2 names the collision: two models
("per-RFC repos" vs "monorepo") both exist, monorepo has won in
practice. Operator decision Q1 settles whether to archive the per-RFC
repos.
### Arc 7 — Confirm flotilla SQLite registry state
`sqlite3 ~/.ohm-rfc-app-flotilla/ohm-rfc-app-flotilla.db 'SELECT *
FROM deployments;'` returned the ohm-rfc-app row with
`pin_source_repo='ben/ohm-rfc'` and `corpus_repo='ben/ohm-rfc'`.
Important: after Move 4, the operator must run a flotilla command
to update these values, or the next deploy chases a two-hop redirect.
Flagged in Move 4 cross-repo-cleanup section.
### Arc 8 — Write the proposal
Wrote `~/git/ohm-infra/REPO-NAMING-PROPOSAL.md` (~22 KB) covering:
- §1 inventory: two host tables + local-only working dirs + topology snapshot
- §2 move proposal: 8 moves with risk, ordering, URL-breakage callouts, specific paths
- §3 discovered friction: 6 items (confirmed roadmap #17's plus 4 new)
- §4 open questions: 6 (Q1 RFC-shape, Q2 mirror destination, Q3 stub fate, Q4 ohm-infra-as-repo, Q5 username alias, Q6 backup before)
- §5 suggested execution-session shape: pre-flight checklist, ordered execution, Gitea-admin vs git-cli vs edit-text gestures, verify
- §6 §19.2 candidates surfaced
- §7 summary table
No git operations against any remote. No edits to anything besides
the proposal doc itself.
---
## Cut state (end of subsession)
| | |
| --- | --- |
| `~/git/ohm-infra/REPO-NAMING-PROPOSAL.md` | new file, ~22 KB, M.2's only artifact |
| Every other repo (local + remote) | untouched |
| OHM live | unchanged (deploys.id=21, v0.17.0, healthy) |
| flotilla SQLite registry | unchanged |
| Parent session M state | unchanged |
| M.2 deliverable ledger | Status |
| --- | --- |
| Read roadmap #17 in full | done |
| Inventory wiggleverse.org repos | done (9 found) |
| Inventory benstull.org repos | done (3 found) |
| Cross-reference local checkouts | done |
| Read CLAUDE.md/SPEC.md/ROADMAP.md for URL refs | done |
| Write proposal doc | done |
| Write transcript | done (this file) |
| Publish transcript to ohm-session-history | **driver does this**, not subsession (per protocol §"subagents do not write their own transcripts" — but the brief overrode that and asked M.2 to write one; M.2 wrote, driver publishes) |
---
## §19.2 candidates surfaced
Two surfaced during M.2 (also in proposal §6):
1. **Stop relying on Gitea path aliases.** flotilla currently leans
on the `ben/ohm-rfc``ben.stull/ohm-rfc` redirect alias (urllib
follows 307s). After Moves 3+4, that chain becomes two hops.
Better: resolve canonical paths at `flotilla deployment update`
time, store the resolved path in SQLite, never chase aliases at
deploy time.
2. **flotilla-core extraction reservation as concrete artifact.**
The stub repo `git.benstull.org/benstull/rfc-app-flotilla` (Move 5)
could either be archived or repurposed as the placeholder for the
future `flotilla-core` §19.2 extraction. Operator decision (Q3).
A third candidate **not** in the proposal but worth a separate item:
3. **Promote `~/git/ohm-infra/` to a real git repo.** Currently a
working dir with no version control on `SESSION-PROTOCOL.md`,
the publish script, the transcript-publishing plan, etc. Roadmap
#17 scope doesn't cover it but the inventory surfaced it (§3.4).
Worth its own roadmap item.
---
## What the driver needs to know
1. **Proposal lives at `~/git/ohm-infra/REPO-NAMING-PROPOSAL.md`**
and is ready for operator review.
2. **Six open questions** the operator must settle before the
execution session. Q1 is structural (monorepo vs per-RFC) and may
reshape the proposal entirely.
3. **Biggest finding not in roadmap #17:** `wiggleverse/meta`,
`wiggleverse/ohm-meta`, and `wiggleverse/rfc-0001-human` are three
repos for one job. Monorepo has won in practice; archive the other
two if operator confirms.
4. **Biggest cross-repo cleanup load:** the flotilla SPEC.md needs
~10 URL updates *plus* a conceptual rewrite of the §3.1 paragraph
that explains why rfc-app has a benstull mirror (the "personal
shape" rationale dissolves once rfc-app moves to org namespace).
5. **flotilla test files hardcode `ben/ohm-rfc` in four places**
(`test_plan.py`, `test_pin.py`, `test_bringup_replay.py`,
`test_deployment.py`). These must update with Move 4.
6. **The flotilla SQLite registry** at
`~/.ohm-rfc-app-flotilla/ohm-rfc-app-flotilla.db` has
`pin_source_repo` and `corpus_repo` both set to `ben/ohm-rfc`.
Must be updated post-Move-4 or next deploy chases a redirect.
7. **No shipped frontend artifacts reference the canonical URLs.**
This is the load-lightening discovery: the moves don't break
anything that an OHM end-user sees.
8. **No git operations were performed by M.2** against any remote.
Read-only inventory, single new file (the proposal), this
transcript.
---
## Closing handoff to driver
M.2 is done. The driver returns control to the operator with the
proposal ready for review. The next OHM session that wants to
execute roadmap #17 picks up by:
1. Operator reads `REPO-NAMING-PROPOSAL.md`, settles Q1Q6.
2. Operator backs up `~/git/ohm-infra/` (Q6 / §3.4 surfaced) if not
yet a real repo.
3. Driver session opens, follows the execution shape in proposal §5.
That execution session is operator-led (per the brief that dispatched
M.2: "step 3 is operator-led in a future session").
@@ -0,0 +1,311 @@
# Session M.3 — Transcript
> Parent: SESSION-M-TRANSCRIPT-2026-05-28T05-26--<end>.md
>
> Date: 2026-05-28 (PST)
> Start: 2026-05-28T05-30 PST
> End: 2026-05-28T05-58 PST
> Goal: Inventory OHM's email + Gitea-webhook posture (roadmap items
> #18 + #20, bundled) and draft a hardening runbook plus a
> framework-side template-hygiene proposal. Read-only only —
> no DNS edits, no SMTP-provider changes, no rfc-app code,
> no secret rotations, no test emails.
>
> Outcome: **Two docs written and left on disk for the driver at
> `~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` and
> `~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`. Five §19.2
> candidates surfaced. Three high-impact deployment-side gestures
> and three high-impact framework-side changes prioritized for the
> operator. Two surprising findings: SMTP_HOST is missing from the
> live flotilla overlay (gap to disambiguate), and a stale Gitea
> webhook on `wiggleverse/meta` still points at the deprovisioned
> `http://rfc.wiggleverse.org`.**
---
## Pre-session state
- Parent: Session M driver, dispatched M.3 with scope = inventory
for items #18 + #20.
- `~/git/ohm-infra/` is a plain directory (not a git repo) — driver
noted this; M.3 leaves docs on disk for the driver to handle
publishing.
- `rfc-app` at v0.17.0 on disk; live OHM running v0.17.0 per
`flotilla pin check ohm-rfc-app` (deploys.id=21).
- Hard constraints from dispatch: no operator-led gestures, no
secret bytes in transcript, read-only inventory only.
---
## Turn-by-turn arc
### Arc 1 — DNS inventory + roadmap read
Ran `dig +short TXT wiggleverse.org`, `dig +short TXT
_dmarc.wiggleverse.org`, `dig +short MX wiggleverse.org`, and
probed common DKIM selectors (`default`, `mail`, `s1`, `s2`,
`selector1`, `k1`, `mandrill`, `smtp`, `google`, etc.).
Findings:
- **SPF:** `"v=spf1 include:_spf.google.com ~all"` (soft-fail).
- **DMARC:** `"v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org"`
(monitoring-only, no enforcement, no `pct=`, no `sp=`).
- **DKIM:** present at `google._domainkey.wiggleverse.org` — a
2048-bit RSA Workspace key. All other probed selectors empty.
- **MX:** `aspmx.l.google.com` family — Google Workspace mailbox.
Read roadmap items #18 (l.362-403) and #20 (l.458-547) from
`~/projects/wiggleverse/ohm-rfc/ROADMAP.md`. Both confirm they
should ship together because the same DNS edits serve both. #20
names BIMI as a follow-on after DMARC reaches `p=reject`. #18 names
"refresh webhook-shared-secret + replay-resistance + ingress
restriction" as framework-side work.
### Arc 2 — Flotilla overlay + secret inventory
```
.venv/bin/ohm-rfc-app-flotilla overlay show ohm-rfc-app
.venv/bin/ohm-rfc-app-flotilla secret list ohm-rfc-app
```
Overlay reveals:
- `SMTP_PORT=587`, `SMTP_STARTTLS=1`, `EMAIL_FROM=notifications@wiggleverse.org`,
`EMAIL_FROM_NAME=Wiggleverse` — present.
- `SMTP_HOST`**missing**. `SMTP_USER`**missing**.
- Secret refs include `GITEA_WEBHOOK_SECRET` bound to
`wiggleverse-ohm/ohm-rfc-app-gitea-webhook-secret@latest`.
Greps:
- `~/projects/wiggleverse/ohm-rfc-app-flotilla/scripts/register-ohm.sh:40`
sets `SMTP_HOST=smtp-relay.gmail.com` — but the live overlay
doesn't have it. Either the bootstrap script wasn't re-run after
a reshape, or SMTP env lives out-of-band on the VM (e.g.,
systemd-unit override). This is a finding for the runbook —
operator must disambiguate via `gcloud ssh` + `systemctl show
ohm-app.service`.
Confirmed provider = **Google Workspace SMTP relay** (the bootstrap
script + the `google._domainkey` DKIM + the MX records align).
### Arc 3 — Framework email-helper read
Read all of:
- `~/git/rfc-app/backend/app/email.py` — §15.4 notification mailer
(`_deliver`, `_send_one`, `_send_bundle`, `make_unsubscribe_url`).
- `~/git/rfc-app/backend/app/email_otc.py` — v0.7.0 OTC helper.
- `~/git/rfc-app/backend/app/email_invite.py` — v0.17.0 admin-create
invite helper.
Built header-presence matrix across all four kinds (OTC / invite /
watcher-single / watcher-bundle):
- `Date`, `Message-ID`, `Auto-Submitted`, `List-Unsubscribe`,
`List-Unsubscribe-Post`, `multipart/alternative` — **all
absent** on all four paths. Google's SMTP relay adds `Date` /
`Message-ID` during ingestion, but the framework doesn't control
the format.
- Plain-text bodies only. No HTML variant anywhere.
- `_deliver` (`email.py:308`) embeds unsubscribe URL in body footer
but doesn't set the header.
Stdlib spot-check: `python3 -c "from email.message import
EmailMessage; …"` confirms `set_content` doesn't auto-populate
`Date` or `Message-ID`. Inspected `smtplib.SMTP.send_message`
source — also doesn't set them.
### Arc 4 — Gitea-webhook inventory
Found the handler at `~/git/rfc-app/backend/app/webhooks.py` (78
lines). HMAC-SHA-256 over the raw body, `hmac.compare_digest` for
constant-time compare. **But:** `webhooks.py:43` gates verification
behind `if config.webhook_secret:` — empty secret = no auth. Per
`config.py:75`, `webhook_secret` is `_optional`. Insecure default.
Pulled `gitea-bot-token` from GCP Secret Manager into a shell var
(`GITEA_TOKEN=$(gcloud secrets versions access latest …)`), used
it for read-only `curl` calls against the Gitea API, immediately
`unset` after each call. Never echoed the value to stdout or any
file.
Hook inventory across the wiggleverse org:
| Repo | Hook | Notes |
|---|---|---|
| `wiggleverse/ohm-meta` | `https://ohm.wiggleverse.org/api/webhooks/gitea` | active, 12 events — correct |
| `wiggleverse/meta` | `http://rfc.wiggleverse.org/api/webhooks/gitea` | **active, plain HTTP, deprovisioned domain** |
| `wiggleverse/rfc-0001-human` | (none) | the only published RFC has no hook |
| `ben.stull/ohm-rfc` | (bot 403) | can't inventory, operator self-checks |
The org-level hooks endpoint
(`/api/v1/orgs/wiggleverse/hooks`) returned `[]` — no org-wide
hooks, all are repo-level.
Gitea's API exposes hook `config` keys = `['url', 'content_type']`
— it does NOT return the configured `secret` field, so I can't
confirm from the API side that the secret matches what OHM has.
But OHM's cache is current and webhook receiver verifies, so the
secrets must align (otherwise everything would 401).
### Arc 5 — Bounce-handling + observability gap
Read `~/git/rfc-app/backend/app/api_notifications.py:471-502`
`/api/webhooks/email-bounce` exists, gated on optional
`WEBHOOK_EMAIL_BOUNCE_SECRET` per SPEC.md §3667 Slice 8 settlement.
But **no provider is wired to POST here** — Google Workspace SMTP
relay doesn't natively webhook bounces (it bounces back to the
sender mailbox). So today: hard bounces silently retry every
`flush_pending` pass.
All four send helpers catch `Exception`, log via `log.exception`,
return `False`. No DB write of the failure, no admin notification.
The operator has zero visibility into send failures without
grepping VM logs.
### Arc 6 — Draft the two docs
Wrote `EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` (~700 lines) with
sections: current posture / hardened shape / operator-execution
sequence / framework-vs-deployment split / §19.2 candidates.
Wrote `RFC-APP-EMAIL-HYGIENE-PROPOSAL.md` (~280 lines) with the
framework-side minor proposal: `build_envelope` shared helper,
webhook handler tightening (mandatory secret), `outbound_emails`
audit table, bounce-correlation, suggested 5-slice shipping plan.
Cross-referenced both docs at each end. Both name files + line
numbers for every framework citation.
### Arc 7 — Transcript
This file. ohm-infra is not a git repo (driver's check confirmed
pre-session), so no commit; driver decides whether to add a remote
or just publish the transcripts via `publish-transcript.sh`.
---
## Cut state (end of subsession)
| Surface | State |
|---|---|
| `~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` | written, ~21 KB |
| `~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md` | written, ~9 KB |
| `~/git/ohm-infra/SESSION-M.3-TRANSCRIPT-…md` | this file |
| OHM live | unchanged (v0.17.0, deploys.id=21) |
| DNS / Gitea / SMTP provider | unchanged (read-only inventory only) |
| flotilla overlay / secrets | unchanged (read-only `show` + `list` only) |
---
## §19.2 candidates surfaced
1. **rfc-app: `GITEA_WEBHOOK_SECRET` should be required, not optional.**
Today the framework silently accepts unsigned POSTs when the env
var is empty. Recommend `_required` with documented
`RFC_APP_INSECURE_WEBHOOKS=1` dev-bypass.
2. **rfc-app: outbound-email observability seam.** Add
`outbound_emails` table + write on every send. Today failures
log-and-forget; the operator has no audit trail.
3. **flotilla: `overlay show` doesn't surface "expected but missing"
keys.** The SMTP_HOST gap is invisible without grepping
register-ohm.sh + framework source side-by-side. A per-deployment
"required keys" manifest + `flotilla overlay validate` verb would
catch this class of regression.
4. **flotilla: lacks a webhook-inventory verb.** Listing / diffing
the webhooks on the org's Gitea repos against the deployment's
expected set is a recurring operator gesture. A `flotilla webhook
list / check <deployment>` verb would formalize it. Generic
enough for a future `flotilla-core` extraction.
5. **rfc-app: multiple From-identity references should derive from
one source.** `EMAIL_FROM` is read in `email.py:82`,
`email_otc.py` via cfg, `email_invite.py` via cfg, plus implied
by DMARC alignment + Workspace allowed-senders. A spec section
binding all surfaces to one source would prevent skew.
---
## What lands on the driver's plate
1. **Decide where to put the two docs.** `~/git/ohm-infra/` is not
a git repo. Options: (a) leave as plain files, publish only via
`publish-transcript.sh` (which today only handles transcripts);
(b) git-init `~/git/ohm-infra/` and add a Gitea remote so
ohm-infra becomes a versioned doc repo; (c) move the two docs
into `~/projects/wiggleverse/ohm-rfc/` under a `docs/` subdir
since they're OHM-deployment-specific. Driver decides.
2. **Schedule the disambiguation gesture.** The SMTP_HOST gap is
the most urgent finding — if the framework is actually falling
to stdout-fallback, OHM's emails (OTC, invites, notifications,
beta-access) aren't leaving the VM. Operator runs `gcloud ssh
ohm-app` + `systemctl show ohm-app.service | grep
Environment` next session.
3. **Schedule the stale-hook deletion.** Five-second gesture; the
hook on `wiggleverse/meta` pointing at deprovisioned
`http://rfc.wiggleverse.org/api/webhooks/gitea` should go.
4. **Decide on the From-identity question.** `ohm@wiggleverse.org`
(roadmap's suggestion) or keep `notifications@wiggleverse.org`?
Pinning this is a prerequisite for the framework-side proposal's
`Reply-To` decision.
5. **Slot the rfc-app email-hygiene minor.** Per the proposal, v0.18.0
or v0.19.0; 5 slices; mostly backward-compatible. Operator
decides whether to bundle with another minor (e.g., #19's
CONTRIBUTING.md docs) or ship solo.
6. **Confirm `ben.stull/ohm-rfc`'s hooks.** Bot lacks admin so M.3
couldn't inventory. Operator self-checks via Gitea web UI.
---
## What the driver needs to know (report-back summary)
**DNS posture (literal values):**
- SPF: `v=spf1 include:_spf.google.com ~all` (soft-fail).
- DMARC: `v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org`
(monitoring-only).
- DKIM: present at `google._domainkey.wiggleverse.org` (2048-bit
Workspace key).
**SMTP provider:** Google Workspace SMTP relay
(`smtp-relay.gmail.com`) — inferred from bootstrap script + DKIM
+ MX. **But SMTP_HOST is missing from the live flotilla overlay**
— either the bootstrap was reshaped and these were dropped, or
they're set on the VM out-of-band. Operator must disambiguate.
**Top 3 deployment-side hardening gestures (priority order):**
1. Delete stale `wiggleverse/meta` hook (5 seconds, removes
plain-HTTP-to-deprovisioned-domain liability).
2. Disambiguate SMTP_HOST gap (5 minutes, confirms email actually
leaves the VM).
3. DMARC Phase A: add `pct=100; sp=quarantine; adkim=s; aspf=s` to
the existing `p=none` record; wait 1 week; flip to `p=quarantine`.
**Top 3 framework-side changes (rfc-app minor, target v0.18.0):**
1. `build_envelope` shared helper adds `Date`, `Message-ID`,
`Auto-Submitted`, `List-Unsubscribe`, `List-Unsubscribe-Post`,
and `multipart/alternative` to all four email paths.
2. Make `GITEA_WEBHOOK_SECRET` required (not silently optional).
3. `outbound_emails` audit table + bounce-correlation hook.
**Webhook posture:** signature verification is correct
(`hmac.compare_digest`, HMAC-SHA-256 over raw body) **when the
secret is set**. On OHM the secret is bound. Two open issues:
(a) framework's silent-accept-when-unset insecure default; (b)
stale `wiggleverse/meta` hook on a deprovisioned domain.
**§19.2 candidates surfaced:** 5 (listed above).
**Doc paths:**
- `/Users/benstull/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md`
- `/Users/benstull/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`
- `/Users/benstull/git/ohm-infra/SESSION-M.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md`