Lands roadmap item #19 (CONTRIBUTING + transcript-linked onboarding) and #21 Part B (standing analytics instrumentation discipline) as a single docs commit. No code change, no version bump — these ride a future release or land as a no-bump docs commit at the operator's discretion. CONTRIBUTING.md (new): - Project-evolution framing pointing at wiggleverse/ohm-session-history as the authoritative how-we-got-here record. - Worked-example links into Sessions E (clean small release), I (recovery from deploy fault), K (multi-feature wave with operator-secret pause), L (squash-merge across three parallel features + #21 Part C identity-lifecycle wiring). - PR-shape contract: subagents push feature branches; operator tags + deploys (the pattern driver sessions use). - CHANGELOG strict-descending convention. - RFC 2119 keyword discipline for Upgrade-steps blocks (per §20.4). - §19.2 candidate discipline — architectural deferrals get noted rather than scope-creeping a release. - Test-coverage expectations characterized from the actual ~250-test backend pytest surface (no frontend test runner today; the discipline is build-clean + backend HTTP contract coverage). - Operator-only gestures explicitly enumerated (no tagging, no deploying, no pin moves, no secret bytes in conversation). - Analytics instrumentation checklist (#21 Part B's CONTRIBUTING artifact): named events, autocapture-friendly DOM, replay masking, PR description discipline. SPEC.md §21 (new chapter — Analytics instrumentation and identity): - Placed AFTER §20 versioning rather than between §15 and §16, to avoid renumbering §19.2 / §19.3 — those numbers are load-bearing project nouns referenced from CLAUDE.md, transcripts, and prior commits. §15 carries a new forward-pointer naming §21 as the peer chapter. - §21.1 event-taxonomy conventions (Title Case "Subject Verb", snake_case props, no PII). - §21.2 required prop families per event kind, including the hashed-target_email convention for invite-side events that target a not-yet-user (#12). - §21.3 autocapture-friendly DOM patterns (stable text, aria-label on icon-only buttons, data-amp-track-* on repeated rows, data-amp-track-suppress for noise surfaces). - §21.4 session-replay masking — credentials MUST be masked, PII SHOULD be masked, privacy policy MUST match reality. - §21.5 consent-gate contract — pre-consent no init / no network / no recording; granted→denied → setOptOut(true) within one tick. - §21.6 identity lifecycle (per #21 Part C) — identify with user_id + properties on sign-in; setUserProperties on mid-session change; anonymize on sign-out (after the User Signed Out track); identify-BEFORE-track on invite-claim paths. §21.6.1 set vs setOnce taxonomy with explicit classification rule. - §21.7 cohort-shape implications (informative — what the Amplitude dashboard can actually answer with the conventions). - §21.8 secret-vs-public framing for the overlay binding — Amplitude browser key is public (bundle-embedded), Turnstile secret half is real-secret; canonical pbpaste-pipe gesture for the operator. - §21.9 new §19.2 candidates surfaced (session-replay-specific consent category, bundle-size budget measurement, property- shape CI lint, centralized email-hash helper). - §21.10 open question (consent-category split timing). Grounded in real code: the SPEC chapter cites frontend/src/lib/analytics.js v0.15.0 + the v0.16.0 (AcceptInvitation) + v0.17.0 (InviteClaim) inline #21 Part C wiring as worked examples. RFC 2119 keywords used precisely throughout.
18 KiB
Contributing to rfc-app
rfc-app is the framework that hosts RFC-shaped collections of
documents — one repo per RFC, a meta repo per collection, a web app
that turns the Git substrate into a writeable surface. The Open
Human Model (OHM) deployment at ohm.wiggleverse.org is one
instance. The framework is intended to host more.
This document explains how to propose a change to the framework
itself — a new endpoint, a schema migration, a UI affordance, a
spec clarification. For changes to content hosted by a specific
deployment (the OHM RFCs, the OHM roadmap), see that deployment's
own contribution guide (e.g. ohm-rfc/CONTRIBUTING.md).
How the project actually evolves
rfc-app is built in the open in the literal sense: every build
session produces a full transcript at
wiggleverse/ohm-session-history
on git.wiggleverse.org. The transcripts are the authoritative
record of how the framework got from one release to the next — the
decisions, the friction, the dead ends, the reasoning. They are not
curated retrospectives; wrong turns stay in.
If you are proposing a change to rfc-app, read at least the most recent session transcript before opening a PR. The transcripts show what shape a feature lands in, where the spec gets touched, what the operator pushes back on, and how the release rides into deployment. A PR that matches that texture is much more likely to land cleanly than one shaped by the README alone.
Worked examples to start with:
- Session E (transcript) — a clean small release. Read this for the simplest possible release shape: one feature, one version bump, one upgrade-steps block, no surprises.
- Session I — recovery from a deploy fault. Read this for how the project handles things going wrong mid-deploy, and for the honest no-curation discipline.
- Session K — a multi-feature wave with one item paused on an operator-provided secret. Read this for the subagent dispatch pattern (the model the project uses to ship multiple features in parallel), and for the binding rule that the assistant never asks the operator to paste secret bytes into the conversation.
- Session L — squash-merge integration across three parallel
features (v0.15.0 / v0.16.0 / v0.17.0), with
#21 Part Cidentity-lifecycle Amplitude wiring folded inline across all three releases. Read this for how cross-cutting concerns (analytics, observability) get layered into already-in-flight features without scope-creeping any single release.
The repository where transcripts live —
wiggleverse/ohm-session-history —
is the canonical history. The git log of rfc-app is the artifact;
the transcripts are the story behind it.
How a contribution flows
The framework runs on a subagents push feature branches; operator
tags and deploys model. Contributors — whether human or AI agents
running in a Claude Code subsession — open feature branches and
submit PRs. The operator (the person running the deployment) is the
one who merges, tags, bumps VERSION, runs flotilla deploy (or
the equivalent for non-OHM deployments), and moves the deployment's
.rfc-app-version pin. The driver session transcripts inherit
this shape; contributors inherit it from them.
Concretely:
- Read the most recent session transcript. Understand what just shipped and what is in flight.
- Open an Issue first if your change is exploratory, structural, or might overlap with in-flight work. The operator will name any collision.
- Branch from
main. Name the branchfeature/<short-description>for additive work,fix/<short- description>for bug fixes,docs/<short-description>for documentation-only work. The driver sessions usefeature/v<target-version>-<slug>(e.g.feature/v0.16.0-owner-invite) — that shape is welcome but not required for outside contributors, since contributors do not pick the target version. - Do not bump
VERSIONorfrontend/package.json#versionin your PR. The operator picks the target version at integration time; bumping ahead causes cherry-pick conflicts. The same applies to theCHANGELOG.mdentry header — see below. - Do not tag releases, do not run any deploy gesture, do not
touch any deployment's
.rfc-app-versionpin. The operator alone owns those gestures. (For OHM specifically: "I'm the only one that gets to yolo." See the boundary section inohm-rfc/CONTRIBUTING.md.) - Push your branch and open a PR. Describe what you're proposing
and why, in language the operator can paste into the eventual
release commit. If the change touches
SPEC.md, name which section(s) and the contract change.
CHANGELOG convention: strict descending
CHANGELOG.md is ordered newest-on-top. The header line for
the in-progress version goes at the top of the file; older
releases descend below it. This is the binding convention; the
operator hand-resolves the conflict when two parallel feature
branches both insert at the top of the file (the squash-merge
integration that ships parallel-feature waves keeps the strict-
descending shape — see Session K for the cherry-pick mechanics and
Session L for the hand-resolved-with-a-small-script variant).
A new entry has this shape (read the existing 0.15.0 / 0.16.0 / 0.17.0 entries for worked examples):
## 0.X.Y — YYYY-MM-DD
**Minor — schema migration auto-applied; no operator action.** This
release ships <one or two sentences naming the feature and why>.
### Added
- **<New module/endpoint/component>** — what it does, where it lives,
why it exists. Include file paths inline so a reader can click through.
### Changed
- **<Existing surface>** — what changed and how a deployment notices.
### Migration
- **`<NNN_name>.sql`** — auto-applied by `db.run_migrations()` on
backend start. <Describe the schema delta in one sentence.>
### Upgrade steps (from 0.(X-1).Y)
- You **MUST** … (per RFC 2119; see SPEC.md §20.4).
- You **MUST NOT** …
- You **SHOULD** …
- You **MAY** …
The header version number is filled in by the operator at merge
time. Your PR's CHANGELOG diff can leave the version as
0.X.Y — YYYY-MM-DD (literal placeholder), or use a guessed value
the operator overwrites; either is fine.
Upgrade steps: blocks use RFC 2119 keywords
If your change requires deployments to do anything when they
upgrade — set an env var, apply a migration, restart a process,
flip an overlay value, accept a behavioral change — your CHANGELOG
entry must include an ### Upgrade steps block, and that
block must use the RFC 2119
/ RFC 8174 keywords as
defined in SPEC.md §20.4:
- MUST / SHALL / REQUIRED — without this step the deployment will not function correctly. Skipping is a regression the framework does not handle.
- MUST NOT / SHALL NOT — previously valid, now no longer supported.
- SHOULD / RECOMMENDED — the framework's tested path. A deployment may deviate when it has a reason.
- SHOULD NOT / NOT RECOMMENDED — discouraged without being forbidden.
- MAY / OPTIONAL — an affordance you can take or skip.
Cross-version upgrades (jumping more than one minor) are computed by the operator composing each intervening release's steps in order. Each adjacent step must therefore be locally unambiguous — this is the whole reason the keyword discipline is binding. Avoid words like "should probably" or "might want to" inside an upgrade step; either the framework needs the action or it doesn't.
If your change touches the env contract, also update
backend/.env.example and/or frontend/.env.example in the same
PR so the contract and the documentation land together (§20.4).
SPEC.md and §19.2 candidates
SPEC.md is the framework's binding spec. It is honest about open
questions — large sections of it carry "§19.2 candidates," which
are decisions the project has deliberately deferred rather than
guessed at.
The discipline: architectural or process deferrals get noted as §19.2 candidates rather than scope-creeping a release. When you notice that your change opens a question larger than the change itself (a different DB shape, a new auth contract, a cross-cutting UX rethink), the right move is usually to land the narrow change and add a §19.2 candidate naming the larger question. The candidate documents what was set aside and why, so a future session can pick it up with context.
Worked examples from recent sessions:
- v0.11.0 (Session K) shipped device trust and surfaced three new §19.2 candidates: cross-device session revocation, password- equivalent change invalidating trust, device-trust window tunables via env. None of those were in the v0.11.0 scope; they were noted in SPEC.md §19.2 so a future session can address them on their own terms.
- v0.15.0 (Session L) shipped the Amplitude wrapper and added candidates around session-replay-specific consent category + bundle-size measurement, both deferred to the future Part-A audit.
When you spot a deferred decision in your PR's territory, name it
in your PR description and add it to SPEC.md §19.2 in the same
diff. Do not silently expand scope to settle it.
Test-coverage expectations
The backend has the load-bearing test suite at
backend/tests/. Tests are organized as *_vertical.py files,
each covering one feature end-to-end through the FastAPI app
(provisioning fixtures, hitting the HTTP surface, asserting on the
database state). At time of writing, the suite is ~250 tests across
~25 files. Examples:
test_admin_create_user_invite_vertical.py— v0.17.0's admin-create user + invite + claim flow, 15 tests covering happy path + every refusal shape + the audit-trail row.test_rfc_invitations_vertical.py— v0.16.0's per-RFC invite + accept flow, 18 tests.test_device_trust_vertical.py— v0.11.0's 30-day device trust, 14 tests including cookie shape, hash-vs-raw-token discipline, expired / revoked / forged / cross-user invariants.
Expected coverage for a new feature:
- Backend feature — one new
test_<feature>_vertical.pyfile that covers the happy path, every documented refusal/error code, and any cross-surface effect (rows the feature writes to existing tables, fields it adds to existing endpoints). Reuse fixtures from neighboring test files (e.g.test_propose_vertical.py'sFakeGiteais widely reused). - Migration — verify migrations are reachable from
backend/.venvbefore pushing:cd backend && PYTHONPATH=. .venv/bin/pytest -qexercisesdb.run_migrations()through the fixture setup. - Frontend feature — there is currently no frontend test
runner. The discipline is: keep the change ships-clean
(
cd frontend && npm run buildsucceeds), and the backend vertical test exercises the HTTP contract the frontend consumes, which is the meaningful behavioral guarantee. Frontend changes that ride along with a backend feature land with the backend test as the regression boundary. - Bug fix — add a regression test in the same vertical file that proves the original failure mode and verifies the fix.
Run the backend suite before pushing. From backend/:
PYTHONPATH=. .venv/bin/pytest -q
(The PYTHONPATH=. is a known ergonomic gap — see SPEC.md §19.2
candidate; the suite does not pick up app/ without it.)
If your PR doesn't include tests, the operator will ask for them before merge unless the change is genuinely test-irrelevant (documentation, comments, dev-only tooling).
Analytics instrumentation checklist
(This section codifies
ohm-rfc/ROADMAP.md#21 Part B's CONTRIBUTING checklist. It is discipline, not a gate — but the operator will push back on PRs that skip it.)
If your PR adds or changes a user-facing feature, walk this
checklist before opening the PR. The instrumentation conventions
themselves are specified in SPEC.md §21 (Analytics instrumentation
and identity); this section is the procedural reminder.
-
What named event(s) does this feature need? Open
frontend/src/lib/analytics.jsand look at theEVENTSconstant. Does an existing event cover your feature? If not, is the new event in the spec's "Subject Verb" Title Case form (Comment Posted,Invitation Sent)? Are the prop families consistent with SPEC.md §21's required-prop catalog (opaque ids only, no PII, enums lowercased like'otc'not'OTC')? -
Do interactive elements have stable text / ARIA labels /
data-amp-track-*so autocapture is meaningful? The frontend shipsautocapture: true, which instruments every click and form interaction. The value of those events depends on the DOM the SDK sees: a<button>with stable visible text or anaria-labelshows up as a meaningful dashboard row; an icon-only<button>with no label shows up as garbage. New components that introduce interactive elements should either carry meaningful labels (visible text or ARIA) or carry adata-amp-track-name="<Stable Name>"attribute. For repeated rows (per-RFC lists, comment lists), use a stabledata-amp-track-*identifier so per-row click counts aggregate to the row's identity rather than to a generic label. -
Does any new form field need replay masking? Session replay records at
sampleRate: 1(100% of consented sessions). New form inputs that capture passwords, OTC codes, tokens, magic-link URLs, or other secret/credential-equivalent material MUST be masked with Amplitude's masking conventions (the.amp-maskclass or thedata-amp-maskattribute, whichever the wrapper integration expects in this version). New inputs that capture arguably-PII (email, real name, free- text drafts) SHOULD also be masked; if a deliberate un-masking decision is taken, document it in the PR description and inSPEC.md§21. -
Does the PR description name the instrumentation decisions? A one-sentence summary in the PR description — "fires
Comment Postedwith{rfc_slug, comment_id}; no new form fields, no new replay-masking concerns" — is enough. If the decision is "we chose not to instrument this," say that too; the absence of an event is itself a decision the operator wants visible. The relevant SPEC chapter (§21) is the binding reference for what shapes are correct.
If your feature touches an identity-meaningful surface (sign-in,
sign-out, invite-claim, role change, account state change), also
walk the identity lifecycle contract in SPEC.md §21.6: every
new claim/sign-in path MUST call identify({ user_id, properties })
BEFORE the first track() event on that surface, so the Amplitude
user record is created with the OHM user_id from the very first
event rather than as an anonymous device that retroactively links.
v0.16.0's AcceptInvitation.jsx and v0.17.0's InviteClaim.jsx
are the worked examples; mirror their shape.
The operator-only gestures
Some gestures are operator-only. Contributors do not perform them; PRs that perform them get rejected on principle, not on merit:
- Tagging a release (
git tag v0.X.Y+git push --tags). - Pushing to
mainafter merge (the operator merges; the framework'smainbranch tracks releases the operator has shipped). - Bumping
VERSIONandfrontend/package.json#versionto the shipped value. The operator does this at integration time so the version line is consistent across the release commit. - Running
flotilla deployor any equivalent deployment gesture in any deployment of rfc-app. Contributors do not deploy. - Moving a deployment's
.rfc-app-versionpin. That pin lives in the deployment's content repo (e.g.ohm-rfc/.rfc-app-version) and is moved by the deployment's operator. Contributors to that deployment do not move it; contributors to the framework certainly do not. - Setting secrets (anywhere — Secret Manager, env files,
flotilla secret set, vendor dashboards, anything). The binding rule baked in mid-Session-K is: the assistant never asks the operator to paste secret bytes into a conversation, even as one offered option. The corollary for contributors: do not include secret values in PR descriptions, commit messages, or issue comments. Reference secrets by their binding name (SMTP_PASSWORD,AMPLITUDE_API_KEY) and let the operator handle the bytes.
If your change requires a new secret or env var, document the
requirement in the CHANGELOG ### Upgrade steps block in the
RFC 2119 form ("operators MUST set <NEW_VAR> ...") and
update the *.env.example file. The operator will run the
secret/overlay-set gesture themselves at deploy time.
When in doubt
- Open an Issue first. Especially for any change that touches SPEC.md, the auth/permissions model (§6), the storage shape (§4 / §5), or the deploy contract (§20). The operator (or a future driver session) will name what they want before you write code.
- Read the most recent session transcript. It will tell you what shipped last and what's in flight.
- Cite SPEC.md sections in your PR description. "Touches §15.4 (per-category email toggles) and adds §19.2 candidate around per-channel mute granularity" gives the operator a map of where to read.
License
The framework is released under the MIT License (see
LICENSE). By contributing, you agree your work
ships under those terms.
See also
SPEC.md— the framework's binding spec. §19.2 is the deferred-decisions queue; §20 is the versioning + deploy contract; §21 is the analytics instrumentation contract.CHANGELOG.md— release history in strict descending order. Read recent entries for the shape your PR's release-commit will take.PHILOSOPHY.md— what the framework is for. PRs whose shape conflicts with the philosophy get a longer conversation than PRs that fit.wiggleverse/ohm-session-history— the authoritative record of how the project has actually evolved, session by session.