Thanks for your interest! Here's how to get started.
git clone https://github.com/sipyourdrink-ltd/bernstein && cd bernstein
uv venv && uv pip install -e ".[dev]"uv run python scripts/run_tests.py -x # all tests (isolated per-file, stops on first failure)
uv run python scripts/run_tests.py -k router # filter by keyword
uv run pytest tests/unit/test_foo.py -x -q # single file (fast)WARNING: Never run
uv run pytest tests/ -x -q- the full suite keeps references across 2000+ tests and can leak 100+ GB RAM. The isolated runner inscripts/run_tests.pycaps each file at ~200 MB.
uv run ruff check src/
uv run ruff format src/
uv run pyright src/All three must pass before committing. No exceptions, no "fix later."
- Fork the repo and create a branch:
git checkout -b feat/my-feature - Make your changes
- Run checks:
uv run ruff check src/ uv run pyright src/ uv run lint-imports # architecture contracts (adapter/core boundary) uv run python scripts/run_tests.py -x - Commit with a clear message
- Open a PR against
main
All non-trivial changes land via PR with at least one approving review. Security-touching changes need two approvals or operator-only push. Full process and reviewer expectations: docs/CODE_REVIEW.md.
Every PR that adds or changes a feature MUST update docs in the same PR:
- User-visible behaviour: update the relevant
README.mdsection. - Operator workflows: update
docs/operations/<area>.md. - Public API surface: regenerate
docs/api/schemas. - Architecture or new module: update
docs/sdd/and runuv run bernstein agents-md syncso AGENTS.md, CLAUDE.md,.goosehints,CONVENTIONS.md, and.cursor/rules/*.mdcstay aligned. - New test layer: also update
docs/contributing/testing.md.
PRs without the matching docs change will be sent back. Docs and code ship together.
Install the versioned pre-push hook to catch lint and architecture-contract violations before they reach CI:
ln -sf ../../scripts/git-hooks/pre-push .git/hooks/pre-push
chmod +x scripts/git-hooks/pre-pushThe hook is blocking on ruff and lint-imports, advisory on pyright.
The most common architecture-contract violation it catches is an adapter
importing a scheduler-internal module, e.g.
from bernstein.core.tasks.models import ModelConfig (wrong) instead of
from bernstein.core.models import ModelConfig (canonical). See
.importlinter for the full contract list.
When CI fails on main, the bernstein-ci-fix workflow
(.github/workflows/bernstein-ci-fix.yml) runs Bernstein in headless mode
against the failing commit, opens an auto-heal/<sha> branch with the
proposed fix, and creates an auto-heal: fix CI on <sha> PR for review.
If Bernstein can't produce a clean diff in 3 iterations within $5, the
workflow falls back to opening a ci-fix issue. Auto-heal is gated by
the BERNSTEIN_CI_FIX_ENABLED repo variable, refuses to recurse on
auto-heal: PRs, and only fires for canonical-repo pushes (never forks).
- Python 3.12+, type hints on every public function and method
from __future__ import annotationsat the top of every module- Max line length: 120 (enforced by ruff)
- Ruff rules: E, F, W, I, UP, B, SIM, TCH, RUF
- No dict soup - use
@dataclassorTypedDict, not rawdict[str, Any] - Enums over string literals for any value with a fixed set of options
- Google-style docstrings on all public symbols
- Async only for IO-bound code; sync for CPU-bound/pure logic
- Thin orchestration facade, isolated core logic, separate adapters
src/bernstein/core/routes/**.py is policed by a CI lint
(scripts/check_routes_broad_except.py) that fails the build when a
bare except Exception: clause appears without a justification marker on
the line itself or in one of the three preceding comment lines.
Two markers are recognised:
intentional-broad-except: legitimate best-effort path (telemetry, optional analytics, lineage append, etc.). The body should route any sensitive message throughbernstein.core.sanitize.sanitize_log.bot-ack: <short-tag>: previously reviewed broad clause; the tag identifies the rationale (e.g.bot-ack: pre-existing-1723,bot-ack: legacy-shim).
If the clause is not actually best-effort, narrow it to the realistic
exceptions the helper can raise (typically OSError, json.JSONDecodeError,
KeyError, ValueError, or a domain-specific class). Never convert a
broad except Exception: into except BaseException: -- KeyboardInterrupt
and SystemExit must propagate.
Run locally before committing:
uv run python scripts/check_routes_broad_except.pySee AGENTS.md for the full doctrine, including doctrine, change classification, conflict protocol, and zero-tolerance failures.
The CLI is split into two layers under src/bernstein/cli/:
Top-level (cli/): main.py (Click group), run.py, run_cmd.py, live.py, dashboard.py, helpers.py, ui.py, status.py
Commands sub-package (cli/commands/): 70+ command modules including:
| Module | Purpose |
|---|---|
run_cmd.py |
bernstein run / -g orchestration entry point |
stop_cmd.py |
bernstein stop graceful shutdown |
status_cmd.py |
bernstein status / bernstein ps |
evolve_cmd.py |
bernstein evolve subcommands |
agents_cmd.py |
bernstein agents catalog commands |
advanced_cmd.py |
Advanced / less common commands |
debug_cmd.py |
bernstein debug-bundle diagnostics |
cost.py |
Cost tracking display |
doctor_cmd.py |
Pre-flight health checks |
checkpoint_cmd.py |
Checkpoint save/restore |
task_cmd.py |
Direct task manipulation |
workspace_cmd.py |
Multi-repo workspace commands |
audit_cmd.py |
Audit log inspection |
ci_cmd.py |
CI integration commands |
policy_cmd.py |
Policy management |
triggers_cmd.py |
External trigger management |
When adding a new CLI command, create a new *_cmd.py module in cli/commands/ and register it in main.py.
Bernstein ships with 40+ CLI agent adapters, plus a generic catch-all. src/bernstein/adapters/registry.py is the source of truth for the exact set - check it before writing a new adapter. A subset is shown here for orientation:
| Adapter | File | Agent |
|---|---|---|
aider |
adapters/aider.py |
Aider |
amp |
adapters/amp.py |
Amp |
claude |
adapters/claude.py |
Claude Code |
codex |
adapters/codex.py |
Codex CLI |
cody |
adapters/cody.py |
Cody |
continue |
adapters/continue_dev.py |
Continue |
cursor |
adapters/cursor.py |
Cursor |
devin_terminal |
adapters/devin_terminal.py |
Devin Terminal (Cognition) |
gemini |
adapters/gemini.py |
Gemini CLI |
goose |
adapters/goose.py |
Goose |
iac |
adapters/iac.py |
Infrastructure-as-Code agent |
junie |
adapters/junie.py |
JetBrains Junie |
kilo |
adapters/kilo.py |
Kilo |
kiro |
adapters/kiro.py |
Kiro |
ollama |
adapters/ollama.py |
Ollama (local models) |
openai_agents |
adapters/openai_agents.py |
OpenAI Agents SDK v2 |
opencode |
adapters/opencode.py |
OpenCode |
q_dev |
adapters/q_dev.py |
AWS Q Developer CLI |
qwen |
adapters/qwen.py |
Qwen Code |
generic |
adapters/generic.py |
Any CLI agent (catch-all) |
Full list: src/bernstein/adapters/registry.py. Adapter guide: docs/adapters/ADAPTER_GUIDE.md.
Adapters implement the CLIAdapter ABC from adapters/base.py:
class CLIAdapter(ABC):
@abstractmethod
def spawn(self, *, prompt, workdir, model_config, session_id, mcp_config=None) -> SpawnResult: ...
@abstractmethod
def is_alive(self, pid: int) -> bool: ...
@abstractmethod
def kill(self, pid: int) -> None: ...
@abstractmethod
def name(self) -> str: ...
def detect_tier(self) -> ApiTierInfo | None: ... # optionalSteps:
- Create
src/bernstein/adapters/mycli.pyimplementing all four abstract methods. Seeadapters/claude.pyfor a complete reference. - Register in
adapters/registry.py:_ADAPTERS["mycli"] = MyCLIAdapter - Run checks:
uv run ruff check src/ && uv run pyright src/ && uv run python scripts/run_tests.py -x - Open a PR - include a short note on how you tested it.
Important: All adapter .spawn() implementations must wrap the CLI command with build_worker_cmd() from adapters/base.py. This sets the process title and writes the PID metadata file that the orchestrator uses for bernstein ps and crash detection.
CI parsers implement the CILogParser protocol from core/ci_log_parser.py:
class CILogParser(Protocol):
name: str
def parse(self, raw_log: str) -> list[CIFailure]: ...Steps:
- Create
src/bernstein/adapters/ci/<name>.pyfrom the template intemplates/ci-parsers/TEMPLATE.py. Seeadapters/ci/github_actions.pyfor a working example. - Register:
from bernstein.core.ci_log_parser import register_parser; register_parser(MyCIParser()) - Run checks and open a PR.
Role templates live in templates/roles/<role-name>/ with three files:
system_prompt.md- agent persona and standing instructionstask_prompt.md- per-task instructions templateconfig.yaml- default model and effort
Built-in roles: adversary, analyst, architect, backend, ci-fixer, devops, docs, frontend, manager, ml-engineer, prompt-engineer, qa, resolver, retrieval, reviewer, security, visionary, vp.
Copy an existing role and customize:
cp -r templates/roles/backend templates/roles/data-engineerThe new role is available immediately - no code changes required.
- Deterministic orchestrator - no LLM calls for scheduling/coordination
- Short-lived agents - spawn per task batch, exit when done
- File-based state - everything in
.sdd/, no databases - Pluggable adapters - new CLI agents via
adapters/base.pyABC - Branch is
main- nevermaster - No monoliths - don't create or extend god-files (>400 LOC soft, >600 LOC hard stop)
Bernstein writes PID metadata to .sdd/runtime/pids/. Use those to find and stop processes. Never pkill -f bernstein or pgrep bernstein - it will kill the orchestrator indiscriminately.
# Correct: signal via file
echo "stop" > .sdd/runtime/signals/<role>-<session>/SHUTDOWN
# Correct: use bernstein CLI
bernstein stop
# WRONG: grep-kill
pkill -f bernstein # kills everything including your own shell sessionAll contributors are listed in CONTRIBUTORS.md. Outstanding contributions are featured in our monthly Community Spotlight blog posts, which are shared on Twitter/X, LinkedIn, and dev.to.
By contributing, you agree that your contributions will be licensed under the Apache License 2.0.