Contributing to Osprey#
Thank you for your interest in contributing to the Osprey Framework. This guide covers environment setup, Git workflow, code standards, and community guidelines.
Environment Setup#
Prerequisites: Python 3.11+, Git, a GitHub account, and uv.
1. Fork and Clone
git clone https://github.com/YOUR-USERNAME/osprey.git
cd osprey
2. Install Dependencies
# Install all dev and docs dependencies (creates .venv automatically)
uv sync --extra dev --extra docs
# Add a new dependency
uv add <package>
3. Set Up Pre-commit Hooks
pre-commit install
Hooks auto-fix formatting and prevent commits with common problems.
4. Verify Installation
uv run pytest tests/ --ignore=tests/e2e -v
If all tests pass, you are ready to contribute.
Git and GitHub Workflow#
Branch Strategy#
Osprey follows GitHub Flow: a single long-lived branch (main) with
short-lived topic branches that PR back into it. Releases are CalVer tags
(vYYYY.M.P) on main — no separate release branch.
What this means for contributors:
Branch your work off
main, and open your PR againstmain.mainis always the integration target. CI gates every PR; protected status checks must pass before merge.Releases are cut by maintainers tagging a commit on
main; the PyPI publish workflow runs onv*.*.*tags.The
osprey-connectorsworkspace package releases independently viaosprey-connectors-v*tags. Because the framework wheel depends on it from PyPI, a connectors version satisfying the framework’s requirement must be published before the framework tag that needs it.Hotfixes follow the same path: branch from the tag (or
main), PR back, tag again asvYYYY.M.P+1. No special hotfix branches.
Branch Naming#
feature/description– New featuresfix/description– Bug fixesdocs/description– Documentationrefactor/description– Code refactoringtest/description– Test improvements
Making Changes#
1. Create a branch:
git checkout -b feature/your-feature-name
2. Make changes – follow the code standards below, add tests, update docs.
3. Test locally using the three-tier system:
# Tier 1: Quick check (< 30s) -- before every commit
./scripts/quick_check.sh
# Tier 2: Full CI check (2-3 min) -- before pushing
./scripts/ci_check.sh
# Tier 3: Pre-merge check -- before creating a PR (compare against your PR target)
./scripts/premerge_check.sh main
4. Commit changes using conventional commit format:
git add .
git commit -m "feat(scope): short description
- Detail about what changed
- Another detail"
Commit Message Format#
feat:– New featuresfix:– Bug fixesdocs:– Documentationrefactor:– Code refactoringtest:– Testschore:– Dependencies, build
Every commit needs a corresponding CHANGELOG entry added before committing.
Pull Request Process#
Push your branch:
git push origin feature/your-feature-nameOpen a PR on GitHub with a description, related issues, and testing performed.
PR requirements: pass all required CI checks, include a
CHANGELOG.mdentry for any user-visible change, and add appropriate tests. Internal-mode contributors with push access self-merge after CI is green (the ruleset does not require human approval); fork-mode contributions wait for a maintainer to merge.During review: respond to feedback promptly, make requested changes, ask questions if unclear.
Branch Protection on main#
Direct pushes to main are rejected. All changes land via PR. The ruleset
enforces:
All required CI checks must pass (no admin bypass).
Linear history (use
gh pr merge --rebase; merge commits are rejected).Force-pushes and branch deletion on
mainare denied.
If a required check turns out to be wrong, fix it forward — there is no escape hatch.
Dependency Update Pull Requests#
Dependabot opens pull requests for dependency bumps, subject to a seven-day cooldown: a release is not proposed until it has been public for a week, so that a hijacked or malicious version has time to be found and yanked before it reaches this repository. Security updates are exempt, by design — a fix for a known vulnerability should not wait.
These pull requests run with a deliberate gap in coverage. GitHub treats a
Dependabot-triggered run as if it came from a fork: it receives only the
Dependabot secret store, never the repository’s Actions secrets. The lanes that
need a live model endpoint therefore skip rather than run — the agentic
flows, the E2E suite, the dispatch stacks, and the two chat bridges. The run
summary of the All CI Checks Passed job names them explicitly, so a green
check on a dependency PR is never mistaken for full coverage.
To close that gap before merging, review the diff and then revalidate the branch yourself. Because you trigger it, that run gets the normal secrets:
gh workflow run ci.yml --ref <dependabot-branch> -f revalidate_secret_lanes=true
Find the branch name with gh pr view <number> --json headRefName. Watch the
resulting run to completion before merging.
The reason this is a manual step rather than an automatic one is worth stating: mirroring the model API key into the Dependabot secret store would make these lanes pass unattended, but it would also hand a live credential to a newly-published third-party package at install time — the precise supply-chain exposure the cooldown exists to reduce. The human read of the diff is the point, not an inconvenience around it.
Osprey Agent Workflow Skill#
If you use the Osprey agent (e.g., via Claude Code),
install the bundled osprey-contribute skill to get guided help following
this workflow:
uv run osprey skills install osprey-contribute
The skill walks you through branching, commits, push, PR, and CI iteration,
auto-detecting whether you have push access to als-apg/osprey or are
contributing from a fork. It composes with the other bundled skills:
osprey-pre-commit– standalone validation runscommit-organize– splits a messy working tree into atomic commitsosprey-release– the release-cutting flow for maintainersosprey-design-philosophy– OSPREY’s design and architecture principles, for designing or reviewing a feature before you open the PR
List all installable skills with uv run osprey skills install --help.
Code Standards#
Design Principles#
Before designing a new connector, MCP server, provider, capability, or any non-trivial feature, consult OSPREY’s design and architecture principles – the safe-state default, facility-neutral core, measured symmetry with peer subsystems, swappable components, and discoverable user-facing features. Install the bundled skill so the Osprey agent applies them as you design and review:
uv run osprey skills install osprey-design-philosophy
The principles guide decisions; they are not mechanical rules. When a change feels wrong but the reason is hard to name, they help you name the drift and correct it before you open the PR.
Python Style#
We follow PEP 8 with Ruff enforcement:
Line length: 100 characters
Type hints: Gradual typing enforced with mypy
Docstrings: Google style
Classes: PascalCase, Functions: snake_case, Constants: UPPER_SNAKE_CASE
Import organization: standard library, then third-party, then local (from osprey...).
Linting and Formatting#
# Lint and format
uv run ruff check src/ tests/
uv run ruff format src/ tests/
# Auto-fix lint issues
uv run ruff check --fix src/ tests/
# Type checking
uv run mypy src/
Testing#
All new functionality must include tests.
Type |
When to Use |
Cost/Speed |
|---|---|---|
Unit |
Pure functions, business logic, utilities |
Fast, no external dependencies |
Integration |
Component interactions, API endpoints |
Medium |
E2E |
Critical user flows, deployment validation |
Slow, requires API keys ($0.10-$0.25/run) |
Browser |
Real-browser page loads, theming, JS module loading (Playwright + Chromium) |
Slow; needs Chromium (auto-installed in CI, skips locally if absent) |
Running tests:
# Unit tests (fast, no API keys required)
uv run pytest tests/ --ignore=tests/e2e -v
# Single test file
uv run pytest tests/path/to/test_file.py -v
# Single test function
uv run pytest tests/path/to/test_file.py::test_function_name -v
# E2E tests (requires API keys) -- MUST use path, NOT marker
uv run pytest tests/e2e/ -v
# Browser smokes (Playwright + Chromium; skips if the browser is absent)
uv run pytest tests/interfaces/ -m browser -v
# With coverage
uv run pytest tests/ --ignore=tests/e2e --cov=src/osprey
Warning
E2E tests must be run with pytest tests/e2e/ not pytest -m e2e.
The marker-based approach causes registry state leaks and service conflicts.
Front-End (JavaScript) Testing#
Front-end code (static/js/ under each src/osprey/interfaces/<name>/) gets
its own dev/CI-only Node toolchain – tsc --noEmit for types and
Vitest for unit tests. Neither is needed to install or
run Osprey; both run only in dev and CI, the front-end analogues of mypy
and pytest.
# Type-check every // @ts-check'd file (opt-in per file -- see below)
npm run typecheck
# All Vitest units (happy-dom, no real browser)
npm run test:js
# A single Vitest file
npx vitest run tests/interfaces/artifacts/preview.test.mjs
A front-end change is covered by up to five rails, narrowest/fastest to broadest/slowest:
Rail |
Covers |
Run |
|---|---|---|
Vitest unit |
Pure logic in one module: mocked |
|
Loads-clean |
Page boots in a real browser with no uncaught JS exception and no failed same-origin script/stylesheet fetch |
|
Contract |
Shell<->panel chrome contract: |
|
Visual |
Pixel-level screenshot diff per interface x theme against a committed baseline PNG |
|
Interaction pin |
A real user flow through a real browser and a real backend, proving a multi-module wiring didn’t drop a call across a split – the one net a per-module Vitest suite (which mocks its neighbors) cannot cast |
|
Warning
test_visual.py is marked slow, not browser – an
-m browser selector silently matches zero of its cases. Select it with
-k <target-name> (or run the file directly) instead.
What a new panel or extracted module must ship with:
// @ts-checkas line 1, plus full JSDoc (@param/@returns).tsconfig.jsonsetscheckJs: falserepo-wide, so a file is type-checked only once it opts in – a module missing the pragma makesnpm run typecheckpass while checking nothing inside it.A Vitest file, one-to-one by name (
foo.js->foo.test.mjs), covering the module’s pure logic and DOM-visible behavior in isolation.If the module is wired into a page: a
test_load_smokes.pyentry for that page, so a broken import or a typo’d export name surfaces as a real thrown exception instead of a silent no-op.If it’s a panel (embeddable in the Web Terminal hub): support both standalone and embedded modes –
applyEmbedded()on load, an<osprey-theme-switcher>that hides itself when embedded (the hub owns theme chrome there), branding hidden when embedded – plus atest_contract_params.pycase, which is the up-to-date spec for the dual-mode checklist and well-known parameters. See Panels for how panels embed in the hub.If the change is visible on screen: a
test_visual.pyTARGETSentry and a committed baseline PNG (regenerate with--regen-baselineson Linux/CI – a baseline captured on macOS will mismatch there).If the change moves real behavior across a module boundary (a callback that used to call a sibling directly now goes through an injected factory, a delegator, or a re-exported method): an interaction pin that drives the whole chain through a real browser, not just each module’s own mocked-neighbor Vitest suite.
JSDoc / cast conventions (src/osprey/interfaces/vendor-globals.d.ts and
the exemplars below have worked examples):
Vendored classic-script globals that never get real npm types (Plotly,
marked,hljs, KaTeX) get one shared ambient-declarations file (vendor-globals.d.ts,declare const X: any;) rather than a per-call-site cast.A
document.getElementById/querySelectorresult that needs a property the genericElement/HTMLElementtype doesn’t have (.value,.checked,.dataset,.disabled) gets an inline type-assertion cast to the concrete element type at the call site –/** @type {HTMLInputElement} */ (document.getElementById("foo")).For
querySelectorAll(...).forEach(callback)where the callback needs a narrower element type thanElement: cast the collection, not the per-item callback parameter. TypeScript’s contravariant function-parameter checking rejects a callback typed to accept onlyHTMLInputElementwhere one accepting anyElementis expected, even thoughHTMLInputElementnarrowsElement.catch (e)blocks that read the error message usee instanceof Error ? e.message : String(e)(eisunknownunderstrict); acatchthat only logs the raw value needs no cast.Shared design-system helpers are imported by the absolute
/design-system/js/*specifier, mapped in bothtsconfig.jsonandvitest.config.js’sresolve.alias– the same import path resolves under the type-checker, Vitest, and the real browser.
Exemplars (concrete, complete, worth reading before writing your own):
Vitest unit, factory-with-injected-callbacks pattern:
tests/interfaces/artifacts/preview.test.mjs,tests/interfaces/lattice_dashboard/render.test.mjs,tests/interfaces/web_terminal/scaffold-detail.test.mjs.Interaction pin, proving a multi-module split still wires up end to end:
tests/interfaces/artifacts/test_gallery_interactions.py,tests/interfaces/web_terminal/test_scaffold_detail.py,tests/interfaces/lattice_dashboard/test_settings_form.py,tests/interfaces/web_terminal/test_session_page.py.Contract + dual-mode chrome:
tests/interfaces/web_terminal/test_contract_params.py.Visual baselines:
tests/interfaces/design_system/test_visual.py.Loads-clean:
tests/interfaces/test_load_smokes.py.
Docstrings#
All public functions, classes, and methods need Google-style docstrings:
def capability_function(param1: str, param2: int) -> bool:
"""Short description of function.
Args:
param1: Description of first parameter.
param2: Description of second parameter.
Returns:
Description of return value.
Raises:
ValueError: When parameter is invalid.
"""
Refreshing documentation screenshots#
The committed doc images under docs/source/_static/screenshots/ are
regenerated from a declarative registry, not captured by hand. Each image is one
DocShot recipe in docs/screenshots/recipes.py – the authoritative list
of every doc screenshot and how it is produced. List them with:
$ python -m docs.screenshots list
The default is container-free. make screenshots (or python -m
docs.screenshots) captures only the standalone_interface recipes – each
boots a single interface create_app() on a throwaway port, so it needs
neither a container runtime nor seeded data. Regenerate one recipe with
make screenshots-<name>.
Two opt-in environments cover the images that need real data:
SCREENSHOTOPTS=--stack– the ARIEL search/browse/create/status views. Builds thecontrol-assistanttutorial project, brings up Postgres (osprey up -d), and seeds the logbook withosprey sim apply nominal --yes --now <anchor>. Needs a container runtime and a free host port 5432. The--nowanchor freezes the seeded dates, so repeat captures are byte-stable.SCREENSHOTOPTS=--agentic– the Web Terminal hero. Drives a live agent session to produce a real beam-current plot, so it needs a live Claude session on your subscription budget. Success is a structural check (non-blank image, correct viewport, plot present), not a byte comparison.
$ make screenshots # default: standalone only
$ make screenshots SCREENSHOTOPTS=--stack # + ARIEL views (containers)
$ python -m docs.screenshots --agentic --only web_terminal_hero
Provenance is automatic. Every capture stamps manifest.json with the
OSPREY version and UTC timestamp, and each figure’s “Captured with OSPREY
vX.Y.Z” caption is generated from it – never hand-edit the version in a
caption.
This framework is capture-only: it is never a CI gate (the stack needs
Postgres; the hero needs a live agent). It is distinct from the CI visual-drift
guard – pixel diffs of each rendered interface against a committed baseline
live in the front-end Visual tests above (regenerated with
--regen-baselines), and continue to run in CI unchanged.
Reviewing a web-interface redesign (contact sheet)#
When a web interface is being restyled, the contact-sheet renderer boots the
real interface in every theme/mode variant and folds the shots into one
self-contained page, so a whole redesign can be reviewed as a single artifact –
no live agent, provider, hardware, or network. It lives beside the screenshot
framework in docs/screenshots/ but is a review tool, not a committed doc
image: nothing it produces is checked in or CI-gated.
$ uv run python -m docs.screenshots.contact_sheet --out /tmp/sheet
That captures the Web Terminal’s four shells – the dark and light themes
crossed with the expert and simple UI modes – writes one PNG per cell
into the output directory, composes them into contact-sheet.html there, and
prints its path. Open that one file to review every variant side by side.
Comparing accent candidates. Add --accents to render each of the four
variants twice, once under each accent candidate (blue vs teal), so a pending
accent decision can be made from real output rather than a mockup:
$ uv run python -m docs.screenshots.contact_sheet --out /tmp/sheet --accents
To keep every cell looking like a working session with no live backend, the renderer points the workspace panel at a pre-seeded demo store and replays a canned terminal transcript. That transcript is width-guarded against the narrow terminal card – the run fails fast if a line would overflow, before any browser launches. Where no browser runtime is available the run skips with a one-line notice instead of erroring.
Extending it to another target. The variant grid is the VARIANTS list of
(theme, mode) tuples near the top of contact_sheet.py, and the
completeness invariant _FULL_MATRIX mirrors it – add a cell to both to
capture a new theme/mode combination. To cover a new panel, seed its backing
store the way seed_demo_workspace seeds the workspace artifacts and wire it
into hermetic_hub so the panel renders populated; the capture loop and the
composed sheet then pick it up unchanged.
Community Guidelines#
Code of Conduct: We are committed to a welcoming and inclusive environment. Be respectful, welcome newcomers, accept constructive criticism, and show empathy. Harassment, personal attacks, trolling, or publishing private information are unacceptable. Report issues to the maintainers; all reports are handled confidentially.
Communication Channels:
GitHub Issues – Bug reports, feature requests, task tracking
GitHub Discussions – Questions, ideas, brainstorming
Pull Requests – Code contributions, documentation, code review
Reporting Bugs: Search existing issues first, then open a bug report with a clear description, reproduction steps, environment details (OS, Python version, Osprey version), and full error messages.
Feature Requests: Describe your use case, current limitations, proposed solution, and alternatives considered.
Response Expectations: Maintainers are volunteers. Please be patient and provide clear, detailed information.
Getting Help#
GitHub Discussions – Ask questions, share ideas
GitHub Issues – Report bugs, request features