reshape: move OHM transcripts under ohm/ for org-wide multi-app layout; add org-wide root README

This commit is contained in:
2026-06-03 06:54:38 -07:00
parent bd21825bc2
commit 3713299f08
73 changed files with 200 additions and 181 deletions
@@ -0,0 +1,466 @@
# Session 0017.0 — Transcript
> Date: 2026-05-28
> Start: 2026-05-28T08-50 PST
> End: 2026-05-28T09-22 PST
> Goal: drive Wave 8 — pick a shippable scope from the post-Wave-7 candidate list now that the OTC sign-in gate is open; the operator marked roadmap #30 (docs nav + on-site sessions browser + session-history repo restructure) as **NEXT** in ROADMAP.md, so that became the target.
>
> Outcome: **Wave 8 = roadmap #30 shipped end-to-end. rfc-app v0.19.0 tagged + pushed (canonical + benstull mirror); ohm-rfc pin bumped to 0.19.0; OHM live at v0.19.0 healthy. `wiggleverse/ohm-session-history` restructured into one folder per session + `README.md` (operator-review draft) + `sessions.json` title manifest (15 entries, all titled). `publish-transcript.sh` extended for the folder convention + sessions.json upsert. SESSION-PROTOCOL.md §1 + §5 amended. Three subagents (0017.1 rfc-app code, 0017.2 session-history restructure, plus the driver-inline publish-script + protocol edits) all delivered.**
>
> Highlights:
> - The OTC sign-in end-to-end gate (Wave 7 carry-forward) was operator-confirmed working at session start — first-time-since-launch.
> - Two other sessions were running concurrently (0015.0 troubleshooting an OTC-config edge case; 0016.0 completed). Driver took ID 0017.0 per operator confirmation.
> - Settled at session time: gitea-only (no GitHub mirror); runtime fetch with cache for `/docs/sessions`; full `SESSION-NNNN.M-TRANSCRIPT-…md` filename retained inside each folder (overrode roadmap example's trimmed form).
> - SSE-keepalive SSH-watchdog timeout fired again at deploy time (4th occurrence; flotilla v1.2.0 §19.2 candidate). Service came up at v0.19.0 healthy regardless.
---
## Pre-session state
- rfc-app `main` @ `ac3513a` ("Merge: CONTRIBUTING.md + SPEC.md §21 analytics chapter (Session 0013.1)"); `v0.18.0` was the latest tag; clean working tree.
- ohm-rfc `main` @ `74a3e12` ("ROADMAP: +#24..#29"); pin `.rfc-app-version=0.18.0`. (Between session start and the post-deploy pin push, the operator landed at least one more commit on ohm-rfc — `9c47792` — adding roadmap item #30 with its **NEXT** marker; my pin-bump committed onto the updated `9c47792` cleanly.)
- ohm-rfc-app-flotilla `main` @ `417b6c5` (v1.1.0, Wave 7 deploy with VITE_* overlay propagation). Local working tree had one modified file: `CLAUDE.md`.
- OHM live: v0.18.0 healthy (`/api/health` 200, `{"version":"0.18.0","status":"ok"}`).
- ohm-infra (`~/git/ohm-infra/`, not a git repo locally): `publish-transcript.sh` at the post-#23 form (accepting numeric + legacy letter regex); `SESSION-PROTOCOL.md` at the post-Session-0014.0 amendment shape. Stale `SESSION-0012.0-TRANSCRIPT-…INPROGRESS.md` still on disk (handoff said leave as-is).
- Session 0014.0's main driver transcript NOT yet on disk at session start (it appeared at 08:43 mid-session — the parallel-running driver was finalizing). 0014.1 subagent transcript was on disk from 07:42. 0016.0 transcript not seen on disk this session.
The operator's handoff prompt (pasted at session start) was the load-bearing summary of what Wave 7 shipped + the Wave 8 candidate list. Two clarifying gates the operator surfaced at the top:
1. **OTC sign-in end-to-end was unverified.** Wave 7's full triage stopped before the operator retested.
2. **Two other sessions were running.** Session ID for this driver had to be calculated from what the other sessions had claimed.
---
## Turn-by-turn arc
### Arc 1 — Orient + clear the two gates
Read `SESSION-PROTOCOL.md`, `ohm-rfc/ROADMAP.md` (which surfaced a NEW item #30 marked **NEXT** by the operator — newer than the handoff prompt and effectively re-shaping the Wave 8 priority order), the on-disk transcript listing (showed 00010013.3 + 0014.1 in numeric form), and the publish-transcript.sh + flotilla state. Health-probed OHM (v0.18.0 ok). Confirmed `flotilla deploy ohm-rfc-app` venv at `.venv/bin/ohm-rfc-app-flotilla`.
Asked the operator two questions in one tool call:
1. Session ID — other sessions claimed 0015.0 + 0016.0; driver took **0017.0**.
2. OTC sign-in test in a fresh incognito window — **"Code arrived, sign-in worked"** — first-time-since-launch confirmation of the Wave 7 v0.18.0 chain working end-to-end. The Wave 7 load-bearing gate is closed.
Asked a follow-up: what are 0015.0 and 0016.0 doing, so Wave 8 doesn't collide? **0016.0 is complete (transcript pending publish — at this point not yet on disk locally, but operator confirmed it's done); 0015.0 is troubleshooting an OTC-not-being-sent edge case described as a config issue, no code changes**, and the operator was going to context-switch to 0015.0 directly. That cleared the way for #30 since neither competing session was touching the rfc-app or session-history surfaces.
### Arc 2 — Propose Wave 8 scope + settle the open #30 decisions
Recommended Wave 8 = #30 since the operator had marked it **NEXT** and nothing else was more pressing. Proposed shape: three parallel subagents touching three different repos (session-history restructure + rfc-app v0.19.0 + ohm-infra publish-script + protocol updates).
Asked the operator's two open session-time questions from the roadmap text:
1. **Repo host**: gitea-only / gitea+GitHub mirror / GitHub canonical. Operator chose **gitea-only**.
2. **Fetch mode**: runtime fetch with cache (recommended) / build-time bundle. Operator chose **runtime fetch with cache**.
Then asked two coordination questions:
3. **Is restructuring the session-history repo safe to start now?** (The handoff's "don't do transcript renames yet — another session is running" caveat might still apply.) Operator chose **"Wait — I'll signal when clear"** for the restructure subagent specifically; authorized the other two subagents to proceed.
4. **Session titles + README prose drafting**: subagent drafts / operator hand-writes / hybrid. Operator chose **subagent drafts both, operator reviews**.
Renumbered the subagent labels by dispatch order per protocol §5: subagent dispatched first = `.1`, second = `.2`. So:
- 0017.1 = rfc-app v0.19.0 code subagent (dispatched first)
- 0017.2 = session-history restructure subagent (held until operator signal)
Driver does the ohm-infra publish-script + protocol updates inline (small enough; not a forked subagent — captured in this transcript).
### Arc 3 — Driver-inline edits: publish-script + SESSION-PROTOCOL.md
Read the existing `scripts/publish-transcript.sh`. Rewrote the body to:
- Parse out `NNNN` from `SESSION-NNNN.M-TRANSCRIPT-…md` via bash regex `^SESSION-([0-9]{4})\.[0-9]+(\.[0-9]+)*-TRANSCRIPT`.
- For numeric form: place transcript in `NNNN/` subfolder (`mkdir -p`); for legacy letter form: place at repo root (backward compat).
- Upsert `sessions.json` via inline `python3` heredoc (pure stdlib, no jq): if `NNNN` key is missing, add `{"NNNN": {"title": ""}}`, sort keys numerically, write 2-space-indented JSON with trailing newline. If `sessions.json` is malformed JSON, fail cleanly with exit 2.
- Compute commit message based on what's in the diff (`add NNNN/SESSION-… + sessions.json: add entry for NNNN`, or just one of those).
- Use `git status --porcelain` after staging to detect any change; no-op exit 0 if nothing changed.
- Keep dry-run behavior; expand its logging to name both the transcript-action and the manifest-action.
Smoke-tested the regex locally (numeric forms parse, legacy letters reject) and the Python upsert in isolation (first-add returns 1, re-add returns 0, malformed JSON exits 2, sort order works).
Edited `SESSION-PROTOCOL.md`:
- §1 gained a **"Published repo layout (Session 0017.0 amendment, 2026-05-28)"** subsection describing the `NNNN/` folder shape, the `sessions.json` schema, and the "local files stay flat — folders are publish-time" rule. Documents the decision to retain the full `SESSION-NNNN.M-TRANSCRIPT-…md` filename inside folders (no trim of the `SESSION-` prefix; overrides the roadmap example).
- §5 gained a short **"Published-repo placement"** note saying subagent transcripts inherit their parent's `NNNN/` folder (publish-script handles this by parsing the leading `NNNN`).
Documented that the local working files at `~/git/ohm-infra/SESSION-…` keep the flat shape — folders are a publish-time gesture only.
### Arc 4 — Dispatch subsession 0017.1 (rfc-app v0.19.0)
Dispatched a forked general-purpose subagent in the background with a self-contained prompt:
- Pre-assigned subsession ID `0017.1`.
- Repo `/Users/benstull/git/rfc-app`, branch `feature/v0.19.0-docs-sessions-browser` off `main@ac3513a`.
- Settled decisions: gitea-only, runtime fetch + cache.
- Concrete scope (backend endpoints + TTL cache + frontend route tree + flyout nav + CHANGELOG + version bumps).
- Hard rules: do NOT tag, do NOT bump ohm-rfc pin, do NOT push to benstull mirror, do NOT deploy. Driver handles all of those after 0017.2 also returns.
- Transcript expectations per §5 (write `SESSION-0017.1-TRANSCRIPT-…md` BEFORE returning report).
- Pasted roadmap item #30 verbatim for self-containment.
Subagent ran ~15 minutes and reported back:
- Branch `feature/v0.19.0-docs-sessions-browser` @ `477f496`, pushed to origin only.
- 313 backend pytest passing (18 new in `test_docs_sessions_vertical.py`); frontend `npm run build` green; no frontend test framework (per dispatch); dev-server smoke not run (sandboxed agent, no browser — flagged for driver).
- Settled in-session: per-session listing source = separate `/api/docs/sessions/<NNNN>/index` endpoint (not extending the manifest, keeps it cheap); negative-cache 404s (rationale: at ship time the repo restructure is racing the rfc-app deploy; empty-state path may be hot for hours); the dispatch's `pyproject.toml` was a non-op (repo uses `backend/requirements.txt` only — canonical version sources are `VERSION` + `frontend/package.json#version`, both bumped).
- 5 §19.2 candidates surfaced (`package-lock.json` version drift, missing `pyproject.toml`, validator-leak, CHANGELOG style drift, cache-invalidation seam).
- Transcript at `SESSION-0017.1-TRANSCRIPT-2026-05-28T08-52--2026-05-28T09-07.md`.
- Notable in-flight snag (full detail in its transcript): the shared `app_with_fake_gitea` fixture monkeypatches `httpx.AsyncClient` globally on the module — the first-attempt test fixture captured a `real_client_cls = httpx.AsyncClient` that was already the gitea-patched wrapper; subagent fixed by importing the truly-unpatched `AsyncClient` from `httpx._client` directly. `_UpstreamHandler` gained a host-marker filter so reconciler-side gitea calls don't inflate cache-hit assertions.
Driver verified deliverables on disk (don't depend on report-back text per §5 amendment):
- `feature/v0.19.0-docs-sessions-browser` exists locally at `477f496`, matches origin.
- `VERSION` = `0.19.0`; `frontend/package.json` version = `0.19.0`; `main` untouched at `ac3513a`.
- Transcript exists at the named path, 350 lines, header matches expected shape.
- CHANGELOG diff: 12 lines added; full v0.19.0 entry with `Upgrade steps:` block.
- Backend diff: 855 LOC across `api.py` + `docs_sessions.py` + tests.
- Frontend diff: 864 LOC across `App.css` / `App.jsx` / `api.js` / 5 new components / `analytics.js` (+ `Docs.jsx` deletion).
Noticed one quick fix the subagent flagged as §19.2 (`## v0.19.0` vs `## 0.18.0` style mismatch — every prior CHANGELOG header drops the `v` prefix; the new entry having `v` was MY dispatch-prompt typo, not architectural). Made a small driver-inline commit on the feature branch normalizing the header — `bb5137f CHANGELOG: normalize 0.19.0 header (drop 'v' prefix to match surrounding style)`, pushed to origin. The other §19.2 candidates are genuinely architectural and stay deferred.
(One operator-config note: git emitted the "your name and email were configured automatically based on hostname" warning for the commit — `benstull@mac.lan` rather than `ben@wiggleverse.org`. Per the "no `git config` edits" hard rule I left it; same identity shape as the subagent's commits and as 0017.2's later commit. Operator can normalize globally if desired.)
### Arc 5 — Operator clears the restructure gate; dispatch 0017.2
Operator said "we're good to go. Go for it" — clearing 0017.2's hold.
Dispatched the second forked general-purpose subagent in the background:
- Pre-assigned subsession ID `0017.2`.
- Mission: restructure `wiggleverse/ohm-session-history` (gitea SSH remote at `ssh://git@git.wiggleverse.org:2222/wiggleverse/ohm-session-history.git`); one atomic commit on `main`.
- Settled decisions baked into the prompt (keep `SESSION-` prefix in folder filenames; gitea-only).
- README ingredients (per roadmap #30): how OHM is built, why transcripts are public, the verbatim "Oh and Jan, your joke is in there somewhere." line, link to canonical repo. Subagent drafts; operator reviews via embedded `<!-- operator: rewrite this section in your voice -->` markers.
- Title-drafting heuristic: read each transcript's `> Goal:`/`> Outcome:` header block. Driver supplied starter sketches for 0001 + 00090014; subagent filled in 00020008 + 0015 from the actual transcripts.
Subagent ran ~5 minutes and reported back:
- Public repo `main` @ `ad01c08` (fast-forward push from `b6a7b7d`); 19 transcripts `git mv`'d into 15 `NNNN/` folders, rename detection 100%; zero legacy letter-form at root (Session 0014.0's #23 rename had already cleared them).
- `sessions.json`: 15 entries, every session titled, no placeholders. Did NOT include 0016/0017 (unpublished in the public repo as of 0017.2's run).
- README.md drafted with 3 operator-review markers on key sections (~520 words, includes canonical-link callout + publish-script how-to + license pointer).
- Transcript at `SESSION-0017.2-TRANSCRIPT-2026-05-28T09-10--2026-05-28T09-15.md`.
- 3 §19.2 candidates (publish-script placeholder-on-first-publish contract; README ↔ /docs/sessions/about render contract; local-vs-published shape asymmetry).
- Cosmetic note: committer identity `Ben Stull <benstull@mac.lan>` (same as 0017.1's and the driver inline commit).
Driver verified:
- Transcript exists at named path, 13.5 KB.
- Gitea raw probe: `sessions.json` returns 200; `README.md` returns 200; `0014/SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--2026-05-28T08-25.md` returns 200.
- `sessions.json` content sane (15 entries; titles read like decent first drafts; 0013 is a semicolon-list — likely worth compression; 0014 uses subagent's phrasing rather than §1-example verbatim form — operator's call).
### Arc 6 — Merge, tag, push, pin, deploy, verify
With both subagents returned and verified, executed the coordinated landing:
1. `git checkout main` on rfc-app, `git merge --no-ff feature/v0.19.0-docs-sessions-browser -m "Merge feature/v0.19.0-docs-sessions-browser"` → merge commit `69a166a` (16 files changed, 1732+/57-).
2. `git tag v0.19.0` on the merge commit.
3. `git push origin main v0.19.0` → success.
4. `git push benstull main v0.19.0` → success.
5. `cd ohm-rfc && echo "0.19.0" > .rfc-app-version && git commit -m "Pin rfc-app 0.19.0 — Wave 8 deploy: /docs nav + on-site sessions browser (#30)"` → commit `de17e79`. (Local main was at `9c47792`, not `74a3e12` as seen at session start — operator landed at least one more ohm-rfc commit in between; commit graph stayed clean.)
6. `git push` → success.
7. `.venv/bin/ohm-rfc-app-flotilla deploy ohm-rfc-app`**hit the §19.2 SSE-keepalive SSH-watchdog timeout** at phase 7 (`SshError: ssh to ohm-app timed out after 60.0s running: 'sudo sh -c ... systemctl restart ohm-app.service ...'`). 4th occurrence of this pattern (Session 0009.0 §19.2 / Session 0014.0 carry-forward).
8. **Verification via `/api/health`** (not the flotilla exit code, per Session 0014.0's lesson): first probe right after the timeout returned 502 from nginx (service was mid-restart); second probe 15s later also 502; but then `.venv/bin/ohm-rfc-app-flotilla deploy status ohm-rfc-app` returned `HTTP 200 version=0.19.0 status=ok`. Re-probed the public URL — 5 consecutive 200s, all `{"version":"0.19.0","status":"ok"}`, ~190 ms each. So: deploy succeeded; the 502 window was transient (a few seconds while the service was finishing restart); SSH watchdog falling off doesn't reflect actual deploy outcome.
(Side observation: an `ssh ohm-app` attempt to read journalctl failed "Could not resolve hostname ohm-app" — my local SSH config doesn't have the alias the flotilla CLI uses internally. `flotilla deploy status` was the canonical path anyway. Not blocking.)
deploys.id row for this deploy will read aborted-on-SSH-timeout despite the service running healthy at v0.19.0 — same dichotomy as deploys.id 23/24/25 from Session 0014.0.
### Arc 7 — Operator asks for the next-session prompt mid-arc; transcript wraps after
Operator asked for the closing-handoff prompt at the moment the deploy verification finished. Driver drafted it inline in the conversation (full prompt included below in "Prompt the operator can paste into the next Claude Code session"). Browser-side verification of `/docs/sessions/about` + a per-transcript view is the next session's first gesture, not 0017.0's — the deploy is up; the on-site rendering is the operator's eyeball confirmation.
Then this transcript got written (you are reading it).
---
## Cut state (end of session)
| | |
| --- | --- |
| rfc-app | `main` @ `69a166a` (merge commit); tag `v0.19.0`; pushed to origin + benstull mirror |
| ohm-rfc | `main` @ `de17e79``.rfc-app-version=0.19.0` |
| ohm-rfc-app-flotilla | `main` @ `417b6c5` — no flotilla code change this session (`CLAUDE.md` modified file from session start was carried through unchanged) |
| `wiggleverse/ohm-session-history` | `main` @ `ad01c08` — restructured into 15 `NNNN/` folders + `README.md` (with 3 operator-rewrite markers) + `sessions.json` (15 entries, all titled) |
| OHM live | v0.19.0 healthy (`/api/health` 200, `{"version":"0.19.0","status":"ok"}`); deploys.id row = aborted-on-SSH-timeout per §19.2 (h) below |
| ohm-infra | `scripts/publish-transcript.sh` extended for NNNN/ folders + sessions.json upsert; `SESSION-PROTOCOL.md` §1 + §5 amended. (Not a git repo locally; on-disk changes are the artifact.) |
| Session 0017 transcripts on disk | `SESSION-0017.0-TRANSCRIPT-2026-05-28T08-50--2026-05-28T09-22.md` (this file), `SESSION-0017.1-TRANSCRIPT-2026-05-28T08-52--2026-05-28T09-07.md`, `SESSION-0017.2-TRANSCRIPT-2026-05-28T09-10--2026-05-28T09-15.md`. All three publish via `publish-transcript.sh` at session close. |
| Wave 8 ledger | Status |
| --- | --- |
| #30 docs nav + on-site sessions browser + session-history restructure (rfc-app v0.19.0 + ohm-session-history reshape) | ✅ shipped |
---
## §19.2 candidates surfaced
a. **`frontend/package-lock.json` version drift** (0017.1). Lockfile top-level `"version"` reads `0.15.0` while `frontend/package.json#version` is `0.19.0`. Cosmetic but ugly; symptom of no automated lockfile-discipline check. Repo doesn't run `npm install` as part of the version bump, so the lockfile drifts on every minor.
b. **rfc-app backend has no `pyproject.toml`** (0017.1). Dispatch named one as a version source; the repo uses `backend/requirements.txt` only. Canonical version sources are `VERSION` + `frontend/package.json#version`. If a backend `pyproject.toml` is desired (for editable installs, build-tool integration, etc.), it earns its own small release.
c. **`docs_sessions.py` private validators leak into the route layer** (0017.1). `_is_valid_session_dir` / `_is_valid_transcript_filename` are referenced from route handlers; consolidating into a public `validate_path()` would tighten the seam.
d. **`docs_sessions` cache has no external invalidation** (0017.1). In-process TTL only; no `reset_cache()` admin endpoint or test seam. Operationally fine (TTL is short); developer-experience worth improving.
e. **`publish-transcript.sh` should auto-populate `sessions.json` placeholder entries on first-publish** (0017.2). **Already partially shipped** in the driver-inline edit (publish-script upserts `{"NNNN": {"title": ""}}` if the key is missing). Worth confirming the contract end-to-end against a real fresh-session publish; surfaced as a §19.2 mostly to track the testing follow-up.
f. **Contract between README and rfc-app `/docs/sessions/about` renderer** (0017.2). Operator-review HTML comments (`<!-- ... -->`) should render as nothing on-site (markdown stripping). Worth confirming in the next session's first browser-verification gesture; if they show up as raw text the renderer needs a small fix.
g. **Local-vs-published shape asymmetry** (0017.2). Local working dir `~/git/ohm-infra/` stays flat; published repo is nested in `NNNN/` folders. Already documented in `SESSION-PROTOCOL.md` §1, but worth a one-line callout if future drivers get confused.
h. **SSE-keepalive SSH-watchdog timeout — 4th occurrence** (driver). Hit at phase 7 of every recent deploy: SIGTERM doesn't drain SSE connections in under 60 s, the SSH watchdog falls off, flotilla reports aborted, the service actually restarts and comes up healthy. flotilla v1.2.0 candidate: pre-drain SSE connections (or raise the watchdog to e.g. 180 s) before `systemctl restart`. Driver verification now leans entirely on `/api/health` + `flotilla deploy status` post-deploy rather than the flotilla exit code.
i. **Driver-inline commit identity drift** (driver). Three commits this session (one driver-inline CHANGELOG fix + two subagent merges + the pin bump) carried `Ben Stull <benstull@mac.lan>` as the committer rather than the operator's preferred identity. Per the hard rule, no `git config` edit; operator can rewrite if desired. Worth surfacing as an operator-tooling §19.2: a one-line "ensure git identity is set on this machine" gesture would prevent it.
j. **`publish-transcript.sh` `sessions.json` formatting churn** (driver, noticed at publish-time after 0017.2's restructure had already shipped). The script's inline-Python upsert uses `json.dump`'s default `indent=2` (expands every entry to multi-line) AND default `ensure_ascii=True` (escapes em-dashes / `§` / en-dashes as `—` / `§` / ``). 0017.2's subagent had hand-written `sessions.json` with compact one-line entries per session + raw UTF-8. The first publish-time upsert (0017.0's transcript publish, which added the `0017` placeholder) rewrote the file in the expanded ASCII-escaped form, so the operator's next edit starts from that shape rather than the subagent's. Fix is one-line in `publish-transcript.sh`: `json.dump(out, f, indent=2, ensure_ascii=False)` plus a small custom emitter to keep each session's `{ "title": "…" }` on one line. Cosmetic; no functional impact.
---
## What lands on the operator's plate
1. **Browser-verify `/docs/*` surfaces** (next-session first gesture). Concrete URLs in the next-session prompt below. ~5 minutes.
2. **Rewrite the 3 README.md placeholder sections** on `wiggleverse/ohm-session-history` (sections marked with `<!-- operator: rewrite this section in your voice -->`). Direct commit to main; no PR ceremony needed.
3. **Refine `sessions.json` titles** if desired. 0013's title is a semicolon-list of three subagent workstreams (may want compression); 0014's title is the subagent's phrasing rather than the §1-example verbatim form. Add a title for **0017** (currently absent from the manifest — it'll be added as `{"title": ""}` when this transcript publishes; operator fills in something like `"Wave 8 driver — docs nav + on-site sessions browser (#30)"`).
4. **Merge the two Session-M docs PRs** still awaiting operator merge:
- rfc-app `feature/contributing-and-spec-analytics` @ `213f686`
- ohm-rfc `feature/contributing-roadmap-guide` @ `f26fae4`
5. **DMARC Phase A → Phase B**: ≥1 week of clean `rua` reports at `ben.stull@wiggleverse.org` since Session 0014.0; ETA 2026-06-04 to flip `p=quarantine`.
6. **#1 VM rename + flotilla v1.2.0 SSE-keepalive drain** (combined session) — the watchdog pattern is now 4× confirmed; worth fixing alongside the rename when the maintenance window opens.
7. **Update ohm-rfc/ROADMAP.md**: strikethrough #30 row in the version-target table; link to v0.19.0 + the deploys.id row (when one materializes — currently aborted-on-timeout).
---
## Prompt the operator can paste into the next Claude Code session
```
You are the OHM roadmap driver, Session 0018.0. Read this whole prompt
before doing anything.
# What Session 0017.0 shipped
Wave 8 was roadmap item #30 — the docs nav + on-site sessions browser
+ session-history repo restructure — picked because the operator
marked #30 **NEXT** in ROADMAP.md and the load-bearing OTC sign-in
gate (the Wave 7 / v0.18.0 carry-forward) was confirmed working
end-to-end at session start. Three parallel subagents:
1. **flotilla / ohm-infra** (driver inline) — `publish-transcript.sh`
extended to place new transcripts into their `NNNN/` subfolder and
upsert `sessions.json` (a top-level title manifest, keyed by
4-digit `NNNN`, value `{"title": ""}`). Backward-compat: legacy
letter-form transcripts still land at repo root. JSON upsert is
pure stdlib via inline `python3` heredoc, no jq dep.
SESSION-PROTOCOL.md §1 gained a "Published repo layout" amendment
documenting the new folder shape, the `sessions.json` schema, and
the "local files stay flat — folders are publish-time" rule. §5
gained a short "Published-repo placement" note saying subsession
transcripts inherit their parent's `NNNN/`.
2. **subsession 0017.1 — rfc-app v0.19.0** (merged + tagged + pushed).
Backend: `/api/docs/sessions/{manifest,about,<NNNN>/index,<NNNN>/<file>}`
with in-process TTL cache (60s manifest, 5min content; both
env-tunable via `OHM_DOCS_SESSIONS_MANIFEST_TTL_SEC` /
`OHM_DOCS_SESSIONS_CONTENT_TTL_SEC`). Negative caching for 404s
(rationale: at ship time the repo restructure is racing the rfc-app
deploy; empty-state path may be hot for hours). Gitea raw base
configurable via `OHM_SESSION_HISTORY_RAW_BASE`. 18 new pytest
cases (`test_docs_sessions_vertical.py`, 313 backend total green).
Frontend: `/docs` → redirect to `/docs/user-guide`; `/docs/sessions`
→ redirect to `/docs/sessions/about`; `/docs/sessions/<NNNN>` and
`/docs/sessions/<NNNN>/<filename>` for index + transcript view;
`DocsLayout.jsx` with left-side flyout nav driven by the manifest;
reuses the v0.14.0 react-markdown renderer; Amplitude `Doc Viewed`
events + `data-amp-track-*` decoration. Branch
`feature/v0.19.0-docs-sessions-browser` merged to main at `69a166a`,
tag v0.19.0, pushed to origin + benstull mirror.
3. **subsession 0017.2 — wiggleverse/ohm-session-history restructure**
(one atomic commit `ad01c08` on `main`, fast-forward push). 19
transcripts `git mv`'d into 15 `NNNN/` folders (rename detection
100%); `0013/` holds 4 transcripts (driver + 3 subagents from
Session 0013.0); `0014/` holds 2 (driver + 1 subagent). Top-level
`README.md` rewritten with 3 `<!-- operator: rewrite this section
in your voice -->` markers (sections: "How OHM gets built", "Why
transcripts are public" — which includes the verbatim
"Oh and Jan, your joke is in there somewhere." line — and
"What's in the repo"). Top-level `sessions.json` drafted with 15
entries, every session titled (no placeholders); titles read off
each transcript's `Goal:`/`Outcome:` header block. Operator-review
notes: 0013's title is a semicolon-list of three subagent
workstreams (may want compression); 0014's title is the
subagent's phrasing rather than the §1-example verbatim form.
Filename convention inside each folder: full
`SESSION-NNNN.M-TRANSCRIPT-…md` form retained (no trim of the
`SESSION-` prefix). The roadmap example showed trimmed form;
Session 0017.0 settled on retained-prefix for file-self-
identification. Document in SPEC.md if operator wants to flip.
# Cut state at handoff
- rfc-app `main`: `69a166a` (merge) → tag `v0.19.0` published; pushed
to origin + benstull mirror.
- ohm-rfc `main`: `de17e79` — `.rfc-app-version=0.19.0`.
- ohm-rfc-app-flotilla `main`: `417b6c5` (no flotilla code change
this session).
- wiggleverse/ohm-session-history `main`: `ad01c08` — restructured
into `NNNN/` folders + README.md + sessions.json at root.
- OHM live: **v0.19.0 healthy** (`/api/health` returns
`{"version":"0.19.0","status":"ok"}`).
- deploys.id row for v0.19.0: aborted-due-to-SSH-timeout (the
§19.2 SSE-keepalive pattern fired again; service IS healthy at
v0.19.0; the aborted row reflects the SSH watchdog falling off,
not the deploy itself — 4th occurrence; flotilla v1.2.0
candidate).
- ohm-infra: `scripts/publish-transcript.sh` extended;
`SESSION-PROTOCOL.md` §1 + §5 amended (NOT in a git repo locally,
so no commit; the files ARE what they are on disk).
- DMARC Phase A unchanged from Session 0014.0 (`p=none`, `pct=100`,
TTL=3600s, `rua=mailto:ben.stull@wiggleverse.org`). Watch `rua=`
mailbox for ≥1 week of clean reports (around 2026-06-04) 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
- ✅ OTC sign-in end-to-end (Session 0017.0 operator-confirmed at
session start — the Wave 7 / v0.18.0 carry-forward gate).
- ✅ v0.19.0 service health (`/api/health` returns 0.19.0).
- ✅ wiggleverse/ohm-session-history restructure (gitea raw probes
on `sessions.json`, `README.md`, and a sample 0014/ transcript
all return 200).
- ⏸ End-to-end `/docs/*` surfaces in a real browser **not yet
operator-confirmed**. Session 0017.0 ended before the operator
loaded the site. First gesture this session: have the operator
load these URLs in a fresh browser tab:
- `https://ohm.wiggleverse.org/docs` → should redirect to
`/docs/user-guide` (existing v0.14.0 content, no behavior change
beyond the URL move).
- `https://ohm.wiggleverse.org/docs/sessions/about` → renders the
README.md from `wiggleverse/ohm-session-history`. Expect the
3 operator-review HTML comments to render as nothing on-site
(one of the §19.2 candidates is to verify this; if they're
visible as raw text, that's a markdown-renderer bug to fix).
- `https://ohm.wiggleverse.org/docs/sessions/0009` → session index
page for Session 0009.0 (the multi-wave driver). Should show
"Session 0009.0 — multi-wave driver (rfc-app v0.4v0.13, skipping
vacants)" as the title from sessions.json.
- `https://ohm.wiggleverse.org/docs/sessions/0013` → should show
4 transcripts (driver + 3 subagents) and resolve the right list
via the per-session `/index` endpoint.
- `https://ohm.wiggleverse.org/docs/sessions/0014/SESSION-0014.0-TRANSCRIPT-2026-05-28T07-01--2026-05-28T08-25.md`
→ renders Wave 7 driver transcript in-browser.
- Flyout nav: open from any /docs/* route; on mobile, confirm the
drawer toggles.
- ⏸ Operator hasn't reviewed the README draft prose or sessions.json
titles — they're shipped as drafted; refinement is a follow-up.
# Wave 9 candidates (priority is operator's call)
1. **Verify /docs/* surfaces in browser** (above). Five minutes if
they work; longer if not.
2. **Operator rewrites the 3 README.md placeholder sections** on
`wiggleverse/ohm-session-history`. Operator-only gesture; commit
directly to main.
3. **Operator refines sessions.json titles** if desired (0013's
semicolon-list, 0014's phrasing, or anywhere else); also fills in
the empty title for 0017 (added as `{"title": ""}` when 0017.0's
transcript published).
4. **Operator merges the two M docs PRs**.
5. **#1 VM rename + flotilla v1.2.0 SSE-keepalive drain** — the
§19.2 SSE-keepalive timeout pattern was hit a 4th time this
session; worth fixing alongside the rename. flotilla v1.2.0
would pre-drain SSE connections before `systemctl restart` to
close the watchdog gap.
6. **DMARC Phase A → Phase B (`p=quarantine`)** — ≥1 week of clean
rua data; ETA 2026-06-04.
7. **#20 ops side**: delete stale `wiggleverse/meta` hook on
deprovisioned `rfc.wiggleverse.org`.
8. **#18 ops side**: bounce-source wiring (Path A or B in the
runbook).
9. **#21 Part A Amplitude audit** — defer to ~2026-06-04 for a week
of post-fix dashboard data (Amplitude + OTC both now confirmed
working).
10. **#22 pro-consent copy** — operator-drafted + counsel-reviewed;
subagent wires once approved.
11. **#26-29** — four operator-added items from Session 0014.0
(propose-RFC fields, Haiku tags, PR cross-references, sign-in
resume). Each is a clean rfc-app minor.
12. **#17 repo naming alignment** — operator decides Q1 first.
# §19.2 candidates surfaced in Session 0017.0
From subsession 0017.1 (rfc-app v0.19.0):
a. `frontend/package-lock.json` top-level "version" field drifted
(lockfile reads `0.15.0` while `package.json` is `0.19.0`).
Cosmetic but ugly. Symptom of no automated lockfile-discipline
check.
b. rfc-app backend has no `pyproject.toml` — the dispatch assumed
one. Repo uses `backend/requirements.txt` only. Canonical version
sources are `VERSION` + `frontend/package.json#version`.
c. `docs_sessions.py`'s private validators (`_is_valid_session_dir`,
`_is_valid_transcript_filename`) leak into the route layer —
consider consolidating into a public `validate_path()`.
d. `docs_sessions` in-process cache cannot be invalidated externally
without a process restart or a `reset_cache()` test seam.
Operationally fine (TTL is short); developer-experience worth
improving.
From subsession 0017.2 (session-history restructure):
e. publish-transcript.sh should auto-add `sessions.json` placeholder
entries on first-publish for a new NNNN. **Already partially
shipped** in 0017.0's publish-script extension (the upsert logic
adds `{"NNNN": {"title": ""}}` if the key is missing). Worth
confirming the contract end-to-end.
f. Contract between README and rfc-app `/docs/sessions/about`
renderer — operator-review HTML comments (`<!-- ... -->`) should
render as nothing on-site. Worth confirming in the first-test
gesture.
g. Local-vs-published shape asymmetry (local flat in
`~/git/ohm-infra/`, published nested in `NNNN/` folders) is
already documented in SESSION-PROTOCOL.md §1; worth surfacing as a
one-line callout if it confuses future drivers.
From driver:
h. The §19.2 SSE-keepalive SIGTERM holding past the 60s SSH watchdog
— hit AGAIN this session (4th occurrence). flotilla v1.2.0 should
pre-drain SSE connections before `systemctl restart`. Driver
verification now leans entirely on `/api/health` post-deploy
rather than the flotilla exit code.
i. Driver-inline commit identity drift (`benstull@mac.lan` rather
than `ben@wiggleverse.org`). Per hard rule no `git config` edit
this session; surfaced for operator tooling.
j. `publish-transcript.sh` `sessions.json` formatting churn. The
script's inline-Python upsert uses `json.dump`'s default
`indent=2` (every entry expands to multi-line) and default
`ensure_ascii=True` (em-dashes / `§` / en-dashes escape to
`\uXXXX`). 0017.2's subagent had hand-written compact one-line
entries with raw UTF-8; the first publish-time upsert (0017.0's
publish, adding the `0017` placeholder) rewrote the file in the
expanded ASCII-escaped form. Operator's next edit starts from
that shape. One-line fix: `json.dump(out, f, indent=2,
ensure_ascii=False)` + a small custom emitter to keep each
session on one line. Cosmetic only; no functional impact.
# 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 onward): forked subagents write their own transcripts
BEFORE returning their report. Driver re-verifies deliverables on
disk rather than depending on the report-back text.
- **Protocol amendments must land with the code change.**
- **Naming**: this is Session 0018.0. Driver transcript:
`SESSION-0018.0-TRANSCRIPT-<start>--<end>.md`. Subagent transcripts:
`SESSION-0018.1-…`, etc.
- **Numeric session ID**: if other sessions are running concurrently,
ask the operator which IDs are taken before claiming yours.
# First actions for Session 0018.0
1. Read `~/git/ohm-infra/SESSION-PROTOCOL.md` (the §1 + §5
amendments from Session 0017.0 are the relevant new content).
2. Read `/Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md`
end-to-end. #30 is the most recent item shipped (strikethrough
pending — operator may or may not have updated the version-target
table; if not, do it on the next ohm-rfc edit you make).
3. Read the Session 0017.0 transcript at
`~/git/ohm-infra/SESSION-0017.0-TRANSCRIPT-2026-05-28T08-50--2026-05-28T09-22.md`
for the full triage detail (this prompt is the summary).
4. Have the operator load `/docs/sessions/about` and at least one
`/docs/sessions/<NNNN>/<filename>` in a fresh browser tab. That's
the load-bearing first test.
5. Pick Wave 9's shippable scope. The roadmap doesn't have an
explicit **NEXT** marker after #30 ships, so the picking is the
operator's call.
```
@@ -0,0 +1,350 @@
# Session 0017.1 — Subsession transcript
> Parent: SESSION-0017.0-TRANSCRIPT-…md (not yet written; driver writes
> at end of session 0017.0)
> Date: 2026-05-28
> Goal: rfc-app v0.19.0 — /docs/sessions browser + flyout nav (roadmap
> item #30)
> Outcome: **Shipped on branch `feature/v0.19.0-docs-sessions-browser`
> at `477f496`. Backend mediates `/api/docs/sessions/{manifest,about,
> <NNNN>/index, <NNNN>/<file>}` with an in-process TTL cache (60s
> manifest, 5min content; both env-tunable + negative-cached on 404).
> Frontend reorganizes `/docs` into a left-flyout hub with the new
> session browser sub-routes; existing DOCS.md content moves to
> `/docs/user-guide`. 313/313 backend tests pass (18 new); frontend
> builds clean. No tag, no pin bump, no benstull mirror push — driver
> takes over after subsession 0017.2 lands.**
---
## Pre-session state
- `rfc-app` was at `main @ ac3513a`, VERSION `0.18.0`, working tree
clean. Two remotes:
- `origin``git.wiggleverse.org/ben.stull/rfc-app` (canonical)
- `benstull``git.benstull.org/benstull/rfc-app` (mirror)
- The most recent release on `main` was v0.18.0 (email + webhook
hygiene, roadmap items #18 + #20).
- Existing `/docs` surface was the v0.14.0 single-route DOCS.md
shape: `components/Docs.jsx` rendering markdown via the shared
`MarkdownPreview` (marked + lazy-loaded mermaid). Backend served
the body at `/api/docs` from a disk-cached `DOCS.md` at the repo
root.
- Dispatch settled two design choices before this subsession opened:
the transcript repo is gitea-only (canonical at
`git.wiggleverse.org/wiggleverse/ohm-session-history`, no GitHub
mirror) and the rendering shape is runtime fetch with cache (no
build-time bundling of transcripts).
---
## Turn-by-turn arc
### Arc 1 — Orientation
Read `~/git/ohm-infra/SESSION-PROTOCOL.md` (full), `rfc-app/CLAUDE.md`,
the SPEC §21 analytics chapter (DOM patterns + the autocapture
attribute conventions), `backend/app/docs.py` and `backend/app/api.py`
(to see how the existing v0.14.0 `/api/docs` was wired), and
`backend/tests/test_propose_vertical.py` for the `app_with_fake_gitea`
fixture shape that every existing test reuses.
Noted that `pyproject.toml` doesn't exist in rfc-app (the dispatch
prompt's "bump pyproject.toml" instruction was a non-op — there's a
`backend/requirements.txt` only, and the canonical version sources
are `VERSION` + `frontend/package.json#version` per
SPEC §20.1).
### Arc 2 — Design choice: per-session listing
The dispatch prompt named the open design question for the per-session
index page (`/docs/sessions/:nnnn`). Two options:
1. Extend the manifest endpoint to also list per-session filenames
via the gitea contents API.
2. Add a separate per-session listing endpoint that hits gitea
contents only when the session-index page is visited.
I went with **option 2** (separate `/api/docs/sessions/<NNNN>/index`
endpoint). Reasoning: the manifest stays cheap and small (one fetch,
one cache entry, 60s TTL); per-session listings are fetched on
demand and cached at the content TTL (5min). This means a user who
only browses the flyout's session list (via the manifest) doesn't
pay for `N` extra gitea contents-API calls — only the user who
clicks into a session pays. The endpoint filters the contents-API
response to entries matching the `_TRANSCRIPT_FILENAME_RE` so any
sibling files in a `NNNN/` folder (e.g. an attached `notes.md`)
don't show up in the list.
### Arc 3 — Design choice: negative caching for 404s
The dispatch prompt left negative caching for 404s as optional.
**Decided to implement it**: cache 404 results at the content TTL
across the manifest, about, transcript, and session-index endpoints.
Reasoning: at v0.19.0 ship time, the parallel
ohm-session-history repo restructure (subsession 0017.2) hasn't
shipped yet — the repo is still flat, so the manifest, about,
session-index, and per-transcript fetches will all 404 from gitea
for the first few hours / days. Without negative caching, every
page-view in that window would trigger a fresh gitea request even
though we already know it's 404. With negative caching, repeated
visits to a known-missing transcript only hit gitea once per
5-minute window. Manifest 404 cache means the first OHM user
loading `/docs/sessions/*` doesn't hammer the upstream while
0017.2 is still mid-flight.
The behavior is exercised by `test_transcript_404_is_cached` in
the new test file.
### Arc 4 — Backend slice
Wrote `backend/app/docs_sessions.py` (the fetcher + cache module)
and added four routes to `backend/app/api.py`:
- `GET /api/docs/sessions/manifest` — returns `sessions.json` as a
JSON object. 404 from gitea → `{}` at HTTP 200 (empty-state
contract, lets the frontend short-circuit without an error
banner). 5xx / timeout → HTTP 502 with
`{detail: {error, detail}}`.
- `GET /api/docs/sessions/about` — returns README.md as
`text/markdown; charset=utf-8`. 404 → HTTP 404; 5xx → 502.
- `GET /api/docs/sessions/{nnnn}/index` — returns
`{"files": [...]}`. The path param `nnnn` must match
`^\d{4}$`; otherwise 400 before any network call. 404 from
gitea (no such folder) → HTTP 404.
- `GET /api/docs/sessions/{nnnn}/{filename}` — returns the
transcript body. Both path params are regex-validated before
the network call (`^\d{4}$` for nnnn,
`^SESSION-\d{4}\.\d+(\.\d+)*-TRANSCRIPT(-<start>--<end>)?\.md$`
for filename — `<start>--<end>` is optional so legacy renamed-
letter transcripts without timestamps remain reachable).
Returns the body as `text/markdown; charset=utf-8`.
The fetcher uses `httpx.AsyncClient` with a 5s timeout and no auth
header (the gitea repo is public). The cache is plain
`dict` keyed by URL-path + `time.monotonic()` checks; the
`_lock` is a `threading.Lock` so the cache is safe under
FastAPI's worker thread pool.
Env knobs:
- `OHM_SESSION_HISTORY_RAW_BASE` (default points at
`wiggleverse/ohm-session-history` on git.wiggleverse.org)
- `OHM_SESSION_HISTORY_CONTENTS_BASE` (default mirrors the raw base
on the contents-API mount)
- `OHM_DOCS_SESSIONS_MANIFEST_TTL_SEC` (default 60)
- `OHM_DOCS_SESSIONS_CONTENT_TTL_SEC` (default 300)
### Arc 5 — Backend tests + the monkeypatch ordering snag
Wrote 18 new tests in
`backend/tests/test_docs_sessions_vertical.py`. The tricky bit
took a couple iterations to nail:
The shared `app_with_fake_gitea` fixture from
`test_propose_vertical.py` monkeypatches `httpx.AsyncClient` via
`monkeypatch.setattr("app.gitea.httpx.AsyncClient", patched)`.
Since `app.gitea.httpx` IS the shared `httpx` module, that
setattr mutates the global `httpx.AsyncClient` attribute. My
docs-sessions test fixture then did the same for
`"app.docs_sessions.httpx.AsyncClient"` — meant to override —
but the first version captured
`real_client_cls = httpx.AsyncClient` at fixture-setup time.
At that point `httpx.AsyncClient` was already the gitea-patched
wrapper, so my "patched" function ended up wrapping the
gitea-patched wrapper, and gitea-bound requests still routed
through the FakeGitea handler (and the FakeGitea returned
404 for session-history URLs, making my test see "manifest
empty").
Fix: import the truly-unpatched `AsyncClient` from
`httpx._client` and use that as the base inside my install
closure. The `_UpstreamHandler` also gained a host-marker
filter so it only counts session-history URLs (not the
reconciler's incidental Gitea calls that also hit our handler
via the shared httpx module) in its `.calls` list — that lets
the cache-hit assertions stay exact.
Test inventory after the fix lands:
- Manifest happy-path / empty-state / 502
- About happy-path / 404 / 502
- Transcript happy-path / 404 / 502
- Path-validation rejection: invalid nnnn, path-traversal,
legacy flat-root filename — all rejected 400 before network
- Session-index happy-path / 404 / invalid-nnnn rejection
- Cache-hit-within-TTL for manifest + transcript
- Negative cache for transcript 404s
All 18 pass; full backend suite is **313/313**.
### Arc 6 — Frontend slice
The dispatch prompt was specific about reusing the existing
markdown renderer (`MarkdownPreview` — marked + mermaid lazy-load),
so the four new sub-route components each render their body
through that same component. No new markdown library.
Component layout:
- `DocsLayout.jsx` — shell with the flyout nav + `<Outlet/>` for
sub-routes. Owns the manifest fetch + the mobile-drawer toggle.
- `DocsUserGuide.jsx` — DOCS.md content (the old `/docs` route).
- `DocsSessionsAbout.jsx` — renders the session-history README.
- `DocsSessionIndex.jsx` — per-session transcript list. Fetches
the manifest (for the title) AND the per-session index
endpoint (for the file list). Treats title-fetch failure as
decorative (just renders the bare `Session NNNN` header) while
the file-list fetch is the load-bearing one.
- `DocsSessionTranscript.jsx` — renders a single transcript.
`App.jsx` route registration replaced the single
`Route path="/docs"` with:
```
/docs → Navigate to /docs/user-guide
/docs/* → DocsWithSidebar (nested Routes inside)
├── /user-guide
├── /sessions → Navigate to /sessions/about
├── /sessions/about
├── /sessions/:nnnn
└── /sessions/:nnnn/:filename
```
Mobile drawer: at viewport ≤ 720px the sidebar is hidden by a
`transform: translateX(-100%)` and revealed via a `☰` button in
the header. A scrim overlay (`.docs-drawer-scrim`) intercepts
outside clicks. The drawer auto-closes on every route change
(via a `useEffect` keyed on `location.pathname`) so a click in
the flyout doesn't strand the user on a drawer-open view.
Analytics (per SPEC §21):
- Added `EVENTS.DOC_VIEWED` to `frontend/src/lib/analytics.js`
(new constant, taxonomy entry per §21.1's "new events SHOULD
land via a release"). Each sub-route component fires
`track(EVENTS.DOC_VIEWED, { section: '...' })` on mount.
- Every interactive nav element (links, buttons) carries
`aria-label` + `data-amp-track-name` so autocapture rows are
readable per §21.3. The per-row session links also carry
`data-amp-track-session={nnnn}` so the dashboard can
aggregate by session.
The old `Docs.jsx` is now dead code; deleted in the same commit.
### Arc 7 — Version bumps + CHANGELOG + push
Bumped `VERSION` (root) and `frontend/package.json#version` from
`0.18.0``0.19.0`. The dispatch prompt named `pyproject.toml`
but there isn't one in this repo (the backend uses a plain
`requirements.txt`); noted in this transcript but no edit
performed.
`frontend/package-lock.json` still reads `0.15.0` — it has been
drifting since the v0.15.0 release (four minor versions). Not
touched in this release (separate cleanup; see §19.2 candidates).
CHANGELOG entry was prepended above the v0.18.0 entry verbatim
per the dispatch prompt (header date, body paragraph, upgrade-
steps MAY block, degradation note about the parallel
session-history restructure shipping in subsession 0017.2).
Three commits on the feature branch, pushed to origin only:
- `39e5770` v0.19.0 backend: /api/docs/sessions/* endpoints + TTL cache
- `822f426` v0.19.0 frontend: /docs/* route tree + flyout nav + sessions browser
- `477f496` Release v0.19.0: /docs nav + on-site sessions browser
`benstull` mirror untouched per dispatch.
---
## Cut state
| | |
| --- | --- |
| rfc-app | feature/v0.19.0-docs-sessions-browser @ `477f496`, pushed to `origin` only |
| Tests | pytest backend: **313 passing** (18 new in test_docs_sessions_vertical.py); frontend has no test framework — `npm run build` is green |
| Frontend dev-server smoke check | Skipped (subsession is a sandboxed agent, no browser to drive). The CHANGELOG-named degradation path is structurally exercised by the backend's 404 → frontend's empty-state contract; route-mount errors would have surfaced at `npm run build` |
| What the driver does next | tag v0.19.0 after 0017.2 lands → merge to main → push to origin + benstull mirror → bump ohm-rfc pin → `flotilla deploy ohm-rfc-app` → verify /docs/user-guide + /docs/sessions/about surfaces in the deployed environment |
---
## §19.2 candidates surfaced
1. **`frontend/package-lock.json` version drift** — what / lock
reads `0.15.0` while `package.json` is now `0.19.0` (the lock
has been stale since the v0.15.0 release). why / it's an
audit-log inconsistency in the npm metadata. when / one-shot
cleanup, low-priority — the lock doesn't affect builds because
the resolved versions are correct, just the top-level `name`
stanza. Fix in the next session that touches frontend deps.
2. **rfc-app backend has no `pyproject.toml`** — what / the
dispatch prompt assumed a `pyproject.toml` exists; the repo
uses a plain `backend/requirements.txt`. why / standard
Python tooling (uv, hatch, ruff config, mypy config) all
prefer pyproject.toml; absence makes those tools less
ergonomic. when / a session that brings in another tool that
wants the file. Could pair with a §20.1-style canonical-source
audit ("VERSION here, pyproject.toml there — which wins?").
3. **`docs_sessions.py`'s private symbols leak into `api.py`** —
what / I called `docs_sessions._is_valid_session_dir` and
`docs_sessions._is_valid_transcript_filename` from the route
layer (underscore-prefixed names). why / the validation is
load-bearing for path-traversal rejection and shouldn't be
re-implemented in the route, but exporting two helpers vs.
one consolidated `validate_path(nnnn, filename) -> bool` would
keep the API cleaner. when / next docs-sessions touch.
4. **CHANGELOG entry header style drifted** — what / the v0.18.0
entry header is `## 0.18.0 — 2026-05-28` (no `v` prefix); my
new v0.19.0 entry is `## v0.19.0 — 2026-05-28` (with `v`
prefix) — I followed the dispatch prompt's verbatim block.
why / cosmetic inconsistency in the changelog history. when /
a release that audits the file for consistency could rename
one direction or the other.
5. **`docs_sessions` cache cannot be invalidated externally** —
what / the in-process cache exposes `reset_cache()` but no
"invalidate this URL" gesture. why / if the operator publishes
a transcript correction and the cache has the old body, they
wait up to 5 minutes for the next reader to see the fix. when /
a session that wires the reconciler's existing periodic refresh
gesture in `cache.py` to also tick this module — or that adds
an admin POST `/api/docs/sessions/refresh` for an explicit
bust. Low priority; the publish gesture is rare.
---
## Notes for the driver
- **Path-traversal corner**: FastAPI / Starlette normalizes `..`
in the URL path before routing, so `/api/docs/sessions/0001/../etc/passwd`
resolves to a path that doesn't match the
`/api/docs/sessions/{nnnn}/{filename}` route. The defense-in-depth
is still in place (regex validates `nnnn` AND `filename` before
any network call), but the test for path traversal had to use
`etc%2Fpasswd` (URL-encoded slash) to actually reach the
handler — the bare `../etc/passwd` was 404'd by the router
itself.
- **TestClient + lifespan**: the backend tests use
`with TestClient(app) as client:` which triggers the FastAPI
lifespan. That starts the reconciler, which makes Gitea calls
that ALSO hit our patched `httpx.AsyncClient` (since httpx is a
shared module — see Arc 5). The `_UpstreamHandler` filters its
`.calls` list to session-history URLs to keep the cache-hit
assertions exact. Reconciler hits return 404 (no matching key
in the responses dict), which the reconciler logs as a warning
and proceeds — harmless for our test surface.
- **Frontend dev-server check not run**: the dispatch prompt
named this as optional. The subsession is a sandboxed agent
without a browser to drive; the build's success and the route
registration's symmetry with the existing `/philosophy` chrome
is the closest analog I could verify. Recommend the driver run
`npm run dev` against the feature branch before merging to
spot-check the flyout's render + drawer toggle on a real
viewport.
- **No tag, no pin, no mirror push** — per dispatch. Driver
handles tag → ohm-rfc pin bump → flotilla deploy after 0017.2
lands.
@@ -0,0 +1,262 @@
# Session 0017.2 — Subsession transcript
> Parent: SESSION-0017.0-TRANSCRIPT-…md (not yet written; driver writes at end of session 0017.0)
> Date: 2026-05-28
> Start: 2026-05-28T09-10 PST
> End: 2026-05-28T09-15 PST
> Goal: wiggleverse/ohm-session-history restructure — per-session folders + README + sessions.json (roadmap item #30, Track Ω half)
> Outcome: **Single commit `ad01c08` on `main` (b6a7b7d..ad01c08, fast-forward push), 19 transcripts `git mv`'d into 15 `NNNN/` folders (rename detection 100% on every file), top-level `README.md` rewritten with operator-review markers on 3 sections, top-level `sessions.json` drafted with 15 entries — no placeholders, every session titled.**
## Pre-session state
Remote `wiggleverse/ohm-session-history` `main` was at `b6a7b7d` at
clone time (commit subject: `update SESSION-0014.0-TRANSCRIPT-…md`).
Repo was fully flat — root contents:
- `LICENSE` (CC BY 4.0, ~17 KB, untouched by this session)
- `README.md` (~4.7 KB, pre-rewrite; documented the old letter-form
layout and the legacy publish flow)
- 19 transcripts in numeric form:
- `SESSION-0001.0-…md` through `SESSION-0015.0-…md`
- including 3 subagent transcripts in the `0013/` cohort
(`0013.1`, `0013.2`, `0013.3`) and 1 in the `0014/` cohort
(`0014.1`)
No legacy letter-form transcripts (`SESSION-0001.0-TRANSCRIPT-…`,
`SESSION-0013.1-TRANSCRIPT-…`, etc.) at root — Session 0014.0 / roadmap
#23 already renamed all of them in commit `1afa9f5`.
## Turn-by-turn arc
### Arc 1 — Clone + inventory
Read `~/git/ohm-infra/SESSION-PROTOCOL.md` first; the §1 amendment
(Session 0017.0, 2026-05-28) names the binding shape — folder per
session, full `SESSION-` prefix retained, `sessions.json` flat object
keyed by 4-digit NNNN, subagent transcripts share the parent's folder.
Cloned the remote into `/tmp/0017.2-work/ohm-session-history`. Did
NOT work in `~/git/ohm-infra/` (that's the local-transcripts working
dir, not a git repo). Inventoried — 19 transcripts at root, all
numeric-form, plus `LICENSE` and `README.md`. No surprises.
### Arc 2 — git mv into NNNN/ folders
Ran one loop that for each `SESSION-NNNN.M-TRANSCRIPT-…md` extracted
the leading `NNNN` via `sed`, `mkdir -p NNNN`, and
`git mv SESSION-… NNNN/SESSION-…`. All 19 renames worked first try.
`git status` after showed 19 renames (no `deleted+added`
`git mv` preserves rename detection cleanly), zero surprises. The 15
folders created: `0001/`, `0002/`, `0003/`, `0004/`, `0005/`,
`0006/`, `0007/`, `0008/`, `0009/`, `0010/`, `0011/`, `0012/`,
`0013/` (4 files), `0014/` (2 files), `0015/`.
### Arc 3 — sessions.json draft titles
Method: read the `Goal:` and `Outcome:` lines from each transcript's
header (the `> Date: … > Goal: … > Outcome:` block at the top), then
distilled into a tight phrase. For sessions that already named
themselves "Session X" in the header (because the rename was filename-
only, not body), I kept that internal letter-name in parens or as a
prefix so the rendered nav has a hook for the chronology the operator
already references in conversation.
Drafted titles (operator review needed — these are starter sketches):
```
0001: "Bootstrap — git.benstull.org personal Gitea on GCP"
0002: "Session 0002.0 — first OHM deployment (ohm.wiggleverse.org from zero)"
0003: "Session 0003.0 — rfc-app 0.3.0 release + OHM upgrade (allowlist + anon read)"
0004: "Session 0004.0 — flotilla SPEC + rfc.wiggleverse.org deprovision"
0005: "Session 0005.0 — flotilla Slices 1 & 2 (registry + overlay; secret refs)"
0006: "Session 0006.0 — flotilla Slice 3 (pin reading + deploy --dry-run, v0.3.0)"
0007: "Session 0007.0 — flotilla Slice 4 (full deploy gesture + health probe, v0.4.0)"
0008: "Session 0008.0 — flotilla v1.0.0 + first real OHM upgrade via flotilla"
0009: "Session 0009.0 — multi-wave driver (rfc-app v0.4v0.13, skipping vacants)"
0010: "Session 0010.0 — DOCS.md + /docs route (rfc-app v0.14.0)"
0011: "Session 0011.0 — Wave 4 (admin /users, device trust, CloudFlare Turnstile)"
0012: "Session 0012.0 — Wave 5 (Amplitude, owner-invite, admin-create)"
0013: "Session 0013.0 — Wave 6 (CONTRIBUTING + SPEC §21 analytics; repo-naming; email/webhook runbook)"
0014: "Session 0014.0 — Wave 7 driver (SMTP + webhook hardening, #23 transcript rename)"
0015: "Roadmap-capture parallel session (items #24#30 added to ROADMAP.md)"
```
No placeholders — every published session has a drafted title. Did
NOT include entries for 0016 or 0017 (those are unpublished as of
this subsession; the publish script will add them when their
transcripts land).
JSON shape: flat object, 4-digit zero-padded NNNN keys, two-space
indent, trailing newline. Matches the §1-amendment example exactly.
Mid-arc reconsideration: I first wrote `sessions.json` with stub
entries `0016` and `0017`. Then re-read the dispatch — "one entry
per session number per reading each transcript's header" — and
realized 0016/0017 don't have transcripts in the public repo yet,
so including them would be drafting titles for sessions that haven't
shipped. Stripped both. The driver Session 0017.0 will add its own
entry when it publishes its transcript.
### Arc 4 — README draft
Rewrote `README.md` end-to-end. Kept the structure of the existing
README (intro → how / why → layout → publish → not-this → related →
license) but rewrote every section to match the new layout and the
post-#23 / post-#30 reality. Ingredients per the dispatch:
- **How OHM gets built** section: paired-session discipline, one-
feature-per-session, parallel-subagent fan-out, cross-repo
coherence. Marked for operator rewrite (this is the highest-
voice-stakes section).
- **Why transcripts are public** section: the §19.3 lesson ("the
build is the artifact; cleanup is dishonest"), the cross-session
driver brief, the invitation to read. Marked for operator rewrite.
Includes the verbatim shout: **"Oh and Jan, your joke is in there
somewhere."** Placed at the end of this section because that's
where the affectionate aside fits the tonal arc.
- **What's in the repo** section: documents the new folder layout,
subagent placement under parent folder, sessions.json's role,
legacy-letter-form mapping (pointer to SESSION-PROTOCOL.md §1).
Marked for operator rewrite.
- **How transcripts get added** section: kept the publish-script
example, updated the path to use a 0017.0-style filename. Not
marked for rewrite — this is operational, not voice-stakes.
- **What this repo is not** / **Related repos** / **License**:
preserved with minor edits to reflect new layout and updated
remote URLs. Not marked.
- Canonical-link callout at the top: gitea-only, no GitHub mirror,
on-site at `ohm.wiggleverse.org/docs/sessions`.
3 sections marked with `<!-- operator: rewrite this section in your
voice -->`. Length: ~520 words (target was ~400600).
### Arc 5 — Commit + push
Staged all (19 renames + README modified + sessions.json new). Verified
`git status` showed exactly the expected 19 renames + 1 modify + 1 new
file, no stragglers.
Single commit, message verbatim from the dispatch prompt:
```
#30: per-session folder layout + README + sessions.json
Session 0017.0; see SESSION-PROTOCOL.md §1 amendment for the binding shape.
```
Committed as `ad01c08`. Git emitted a "Committer: Ben Stull <benstull@mac.lan>"
notice (default identity from the local hostname); did NOT touch
`git config` to suppress it (per CLAUDE.md / safety: never update git
config). The notice is informational; the committer is fine for a
local-clone tempdir push.
Pushed to `origin main`: clean fast-forward, `b6a7b7d..ad01c08`. No
non-fast-forward retry needed; nothing landed in the public repo
between clone and push.
## Cut state
| | |
| --- | --- |
| Public repo | `wiggleverse/ohm-session-history` `main` @ `ad01c08`; 19 transcripts now in 15 `NNNN/` folders + `README.md` + `sessions.json` at root; `LICENSE` unchanged. |
| Files moved | 19 (all via `git mv`, rename detection 100%); legacy letter-form left at root: 0 (none existed post-#23) |
| Titles drafted | 15; operator-empty placeholders: 0 |
| README markers | 3 sections marked `<!-- operator: rewrite this section in your voice -->` (How / Why / Layout) |
## §19.2 candidates surfaced
1. **`sessions.json` update on every publish** — the publish-transcript.sh
script needs to gain logic that adds/updates the `sessions.json` entry
for the session it's publishing. Today it just copies the file in;
it does not touch `sessions.json`. For the first publish of a session,
the script should add `"NNNN": {"title": ""}` as a placeholder and
leave a hint that the operator (or the session's own README/transcript
pass) needs to fill it. Subsequent publishes (same NNNN, different .N,
or `update SESSION-…`) should not touch an existing title. This is a
§19.2 because the subsession 0017.1 (rfc-app side) and 0017.2 (this
one) shipped under a manual-fill convention; a §19.2 candidate would
automate it.
2. **README rendered into rfc-app /docs/sessions/about** — the dispatch
says rfc-app v0.19.0+ renders this README at `/docs/sessions/about`.
The README I wrote includes operator-only HTML comments
(`<!-- operator: rewrite this section in your voice -->`) that should
NOT render on the public site. Markdown renderers typically strip HTML
comments, but if the rfc-app docs renderer happens to pass them through
as raw HTML, the markers would still be invisible (HTML comments are
not displayed). Worth confirming — if rfc-app's renderer is strict
sanitization, the markers vanish; if it's loose, they pass through
inert. Either way, a §19.2 candidate is "what's the contract between
ohm-session-history/README.md and the on-site docs renderer? Are
there sections of the README that should NOT render on-site (e.g.,
the publish-script how-to is operator-only)?"
3. **Local-vs-published transcript shape divergence** — local working
files at `~/git/ohm-infra/SESSION-NNNN.M-TRANSCRIPT-…md` keep the
flat shape; publish-time gesture moves them into folders. This
subsession's transcript (the file you're reading) is at
`~/git/ohm-infra/SESSION-0017.2-TRANSCRIPT-…md` — flat. When the
driver publishes it, it lands at `0017/SESSION-0017.2-…md`. The
asymmetry is fine but worth a one-line acknowledgement in the
protocol doc (§1 already covers it but the wording is buried).
## Notes for the driver
Operator-review checklist for sessions.json titles (paste into the
next operator-attention session):
- **0001**: Currently "Bootstrap — git.benstull.org personal Gitea on
GCP." The transcript subtitle is "_For Jan, with affection and
discomfort_" — operator may prefer a title that gestures at Jan
rather than the technical surface.
- **00020008**: Each titled "Session X — <work>". Operator may
prefer dropping the "Session X" prefix (the folder/filename already
encodes the letter via the legacy mapping); the work-summary alone
may read cleaner in the nav. Or operator may prefer the prefix as
a chronology hook.
- **0013**: "Session 0013.0 — Wave 6 (CONTRIBUTING + SPEC §21 analytics;
repo-naming; email/webhook runbook)" — Session 0013.0 dispatched three
subagents whose work spans three distinct workstreams. The
semicolon-list format may not be ideal; consider a one-phrase
framing like "Track Ω inventory wave (#17, #18, #19, #20, #21)".
- **0014**: Pulled the phrasing straight from SESSION-PROTOCOL.md §1
example ("Wave 7 driver — SMTP + webhook hardening (#18 + #20 +
#23)"). I shortened it slightly to fit nav width. Operator may
prefer the example's verbatim form.
- **0015**: This session was the "roadmap-capture parallel session"
that ran concurrent with 0014.0 and added items #24#30. The title
reflects that. Operator may prefer "Companion to Wave 7 — roadmap
items #24#30" or similar.
README operator-rewrite checklist:
- **Section "How OHM gets built"** (~280 words) — voice-heavy; the
operator should own this.
- **Section "Why transcripts are public"** (~190 words) — includes
the Jan shout verbatim per dispatch; rest of section is voice-
heavy; operator should own this.
- **Section "What's in the repo"** (~150 words) — mostly mechanical
(layout description) but the prose framing is mine, operator may
prefer their own.
Three other notes:
- The publish-transcript.sh script needs to know about the folder
layout. Subsession 0017.1 (rfc-app side) was named in the dispatch
as also handling a publish-script extension; I assume that ran in
parallel and the script now routes by parsing the leading NNNN. If
not, the next session that tries to publish a flat-form transcript
to the now-folder-shaped repo will fail. (Subsession 0017.1's
report-back to the driver should confirm.)
- Future sessions that publish under the new layout will need their
parent session's `sessions.json` entry to exist (or the publish
script to add it). This subsession added entries 00010015; the
driver session 0017.0 should plan to add its own `0017` entry when
it publishes its own transcript, and to ensure 0017.1 and 0017.2
(this one) inherit from it.
- No git config edit happened. The commit shows
`benstull@mac.lan` as committer (default from hostname). This is
cosmetic — the audit trail is intact, the author email could be
set later via `--amend` if the operator prefers `ben@wiggleverse.org`.
Per safety rules I did not amend.