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.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.
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. See Embedding OSPREY Panels for the full dual-mode checklist and the well-known query/hash parameters.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.
"""
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