- Always use requirements:
x.x(ensures patch compatibility) - Example:
serde = "1.0"
Why This Matters:
- Silent failures corrupt data and leave systems in undefined states
- Half-completed operations are worse than crashes (harder to debug, data inconsistency)
- Errors cascade: one swallowed error causes 10 mysterious failures downstream
- Logging without propagating gives false confidence that errors are "handled"
The Rule: Every error MUST propagate up the call stack. The program halts on errors.
CORRECT - Always propagate errors:
// Best: Use ? operator
operation()?;
// With context: Add context AND propagate
operation().context("failed during initialization")?;
// Log for observability AND propagate (both required!)
let result = operation().map_err(|e| {
tracing::error!("Operation failed: {e}");
e
})?;
// Explicit match when you need it
match operation() {
Ok(val) => process(val),
Err(e) => return Err(e.into()),
}FORBIDDEN - These all swallow errors:
if let Err(e) = operation() { log::error!("{e}"); } // No return!
operation().unwrap_or_default(); // Silent fallback
operation().ok(); // Discards error
let _ = operation(); // Explicitly ignoresSelf-Check: If you see if let Err or match ... Err without return Err or ?, it's a bug.
NOT "Error Handling": Adding logging is NOT fixing/handling an error. The error must propagate.
Preserve Error Chains: When converting anyhow::Error to String (for logging, wrapping in other error types, etc.), ALWAYS use format!("{:#}", e) (alternate Display). NEVER use e.to_string() or format!("{}", e) -- these show only the outermost context and hide the root cause.
- Library crates/modules: Use
thiserrorwith backtrace support - Binary main.rs & tests: Use
anyhow - Other derives: Use
derive_more(Display, From, Into, etc.)
- Always use Cargo workspace with single-responsibility crates
- Root
Cargo.tomldefines workspace, contains no code - CLI must be separate subcrate
- Structure:
project/,project-cli/,project-client/, etc.
- NEVER use
std::env::set_var()in tests (pollutes environment) - ALWAYS pass config through function parameters
- External integration tests: tests requiring live OpenShell, Claude Code inside a sandbox, real ffmpeg/Whisper inference, or network model downloads must be
#[ignore]locally and explicitly invoked by GitHub Actions jobs. Use stable ignore reasons and workspace-filterable test name prefixes together:ci-openshell: ...withci_openshell_,ci-claude: ...withci_claude_,ci-stt: ...withci_stt_.crates/right/tests/ci_ignored_contract.rsenforces this so future packages are not missed. - Cadence: TDD still applies, but use the narrowest useful command for the loop. Run the new/regression test first and verify it fails; after implementation rerun that test or the nearest package/module suite. Do not run full workspace tests after every edit or every small plan step.
- Targeting: Prefer
devenv shell -- cargo nextest run -p <crate> <filter>ordevenv shell -- cargo nextest run -p <crate>during development (cargo teststill works but nextest is the recommended runner; doctests run only undercargo test --doc). Use workspace-wide tests midstream only for broad cross-crate changes or when targeted results cannot prove the behavior. - Worktrees: At worktree start, run one baseline verification appropriate to the planned scope and record existing failures. At worktree completion, run the final full workspace test from inside that worktree.
- Shared test sandbox: live OpenShell I/O tests reuse one cross-process sandbox per runner invocation, named
right-test-shared-<label>-<runid>(runid=RIGHT_TEST_RUN_IDor the runner's pid). It is not deleted on test exit. CI runners are ephemeral so nothing accumulates there; locally, prune leftovers withopenshell sandbox listthenopenshell sandbox delete right-test-shared-.... Never delete one mid-run — a differentrunidmay be live. - Final verification: Before declaring code work complete, run
devenv shell -- cargo nextest run --workspaceplusdevenv shell -- cargo test --doc --workspace. This is mandatory even when all targeted tests passed. - Tests in same file using
#[cfg(test)]module - Large files: If file exceeds 800 LoC and tests are >50% of content, extract tests to separate file:
Keep test file in same directory as source (e.g.,
#[cfg(test)] #[path = "mymodule_tests.rs"] mod tests;
src/mymodule.rs->src/mymodule_tests.rs)
- CLI-First: Never bypass CLI argument parsing
- NEVER use
Defaulttrait that reads environment - ALWAYS use
from_cli_args()factory methods - Config flows: CLI args -> Config struct -> Client
- Location:
helpers/directory - Initialize:
uv init helpers/ - ALWAYS use
uv add <package>(NEVERuv pip install)
- Visibility: Private (default) > pub(crate) > pub
- Magic Numbers: Use
constor CLI args, never literals - Async: Use tokio consistently
- Breaking Changes: OK for internal crates, preserve HTTP/WebSocket compatibility
- Cargo edition: Use 2024 edition