# Session 0010.0 — Transcript

> Date: 2026-05-27 → 2026-05-28
> Goal: Answer an operator UX question ("what's the difference between
> admin and owner?" — surfaced by the `/admin/users` role-change
> dropdown), translate the answer into public-facing documentation
> reachable from the live deployment, ship it as a focused rfc-app
> release, and deploy it via flotilla to OHM. Concurrent with the
> Session-I driver run that was advancing the roadmap; the two
> sessions ran in parallel on the same laptop with the operator
> pivoting between them.
>
> Outcome: **Shipped.**
>
> - **rfc-app v0.14.0** released, tagged, pushed to both remotes
>   (`origin = git.wiggleverse.org`, `benstull = git.benstull.org`).
>   New `DOCS.md` at the repo root, new `backend/app/docs.py` loader,
>   new `GET /api/docs` endpoint, new `frontend/src/components/Docs.jsx`
>   route, new "Docs" header link alongside "About". 605-line user
>   guide covering roles & permissions (the original
>   admin-vs-owner question), proposing, contributing, branches & PRs,
>   graduation, discussion vs contribution, AI in the chat,
>   notifications, write-mute vs notification mutes. Framework-neutral,
>   no deployment names baked in. Mirrors the `/philosophy` shape
>   end-to-end.
> - **OHM live on v0.14.0** (`deploys.id=14`, all 9 phases green,
>   verify 2.5s). `/api/health` returns
>   `{"version":"0.14.0","status":"ok"}`.
> - **Two prior failed deploys recorded** (`id=12`, `id=13`) — both
>   failed at phase 3 because the production VM's `origin` is
>   `git.benstull.org/benstull/rfc-app.git`, NOT the pin-source URL
>   `git.wiggleverse.org/ben.stull/rfc-app.git`. The two Gitea remotes
>   are linked but not auto-mirrored; my push had only landed on
>   origin. Fix: push tag + main to `benstull` as well. New §19.2
>   candidate spawned as a follow-up task (a flotilla preflight that
>   resolves the VM-side origin and checks the target tag is
>   reachable there before starting phase 3).
>
> Three findings worth flagging:
>
> 1. **The operator hand-off recovery gesture (`git stash push -u` →
>    `git reset --hard main` → `git stash pop`) doesn't undo a
>    ghost-revert state.** The hand-off prompt described this gesture
>    as the way to reconcile a working tree that diffs against a
>    `git update-ref`-advanced HEAD. In practice the stash captures
>    the *full* working-tree-vs-HEAD diff, including the ghost-revert
>    appearance; `reset --hard main` brings the tree to current HEAD;
>    `stash pop` then replays the same ghost-revert diff back on top.
>    Exit code 0, stash dropped, end state indistinguishable from
>    start. **What actually worked**: `git restore .` (wipes all
>    tracked-file modifications, leaves the 3 untracked docs-feature
>    files alone), then manually re-applying the 3 wiring edits
>    against the live v0.10.0 versions of `api.py`, `App.jsx`, and
>    `api.js`. Worth a §19.3 rule-2 correction to the
>    `SESSION-PROTOCOL.md` or to whatever operating-instructions
>    document carries this recovery recipe — the stash route is a
>    trap when the working tree's "modifications" are actually
>    "this file held an earlier version's bytes."
>
> 2. **The pin-source URL and the VM's actual `origin` URL can
>    diverge silently.** The flotilla `deployment show` output names
>    the pin-source host (`https://git.wiggleverse.org`) but doesn't
>    surface what the VM is actually fetching from. In OHM's case
>    `/opt/ohm-app/.git` has `origin =
>    https://git.benstull.org/benstull/rfc-app.git` — a separate
>    Gitea instance. Tags land on the VM-side remote via whatever
>    mirror sync runs between the two hosts (the historical tags
>    v0.4.0–v0.13.0 are all there), but the sync is not real-time:
>    a push-to-origin + immediate `flotilla deploy` hits a window
>    where the target tag exists on the pin source but not on the VM
>    origin. The failure message ("pathspec did not match") doesn't
>    name the mismatch, so an operator first hitting this has to SSH
>    in and discover it manually. Two-push discipline (`git push
>    origin … && git push benstull …`) works around it; flotilla
>    preflight + GCP-side mirror config are the two structural fixes.
>    Follow-up task chip spawned in-session.
>
> 3. **v0.14.0 collided with the roadmap's reservation for #12
>    Owner-only invite.** The version-target table in `ohm-rfc/ROADMAP.md`
>    had reserved v0.14.0 for #12. My docs release was not a roadmap
>    item — it was operator-driven from a UX question. I claimed the
>    next clean-sequential slot (v0.14.0) when picking the version,
>    matching the operator's prompt option ("v0.14.0 — clean
>    sequential"). Item #12 now has to bump to the next free slot
>    (v0.16.0 — v0.15.0 is reserved for #13 Amplitude). The roadmap
>    edit at end-of-session records both: a new struck-through row
>    #15 capturing what shipped, and a noted update to row #12's
>    target version.
>
> One §19.3 rule-2 spec correction landed in `rfc-app/SPEC.md` (none
> — the docs feature is downstream of existing SPEC sections, no spec
> contract changed). The framework-neutrality rule from `CLAUDE.md`
> shaped the content of `DOCS.md`: no deployment names ("Wiggleverse
> Open Human Model", "OHM"), no corpus references ("vocabulary",
> "Human"), but the framework name ("Wiggleverse RFC") does appear
> per the CLAUDE.md exception.
>
> `rfc-app/CLAUDE.md` Session-protocol section was again deferred
> (Session 0009.0 had also deferred it for the same reason — the working
> tree was unstable). I did not add it because the working tree
> *was* clean by the time I shipped v0.14.0, but I deprioritized it
> in the rush to deploy. The next OHM-touching rfc-app session
> should add the Session-protocol section per `ohm-rfc/CLAUDE.md`'s
> shape.

---

## Pre-session state

- rfc-app on `main` at `55beba5` (Release 0.10.0 — most recent shipped
  by SESSION-I). `VERSION` and `frontend/package.json` at `0.10.0`.
- ohm-rfc at `a97a10e` (the Session-I CLAUDE.md Session-protocol
  pointer commit). `.rfc-app-version` at `0.10.0`.
- flotilla v1.0.1, OHM live on v0.10.0 (`deploys.id=11`).
- This session's docs-feature work — `DOCS.md`,
  `backend/app/docs.py`, `frontend/src/components/Docs.jsx`, plus
  edits to `api.py` / `App.jsx` / `api.js` — predated Session 0010.0's
  *explicit* arc and was the "operator-in-progress" state Session-I
  observed in `/Users/benstull/git/rfc-app` (Arc 8 of that
  transcript). The two sessions overlapped temporally; Session 0009.0
  routed around the in-progress files via throwaway worktrees +
  `git update-ref refs/heads/main` while Session 0010.0 continued
  building the docs feature in the main checkout.
- The operator was driving both sessions, pivoting between them as
  each surfaced something needing a human decision.

The `git status` Session 0010.0 inherited at its first significant
checkpoint showed the Session-I-induced ghost-revert: 28 staged
"modifications" (actually the diff between the v0.7.0-era working-
tree files and the post-`update-ref` v0.10.0 HEAD) plus my 3
untracked files. The driver in Session 0009.0 had documented this
exact shape in its Arc-15 "cross-session staged state question"
section — I inherited the artifact.

---

## Turn-by-turn arc

### Arc 1 — The admin-vs-owner question

The operator opened the session with a screenshot of the
`/admin/users` page mid-click on the role-change dropdown. The
dropdown showed `Contributor / Admin / Owner` with `Owner` already
selected, and a warning "Use the explicit succession path to change
your own role." Question: "What is the difference between admin and
owner?"

I read `rfc-app/SPEC.md` §6.1 (Four roles, each a strict superset of
the one below) and §6.2 (write-mute), summarized:

- **Admin** = everything contributor can do, plus framework-wide
  acting authority: merge any PR, graduate any super-draft, set
  branch visibility on anyone's behalf, write-mute / restore
  contributors, grant or revoke the **admin** role itself, withdraw
  or reopen entries.
- **Owner** = everything admin can do, plus the two things admins
  can't: grant or revoke the **owner** role itself, disable an
  account entirely.

The practical difference: admin is the operational tier; owner is
the tier that controls the admin tier. The succession-path warning
on the dropdown is the framework refusing to let the last owner
silently demote themselves.

### Arc 2 — Public docs scope question

The operator's follow-up: "We'll need documentation on all of this
that's publicly facing, not just the spec. Can you do that?"

I surveyed `frontend/src/components/Philosophy.jsx` +
`backend/app/philosophy.py` + the `/philosophy` route registration
in `App.jsx` to understand the existing pattern. Asked the operator
two scoping questions via `AskUserQuestion`:

1. **Doc scope**: roles-only / all operator-admin surfaces / full
   user guide. Operator picked **full user guide**.
2. **Location**: new `/docs` route in-app / extend `/philosophy` /
   in-context Admin help. Operator picked **new `/docs` route** —
   the recommended option that mirrors the `/philosophy` shape.

### Arc 3 — Pre-flight repo check, in-progress 0.7.0 rebase discovered

Before any edits I ran `git status` on rfc-app to confirm a clean
baseline. The git-status snapshot in the session-start context had
said "clean" but the actual state was mid-rebase: HEAD detached on
`feature/v0.7.0-email-otc`, picked commit `2590c24` halted with
conflicts on `CHANGELOG.md`, `SPEC.md`, `VERSION`,
`frontend/package.json`, `frontend/package-lock.json`. The
`frontend/package.json` file literally had `<<<<<<< HEAD` /
`=======` / `>>>>>>>` conflict markers from the v0.7.0 release pick.

This was the *parallel* Session-I driver's interrupted state. I
paused, surfaced the discovery, and asked the operator how to
proceed. Three options offered; operator picked **write docs only;
skip version bump** — I'd write `DOCS.md`, wire the endpoint and
the route, but not touch `VERSION` / `package.json` /
`CHANGELOG.md` / `SPEC.md` / `package-lock.json`. The version-bump
and changelog entry would land later, against whatever resolved
tree the other session produced.

### Arc 4 — Wrote the docs feature

In one batch:

- **`DOCS.md` at the repo root** (605 lines). Plain-prose user
  guide. Sections: reading without signing in, signing in,
  proposing an RFC, what a super-draft is, what an active RFC is,
  discussion vs contribution, working on a branch (contribute mode,
  AI proposals, manual edits, flags, branch visibility, contribute
  grants, hygiene), opening and reviewing PRs, graduation,
  withdrawal and reopening, AI in the chat, notifications and
  watch states, roles & permissions (the four app-wide roles
  including the original admin-vs-owner answer, per-RFC owners
  and arbiters, per-branch contribute grants, the write-mute, the
  three structurally distinct "mutes"), where to learn more.
  Framework-neutral per `CLAUDE.md`'s separation-of-concerns rule
  — no deployment-specific names; "Wiggleverse RFC framework" does
  appear per the CLAUDE.md exception for framework references.
- **`backend/app/docs.py`** — 61 lines. Mirrors `philosophy.py`:
  disk-first load, in-process cache, `refresh()` on demand,
  optional `DOCS_PATH` env override, loud-failure placeholder if
  `DOCS.md` is absent at process start.
- **`backend/app/api.py`** — two additive edits. Added
  `docs as docs_mod` to the relative-import block alongside
  `philosophy`; added `GET /api/docs` handler immediately after
  `GET /api/philosophy` (same shape, same anonymous-readable
  contract).
- **`frontend/src/api.js`** — added `getDocs()` exported helper
  alongside `getPhilosophy()`.
- **`frontend/src/components/Docs.jsx`** — 51 lines. Mirrors
  `Philosophy.jsx`: chrome with Back / "USER GUIDE" / Home
  affordances, body rendered through `MarkdownPreview`.
- **`frontend/src/App.jsx`** — four additive edits. New `Docs`
  import; new "Docs" header link alongside "About"; new `/docs`
  route alongside `/philosophy`; new `DocsWithSidebar` chrome
  wrapper alongside `PhilosophyWithSidebar`.

Three of the four App.jsx edits hit a "file has been modified since
read" error mid-batch (the parallel Session-I driver was touching
files), forcing a re-read and re-application. No content lost; just
ceremony.

### Arc 5 — Preview verification, backend down

`preview_start` launched the existing frontend Vite server. Navigated
to `/docs` via `preview_eval`. The snapshot showed the chrome
rendered correctly (header brand + About + Docs nav links, page
chrome with "USER GUIDE" / Back / Home, body with "Could not load
the guide: Failed to execute 'text' on 'Response': body stream
already read"). The fetch error was the FastAPI backend not running
in this preview environment (every `/api/*` call returned 502
including `/api/auth/me` and `/api/rfcs`), not a bug in my code. I
confirmed by also navigating to `/philosophy` — same error shape,
same backend-down symptom — proving the two routes were wired
symmetrically.

Could not boot the FastAPI backend cleanly during this preview
window because the parallel session had `requirements.txt` and
`auth.py` and migration `012_otc.sql` mid-rebase; starting the
backend would have surfaced spurious errors that weren't about my
docs work.

Verified the backend module's load path directly via `python3 -c
"from app import docs; r = docs.load(); print(r['path'], len(r['body']))"`
— DOCS.md loaded, 24,597 bytes, first line `# Using the RFC app`.

Also verified the api.py wiring with `ast`-style grep: the
`docs_mod` import landed, the `/api/docs` handler is defined, the
`docs_mod.load()` call is present.

### Arc 6 — Long pause, parallel session continued

Operator: "ok, will let you know when we're ready for that" — for
the version bump + changelog entry. I waited.

### Arc 7 — Operator returned, state still in flux

Operator: "I think we're ready - go ahead and check."

Re-ran `git status`. The state had changed but was *still*
mid-revert: HEAD had advanced from the v0.7.0 rebase target to
`8aa6501` (Release 0.13.0), and a 17-file staged change set was
sitting uncommitted — an exact inverse of the 0.13.0 commit (same
files, same line counts flipped). The staged commit would assert
"we are 0.7.0 again." My docs files were still safely uncommitted
on top.

I surfaced the picture in detail and asked the operator how to
proceed. Three options; operator selected "Other → Tell me more
about the staged revert." I described the staged revert precisely
(one commit between 0.7.0 and HEAD, the staged diff is its exact
inverse — effectively `git revert HEAD`, semver-unusual because
`VERSION` walks backward), then re-asked. Operator selected
"Other → Hang tight - sounds like we aren't done in another
session. Will let you know when we're ready." Waited again.

### Arc 8 — Multiple check-ins, parallel session kept advancing

Operator: "ok let's try again." State still mid-revert, now
expanded: HEAD at `55beba5` (Release 0.10.0), 28-file staged
change set inversing the 0.13.0 + 0.10.0 + 0.8.0 deltas. Same
constraint as before, surfaced again, operator paused again.

Operator: "How's it looking now?" Same 28-file staged state, same
report.

### Arc 9 — Hand-off prompt with recovery gesture

Operator finally pasted a long hand-off prompt explaining the
*actual* state: SESSION-I's driver had been advancing local `main`
via `git update-ref refs/heads/main refs/remotes/origin/main` to
avoid disturbing the operator-in-progress working tree. Side effect:
my working tree (based on v0.7.0 where my docs work was authored)
now diffed against v0.10.0 HEAD, so every v0.6.0/v0.8.0/v0.10.0/
v0.13.0 file appears as "D" (staged-for-deletion) and every file
modified since v0.7.0 appears as "M" (staged-modification). Nothing
actually got removed; the index is just out of sync with the
working tree.

Prescribed recovery gesture:

```
git status --short | wc -l                            # ~28 lines, expected
git stash push -u -m "docs-feature wip pre-rebase"    # captures tracked + untracked
git status --short                                    # should be empty now
git reset --hard main                                 # working tree = v0.10.0 main, clean
git stash pop                                         # bring back docs work, now diffed vs v0.10.0
git status --short                                    # should show 3 M + 3 ??, NO staged revert
```

With the expectation that the `stash pop` would hit merge conflicts
on api.py / App.jsx / api.js (small, additive, resolvable
"keep both sides").

### Arc 10 — The recovery gesture didn't work

Ran the gesture step by step. Counts and reset checked out cleanly.
The `stash pop` exit code was 0, no merge conflicts reported, stash
dropped. **But the resulting working tree was the same 28-file
"revert" state I'd started with.**

The reason: `git stash push -u` captures the *full diff* of the
working tree against HEAD, including the ghost-revert appearance.
After `git reset --hard main` brought the working tree to v0.10.0,
the `stash pop` simply replayed that same ghost-revert diff back
on top — no conflicts because every line in the stash was an
unambiguous replacement of the current line. The end state was
indistinguishable from the start, except my docs edits were now
entangled with the ghost-revert deletions in the same `M` rows of
api.py / App.jsx / api.js. The stash was dropped, so no recovery
via stash was possible.

Surfaced this to the operator with three forward paths. Operator
selected **"Discard everything, re-apply my docs edits manually."**

### Arc 11 — Manual recovery

`git restore .` wiped all tracked-file modifications (the 28-file
ghost-revert plus my entangled docs edits). The 3 untracked files
(`DOCS.md`, `backend/app/docs.py`,
`frontend/src/components/Docs.jsx`) survived. `VERSION` and
`package.json` were at `0.10.0`. Clean v0.10.0 baseline.

Read the current v0.10.0 versions of `api.py`, `App.jsx`, `api.js`
to find the right anchor points for my re-application. The v0.10.0
files had grown (beta-access columns in `/api/auth/me` response,
passcode endpoints, beta-pending viewer prop, login multi-step
flow), so the surrounding context was different from v0.7.0 but
the anchors I needed (the philosophy block in api.py, the
`getPhilosophy()` export in api.js, the `<PhilosophyWithSidebar>`
pattern in App.jsx) were still present in the same form.

Re-applied the 7 edits via `Edit`. Final state:

- 3 M (`backend/app/api.py` +12 lines, `frontend/src/App.jsx`
  +13 lines, `frontend/src/api.js` +4 lines). All purely
  additive; zero deletions.
- 3 ?? (`DOCS.md`, `backend/app/docs.py`,
  `frontend/src/components/Docs.jsx`).
- `VERSION` and `package.json` unchanged at `0.10.0`.

Verified in the running preview: `/docs` route rendered with the
v0.13.0 cookie-consent banner *also* rendering underneath, proving
that the cookie-consent files (`CookieConsentBanner.jsx`,
`Privacy.jsx`, `Cookies.jsx`, `lib/consent.js`) were intact in the
restored tree. The same backend-down error showed on `/docs` as
before; that's a separate concern.

Stale Vite HMR error logs from the ghost-revert window (timestamped
`t=1779962572215`) were not regenerating; new error log lines after
the reload were absent. The `getDocs` export was confirmed present
via `preview_eval` reading the live `/src/api.js` body.

### Arc 12 — Release as v0.14.0

Operator: "ok, go ahead and release."

Picked **v0.14.0** as the version. The operator had earlier
prompted with two options: v0.9.0 (fill the gap left by item #7
Admin user-management page) or v0.14.0 (clean sequential after
0.13.0). v0.14.0 is also a reserved slot in the roadmap (#12
Owner-only invite), but the "clean sequential" framing won — claim
the next gap-free slot, let #12 reshuffle later.

Bumped `VERSION` to `0.14.0`, `frontend/package.json#version` to
`0.14.0`, prepended a CHANGELOG entry matching the established
descending-version-order layout (73 lines: Added / Changed /
Upgrade steps (from 0.13.0)). Staged the 9 files explicitly by
name. Committed:

```
de28272 Release 0.14.0: public /docs user guide (DOCS.md served at /docs)
9 files changed, 821 insertions, 2 deletions
```

821 = 605 (DOCS.md) + 73 (CHANGELOG entry) + 61 (docs.py) + 51
(Docs.jsx) + 12 (api.py) + 13 (App.jsx) + 4 (api.js) + 1 (VERSION
bump) + 1 (package.json bump). Math checked.

Did NOT touch `SPEC.md` — the docs feature is downstream of
existing spec sections, doesn't introduce a new spec contract.
Offered to draft a SPEC amendment as a follow-up if the operator
wants `DOCS.md` named alongside §14.2's `/philosophy`; operator
hasn't asked for one.

Did NOT update `frontend/.env.example` for the new optional
`DOCS_PATH` env var — the framework default (reading from repo
root) works without it; the CHANGELOG "Upgrade steps (from 0.13.0)"
documents the optional override.

Did NOT update `rfc-app/CLAUDE.md` to add the Session-protocol
section that Session 0009.0 had deferred — deprioritized in the rush
to deploy. Noted in §19.2 below.

The commit landed with committer `Ben Stull <benstull@mac.lan>` —
git's auto-generated identity from username + hostname. Memory
says canonical is `ben@wiggleverse.org`. Surfaced to the operator;
did not touch `git config` (per safety rules).

### Arc 13 — Push

Operator: "you can git push." Pushed `main` to `origin` —
`55beba5..de28272 main -> main`. Did not push the tag yet (Arc 14
catches this).

### Arc 14 — Deploy attempt 1 + 2: VM origin mismatch

Operator: "and then use the flotilla to deploy."

Inspected the flotilla CLI surface
(`/Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla/.venv/bin/ohm-rfc-app-flotilla`).
The deploy gesture is `flotilla deploy ohm-rfc-app` (top-level
command, not a subcommand of `deploy` — that namespace's
subcommands are `abort/log/status/watch` for *observing* deploys,
not initiating them).

Read the operator-guide flow: bump `.rfc-app-version` in the corpus
repo → `flotilla pin check ohm-rfc-app` → `flotilla deploy --dry-run`
→ real deploy.

**Bumped the corpus pin.** Edited
`/Users/benstull/projects/wiggleverse/ohm-rfc/.rfc-app-version`
from `0.10.0` to `0.14.0`. Committed (`d6af91f`), pushed.

**Pin check confirmed** the pin resolves to `0.14.0`, AHEAD of last
deploy.

**Dry-run preview** showed the 9-phase plan clean. Plan named
`git checkout v0.14.0` at phase 3, which requires a git tag — and I
hadn't created one yet. Read the existing tag convention
(annotated tags like `v0.13.0` with subject "Release X.Y.Z: ..."),
created `v0.14.0` on commit `de28272`, pushed to origin.

**Deploy attempt 1** (`deploys.id=12`) — failed at phase 3:

```
[3/9] fetch+checkout: FAILED — git fetch/checkout failed: error:
  pathspec 'v0.14.0' did not match any file(s) known to git
```

Three seconds elapsed in phase 3 — suspiciously fast for a real
fetch over the network. First hypothesis: transient Gitea tag
propagation lag. Retried.

**Deploy attempt 2** (`deploys.id=13`) — same failure, same 3
seconds. Not transient.

Verified the tag was definitely on the pin-source Gitea via
`git ls-remote --tags origin v0.14.0` →
`ce4d9af refs/tags/v0.14.0`. So Gitea-the-pin-source had the tag.

### Arc 15 — Diagnosis: VM origin is a different Gitea host

Read `ohm_rfc_app_flotilla/deploy.py` lines 368–388 (the phase-3
implementation). The SSH command is well-formed:

```
sudo -u ohm-app sh -c 'cd /opt/ohm-app && git fetch --tags && git
checkout v0.14.0 && head=$(git rev-parse HEAD) && tagsha=$(git
rev-parse v0.14.0^{commit}) && [ "$head" = "$tagsha" ] && echo
"HEAD=$head"'
```

No obvious bug. Asked the operator how to diagnose; operator
selected **"SSH into the VM, inspect git remote + try a manual
fetch."**

Read-only diagnostic via `gcloud compute ssh`:

```
git remote -v          → origin    https://git.benstull.org/benstull/rfc-app.git
git config --get remote.origin.fetch
                       → +refs/heads/*:refs/remotes/origin/*
git fetch --tags --verbose
                       → all tags up through v0.13.0 listed as "[up to date]";
                         v0.14.0 absent
git tag --list v0.14.*
                       → (empty)
```

**Root cause**: the VM's `/opt/ohm-app` has `origin` set to
`https://git.benstull.org/benstull/rfc-app.git` — a **different
Gitea host** than the flotilla's `pin_source_host`
(`https://git.wiggleverse.org`). The two hosts are linked (every
historical tag eventually propagates to the VM origin via some
mirror sync) but the sync is not real-time. My push to
`git.wiggleverse.org` had not yet landed on `git.benstull.org`.

### Arc 16 — Fix + deploy

Inspected my local rfc-app remotes:

```
benstull  git@git.benstull.org:benstull/rfc-app.git
origin    https://git.wiggleverse.org/ben.stull/rfc-app.git
```

`benstull` is the VM-origin-matching remote, already configured.
Pushed both `main` and the `v0.14.0` tag to it:

```
git push benstull main      → 55beba5..de28272 main -> main
git push benstull v0.14.0   → [new tag] v0.14.0 -> v0.14.0
```

**Deploy attempt 3** (`deploys.id=14`):

```
[1/9] validate: ok
[2/9] preflight: ok
[3/9] fetch+checkout: ok
[4/9] backend deps: ok
[5/9] frontend build: ok
[6/9] write .env: ok
[7/9] restart: ok
[8/9] verify /api/health: ok
[9/9] finalize: ok
ohm: deployed v0.14.0 (deploys.id=14)
ohm: deployed v0.14.0 (verify took 2.5s)
```

Final verification: `flotilla deploy status ohm-rfc-app` →
`HTTP 200 version=0.14.0 status=ok`.

### Arc 17 — Follow-up task spawned

Surfaced the root cause to the operator as a footgun worth fixing
structurally. Two options: a flotilla preflight that resolves the
VM-side origin and `ls-remote`s it for the target tag, OR a Gitea
mirror config between the two hosts. The preflight is
defense-in-depth and survives future mirror lag or a different
deployment's remote drift.

Operator said: "yes, go for it." Spawned a follow-up session task
via `mcp__ccd_session__spawn_task` rooted in the flotilla repo
(`cwd: /Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla`).
Title: "Add VM-origin preflight to flotilla deploy." The spawned
session's prompt is self-contained: full incident context, files
to touch (`deploy.py` ~lines 351–388, `SPEC.md` §8, `tests/`,
`CHANGELOG.md`, `VERSION`), pseudo-shape for the new check, the
alternative-Gitea-mirror consideration, two "don't do" guardrails
(don't change pin-source resolution; don't auto-push the tag from
the flotilla), and a verify-when-done list.

The operator can one-click-spin-it-up in a fresh worktree, or
dismiss the chip.

### Arc 18 — Transcript

Operator asked for this transcript per the standing protocol.
Read `SESSION-0009.0-TRANSCRIPT-2026-05-27T23-30--2026-05-28T03-15.md`
to match shape and voice. Read the publish-transcript script to
understand the filename regex and idempotency contract. Read
`ohm-rfc/ROADMAP.md` to figure out which row to strike — and
discovered that **no row exists for the docs feature**; my v0.14.0
release was operator-driven, not roadmap-driven. The ROADMAP edit
plan (Arc 19 below) reflects this: add a new row #15 to capture
what shipped, and update row #12's target version to reflect the
slot collision.

### Arc 19 — ROADMAP edit + publish

`ohm-rfc/ROADMAP.md` got two surgical edits:

1. **New row #15** in the version-target table: "Public user guide
   (DOCS.md + /docs route)" targeting v0.14.0, struck through with
   a link to the v0.14.0 tag and `deploys.id=14`.
2. **Row #12** (Owner-only invite) target version changed from
   `v0.14.0 (needs v0.5.0 + v0.7.0)` to `v0.16.0 (needs v0.5.0 +
   v0.7.0; v0.14.0 slot was claimed out-of-band by item #15 per
   Session 0010.0)` to record the slot reshuffling explicitly. v0.15.0
   remains reserved for #13 Amplitude, so #12 jumps to v0.16.0.

Committed to `ohm-rfc/main` as `4c89179` and pushed
(`d6af91f..4c89179 main -> main`).

Transcript written. The filename
`SESSION-0010.0-TRANSCRIPT-2026-05-27T23-50--2026-05-28T03-20.md`
matches the convention. Published via
`~/git/ohm-infra/scripts/publish-transcript.sh` at
commit `44995c9` on `wiggleverse/ohm-session-history`
(`179c1ea..44995c9 main -> main`).

### Arc 20 — Operator-requested transcript update + re-publish

Operator: "update the transcript." Re-read the just-published Arc
19 against the post-write commit SHAs I'd captured but only
described in forward-looking voice, and updated the section to
record the actual results: `4c89179` for the ROADMAP commit,
`44995c9` for the publish-script's commit on
`ohm-session-history`. Also tightened the cut-state table's
ohm-rfc + ohm-session-history rows to name those SHAs concretely.

This Arc 20 itself is the second self-reference: writing it
changes the transcript, so re-running `publish-transcript.sh`
produces a new "update SESSION-J-…" commit on
`wiggleverse/ohm-session-history` carrying this section.
Session-I's Arc 16c noted the same publish-cycle
self-referentiality is now a regular feature — every transcript
extension after publish triggers a re-publish, and the script's
idempotency contract handles the round-trip cleanly.

---

## Cut state (end of session)

| | |
| --- | --- |
| flotilla | Unchanged. v1.0.1 installed; deployment registered; 3 new `deploys` rows added this session (`id=12 failed`, `id=13 failed`, `id=14 succeeded` — all v0.14.0). |
| rfc-app | `de28272` tag `v0.14.0`. Pushed to `origin = git.wiggleverse.org/ben.stull/rfc-app` AND `benstull = git.benstull.org/benstull/rfc-app`. `VERSION = 0.14.0`, `frontend/package.json#version = 0.14.0`. `CHANGELOG.md` has a new top entry. CLAUDE.md Session-protocol pointer **still deferred** — Session 0009.0 also deferred this. |
| ohm-rfc | `4c89179` (`.rfc-app-version` bumped to `0.14.0` via `d6af91f`; ROADMAP edited via `4c89179` with new row #15 struck through and row #12's target version bumped v0.14.0 → v0.16.0). |
| OHM live | `deploys.id=14`, v0.14.0, healthy. `/api/health` returns `{"version":"0.14.0","status":"ok"}`. `/docs` reachable anonymously and serves the new user guide. |
| ohm-infra | New transcript: `SESSION-0010.0-TRANSCRIPT-2026-05-27T23-50--2026-05-28T03-20.md`. No other changes to scripts or protocol. |
| ohm-session-history | This transcript first published at `44995c9` (add); Arc-20 update will re-publish as an "update SESSION-J-…" commit when the operator runs `publish-transcript.sh` again (idempotent — diff push, not a duplicate add). Adds one new file alongside Sessions A–I. |
| Spawned task | One chip showing for the operator: "Add VM-origin preflight to flotilla deploy" — cwd-rooted in the flotilla repo. |

| This session's ledger | Status |
| --- | --- |
| Docs feature shipped | ✅ rfc-app v0.14.0 + OHM `deploys.id=14`. Not a roadmap item; operator-driven from the admin-vs-owner UX question. |
| ROADMAP collision | v0.14.0 slot was reserved for #12 Owner-only invite; that row's target version bumped to v0.16.0 in this session's ROADMAP edit. |
| CLAUDE.md update | Deferred. (Same as Session 0009.0.) |

---

## §19.2 candidates surfaced

1. **`git stash push -u` + `reset --hard main` + `stash pop` is a
   trap for ghost-revert recovery.** When a working tree's
   "modifications" are actually "files held an earlier version's
   bytes after `git update-ref` advanced HEAD," the stash captures
   the full ghost-revert diff and the pop replays it. End state
   identical to start state. **What works**: `git restore .` to wipe
   all tracked modifications (untracked files survive), then manual
   re-application of any genuine edits. Worth recording in
   `SESSION-PROTOCOL.md` or `CLAUDE.md` as a recipe — future driver
   sessions hitting the same shape (a parallel-session
   `update-ref`-advanced HEAD against a working tree that lagged)
   will reach for the same wrong gesture otherwise.

2. **flotilla preflight should verify the VM's actual `origin` URL
   has the target tag.** The pin source (named in
   `deployment show`) and the VM's `git remote get-url origin` can
   diverge silently. When they do, the deploy fails at phase 3 with
   "pathspec did not match" — uninformative. Already spawned as a
   follow-up session task with full context. The alternative is
   GCP-side Gitea mirror config; the flotilla preflight is
   defense-in-depth.

3. **`SESSION-PROTOCOL.md` could note the "claim the next free
   sequential slot" tradeoff for non-roadmap-driven releases.** This
   session shipped v0.14.0 out-of-band, colliding with #12's
   reservation. The roadmap edit at end-of-session captured the
   collision and shifted #12 → v0.16.0. The discipline question: is
   "claim sequential slot, document the bump" the canonical move, or
   should out-of-band releases claim a *post-roadmap* slot (e.g.
   v0.99.0) to avoid shuffling reserved slots? Either is workable;
   the protocol document doesn't currently take a position.

4. **`rfc-app/CLAUDE.md` Session-protocol section is now deferred
   twice.** Session 0009.0 deferred it because the working tree was
   unstable. Session 0010.0 deferred it because the deploy was the
   priority and the tree only stabilized late. The third
   rfc-app-touching session should make the addition non-deferrable
   (e.g. fail the end-of-session checklist if rfc-app's CLAUDE.md
   doesn't carry the Session-protocol pointer).

5. **The flotilla `deployment show` output should surface the VM's
   actual origin URL, not just the pin source URL.** Even with the
   preflight from candidate #2 above, a quick `deployment show`
   visual scan should make the divergence (if any) visible without
   having to SSH in. Cosmetic surface change; small.

---

## What lands on the operator's plate

1. **Follow-up task chip**: "Add VM-origin preflight to flotilla
   deploy." One click spins it up in a fresh flotilla worktree.
   Dismiss if you'd prefer to configure Gitea mirroring instead.

2. **rfc-app `CLAUDE.md` Session-protocol section** is still
   missing. Either add it manually now (one-paragraph pointer to
   `~/git/ohm-infra/SESSION-PROTOCOL.md`, matching
   `flotilla/CLAUDE.md` shape) or assign it to the next
   rfc-app-touching session.

3. **Two failed `deploys` rows** (`id=12`, `id=13`) sit in the
   flotilla history. Audit-trail only; not actionable. The same
   info is captured in this transcript so the rows can stay or be
   pruned at operator discretion.

4. **Local git committer identity** is still
   `Ben Stull <benstull@mac.lan>` per the auto-generated
   username+hostname; memory says canonical is
   `ben@wiggleverse.org`. Surfaced; not fixed in-session.

5. **#12 Owner-only invite target version moved** from v0.14.0 to
   v0.16.0. When that item's session runs (Wave 5 per the existing
   plan), the dispatched subagent should be given the updated
   target.

6. **Roadmap Wave 4 was Session-I's next focus** but did not run
   this session (this session was operator-driven, not
   roadmap-driven). Wave 4 lineup unchanged from Session-I's
   closing prompt: #7 (v0.9.0), #9 (v0.11.0), #10 (v0.12.0).

---

## Prompt the operator can paste into the next Claude Code session

```
You are the OHM roadmap driver. Two sessions just closed in
parallel: Session 0009.0 shipped rfc-app v0.4.0 / v0.5.0 / v0.6.0 /
v0.7.0 / v0.8.0 / v0.10.0 / v0.13.0 to OHM, and Session 0010.0 shipped
rfc-app v0.14.0 (DOCS.md + /docs public user guide; not a
roadmap item — operator-driven from a UX question) to OHM
(deploys.id=14). OHM is currently serving v0.14.0. Read
~/git/ohm-infra/SESSION-PROTOCOL.md at session start for the
binding end-of-session discipline.

Read /Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md
end-to-end and execute Wave 4 per the "Operating instructions
for the next session" section. The Wave 4 lineup per the
roadmap text:
- Session κ (Track C1): #7 Admin user-management page → v0.9.0
- Session λ (Track C2): #9 Device trust 30d → v0.11.0
- Session μ (Track C): #10 CloudFlare verification → v0.12.0
  (CAUTION: requires operator-provided CLOUDFLARE_TURNSTILE_SECRET.
  Subagent CHANGELOG MUST step pauses the wave; do not invent the
  secret. Operator must `flotilla secret set ohm-rfc-app
  CLOUDFLARE_TURNSTILE_SECRET` before the v0.12.0 deploy.)

Same constraints as Sessions H + I: dispatch parallelizable items
as forked subagents in a single message, serialize the deploys,
verify each, strikethrough the version-target table when an item
ships, commit + push ohm-rfc, write the session transcript per
~/git/ohm-infra/SESSION-PROTOCOL.md at end-of-session. Stop and
wait for the operator if a deploy fails, a release's Upgrade
steps require an operator-provided secret, or you encounter
cross-repo ambiguity.

Session-I + Session-J lessons to apply automatically:

- **VM origin vs pin source divergence (Session 0010.0)**: after pushing
  a rfc-app tag to `origin = git.wiggleverse.org`, ALSO push to
  `benstull = git.benstull.org`. The production VM at /opt/ohm-app
  fetches from git.benstull.org, not git.wiggleverse.org; the two
  Gitea hosts are linked but not real-time mirrored. Missing this
  causes phase-3 "pathspec did not match" failures. A follow-up
  task to add a flotilla preflight check is in flight; until it
  lands, the two-push discipline is the workaround:
    git push origin main && git push origin vX.Y.Z
    git push benstull main && git push benstull vX.Y.Z

- **Ghost-revert recovery (Session 0010.0)**: if you find a working tree
  whose `git status` shows 20+ files as staged-deletions/
  modifications that don't match your session's edits, the cause
  is a prior session's `git update-ref refs/heads/main` advancing
  HEAD past a working tree that held older bytes. DO NOT use
  `git stash push -u` + `git reset --hard main` + `git stash pop`
  — the stash captures the ghost-revert and the pop replays it.
  Instead: `git restore .` to wipe tracked modifications
  (untracked files survive), then manually re-apply any genuine
  edits against the current HEAD's file structure.

- **Subagents push feature branches only (Session 0009.0)**. Driver
  tags, bumps the ohm-rfc pin, and runs the deploy. Do NOT have
  subagents bump the pin themselves.

- **Cross-repo subagents** must create their own `git worktree add`
  in the target repo. The `Agent` tool's `isolation:` flag applies
  only to the driver's CWD.

- **Migration numbers**: pre-allocate slots per subagent in the
  prompt. Session 0009.0 used 014 for #6 and 015 for #8. Next free is
  016 — check `ls /Users/benstull/git/rfc-app/backend/migrations/`
  before dispatching to confirm.

- **Phase-7 SSH timeout (60s)** can fire while uvicorn is still in
  SIGTERM-grace waiting for SSE clients. If the deploy logs
  `failed` or `in_progress` at phase 7, check `/api/health` first
  — the service likely restarted fine. If so, `flotilla deploy
  abort` to clear the in-flight row.

- **CHANGELOG.md must be strictly version-descending.** If a
  subagent inserts at the top out of order, fix it during
  integration.

- **Item #12 Owner-only invite** has been re-targeted from v0.14.0
  to v0.16.0 per Session 0010.0's docs-release slot collision. Update
  any upgrade-from anchors accordingly.

- **Item #1 VM rename** and operator-provided secrets stay
  operator territory unless the operator explicitly clears them.

- **rfc-app/CLAUDE.md Session-protocol section** is still missing
  (Sessions I + J both deferred it). If this session's first
  rfc-app gesture has a clean working tree, add it before doing
  anything else, matching flotilla/CLAUDE.md's shape.
```
