ad01c0854a
Restructures the flat repo into one folder per session, with a top-level README.md (the "about sessions" page, also rendered at /docs/sessions/about by rfc-app v0.19.0+) and a sessions.json title manifest keyed by 4-digit NNNN. Filenames inside each folder retain the full SESSION-NNNN.M-TRANSCRIPT-… form (no trim of the SESSION- prefix). Legacy letter-form transcripts, if any are still at the root, are not moved — the folder convention applies to the numeric form only. Session 0017.0; see SESSION-PROTOCOL.md §1 amendment for the binding shape.
914 lines
44 KiB
Markdown
914 lines
44 KiB
Markdown
# Session H — Transcript
|
||
|
||
> Date: 2026-05-27
|
||
> Goal: Execute Slice 5 of the `ohm-rfc-app-flotilla` SPEC §20 slicing
|
||
> plan — hardening + first real upgrade + operator guide. Per §20.5:
|
||
> drive a real rfc-app version bump through `flotilla pin check` →
|
||
> `flotilla deploy`; tighten failure paths discovered in Slice 4; lock
|
||
> down `--json` on every read verb; ship a single-page operator guide;
|
||
> add a bring-up replay CI test. Exit criterion: a real rfc-app upgrade
|
||
> lands on OHM via flotilla with no manual SSH steps; the operator
|
||
> guide is sufficient for laptop re-provisioning. Ships as v1.0.0.
|
||
>
|
||
> Outcome: **flotilla v1.0.0 shipped end-to-end in-session.** The
|
||
> §20.5 exit criterion was met live: `flotilla deploy ohm-rfc-app`
|
||
> drove the rfc-app v0.2.3 → v0.3.0 upgrade against the live OHM VM,
|
||
> all 9 phases green, `/api/health` verified at v0.3.0 in 2.5 seconds.
|
||
> 156 tests passing (132 baseline → 156, +24 across hardening,
|
||
> `secret set`, bring-up replay). Three repos updated in lockstep:
|
||
> rfc-app v0.3.0 tagged + pushed; ohm-rfc pin bumped to 0.3.0; flotilla
|
||
> v1.0.0 committed + tagged + pushed.
|
||
>
|
||
> Significant additions beyond the §20.5 brief:
|
||
>
|
||
> 1. **`flotilla secret set` verb** (§19.3 rule-2 correction to SPEC
|
||
> §7.5 / §7.6). The Slice-5 live bootstrap of GCP Secret Manager
|
||
> surfaced enough manual-gcloud friction (six `secrets create`
|
||
> calls, ADC re-auth, the `${!var}` zsh-history quirk, CRLF source
|
||
> breakage, `OAUTH_CLIENT_SECRET` lost-from-shell, IAM removal
|
||
> with wrong email) that the v1 read-only-flotilla stance from §7.6
|
||
> became untenable for ongoing iteration. `secret set` reads bytes
|
||
> from stdin, creates the SM entry (or adds a version), and binds
|
||
> at `@latest` in one command. Bytes never enter argv or shell
|
||
> history. IAM widens from `secretmanager.secretAccessor` to
|
||
> `secretmanager.admin` (or `secretCreator + secretVersionAdder`)
|
||
> only when using the verb; the bind-only path still only needs
|
||
> accessor.
|
||
>
|
||
> 2. **§19.2 candidate: framework-side required-env manifest.** The
|
||
> user's "how do new features just work?" question surfaced the gap
|
||
> between rfc-app's `Upgrade steps:` CHANGELOG prose and what
|
||
> flotilla can pre-validate. A new §19.2 entry proposes a fifth
|
||
> versioned contract between flotilla and rfc-app — a machine-
|
||
> readable manifest of required env vars per release — so the
|
||
> deploy gesture can refuse to start with missing keys, naming them.
|
||
> Not built in v1.0.0; documented so the option is ready when a
|
||
> forgotten-secret incident surfaces it.
|
||
>
|
||
> 3. **rfc-app v0.3.0 released** (private-beta gate + anonymous read
|
||
> mode + iOS-Safari scroll fix). The 0.3.0 WIP was already authored
|
||
> in the working tree; the scroll fix (`.app: 100vh → 100dvh`) was
|
||
> added to the same release. Diagnosis: iOS Safari measures `100vh`
|
||
> as the URL-bar-hidden viewport, so the `.app` overflows what's
|
||
> visible; combined with `body { overflow: hidden }`, single-finger
|
||
> touches were consumed by the (blocked) page-level scroll attempt
|
||
> and never reached the nested `.chrome-pane` scroll. Two-finger
|
||
> touches bypassed the page-level layer. Switching to `100dvh`
|
||
> (with `100vh` fallback) fixes the trap on iOS 15.4+ / Chrome 108+.
|
||
>
|
||
> 4. **OHM roadmap committed** to `ohm-rfc/ROADMAP.md`. Eleven feature
|
||
> items from the user spanning auth migration, collaboration model,
|
||
> compliance, and instrumentation; one item per session/version
|
||
> discipline. Plus a twelfth: publicly post the session transcripts.
|
||
>
|
||
> Five findings worth flagging permanently:
|
||
>
|
||
> 1. **`100vh` on iOS Safari is a viewport-trap when combined with
|
||
> `body { overflow: hidden }`.** The pattern produces a "two-finger
|
||
> scroll to start, one-finger thereafter" symptom that's hard to
|
||
> diagnose without knowing the URL-bar interaction. Fix is
|
||
> `100dvh` with `100vh` fallback. Any future flex-column app-shell
|
||
> in the rfc-app family should default to `100dvh`.
|
||
>
|
||
> 2. **iOS scrollable containers need the dynamic viewport.** Same
|
||
> finding generalized — every `.chrome-pane`-style nested scroll
|
||
> container in the rfc-app family inherits its constrained height
|
||
> from the `.app` root. If that root uses `100vh`, every nested
|
||
> scroll on iOS gets the trap. The framework's other chrome views
|
||
> (`/admin/*`, `/settings/notifications`) were also affected and
|
||
> fixed by the same change.
|
||
>
|
||
> 3. **`gcloud auth login` is not `gcloud auth application-default
|
||
> login`.** Operators with working `gcloud compute ssh` may still
|
||
> have no ADC, causing flotilla's Secret Manager reads to fail with
|
||
> "no Application Default Credentials available." The browser
|
||
> consent screen for ADC must also have **every scope checkbox
|
||
> selected** — ADC issued without the required scopes will
|
||
> authenticate but be rejected by the Secret Manager API.
|
||
> Documented in `docs/secret-bootstrap.md` §1 and §6 pre-flight.
|
||
>
|
||
4. **GCP IAM principal `ben.stull@wiggleverse.org` differs from
|
||
> Wiggleverse email `ben@wiggleverse.org`.** A `remove-iam-policy-
|
||
> binding` with the latter silently fails with "Policy binding with
|
||
> the specified principal, role, and condition not found." The
|
||
> `gcloud config get-value account` value is canonical for IAM
|
||
> commands. `docs/secret-bootstrap.md` now uses
|
||
> `$OPERATOR_GCP_EMAIL` derived from that, not a hard-coded
|
||
> address.
|
||
>
|
||
> 5. **zsh's history-substitution clashes with bash indirect
|
||
> expansion.** `${!var}` triggers `event not found: var` in zsh
|
||
> rather than expanding indirectly. Bash idioms in the bootstrap
|
||
> doc are now wrapped in `bash <<'BASH' … BASH` to sidestep this.
|
||
> Also surfaced: `source` on a CRLF-line-ending `.env` produces
|
||
> spurious `command not found: ^M` errors; the doc now leads with
|
||
> a `tr -d '\r'` normalization step.
|
||
>
|
||
> Two §19.3 rule-2 spec corrections in this session:
|
||
> - §7.5: `secret set` added as the fourth verb.
|
||
> - §7.6: narrowed (rotation ceremonies + multi-version policy still
|
||
> deferred; bytes-level creation now in v1).
|
||
> - §11.2: IAM widening noted for `secret set`.
|
||
> - §12.1: verb inventory updated.
|
||
> - §19.2: new candidate "framework-side required-env manifest."
|
||
|
||
---
|
||
|
||
## Pre-session state
|
||
|
||
- flotilla repo on `main`, last commit `491e72e Release 0.3.0:
|
||
Slice 3 — pin reading + dry-run deploy`.
|
||
- Slice 4 code lived in the working tree, uncommitted: `deploy.py`,
|
||
`ssh.py`, `health.py`, `deploy_log.py`, migration 004, plus the
|
||
CLI extensions and 58 new tests. `VERSION` was bumped to 0.4.0;
|
||
`CHANGELOG.md` had the 0.4.0 entry written. Per Session G's
|
||
outcome, the v0.4.0 cut was gated on the first real live deploy —
|
||
which had not yet happened.
|
||
- rfc-app at v0.2.3 (latest tag); `/Users/benstull/git/rfc-app` had a
|
||
large in-progress 0.3.0 release authored but uncommitted — private-
|
||
beta gate, allowlist table, `BetaPending.jsx`, nginx config rename,
|
||
admin allowlist UI, anonymous read mode.
|
||
- ohm-rfc `.rfc-app-version` committed at `0.3.0` — but rfc-app had
|
||
no `v0.3.0` tag. Pin was ahead of reality (Session G had not done a
|
||
live deploy, so the spec assertion in §13.3 that current pin is
|
||
0.3.0 was aspirational).
|
||
- OHM VM `ohm-app` in `wiggleverse-ohm` running rfc-app **v0.2.2**
|
||
(the last hand-deployed version). `/api/health` returned 404 —
|
||
expected, because v0.2.2 predates the route's introduction in
|
||
v0.2.3.
|
||
- GCP Secret Manager API on `wiggleverse-ohm`: not enabled. Zero
|
||
`ohm-rfc-app-*` secrets in the project. The Slice 2 bootstrap had
|
||
never been performed; the v0.2.0 CHANGELOG entry was still "pending
|
||
operator gesture."
|
||
|
||
## Turn-by-turn arc
|
||
|
||
The session moves through four arcs, in order:
|
||
|
||
### Arc 1 — Slice 5 hardening (code-only, in-flotilla)
|
||
|
||
Started by reading every Slice 4 file end-to-end (`deploy.py`,
|
||
`ssh.py`, `health.py`, `deploy_log.py`, `cli.py`, the migration, all
|
||
of `test_*.py`). Surfaced these hardening targets:
|
||
|
||
- **§3 invariant 1 hole** in `deploy.py`: `all_secret_bytes =
|
||
resolved.all_bytes()` produced a `list[bytes]` snapshot held on the
|
||
stack across all phases. The `bytes` objects were immutable copies,
|
||
so `resolved.scrub()` zeroing the underlying bytearrays did not
|
||
zero them. Bytes survived on the stack until function return. Fix:
|
||
replace the snapshot with `_ResolvedSecrets.redact_live(text)` that
|
||
builds the needle list lazily from the live bytearrays at the point
|
||
of failure. After the call returns and `scrub()` runs, no copies
|
||
remain.
|
||
- **`deploy status` silently exits 0 on unhealthy responses.** A 404
|
||
from `/api/health` (the exact OHM-current state) produced
|
||
`HTTP 404 version=None status=None` and a zero exit. Fix: classify
|
||
by HTTP status — 4xx is `endpoint not reachable (framework
|
||
pre-0.2.3? proxy mis-routed?)` and exits 1; 5xx is server-side and
|
||
exits 1; 2xx without a parseable body is malformed and exits 1.
|
||
Same exit-code discipline under `--json` so scripts can pipe to
|
||
`jq` and still branch on `$?`.
|
||
- **`pin check` used `list_deploys(limit=50)` + Python filter.** If
|
||
50 failures intervened, the actual last-succeeded would silently
|
||
drop out of the window. Fix: new `deploy_log.last_succeeded()` —
|
||
a direct `WHERE outcome='succeeded' ORDER BY id DESC LIMIT 1`
|
||
query. Test pins this behavior past 60 intervening failures.
|
||
- **`resolve_operator_account` ignored returncode.** Used `proc.
|
||
stdout` even when gcloud returned non-zero or when gcloud emitted
|
||
the literal `(unset)`. Fix: bail to `"unknown"` on non-zero or
|
||
`(unset)`.
|
||
- **`--json` gaps**: `pin check` lacked it; `deploy watch` lacked it.
|
||
Added: `pin check --json` emits a structured comparison (pin,
|
||
status: matches/ahead/no_successful_deploys, last_succeeded_deploy
|
||
row); `deploy watch --json` emits NDJSON, one JSON object per
|
||
reading, suitable for `… | jq -c`.
|
||
- **`deploy log --limit=0/negative`** wasn't guarded. Tiny — added an
|
||
explicit refuse-with-message.
|
||
- Minor: removed unused `field` import in `deploy.py`.
|
||
|
||
Tests for each fix landed in `test_deploy.py`, `test_deploy_log.py`,
|
||
`test_cli_deploy.py`. 132 tests → 146 tests after hardening.
|
||
|
||
### Arc 2 — Operator guide + bring-up replay CI test
|
||
|
||
Two §20.5 deliverables that complete the v1.0.0 documentation surface:
|
||
|
||
- **`docs/operator-guide.md`** — single-page bring-up + day-to-day
|
||
reference. Sections: prerequisites; clone + install; gcloud
|
||
authentication (with the ADC distinction called out); deployment
|
||
registration (pointing at `scripts/register-ohm.sh`); secret
|
||
binding; first-deploy dry-run; the real deploy gesture; verify;
|
||
watch; recovery patterns; where state lives; JSON output inventory.
|
||
Sufficient to bring flotilla up on a new laptop without reading
|
||
the SPEC. The §20.5 exit criterion's "operator-guide-sufficient"
|
||
bar.
|
||
|
||
- **`tests/test_bringup_replay.py`** — one large test that walks the
|
||
operator guide's exact verb sequence against stubbed GCP/SSH/
|
||
network. Every SPEC §12.1 verb is exercised; a coverage-assertion
|
||
set at the end catches new verbs that land without being added to
|
||
the guide. Doc/code drift → CI failure.
|
||
|
||
146 tests → 147 (the replay test is a single test that exercises
|
||
many verbs).
|
||
|
||
### Arc 3 — Live Slice 2 bootstrap + first deploy (where the friction was)
|
||
|
||
Initial plan: cut v1.0.0 immediately and deploy. Reality: every step
|
||
of the path had latent friction that had to be surfaced and fixed.
|
||
This arc is the §19.3 rule-2 fuel for the whole session.
|
||
|
||
#### 3a. Pin bump attempt fails because of repository-side state
|
||
|
||
The user attempted the gesture from `ohm-rfc`:
|
||
```
|
||
echo 0.4.0 > .rfc-app-version && git commit && git push
|
||
```
|
||
which failed with `nothing to commit` — `git commit` without `-a`
|
||
or prior `git add` doesn't stage modifications. Fixed in the
|
||
operator guide §2.1 to show `git commit -am "..."`.
|
||
|
||
But also: `0.4.0` is not a real rfc-app tag. Latest rfc-app tag is
|
||
`v0.2.3`. Pinning to `0.4.0` would have hit phase 3 (`git checkout
|
||
v0.4.0`) failure on the VM. The user picked "pin to 0.2.3 (existing
|
||
tag)" — first real deploy is the upgrade from VM's current v0.2.2 to
|
||
the framework's actual latest v0.2.3.
|
||
|
||
#### 3b. Flotilla state was empty on the user's laptop
|
||
|
||
`ohm-rfc-app-flotilla deployment list` showed `(no deployments
|
||
registered)`. The SQLite at `~/.ohm-rfc-app-flotilla/ohm-rfc-app-flotilla.db`
|
||
existed but had no rows — Slice 1's `scripts/register-ohm.sh` had not
|
||
been run on this laptop. Ran it, deployment registered.
|
||
|
||
Then `register-ohm.sh` failed with `ohm-rfc-app-flotilla: command not
|
||
found` — the script calls the bare CLI name but the user hadn't
|
||
activated the venv. Two fixes documented: `source .venv/bin/activate`
|
||
first, OR `PATH=.venv/bin:$PATH ./scripts/register-ohm.sh`. The user
|
||
chose activate.
|
||
|
||
#### 3c. /api/health was 404 — diagnosis
|
||
|
||
The OHM VM serves `{"detail":"Not Found"}` at `/api/health`. That's
|
||
FastAPI's default 404 body — the framework IS running, the route just
|
||
doesn't exist. Strong hypothesis: deployed code is pre-0.2.3 (which
|
||
is when the route was introduced).
|
||
|
||
Confirmed via `gcloud compute ssh` + `git describe`: VM was on
|
||
`v0.2.2` (`018e323 Release 0.2.2: Philosophy.jsx now renders mermaid`).
|
||
One version below the `/api/health` introduction. The deploy itself
|
||
will fix this.
|
||
|
||
The user also tried `gcloud config set project ohm-rfc-app` and got
|
||
a permission warning — `ohm-rfc-app` is the flotilla **deployment
|
||
name**, not a GCP project. Three different names share the prefix:
|
||
the GCP project is `wiggleverse-ohm`, the VM is `ohm-app`, the
|
||
deployment-record name is `ohm-rfc-app`. The operator guide table now
|
||
makes this distinction explicit.
|
||
|
||
#### 3d. Slice 2 bootstrap (Secret Manager) — the friction tour
|
||
|
||
Pre-flight check `gcloud secrets list --project=wiggleverse-ohm
|
||
--filter='name:ohm-rfc-app'` returned `API [secretmanager.googleapis.com]
|
||
not enabled`. The Slice 2 bootstrap had never been done. Updated the
|
||
runbook in-session as each friction point surfaced:
|
||
|
||
1. **`gcloud services enable secretmanager.googleapis.com`** —
|
||
pre-requisite, done.
|
||
2. **IAM grant** — `gcloud projects add-iam-policy-binding
|
||
wiggleverse-ohm --member='user:ben@wiggleverse.org'
|
||
--role='roles/secretmanager.admin'`. The user's wiggleverse email
|
||
`ben@wiggleverse.org` was used. Later turned out the GCP-side
|
||
principal is actually `ben.stull@wiggleverse.org` — but `add` is
|
||
idempotent and tolerated the typo by creating a new binding for
|
||
the (nonexistent-as-IAM-principal) `ben@wiggleverse.org`. The
|
||
`remove` at the end is where it bit us; see 3i.
|
||
3. **Pull the live `.env`** — `gcloud compute ssh ohm-app …
|
||
'sudo cat /opt/ohm-app/backend/.env' > /tmp/ohm-rfc-app.env`. 1135
|
||
bytes, 39 lines, chmod 600.
|
||
4. **`source` errors with CRLF**. First attempt at `source /tmp/ohm-
|
||
rfc-app.env` produced `command not found: ^M` and `command not
|
||
found: gto_oge…^M`. The `.env` on the VM had Windows line endings.
|
||
`sudo cat` preserved them. zsh's `source` parsed each `\r` as a
|
||
syntax error on otherwise-blank lines; on lines where a value
|
||
wrapped to the next visual row, the wrapped portion was
|
||
interpreted as a standalone command.
|
||
Fix: `tr -d '\r' < /tmp/ohm-rfc-app.env > /tmp/…unix && mv …`.
|
||
Verified with `file /tmp/ohm-rfc-app.env` → ASCII text (no "with
|
||
CRLF").
|
||
5. **Heads-up to the user**: the `^M`-broken `source` had echoed the
|
||
start of `GITEA_BOT_TOKEN` into the terminal scrollback. Worth a
|
||
`clear` + scrollback-clear at end-of-bootstrap.
|
||
6. **Spot-check after re-source**: `GITEA_BOT_TOKEN` length 40,
|
||
`SECRET_KEY` length 65 — both populated, both confidence-passable
|
||
without printing the actual bytes.
|
||
7. **zsh history-substitution on `${!key}`**. The overlay-reconcile
|
||
loop uses bash indirect expansion `${!key:-}`. zsh interprets the
|
||
`!` as history substitution and fails with `event not found: key`.
|
||
Fix: wrap the loops in `bash <<'BASH' … BASH` to run them under
|
||
bash regardless of the operator's interactive shell. Doc updated.
|
||
8. **OAUTH_CLIENT_SECRET missing in shell.** After re-sourcing,
|
||
`length=0` for OAUTH_CLIENT_SECRET despite being present in the
|
||
file. Likely cause: the value's shape conflicted with bash's
|
||
`source` parser (special characters or multi-line that python-
|
||
dotenv handles but `source` does not). Fix: a manual export
|
||
fallback documented in the runbook —
|
||
`export OAUTH_CLIENT_SECRET="$(grep '^OAUTH_CLIENT_SECRET='
|
||
/tmp/ohm-rfc-app.env | sed 's/^OAUTH_CLIENT_SECRET=//' |
|
||
sed 's/^"//; s/"$//')"`.
|
||
9. **SMTP_PASSWORD + WEBHOOK_EMAIL_BOUNCE_SECRET both blank**.
|
||
OHM-current reality: Gmail SMTP relay is IP-allowlisted and needs
|
||
no password; the inbound bounce webhook isn't wired up yet. The
|
||
user picked "mirror reality, skip both." The §13.7 list is
|
||
*intended*; what's actually bootstrapped at any time is the subset
|
||
with live values. Doc records this with an explanatory note —
|
||
future bind happens out-of-band when those keys earn real values.
|
||
10. **The bind step failed** with `no Application Default Credentials
|
||
available — run 'gcloud auth application-default login'`. The user
|
||
had done `gcloud auth login` (since SSH worked) but not the ADC
|
||
variant. ADC is a separate credential set used by SDKs like
|
||
google-cloud-secret-manager. Fix: run ADC login; the doc's §1
|
||
now flags this distinction, and §6 adds a pre-flight
|
||
`gcloud auth application-default print-access-token >/dev/null
|
||
&& echo "ADC ok" || gcloud auth application-default login`.
|
||
11. **Permission-scope nag during ADC**. The browser consent screen
|
||
for ADC offers a list of permission scopes. Operators sometimes
|
||
click through without selecting all of them — ADC issued without
|
||
Secret Manager scope authenticates but the API rejects reads.
|
||
Documented in §1 and §6.
|
||
12. **The bind still failed** — "secret not readable: secret not
|
||
found." The Secret Manager entries themselves hadn't been
|
||
created. The bootstrap had only done the IAM grant + ADC; the
|
||
`make_secret` loop hadn't run yet. Ran it.
|
||
13. **All four bindings succeeded** — `bound GITEA_BOT_TOKEN →
|
||
wiggleverse-ohm/ohm-rfc-app-gitea-bot-token@latest` (plus three
|
||
others). `flotilla secret list ohm-rfc-app` showed four rows, no
|
||
bytes.
|
||
14. **Cleanup**: `shred -u /tmp/ohm-rfc-app.env`,
|
||
`unset GITEA_BOT_TOKEN OAUTH_CLIENT_SECRET SECRET_KEY
|
||
GITEA_WEBHOOK_SECRET …`.
|
||
|
||
#### 3i. IAM downgrade — wrong email surfaces
|
||
|
||
`gcloud projects remove-iam-policy-binding wiggleverse-ohm
|
||
--member='user:ben@wiggleverse.org' --role='roles/secretmanager.admin'`
|
||
failed: "Policy binding with the specified principal, role, and
|
||
condition not found." The policy listing showed the actual principal
|
||
was `user:ben.stull@wiggleverse.org` (with the dot). Two follow-ups:
|
||
|
||
- The doc now uses `$OPERATOR_GCP_EMAIL` derived from
|
||
`gcloud config get-value account`, not a hard-coded address.
|
||
- The policy listing also showed the user has `roles/owner` on the
|
||
project. Owner trumps every IAM role — both `secretmanager.admin`
|
||
and `secretmanager.secretAccessor` are functionally redundant for
|
||
this operator. The doc now flags the owner-shortcut: "if you
|
||
already hold `roles/owner`, the IAM dance is cosmetic — `add` /
|
||
`remove` is hygiene for an operator without owner."
|
||
|
||
#### 3j. Dry-run then real deploy
|
||
|
||
`ohm-rfc-app-flotilla deploy ohm-rfc-app --dry-run` rendered the
|
||
9-phase plan with target version 0.2.3, 14 overlay keys, 4 secret
|
||
refs (the two blanks correctly absent), no bytes leaked. Clean.
|
||
|
||
`ohm-rfc-app-flotilla deploy ohm-rfc-app` then executed all 9 phases:
|
||
|
||
```
|
||
opened deploys row id=1; target v0.2.3 (snapshot …)
|
||
[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.2.3 (verify took …)
|
||
```
|
||
|
||
The first real flotilla deploy ever, and it just worked. `deploy
|
||
status` confirmed `HTTP 200 version=0.2.3 status=ok`. `/api/health`
|
||
on the live URL returned `{"version":"0.2.3","status":"ok"}`. Slice
|
||
2 + Slice 3 + Slice 4 + Slice 5 all validated in a single gesture.
|
||
|
||
### Arc 4 — `secret set` addition, v1.0.0 cut, second live deploy, roadmap
|
||
|
||
#### 4a. The user's "how do new features just work?" question
|
||
|
||
After the deploy succeeded, the user asked: "for ongoing development,
|
||
I want all of this to just work. New feature → deploy instructions →
|
||
flotilla call to set overlay/secrets → deploy. Anything that won't?"
|
||
|
||
The honest answer: the per-thing gestures are one-liners (`overlay
|
||
set` already is, `secret set` would be once added), so a release's
|
||
deploy instructions become a short script the operator copy-pastes.
|
||
SPEC §3 invariant 4 already names this — `Upgrade steps:` in rfc-app's
|
||
CHANGELOG.
|
||
|
||
But: flotilla can't pre-validate that the operator did the prep. If
|
||
rfc-app v0.4.0 requires a new env var and the operator forgets, phase
|
||
7 or 8 will fail after the `.env` was written. Fail-stop preserves
|
||
the previous serving version, but the debug happens post-write.
|
||
|
||
Three options offered:
|
||
1. Add as §19.2 candidate, ship v1.0.0.
|
||
2. Sketch the manifest now, defer the rfc-app side.
|
||
3. Build the contract end-to-end now.
|
||
|
||
User picked (1). New §19.2 candidate added: "framework-side required-
|
||
env manifest" — sketches the fifth versioned contract between
|
||
flotilla and rfc-app (a `deploy/required-env.json`, a `GET /api/
|
||
required-env`, or machine-parseable upgrade-steps schema) where
|
||
flotilla refuses to deploy with missing keys, naming them. Earns its
|
||
session when a forgotten-secret incident actually happens.
|
||
|
||
#### 4b. `flotilla secret set` design + implementation
|
||
|
||
User: "I expect to be adding lots of secrets (and config overlays)
|
||
as we iterate." That tipped the design from "one-shot bootstrap
|
||
script" to "single-key incremental verb."
|
||
|
||
Design:
|
||
```
|
||
echo "$VALUE" | flotilla secret set <deployment> <ENV_KEY>
|
||
```
|
||
- Reads bytes from stdin (never argv → never shell history)
|
||
- Computes `secret_id` from §7.2 convention (`<deployment>-<env_key>`,
|
||
lowercased, underscores→dashes)
|
||
- Creates the SM entry if needed, or adds a new version if it exists
|
||
(rotation works for free)
|
||
- Binds in flotilla at `@latest`
|
||
- Project defaults to deployment's `target_vm_project`; `--project`
|
||
flag overrides
|
||
|
||
Implementation:
|
||
- `secrets.create_secret_if_absent(project, secret_id) -> bool` —
|
||
returns True if newly created, False if AlreadyExists. Both are
|
||
success; caller proceeds to add a version unconditionally.
|
||
- `secrets.add_secret_version(project, secret_id, payload) -> str`
|
||
— returns version number. Bytes argument so caller reads raw
|
||
stdin.
|
||
- `secrets.derive_secret_id(deployment_name, env_key) -> str` — the
|
||
§7.2 convention as pure function.
|
||
- `secrets.SecretWriteError` — sibling to SecretReadError, same
|
||
auth/permission/API failure shape but for writes.
|
||
- CLI verb at `secret set <deployment> <ENV_KEY>` with `--project`
|
||
and `--secret-id` overrides.
|
||
|
||
Tests: derive_secret_id unit tests, FakeSM helper class for CLI-level
|
||
integration tests (create + update + empty stdin + unknown deployment
|
||
+ permission denied + project override). +9 tests. Added to
|
||
`tests/test_bringup_replay.py` so the operator-guide gesture stays
|
||
exercised end-to-end. 147 → 156.
|
||
|
||
SPEC corrections (§19.3 rule 2):
|
||
- §7.5 → four verbs (was three).
|
||
- §7.6 narrowed: rotation ceremonies + multi-version policy still
|
||
§19.2, bytes-level creation now in v1.
|
||
- §11.2 IAM widening note.
|
||
- §12.1 verb inventory.
|
||
|
||
Operator guide §1.4 rewritten with two flows: bind for existing
|
||
entries (steady-state, accessor IAM), set for new ones (incremental,
|
||
admin IAM). secret-bootstrap.md gets a header note pointing at
|
||
`secret set` as the preferred ongoing path; manual gcloud dance
|
||
preserved for the initial-migration case where partitioning a live
|
||
`.env` is still a one-time gesture.
|
||
|
||
#### 4c. rfc-app scroll fix + 0.3.0 cut
|
||
|
||
User: "the philosophy doc isn't scrollable. Two fingers gets it
|
||
started, then one finger works, but people are getting stuck. I
|
||
assume it's a viewport thing?"
|
||
|
||
Diagnosis: `.app { height: 100vh }` + `body { overflow: hidden }`
|
||
is the classic iOS Safari URL-bar trap. `100vh` on iOS measures the
|
||
URL-bar-hidden ("largest") viewport — `.app` is taller than what's
|
||
visible. Single-finger touches get consumed by the (blocked) page-
|
||
level scroll attempt to dismiss the URL bar; two-finger touches
|
||
bypass that and engage the nested `.chrome-pane` scroll directly.
|
||
|
||
Fix: `.app { height: 100vh; height: 100dvh; }` — `dvh` is dynamic
|
||
viewport height (adjusts as URL bar shows/hides), supported on iOS
|
||
15.4+ and Chrome 108+. `100vh` retained as a fallback for older
|
||
browsers (which ignore the unknown `dvh` value).
|
||
|
||
rfc-app's working tree at session start had a full 0.3.0 release
|
||
authored but uncommitted — private-beta gate, allowlist table,
|
||
`/admin/allowlist` UI, `/beta-pending` route, anonymous read mode,
|
||
beta chips, nginx config rename (rfc.wiggleverse.org →
|
||
ohm.wiggleverse.org matching the deprovisioned domain), and a
|
||
complete CHANGELOG entry with RFC 2119 upgrade steps. The scroll fix
|
||
was added to the same 0.3.0 release (small bug fix, orthogonal to
|
||
the substantial 0.3.0 work).
|
||
|
||
Committed as `21fcbc9 Release 0.3.0: private-beta gate + anonymous
|
||
read mode`. Tagged `v0.3.0`. Pushed to both
|
||
`git.wiggleverse.org/ben.stull/rfc-app` and the
|
||
`git.benstull.org/benstull/rfc-app` mirror.
|
||
|
||
ohm-rfc/.rfc-app-version bumped from 0.2.3 → 0.3.0. Committed,
|
||
pushed.
|
||
|
||
#### 4d. Second live deploy
|
||
|
||
`flotilla pin check ohm-rfc-app` → `pin AHEAD of last deploy:
|
||
pin=0.3.0, last deployed=v0.2.3 (deploys.id=1)`. Good. Then:
|
||
|
||
```
|
||
ohm-rfc-app-flotilla deploy ohm-rfc-app
|
||
```
|
||
|
||
All 9 phases green again. `ohm: deployed v0.3.0 (verify took 2.5s)`.
|
||
Second real flotilla deploy — and the first real version-bump
|
||
deploy (the first was v0.2.2 → v0.2.3, this is v0.2.3 → v0.3.0).
|
||
`deploys.id=2`.
|
||
|
||
`deploy status` confirmed `HTTP 200 version=0.3.0 status=ok`. `pin
|
||
check` now says `pin matches last deploy (v0.3.0, deploys.id=2)`.
|
||
`curl https://ohm.wiggleverse.org/api/health` →
|
||
`{"version":"0.3.0","status":"ok"}`. Verified.
|
||
|
||
The §20.5 exit criterion was met live in-session.
|
||
|
||
#### 4e. flotilla v1.0.0 commit
|
||
|
||
`VERSION` updated from `0.4.0` (the stale Slice-4-pending value) to
|
||
`1.0.0`. `pyproject.toml` `version = "1.0.0"`. CHANGELOG `1.0.0`
|
||
entry replaced "pending operator gesture" with `2026-05-27` plus a
|
||
note that the gesture was satisfied during the build session itself
|
||
(`deploys.id=2`, all 9 phases, 2.5s verify).
|
||
|
||
`git add -A` staged 23 files (11 modified, 12 new). Commit
|
||
`8b87c27 Release 1.0.0: Slice 4 + Slice 5 — full deploy gesture,
|
||
hardening, secret set, operator guide`. Tag `v1.0.0`. Pushed to
|
||
`git.wiggleverse.org/wiggleverse/ohm-rfc-app-flotilla` (canonical
|
||
only; no personal-mirror for this org-flavored repo per SPEC §2).
|
||
|
||
#### 4f. Roadmap
|
||
|
||
User listed 11 features they want to ship at one-per-session
|
||
cadence: VM rename, email/OTC login, passcodes, device trust 30d,
|
||
CloudFlare verification, cookie opt-in, anonymous-discuss/contribute
|
||
off-limits, auto-set RFC owner, owner-only invite, discussion-without-
|
||
PR / contribution-needs-PR, Amplitude instrumentation.
|
||
|
||
Categorized by repo, grouped into four phases (operational/easy wins
|
||
→ collaboration model → auth migration block → compliance+data),
|
||
with dependencies noted (passcodes depends on OTC, owner-invite
|
||
depends on the identity model, etc.). Written to
|
||
`ohm-rfc/ROADMAP.md`.
|
||
|
||
User added a twelfth item mid-write: publicly post the session
|
||
transcripts. Added as Phase E with open-questions about where (a
|
||
public gitea repo, an OHM site route, both) and what to redact
|
||
(probably zero — no secret bytes in transcripts; just a confirming
|
||
pass).
|
||
|
||
## Settlement record
|
||
|
||
(Decisions that shifted the SPEC or established new operator-doc
|
||
patterns, with the §19.3 rule applied where relevant.)
|
||
|
||
- **§7.5 grows from three to four verbs.** `secret set` added. The
|
||
Slice 5 build session is the live evidence the SPEC's read-only
|
||
stance was wrong. Implementation surface: `secrets.create_secret_
|
||
if_absent`, `secrets.add_secret_version`, `secrets.derive_secret_id`,
|
||
`secrets.SecretWriteError`. CLI surface: bytes via stdin, never
|
||
via argv.
|
||
- **§7.6 narrows.** The "secret creation through flotilla" entry
|
||
moves out of v1-deferred (now in v1). Rotation ceremonies and
|
||
multi-version pinning policies stay deferred.
|
||
- **§11.2 IAM widens for write ops only.** Operators using `secret
|
||
set` need `secretmanager.admin` (or the narrower `secretCreator +
|
||
secretVersionAdder` pair). Deploy-only operators still only need
|
||
`secretAccessor`.
|
||
- **§12.1 verb inventory updated.** New `ohm-rfc-app-flotilla secret
|
||
set <deployment> KEY` row.
|
||
- **§19.2 candidate added: framework-side required-env manifest.**
|
||
Sketches the fifth versioned contract between flotilla and
|
||
rfc-app. Surfaced by the user's "how do new features just work?"
|
||
question and the gap between rfc-app's `Upgrade steps:` prose
|
||
contract and what flotilla can pre-validate. Not built in v1.0.0.
|
||
- **rfc-app SPEC §3 invariant 4 unchanged.** The four versioned
|
||
contracts (GET /api/health, VERSION, .rfc-app-version, upgrade-
|
||
steps CHANGELOG) all stayed faithful through the live deploy. The
|
||
required-env manifest, if it lands, becomes the fifth — but as a
|
||
candidate, not committed.
|
||
- **Operator guide is now a first-class artifact.** Sufficient for
|
||
laptop re-provisioning end-to-end. Lives at
|
||
`flotilla/docs/operator-guide.md` and is exercised by
|
||
`tests/test_bringup_replay.py`.
|
||
|
||
## What didn't happen this session (and why)
|
||
|
||
- **VM rename `ohm-app` → `ohm-rfc-app`.** Still §19.2. The current
|
||
deploy gesture uses the legacy `ohm-app` names; everything works.
|
||
The rename is a separate operational gesture (gcloud + useradd +
|
||
systemd + nginx + a no-op deploy under the new path), scoped to
|
||
its own session. First item in the new ROADMAP.md.
|
||
- **Auto-rollback on /api/health verify failure.** Still §19.2. The
|
||
two live deploys this session both succeeded; no recovery gesture
|
||
needed. Fail-stop is the v1 commitment.
|
||
- **Cloud Build / worker pools.** Still §19.2. v1 builds the
|
||
frontend on the target VM. Worked fine for both deploys in this
|
||
session.
|
||
- **Hosted flotilla / web surface.** Still §19.2. Single-operator
|
||
laptop-only is correct for OHM right now.
|
||
- **Secret rotation ceremonies.** Still §19.2. `secret set` is the
|
||
primitive; the full drain-swap-reverify dance is a different
|
||
problem.
|
||
|
||
## Cut state at end-of-session
|
||
|
||
- **flotilla**: `8b87c27` on `main`, tag `v1.0.0` pushed. 156 tests
|
||
passing. The first stable cut.
|
||
- **rfc-app**: `21fcbc9` on `main`, tag `v0.3.0` pushed. The 0.3.0
|
||
release lives on both canonical (git.wiggleverse.org) and mirror
|
||
(git.benstull.org).
|
||
- **ohm-rfc**: `6897b3e` on `main`, pin at `0.3.0`. Plus
|
||
`ROADMAP.md` (not yet committed — added in this session, will
|
||
commit at the next opportunity).
|
||
- **OHM VM**: serving rfc-app v0.3.0. /api/health green. Two
|
||
`deploys` rows in the local flotilla SQLite (`id=1` for
|
||
v0.2.3, `id=2` for v0.3.0).
|
||
- **ohm-infra**: this transcript (Session H) at
|
||
`~/git/ohm-infra/SESSION-H-TRANSCRIPT.md`.
|
||
|
||
## Arc 5 — post-cut: live incident, cleanup, v1.0.1, parallel-fork operating instructions
|
||
|
||
The session was supposed to end at "v1.0.0 shipped + roadmap committed."
|
||
It didn't — the user opened the live site, found two regressions
|
||
introduced by the deploy, and the session spent the rest of its
|
||
context surfacing the root cause, cleaning up, cutting a patch, and
|
||
rewriting the roadmap so future sessions don't have to do the
|
||
dispatch by hand.
|
||
|
||
### 5a. Two reported regressions
|
||
|
||
User: "The mermaid diagram is missing now" — followed shortly by
|
||
"The sign in button also doesn't work."
|
||
|
||
Initial diagnosis path:
|
||
- Mermaid: Philosophy.jsx and MarkdownPreview.jsx unchanged between
|
||
v0.2.3 and v0.3.0. The CSS for `.markdown-preview` and `.mermaid-
|
||
block` unchanged. So neither code nor CSS regressed.
|
||
- The deployed bundle still has mermaid chunks (`architectureDiagram-
|
||
*.js`, `blockDiagram-*.js`, etc. all present at
|
||
`/opt/ohm-app/frontend/dist/assets/`). nginx serves them with HTTP
|
||
200 / `application/javascript`. So the build and the serving layer
|
||
are fine.
|
||
- The actual cause for mermaid: `/api/philosophy` body has **zero
|
||
mermaid fences**. The framework reads PHILOSOPHY.md from disk
|
||
(default `/opt/ohm-app/PHILOSOPHY.md` — the framework's own,
|
||
shipped with rfc-app, no mermaid). The OHM-specific PHILOSOPHY.md
|
||
(with the "Why OHM, in three panels" diagram) lives at
|
||
`/opt/ohm-app/meta-content/PHILOSOPHY.md` on the VM, out-of-band.
|
||
For the framework to serve it, the `PHILOSOPHY_PATH` env var must
|
||
point at it. The previous (pre-flotilla) `.env` had that override;
|
||
flotilla's first deploy rewrote the .env from the overlay (which
|
||
doesn't carry `PHILOSOPHY_PATH`) and clobbered the override.
|
||
|
||
Sign-in failure was a stranger story.
|
||
|
||
### 5b. The CRLF time-bomb surfaced
|
||
|
||
While inspecting the VM's `.env`, every single value had a literal
|
||
trailing `\r` escape sequence:
|
||
|
||
```
|
||
APP_URL="https://ohm.wiggleverse.org\r"
|
||
GITEA_URL="https://git.wiggleverse.org\r"
|
||
OAUTH_CLIENT_ID="3e85cebb-6515-43a7-9ded-40e111c81f15\r"
|
||
OAUTH_CLIENT_SECRET=" gto_oge…\r"
|
||
SECRET_KEY="2e05383a849…\r"
|
||
...
|
||
```
|
||
|
||
`OAUTH_CLIENT_SECRET` additionally had a **leading space** (artifact
|
||
of the manual `grep | sed` extraction from earlier in the bootstrap).
|
||
|
||
When python-dotenv parsed these on the framework's startup, the `\r`
|
||
escape sequences decoded into real carriage-return characters in the
|
||
runtime values. `GITEA_URL` became `https://git.wiggleverse.org<CR>`,
|
||
which Gitea rejected on the OAuth redirect chain — hence the broken
|
||
sign-in. The OAuth client ID and secret were also CR-tainted, so
|
||
even if the URL had been clean, the auth would have failed at the
|
||
token-exchange step.
|
||
|
||
Cross-checked against flotilla state via `overlay show --json`:
|
||
**every** overlay row had a `\r`-tainted value, and the four Secret
|
||
Manager versions stored CR-tainted bytes too (`GITEA_BOT_TOKEN`:
|
||
41 bytes instead of 40; `SECRET_KEY`: 65 instead of 64; etc.).
|
||
|
||
Reconstructed cause: during Arc 3 (the Slice 2 bootstrap), the user
|
||
sourced `/tmp/ohm-rfc-app.env` **before** running the `tr -d '\r'`
|
||
normalization. The shell variables picked up trailing CRs. The
|
||
overlay-reconcile loop and the SM-create loop both ran with the
|
||
CR-tainted shell variables, so `flotilla overlay set` and `gcloud
|
||
secrets create | flotilla secret bind` faithfully stored the CRs.
|
||
The subsequent re-source on the CR-stripped file overwrote the
|
||
shell variables but by then the writes had already happened.
|
||
|
||
This wasn't anticipated by the bootstrap doc — §4 had the CRLF
|
||
normalization listed first, but the operator-real ordering put the
|
||
errored source attempt before the fix, and the variables retained
|
||
their first-source values.
|
||
|
||
### 5c. Cleanup: Python orchestration script
|
||
|
||
Wrote `/tmp/cleanup.py` that:
|
||
|
||
1. Reads every overlay row via `flotilla overlay show … --json`.
|
||
2. For each row: strips `\r\n` and surrounding whitespace from the
|
||
value. If the cleaned value is empty, `overlay unset` (catches
|
||
`SMTP_HOST` and `SMTP_USER` which were blank in the source
|
||
but ended up as `\r`-only). Otherwise `overlay set` with the
|
||
clean value.
|
||
3. For each of the four secret bindings: `gcloud secrets versions
|
||
access latest` → `.strip()` → pipe to `flotilla secret set`.
|
||
This creates a new SM version with cleaned bytes and rebinds at
|
||
`@latest`. Sizes proved the strip worked:
|
||
- GITEA_BOT_TOKEN: 41 → 40 bytes
|
||
- OAUTH_CLIENT_SECRET: 58 → 56 bytes (stripped leading space too)
|
||
- SECRET_KEY: 65 → 64 bytes
|
||
- GITEA_WEBHOOK_SECRET: 65 → 64 bytes
|
||
4. Adds `PHILOSOPHY_PATH=/opt/ohm-app/meta-content/PHILOSOPHY.md`
|
||
to the overlay so the next deploy's .env rewrite carries the
|
||
override.
|
||
|
||
Then `flotilla deploy ohm-rfc-app` again — third deploy in the
|
||
session (`deploys.id=3`). All 9 phases green; `/api/health`
|
||
verified at v0.3.0 in 2.4s. Same gesture, same shape; the flotilla
|
||
deploy path is now well-exercised.
|
||
|
||
Post-deploy verification confirmed all three fixes:
|
||
- `/api/philosophy` body now contains 1 mermaid fence.
|
||
- VM `.env` via `cat -A` shows `$` at every line end, no `^M`.
|
||
- `/auth/login` 307s to a clean Gitea OAuth URL.
|
||
|
||
### 5d. v1.0.1 patch — doc + spec hardening
|
||
|
||
The CRLF incident wasn't a code regression — it was a doc-discipline
|
||
failure that the spec hadn't anticipated. Cut as a patch so the
|
||
findings live in the canonical history:
|
||
|
||
- **`docs/secret-bootstrap.md` §4** gains a `tail | od -c`
|
||
verification step that catches trailing-CR pollution in shell
|
||
variables BEFORE the §5/§6 loops fire. Recovery sequence spelled
|
||
out: unset every shell variable → confirm file is CR-free → re-
|
||
source → re-check. The check now runs over both the secret
|
||
variables AND a couple of overlay variables (GITEA_URL, APP_URL)
|
||
to make sure the loop didn't silently miss anything earlier.
|
||
- **flotilla SPEC §19.2 candidate added**: "Input sanitization at
|
||
the overlay/secret boundary." Open question — should flotilla
|
||
reject/normalize CR/LF/whitespace at `overlay set` / `secret set`
|
||
inputs as a structural defense? Pro: doc-independent. Con: blocks
|
||
deployments that legitimately need multi-line values (a PEM key
|
||
in a single env var, say). v1 stays doc-enforced; the candidate
|
||
earns its session if a second incident of the same shape happens
|
||
or if a deployment wants the opt-out.
|
||
|
||
Tagged as `v1.0.1` and pushed. No code changes; pure doc + spec.
|
||
The OHM live deploy doesn't need re-application — the cleanup in
|
||
§5c already fixed the affected state.
|
||
|
||
### 5e. Roadmap revision: two new items + parallel-fork operating instructions
|
||
|
||
User reviewed the roadmap and added two new items in Phase C between
|
||
email/OTC (#5) and passcodes (now #8):
|
||
|
||
- **#6 — Open beta-access request (first/last/why on first OTC).**
|
||
Anyone with a valid email can sign in via #5's OTC. The previous
|
||
v0.3.0 allowlist-of-emails gate becomes vestigial; the new gate
|
||
is "has an admin granted you permissions?" New users land in
|
||
`pending` permission state with required first name, last name,
|
||
and free-text "why I should be included in the beta" captured.
|
||
- **#7 — Admin user-management page + new-request notifications.**
|
||
Admins get notified (email + in-app inbox if §15 is wired) when
|
||
a new request lands; the `/admin/users` page lists every user
|
||
with permission state, sign-up reason, and grant/revoke controls.
|
||
The v0.3.0 `/admin/allowlist` page either merges in or stays as
|
||
a sub-tab.
|
||
|
||
These two are sequential (#7 needs #6's `users`-row shape to exist)
|
||
but the rest of Track C reshuffles: passcodes (#8) and device trust
|
||
(#9) become a separate sub-track that can parallel #6/#7 on careful
|
||
branches. Cloudflare verification (#10) shifts to better-emphasize
|
||
its importance now that anyone can request OTC. Version targets
|
||
shifted: passcodes → v0.10.0, device-trust → v0.11.0, cookie →
|
||
v0.13.0, owner-invite → v0.14.0, Amplitude → v0.15.0.
|
||
|
||
Then the user added the structural ask: **"In the next session,
|
||
roadmap items should be done in parallel when in possible and all
|
||
new roadmap changes should be done in a forked agent with a new
|
||
session. That way I can go to bed and progress can be made without
|
||
me starting new Claude instances."**
|
||
|
||
This is the operational shape change that turns the roadmap from a
|
||
single-session-per-feature serial queue into an autonomous parallel
|
||
pipeline. Added a new section at the bottom of `ohm-rfc/ROADMAP.md`
|
||
— **"Operating instructions for the next session (parallel +
|
||
autonomous)"** — that codifies:
|
||
|
||
1. The next session reads the roadmap and identifies the active
|
||
wave (items with no unmet dependencies).
|
||
2. **Dispatch each shippable item as a separate subagent in
|
||
parallel**, using the `Agent` tool with `subagent_type:
|
||
general-purpose` and `isolation: worktree` (each subagent gets
|
||
an isolated git worktree, removing the "concurrent sessions on
|
||
separate branches" ceremony).
|
||
3. Each subagent's prompt is self-contained: roadmap item text,
|
||
target version, repo path, dependency status, exact ship
|
||
gesture (VERSION + pyproject + CHANGELOG with `Upgrade steps:` +
|
||
commit + tag + push canonical + mirror if applicable + ohm-rfc
|
||
pin bump if it's an rfc-app item).
|
||
4. **Subagents tag and push only.** The driver runs `flotilla
|
||
deploy ohm-rfc-app` **one at a time** after each subagent
|
||
finishes, because the §8.3 lock serializes deploys.
|
||
5. Verify after each deploy. Stop the wave on first deploy failure.
|
||
6. Pause the wave if a release's `Upgrade steps:` require operator-
|
||
provided secret bytes (the driver can't invent them).
|
||
7. Update the roadmap (strikethrough the row, link to release tag
|
||
and deploys.id) when each item ships.
|
||
8. Write a Session I transcript at end-of-wave.
|
||
|
||
Constraints the driver respects: no `git config` edits, no force-
|
||
pushes, no `--amend` of pushed commits, no skipping pre-commit
|
||
hooks. Match the safety rules from the global instructions.
|
||
|
||
The roadmap commit (`58e47b9` on `ohm-rfc/main`) carries all of this.
|
||
|
||
### 5f. Cut state — for real this time
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| flotilla | `cad9b5b` tag `v1.0.1` |
|
||
| rfc-app | `21fcbc9` tag `v0.3.0` |
|
||
| ohm-rfc | `58e47b9` (pin at 0.3.0, ROADMAP updated with #6/#7 + driver instructions) |
|
||
| OHM live | `deploys.id=3`, v0.3.0, cleaned values, mermaid back, sign-in working, PHILOSOPHY_PATH override restored |
|
||
|
||
## Prompt the operator will paste into the next Claude Code session
|
||
|
||
```
|
||
You are the OHM roadmap driver. Read /Users/benstull/projects/wiggleverse/ohm-rfc/ROADMAP.md
|
||
end-to-end, then execute the active wave per the "Operating instructions for the next
|
||
session" section at the bottom of that file.
|
||
|
||
Concretely:
|
||
1. Identify Wave 1 items that are shippable (no unmet dependency).
|
||
2. Dispatch each shippable item as a forked subagent in a single message with multiple
|
||
Agent tool uses (subagent_type: general-purpose, isolation: worktree). Each subagent
|
||
prompt must be self-contained per the operating-instructions checklist.
|
||
3. When subagents finish (tag + push + ohm-rfc pin bump for rfc-app items), run
|
||
`flotilla deploy ohm-rfc-app` from /Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla
|
||
one at a time. Verify each deploy.
|
||
4. Strikethrough the version-target table row for each shipped item; commit + push ohm-rfc
|
||
when the wave is done.
|
||
5. Continue to Wave 2 if its dependencies are now met. Keep going until you hit a blocker
|
||
(operator-provided secret needed, deploy failure, ambiguity).
|
||
6. Write a Session I transcript at ~/git/ohm-infra/SESSION-I-TRANSCRIPT.md following the
|
||
Session H shape.
|
||
|
||
Stop and wait for the operator if: a deploy fails, a release's Upgrade steps require a
|
||
secret you can't generate, or you encounter cross-repo ambiguity. Do not invent secret
|
||
values or guess at unclear design decisions.
|
||
```
|
||
|
||
## Wave 1 ready
|
||
|
||
When the operator pastes the prompt above into a fresh Claude Code
|
||
session, that session will dispatch:
|
||
|
||
- **Session α (Track Ω) — #1 VM rename** (flotilla + ohm-infra; the
|
||
§19.2 candidate). Touches `ohm-app` → `ohm-rfc-app` for VM, unix
|
||
user, install dir, systemd unit, nginx, and a re-`register-ohm.sh`
|
||
on the flotilla side. May ship as flotilla v1.1.0 if any flotilla
|
||
code change is needed (the operator-guide updates probably do
|
||
require one).
|
||
- **Session β (Track A) — #2 Auto-set RFC owner → rfc-app v0.4.0.**
|
||
Smallest item. Touches the propose-RFC modal + the corresponding
|
||
backend endpoint.
|
||
- **Session γ (Track B) — #3 Discussion-without-PR + contribution-
|
||
needs-PR → rfc-app v0.5.0.** Largest of the three. Collaboration-
|
||
model shift; touches RFC view, comment storage, possibly schema
|
||
(new `discussions` table or extension of `comments`).
|
||
|
||
All three are parallel-safe (different tracks, different code regions).
|
||
The driver's first message after reading the roadmap is a single
|
||
Agent block with three sub-agent dispatches; then the driver waits
|
||
for them to finish, runs three deploys in sequence, verifies each,
|
||
and updates the roadmap.
|
||
|
||
By the time the operator wakes up, OHM should be on `v0.5.0` (or
|
||
further if Wave 2 dependencies were met), the flotilla v1.1.0 cut
|
||
should have happened if the VM rename produced one, and Session I's
|
||
transcript should record the journey.
|
||
|
||
If the wave hits a blocker (most likely candidates: the VM rename
|
||
needs operator-level GCP gestures that gcloud can't script around,
|
||
or the collaboration-model change introduces a schema migration that
|
||
needs the operator's blessing), the driver pauses and leaves a clear
|
||
"waiting on operator" note in the partial Session I transcript.
|