# Session 0025.0 — Transcript

> Date: 2026-05-28
> Goal: Pick one roadmap item disjoint from v0.21.0's polished surfaces
> and from #26/#29, build it on a manual worktree while deploys are
> frozen. Recommended fits from the 0022 handoff: #19 (CONTRIBUTING
> docs) or #27 (Haiku tag suggestions).
>
> Outcome: **#19 turned out to be already shipped (Session 0013.1) but
> never struck in the ROADMAP — which is exactly what misdirected this
> session to it. Pivoted to #27. Built Claude Haiku tag suggestions on
> the propose-RFC modal, tagged rfc-app [v0.24.0](https://git.wiggleverse.org/ben.stull/rfc-app/src/tag/v0.24.0),
> pushed to origin + benstull. 351 backend tests green, frontend builds
> clean. NOT deployed — deploys frozen behind the #1 VM-rename blocker
> (Session 0022); pin held at 0.22.0. ROADMAP bookkeeping corrected
> (#19 struck, #21 Part B noted shipped, #27 recorded as built+staged).**
>
> Highlights:
> - The §9.1 "Slice 2" AI-suggested tag chips, deferred since the
>   propose modal first landed, are now wired.
> - Reused the existing §18 provider abstraction (`AnthropicProvider`,
>   `_CLAUDE_VARIANTS["claude-haiku"]`) — added one small factory
>   `providers.construct_haiku()`. `ANTHROPIC_API_KEY` was already a
>   config var (the chat stack reads it), so #27's "new secret" may
>   already be bound on OHM — flagged for the operator to check.
> - Held the pin and staged the deploy, consistent with how v0.23.0's
>   pin is being held during the freeze.

---

## Pre-session state

| repo | sha | state |
| --- | --- | --- |
| rfc-app | `daebb54` | `main`; last tag v0.23.0 (#29). Backend 340 tests green. |
| ohm-rfc | `9f0c4e7` | `main` clean; pin `.rfc-app-version` = 0.22.0 |
| ohm-rfc-app-flotilla | `85f01f8` | session anchored here; `main` clean except untracked `.claude/` |
| OHM live | running v0.21.0 (per the 0019.0 / 0024.0 ledger), healthy | — |

**Deploy freeze in force.** The 0022 handoff: flotilla deploy of
v0.22.0 failed preflight (install dir `/opt/ohm-app` missing on the VM;
clean failure, no lock held, no partial deploy). Session 0022 is
investigating whether the #1 VM rename (`ohm-app` → `ohm-rfc-app`) was
started on the VM, leaving the §13 deployment record pointing at the
old path. Deploys blocked until 0022 clears it; OHM still healthy at
v0.21.0. Deploy order when it unblocks: v0.22.0 → v0.23.0 → mine.

Sessions 0022 (deploy-blocker, INPROGRESS), 0023, 0024 (roadmap-capture
#33/#34, done) preceded this. None were building #27.

---

## Turn-by-turn arc

### Arc 1 — Claim, orient, pick an item

Claimed session ID **0025** via `claim-session-id.sh --start
2026-05-28T13-06` (pushed the placeholder; the push is the atomic
claim). Read SESSION-PROTOCOL.md §7 + the session-start gesture, and
the full ROADMAP (PULL'd ohm-rfc first — already up to date).

Reasoned to **#19** over #27 initially: deploy-free (ships as PRs the
operator merges), zero code-surface collision with the avoid-list,
fully completable this session. #27 by contrast needs a new secret, a
counsel pass, and a deploy — none closeable during the freeze.

### Arc 2 — #19 is already done (the ledger lied)

Before touching anything, checked the repos for existing artifacts.
**Both `CONTRIBUTING.md` files already existed and were committed:**

- rfc-app `CONTRIBUTING.md` + SPEC §21 analytics chapter landed on
  `main` via `213f686` (merge `ac3513a`, *"Session 0013.1"*) — the
  commit message literally reads *"Lands roadmap item #19 ... and #21
  Part B ... as a single docs commit."* Verified `213f686` is an
  ancestor of `origin/main` and the file is in the `main` tree.
- ohm-rfc `CONTRIBUTING.md` committed (`90965d4` + later edits).

So **#19 + #21 Part B shipped back in Session 0013.1** and were simply
never struck in the ROADMAP version-target table. The 0022 handoff
recommended #19 not realizing it was done — the un-struck row is what
misdirected this session. This is the session's main friction finding
(see §19.2 candidates).

Pivoted to the other named fit, **#27**. Confirmed via local
transcripts + rfc-app tags/branches that no live session was building
it (0024 only *lists* #27 as a future candidate; no `v0.24.0` tag, no
`#27` branch).

### Arc 3 — Map the surfaces

Mapped before building (all reads, no subagents — #27 is one cohesive
feature, so the driver built it directly; no §5 subsession transcripts
warranted):

- **Existing LLM infra (§18 chat stack).** `backend/app/providers.py`
  already has a multi-provider abstraction with `AnthropicProvider` and
  `_CLAUDE_VARIANTS["claude-haiku"] = ("claude-haiku-4-5-20251001",
  "Claude Haiku")`. `ANTHROPIC_API_KEY` is already a config var
  (`config.py:44/91`) and `anthropic>=0.39` is already a dep. The
  `models_resolver` docstring even anticipates *"the §9.1 tag
  suggestions"*. So #27's "new secret" is the same key the chat stack
  uses — it may already be bound on OHM.
- **Tags are free-form chip input** ("free-form for slice 1; the
  AI-suggested chips of §9.1 are deferred to Slice 2"). No curated
  taxonomy. So "the existing tag taxonomy" #27 references = the
  de-facto set of distinct tags across `cached_rfcs` (`tags_json`).
- **Propose endpoint** is in `api.py`'s `make_router(config, gitea,
  bot, providers)` closure, gated by `auth.require_contributor`.
- **Provider-call convention** (`api_prs._draft_with_provider`): async
  endpoints call `provider.send()` directly (no threadpool), wrapped in
  try/except → graceful stub fallback. Matched it.
- **Test harness**: no shared conftest; tests self-bootstrap via
  `app_with_fake_gitea` / `sign_in_as` / `provision_user_row` from
  `test_propose_vertical.py`. Runner `./.venv/bin/python -m pytest`
  from `backend/` (340 tests pre-change).

### Arc 4 — Build #27 (worktree)

Manual worktree per the handoff (Agent worktree isolation is broken):
`git worktree add -b feature/v0.24.0-haiku-tags
/Users/benstull/git/rfc-app-v0.24.0-haiku-tags main`, then symlinked
`frontend/node_modules` + `backend/.venv` in. (`.gitignore` ignores
`.venv/`/`node_modules/` as directories; the symlinks showed as
untracked, so every commit staged explicit files — never `-A`.)

Backend:
- **`backend/app/tag_suggest.py`** (new) — `gather_tag_universe()`
  (distinct corpus tags, most-common first, capped 200); `Draft`
  dataclass; `haiku_provider(config)` seam (monkeypatchable; `None`
  when no key); `build_messages` + tolerant `parse_reply` (drops
  invented tags, dedupes case-insensitively to canonical spelling,
  clamps confidence, caps); `suggest()` orchestration; in-process
  per-user sliding-window rate limit (`TAG_SUGGEST_RATE_MAX` /
  `_WINDOW_SECONDS`, defaults 30/60).
- **`providers.construct_haiku(api_key)`** — dedicated Haiku provider,
  model id sourced from `_CLAUDE_VARIANTS`. Independent of
  `ENABLED_MODELS`; no RFC slug at propose time so the §6.7 funder path
  doesn't apply.
- **`POST /api/rfcs/suggest-tags`** in `api.py` — `require_contributor`,
  rate-limit → 429, `SuggestTagsBody {title, pitch, use_case}`. Returns
  `{suggestions:[{tag,confidence}]}`. Degrades to `[]` (200, never an
  error) for no key / empty corpus / empty draft / parse failure.
- `backend/.env.example` — documented that `ANTHROPIC_API_KEY` also
  powers tag suggestions + the two optional rate-limit vars.

Frontend:
- `ProposeModal.jsx` — debounced (700 ms, stale-response-guarded)
  suggestion fetch on `[title, pitch, useCase]`; clickable suggestion
  chips that add to the tag list (nothing auto-applies); the required
  inline disclosure ("Suggestions are generated by Claude (Anthropic).
  The text you've entered above is sent to Anthropic to produce them.").
- `api.suggestTags()` — forgiving; any non-OK → `[]`.

Tests: **`test_tag_suggest_vertical.py`** (11) — anon→401, contributor
gets universe-filtered suggestions (invented tag dropped), no-key→[],
empty-corpus→[] (model never called), rate-limit→429, plus units for
gather/parser-tolerance/max-cap/empty-draft-short-circuit/provider-
failure. One initial failure (the gather unit test needed the db
initialized, which happens in the TestClient lifespan) — fixed by
wrapping in a `TestClient(app)` context. Full suite **351 green**;
frontend `npm run build` clean (needed the deployment `.env` copied in
from the main checkout — gitignored, not committed).

### Arc 5 — Release (tag), staged deploy, bookkeeping

Followed the established release shape: feature commit on the branch
(`fb9b4fa`) → `--no-ff` merge to `main` (`376a6da`) → release commit
bumping `VERSION` + `frontend/package.json` + `CHANGELOG.md` (`28015ed`)
→ tag **`v0.24.0`** → pushed `main` + branch + tag to **origin** and
**benstull**. Re-ran the full suite on `main` post-merge: 351 green.

CHANGELOG `Upgrade steps:` (RFC 2119) names the **MUST**
`flotilla secret set ohm-rfc-app ANTHROPIC_API_KEY` gesture (stdin
pbpaste form; never paste bytes), notes the chat stack may already have
it bound, the optional rate-limit knobs, and a **SHOULD** counsel pass
on the disclosure wording (per #22).

**Pin deliberately NOT bumped** — held at 0.22.0, consistent with how
v0.23.0's pin is being held during the freeze. Bumping to 0.24.0 would
have jumped ahead of un-deployed 0.22.0/0.23.0 and scheduled a frozen
deploy. The deploy + pin bump are staged for the operator.

ROADMAP corrected + pushed (`f62cb90`): struck **#19** (with the
"misdirected 0025" note), annotated **#21 Part B** as shipped in
0013.1, recorded **#27** as built+tagged v0.24.0 with the full
operator-pending-gesture list. Did **not** touch #26/#29 rows — those
are tagged-but-not-struck pending deploy and belong to the still-open
Session 0022.

Removed the build worktree (branch preserved on both remotes).

### Arc 6 — Publish-script bug, fixed in passing

Publishing this transcript first **failed**: `publish-transcript.sh`
`mkdir -p`s the `NNNN/` folder early, then `git rm`s the INPROGRESS
placeholder — and because the placeholder was the *only* file in
`0025/`, git pruned the now-empty dir from the worktree, so the
subsequent `cp` died with "No such file or directory." This bites any
session whose folder holds nothing but its own placeholder (i.e. most
non-subagent sessions). Fixed by re-ensuring `mkdir -p "$(dirname
"$DEST")"` immediately before the `cp`. `~/git/ohm-infra` is a plain
local working dir (no VCS — see #33), so the fix simply lives in the
script file; re-ran publish and it succeeded.

---

## Cut state (end of session)

| | |
| --- | --- |
| rfc-app | `28015ed` `main`, tag **v0.24.0** (#27) — pushed origin + benstull. 351 tests green, frontend builds clean. |
| ohm-rfc | `f62cb90` `main` — ROADMAP corrected; **pin held at 0.22.0** |
| OHM live | unchanged — v0.21.0, healthy. **No deploy** (frozen). |
| flotilla | untouched |

| Roadmap ledger | Status |
| --- | --- |
| #19 CONTRIBUTING + onboarding | ✅ was already shipped (0013.1); struck this session |
| #21 Part B analytics discipline | ✅ was already shipped (0013.1) with #19; noted |
| #27 Haiku tag suggestions (v0.24.0) | 🟡 built + tagged + pushed; ⏸ deploy staged (freeze) |

---

## §19.2 candidates surfaced

1. **The ROADMAP version-target ledger lags reality and it cost a
   session's worth of misdirection.** #19 + #21 Part B shipped in
   0013.1 but were never struck; #26 (v0.22.0) and #29 (v0.23.0) are
   tagged-but-not-struck pending deploy. A future session reading the
   table can't tell "available" from "shipped-but-unbooked." Worth a
   convention call: either (a) strike on *tag* (not deploy) with a
   separate "deployed?" column, or (b) a fast `git tag` vs. table
   reconciliation pass at session start. The first gesture of an
   implementer session should be "is this already done?" — this session
   only avoided wasted work by checking the tree before building.
2. **`ANTHROPIC_API_KEY` is overloaded across two surfaces** (the §18
   chat stack and now #27 tag suggestions). Fine today (same provider),
   but if a deployment wants a *cheaper/separate* key for the
   high-volume suggestion path vs. interactive chat, that's a future
   `TAG_SUGGEST_ANTHROPIC_KEY`-style split. Noted, not built.
3. **Tag suggestion quality is bounded by corpus size.** Constrained to
   existing tags (per #27's explicit scope), a near-empty corpus yields
   near-empty suggestions. The roadmap already names taxonomy-extension
   (letting the model propose genuinely new tags) as the deferred
   follow-up; this is the data point that it'll matter early for OHM.

---

## What lands on the operator's plate

1. **Clear the deploy freeze** (Session 0022's `/opt/ohm-app` blocker),
   then deploy in order: v0.22.0 → v0.23.0 → **v0.24.0**, bumping the
   ohm-rfc pin at each step and verifying.
2. **`ANTHROPIC_API_KEY` — DONE.** The operator bound it during this
   session; `flotilla secret list ohm-rfc-app` confirms
   `ANTHROPIC_API_KEY → wiggleverse-ohm/ohm-rfc-app-anthropic-api-key@latest`.
   It waits in Secret Manager until v0.24.0 deploys. (No bytes ever
   touched the conversation — operator ran the stdin gesture.)
3. **Counsel pass** on the propose-modal disclosure wording before
   exposing the surface, per #22 discipline. The copy is honest as
   written.
4. End-to-end verification of the suggestion surface happens
   post-deploy (couldn't be done this session — no deploy, and no key
   should ever touch the model).

---

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

```
You are an OHM roadmap driver session. Claim your session ID first
(~/git/ohm-infra/scripts/claim-session-id.sh --start <now PST>); your
number is not assigned here. Read SESSION-PROTOCOL.md §7 + ohm-rfc/
ROADMAP.md (PULL ohm-rfc first).

State at handoff (from session 0025):
- rfc-app main @ 28015ed: v0.24.0 tagged + pushed (origin + benstull) —
  roadmap #27, Claude Haiku tag suggestions on the propose-RFC modal.
  351 backend tests green, frontend builds clean. NOT deployed.
- THREE rfc-app releases now sit un-deployed behind the freeze:
  v0.22.0 (#26), v0.23.0 (#29), v0.24.0 (#27). ohm-rfc pin still 0.22.0
  (all three pin-bumps held for deploy).
- ⛔ DEPLOYS STILL BLOCKED pending Session 0022's /opt/ohm-app VM-rename
  investigation. Do NOT run flotilla deploy until that clears —
  coordinate with the operator. Deploy order: v0.22.0 → v0.23.0 →
  v0.24.0, bump pin + verify at each.
- #19 + #21 Part B were already shipped (Session 0013.1) — struck/noted
  in the ROADMAP this session. The ledger had lagged; check the rfc-app
  git tags against the version-target table before assuming a row is
  available (#26/#29 are tagged-but-not-struck pending deploy).

Operator gestures pending for v0.24.0 (#27), at deploy time:
- ANTHROPIC_API_KEY is ALREADY BOUND (operator set it in session 0025;
  `flotilla secret list ohm-rfc-app` shows
  ohm-rfc-app-anthropic-api-key@latest). Nothing to do — it waits in
  Secret Manager until the deploy.
- Counsel pass on the propose-modal disclosure copy (per #22) — still
  pending before exposing the surface to users.

Your job: pick one roadmap item that's shippable and disjoint from the
un-deployed surfaces (#26 ProposeModal/PRModal/RFCView/PRView,
#29 App.jsx sign-in region, #27 ProposeModal suggestion row +
tag_suggest.py + the suggest-tags endpoint) and from v0.21.0's polished
CSS. Build on a manual git worktree (git worktree add -b <branch>
<path> main; symlink frontend/node_modules + backend/.venv). Good
deploy-free fits while frozen: #21 Part A (Amplitude audit — but it
wants real-world data) or another Track-A surface that can build + tag +
stage. Avoid touching the three un-deployed features' code regions.

Hard rules: never ask for secret bytes; subagents write their own §5
subsession transcripts; finalize + publish your transcript last,
delivering the next handoff prompt to the operator before publish.

Claim your ID, then go.
```
