Contributing
# Contributing
Audience: anyone who wants to change ocbot’s code or docs. Purpose: set up a dev environment, meet the quality floor, and get a PR merged. Prerequisites: Getting started through Step 3 (a building workspace); Architecture for the mental model.
# Dev setup
-
Install rustup . The pinned toolchain (1.98.1) is selected automatically from
rust-toolchain.toml— do not override it. -
Clone and build:
git clone <TODO: repo URL> cd ocbot cargo build cargo test --workspace -
Install the auxiliary checkers CI uses (optional locally, required to reproduce CI):
Tool What it does Install cargo-nextest Faster test runner cargo install cargo-nextest --lockedcargo-deny Advisory/license/ban checks cargo install cargo-deny --lockedcargo-machete Unused dependency check cargo install cargo-machetecargo-llvm-cov Coverage cargo install cargo-llvm-covcargo-mutants Mutation testing cargo install cargo-mutantstypos-cli Docs spelling cargo install typos-cli
# Where things live
| You are changing… | It lives in… |
|---|---|
| Decisions: state machine, classification, rendering, policies | crates/ocbot-core/src/ — pure Rust, no I/O allowed |
| The opencode client | crates/opencode-client/src/ — published as opencode-v2-client |
| Discord adapter, handlers, loops, store, bootstrap | crates/ocbot/src/ |
| Structural security gates | scripts/gates.sh |
| Config keys | crates/ocbot/src/config.rs; the example in config/config.toml.example |
Docs have one home per topic — see the index . The glossary is normative: use its names in code comments, docs, and error strings.
# The lint floor
The floor is defined in the workspace Cargo.toml and enforced with -D warnings in CI.
unsafeis forbidden everywhere.- In production code:
unwrap,expect,panic,todo!,unimplemented!,unreachable!,dbg!,print!, andas-conversions are denied. ReturnResult; type-convert explicitly. missing_docsis a warning; public items need doc comments.- clippy default groups are denied;
pedanticandcargogroups warn. - Every
#[allow]needs areason. - Tests are exempt by design:
allow-unwrap-in-testsis set inclippy.toml, and integration tests carry a crate-level allow with a reason — tests assert by unwrapping.
clippy.toml adds structural bans: std::process::Command::new, tokio::process::Command::new, and std::process::exit are disallowed methods — process spawning belongs to the git adapter, exit codes to main’s doctor path.
Formatting: rustfmt with edition 2024, max_width = 100, stable options only.
Run cargo fmt --all before committing.
# The gates
./scripts/gates.sh is the executable spec: structural checks that lint cannot express, G1–G20.
Every detector registers a probe — a snippet that must trigger it — so pattern rot turns CI red instead of silently passing.
Highlights:
| Gate | Enforces |
|---|---|
| G1 | No deny_unknown_fields in the client — unknown wire fields are ignored, not rejected |
| G2 | ocbot-core has no I/O dependency (cargo tree — the compiler-enforced layering) |
| G3 | No unwrap/expect in core production source (test blocks excluded) |
| G7/G11 | Process spawning is confined to the git adapter |
| G8/G15 | No shell interpreter is spawned anywhere in the workspace |
| G9 | The store is born with WAL mode |
| G10 | The bot never pushes and never --forces |
| G12 | The shipped permission baseline guards the dangerous families |
| G13/G14 | per_session_ruleset and allow_freeform_paths ship default-off |
| G17 | No secret value is committed |
| G18 | The metrics listener binds loopback only |
| G19 | The deployment units run unprivileged and memory-capped |
| G20 | Event frames are treated as untrusted input |
Run the whole local suite before pushing:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo nextest run --workspace
./scripts/gates.sh# Commits and branches
[ASSUMED] Trunk-based development: short-lived branches, pull requests into main; CI runs on both.
Commit style follows the existing history: an imperative summary line of at most 72 characters, a blank line, then an optional body explaining why.
Examples from the log:
Supervisor: one connection, owned honestly
store actor cleanup
parse event endpoint into EventFrame headers with telemetryNo strict Conventional Commits prefix scheme — the summary sentence is the convention.
# CI and the PR checklist
CI runs on every push to main and every pull request.
| Job | Runs |
|---|---|
fmt |
cargo fmt --all -- --check |
lint |
cargo clippy --workspace --all-targets -- -D warnings |
test |
cargo nextest run --workspace + doc tests |
deny |
cargo deny check (advisories, licenses, bans) |
machete |
Unused dependencies |
doc |
cargo doc --no-deps with warnings denied |
gates |
./scripts/gates.sh |
coverage |
Per-crate floors — see TESTING |
mutants |
Mutation check scoped to core decision modules |
typos |
typos docs README.md |
client-publish-dry-run |
cargo publish --dry-run -p opencode-v2-client |
A nightly job (schedule or manual dispatch) runs fuzzing, the live contract suite against a real opencode server, the full core mutation run, and the real-SIGKILL chaos half.
Before you open a PR, check locally: fmt, clippy, nextest, gates, and coverage for the crates you touched. Keep PRs small; one behavior per PR. Testing expectations live in TESTING — name the test you added for the behavior.
# Reporting bugs and proposing features
Open an issue at [TODO: repo URL]/issues with:
- what you sent (command, button) and what the bot replied,
/statusoutput,- relevant log lines — logs are already secret-scrubbed, but double-check pastes,
- opencode server version (the pin matters — see client COMPAT ).
Feature proposals: state the operator workflow you want, not the implementation. Single-operator scope is a design constraint, not an oversight — see the non-goals in ARCHITECTURE .
Security issues: do not open a public issue. [TODO: private disclosure contact]
# Code of conduct
Everyone participating follows CODE_OF_CONDUCT .