This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Use rivets ready to see available work, rivets list to see all issues.
Issues live in Rivets at .rivets/issues.jsonl; use the connected Rivets MCP tools or the rivets CLI. See docs/agents/issue-tracker.md.
Use the five default canonical triage labels. See docs/agents/triage-labels.md.
This repo uses a single-context domain-doc layout. See docs/agents/domain.md.
Rivets is a Rust implementation of the Beads project tracking system - a CLI tool for AI-native issue tracking with dependency graphs, stored as JSONL files alongside code.
This project uses rivets for issue tracking (dogfooding our own tool). Issues are stored in .rivets/issues.jsonl. The .rivets/ directory is checked into git (issues travel with the repo).
rivets ready # Show issues ready to work on (no blockers)
rivets list # List all open issues
rivets list --status closed --limit 10 # Recent closed issues
rivets show <id> # Full issue details with dependencies
rivets stats # Project statistics
rivets blocked # Show issues blocked by dependencies# 1. Find work
rivets ready --limit 5
# 2. View issue details
rivets show rivets-xyz
# 3. Update status when starting
rivets update rivets-xyz --status in_progress
# 4. Close when done
rivets close rivets-xyz --reason "Implemented in commit abc123"# Basic issue
rivets create --yes --title "Fix the bug" --kind bug --priority 2
# Full issue with design notes
rivets create \
--yes \
--title "Add new feature" \
--kind feature \
--priority 2 \
--description "Detailed description here" \
--design "Implementation approach" \
--acceptance "- [ ] Criterion 1
- [ ] Criterion 2"Scripting: the issue kind flag is --kind (-k), not --type, and create prompts for confirmation unless --yes is passed.
Multi-line values: rivets stores flag values verbatim and does NOT interpret \n escapes — and neither does bash inside double quotes, so --acceptance "a\nb" stores literal \n text. Use a real newline inside the quotes (as above), or bash ANSI-C quoting: --acceptance $'- [ ] a\n- [ ] b'.
# Add dependency (issue-a blocks issue-b)
rivets dep add issue-a issue-b --type blocks
# View dependency tree
rivets dep tree issue-id
# See what's blocking an issue
rivets show issue-id # Shows dependencies sectioncargo build # Build all crates
cargo nextest run # Run all tests (~1400 tests, parallel)
cargo clippy # Lint (pedantic mode enabled)
cargo fmt --check # Check formatting
cargo fmt # Auto-format
cargo run --bin rivets -- <subcommand> # Run rivets CLI (workspace has 2 binaries; bare `cargo run` fails)Pre-commit hook: git commit re-runs fmt check, clippy, and the full nextest suite — a red test blocks the commit, and there's no need to run the gates separately right before committing.
Testing: Use cargo nextest run instead of cargo test for unit/integration tests. Nextest runs each test in its own process in parallel (~3.7x faster). Use cargo test --doc for doc tests only (nextest does not support doc tests).
unsafe_code = "forbid"- No unsafe code anywhereclippy::pedantic = "warn"- Pedantic lints enabled workspace-wide- CI runs
cargo clippy --all-targets --all-features -- -D warnings— every pedantic warning is a build break. Common surprise sources:clippy::doc_markdown(backtick bare identifiers likeSQLite,CrateInfo),clippy::unnecessary_wraps(Result<(), E>that always returnsOk(())),clippy::similar_names(e.g.by_ca/by_ce),clippy::missing_docs(public struct fields need///).
- Dead code on
pubitems pending a consumer: use#[allow(dead_code)], NOT#[expect(dead_code)].pubitems in a library crate don't triggerdead_codein the test binary target (they're considered API surface), so#[expect]fires as "unfulfilled" under-D warnings. pub(crate)in a lib crate is NOT visible to the bin crate's tests in the same package. Lib and bin are separate compilation units. If a constructor needs to be reachable from#[cfg(test)]blocks in a bin module of a lib+bin package (e.g.rivetsorrivets-mcp), keep itpubwith a doc comment explaining the intent, notpub(crate).
cargo nextest run -p rivets-jsonl # JSONL library tests only
cargo nextest run -p rivets # Core + CLI tests only
cargo nextest run -p rivets-mcp # MCP server tests onlyThese tools are installed and should be used when appropriate:
| Tool | When to use |
|---|---|
cargo nextest run |
Always use instead of cargo test for running tests |
cargo expand |
Debugging derive macros or proc macros — shows the actual expanded code |
cargo machete |
After removing code or refactoring — finds unused dependencies in Cargo.toml |
cargo deny check |
Checking for RUSTSEC advisories and license compliance |
hyperfine |
Benchmarking CLI commands (e.g. hyperfine 'cargo nextest run' 'cargo test') |
tokei |
Quick line-count / language stats for the workspace |
This project uses Conventional Commits.
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
feat: New feature (MINOR version bump)fix: Bug fix (PATCH version bump)docs: Documentation changesstyle: Code style (formatting, semicolons, etc.)refactor: Code refactoringperf: Performance improvementstest: Adding or updating testsbuild: Build system or dependenciesci: CI/CD configurationchore: Maintenance tasks
Note: These are a convention, not a gate — CI no longer validates commit subjects. Follow them for readable history, but a non-listed type (e.g. plan(...) or design(...) from the gilfoyle workflow) will not fail a build.
Scopes must be lowercase. Common scopes:
cli: CLI commands and interfacestorage: Storage layer and persistencemcp: MCP server functionalityjsonl: JSONL library
Use ! after type or add BREAKING CHANGE: footer for MAJOR version bumps.
feat(cli): add export command
fix(storage): handle empty files gracefully
docs: update installation instructions
feat!: redesign API (breaking change)
refactor(mcp): simplify tool registration
Cargo workspace with 3 crates:
| Crate | Type | Purpose |
|---|---|---|
rivets |
bin + lib | Core issue tracker: domain model, storage, CLI, commands |
rivets-jsonl |
lib | JSONL file format library (atomic writes, streaming reads, queries) |
rivets-mcp |
bin + lib | MCP server exposing rivets tools to AI assistants |
Tethys (code intelligence engine) has moved to its own repository.
src/cli/- Clap command definitionssrc/commands/- Command implementationssrc/domain/- Core domain types (Issue, Priority, Status, etc.)src/storage/- Persistence layer (JSONL-backed)src/output/- CLI output formatting
- Design specs:
docs/design/<feature>.md(no date prefix) - Implementation plans:
docs/plans/YYYY-MM-DD-<feature>.md - Long-lived research/comparison docs live next to the crate they discuss, not in
docs/. - The
docs/superpowers/paths some plugin skills (brainstorming, writing-plans) suggest by default do NOT match this convention — override them when invoking those skills.
For substantial fixes, an issue-scoped diagnostic directory at the repo root holds the audit trail: prove-it-prototype probes, oracles, falsifiable design, plan, empirical snapshots, and per-round review decision logs (e.g., .rivets-v465/, .rivets-3d0s/). They're committed alongside the code change as the historical record of why the fix looks the way it does.
- Point-in-time, not maintained. Probe scripts query whatever schema/state was current when the fix was developed; they will go stale and that's expected.
- Per-round review responses go in
.<issue-id>/review-decisions-round-N.md. Don't edit prior rounds; add a new file per round. - Once the issue is closed and the fix has stabilized, these directories are archival reference, not active tooling. Don't treat them as runnable utilities without re-validating against current code.
Core Principle: Write code that speaks for itself. Comment only when necessary to explain WHY, not WHAT.
Comments to Avoid:
- Obvious Comments: Don't state what the code clearly shows ("Initialize counter to zero", "Increment counter by one")
- Redundant Comments: Avoid repeating the code's meaning in prose form
- Outdated Comments: Never let documentation drift from actual implementation
Comments Worth Writing:
- Complex Business Logic: Clarify non-obvious calculations or domain-specific rules
- Algorithm Choices: Explain why you selected a particular algorithm
- Example: "Using Floyd-Warshall for all-pairs shortest paths because we need distances between all nodes"
- Regex Patterns: Describe what complex regular expressions match in plain language
- API Constraints: Document external limitations
- Example: "GitHub API rate limit: 5000 requests/hour for authenticated users"
Decision Framework (before commenting):
- Is the code self-explanatory?
- Would better naming eliminate the need?
- Does this explain WHY, not WHAT?
- Will future maintainers benefit?
Special Cases:
- Public APIs: Use structured documentation (rustdoc
///, JSDoc/**) - Constants: Explain reasoning ("Based on network reliability studies")
- Annotations: Use standard markers: TODO, FIXME, HACK, NOTE, WARNING, PERF, SECURITY, BUG, REFACTOR, DEPRECATED
Anti-Patterns:
- Don't comment out code; use version control instead
- Never maintain change history in comments
- Avoid decorative divider lines
Overview: Follow idiomatic Rust practices based on The Rust Book, Rust API Guidelines, RFC 430, and community standards.
General Instructions:
- Prioritize readability, safety, and maintainability throughout
- Leverage strong typing and Rust's ownership system for memory safety
- Decompose complex functions into smaller, manageable units
- Include explanations for algorithm-related code
- Handle errors gracefully using
Result<T, E>with meaningful messages - Document external dependencies and their purposes
- Follow RFC 430 naming conventions consistently
- Ensure code compiles without warnings
Ownership, Borrowing, and Lifetimes:
- Prefer borrowing (
&T) over cloning unless ownership transfer is necessary - Use
&mut Twhen modifying borrowed data - Explicitly annotate lifetimes when the compiler cannot infer them
- Use
Rc<T>for single-threaded reference counting;Arc<T>for thread-safe scenarios - Use
RefCell<T>for interior mutability in single-threaded contexts;Mutex<T>orRwLock<T>for multi-threaded
Patterns to Follow:
- Use modules (
mod) and public interfaces (pub) for encapsulation - Handle errors properly with
?,match, orif let - Employ
serdefor serialization andthiserror/anyhowfor custom errors - Implement traits to abstract services or dependencies
- Structure async code using
async/awaitwithtokioorasync-std - Prefer enums over flags for type safety
- Use builders for complex object creation
- Separate binary and library code for testability
- Use
rayonfor data parallelism - Prefer iterators over index-based loops
- Use
&strinstead ofStringfor function parameters when ownership isn't needed - Favor borrowing and zero-copy operations
For detailed Rust patterns and code review, load the rust-best-practices skill.
This skill provides 28 rules covering:
- Error handling (Option/Result patterns, expect vs unwrap)
- File I/O safety (atomic writes, TOCTOU avoidance)
- Type safety (enums, newtypes, validation)
- Performance (loop optimization, zero-copy limits)
- CLI development (clap, exit codes, signal handling, config files)
- Common footguns (borrow checker, Path edge cases)
When reviewing Rust code, apply these patterns in addition to project-specific rules above.
Deep-dive files (load when encountering specific issues):
error-handling.md- Option patterns, Path footgunsfile-io.md- Atomic writes, tempfile testingtype-safety.md- Constants, enums, newtypesperformance.md- Loop optimizationcli-development.md- clap, signals, configcommon-footguns.md- TOCTOU, borrow checker
This project uses rstest for parameterized testing and proptest for property-based testing where exhaustive case enumeration isn't practical. Use rstest when you have multiple test cases that share the same test logic.
When to use rstest:
- Multiple similar tests that only differ in input/expected values
- Testing the same behavior with different configurations
- Verifying boundary conditions across multiple values
Key features:
#[rstest]- Marks a test as parameterized#[case(...)]- Defines individual test cases with named variants#[values(...)]- Creates matrix tests across multiple values#[fixture]- Defines reusable test fixtures
Example - Using #[case] for discrete test cases:
use rstest::rstest;
#[rstest]
#[case::simple("hello", 5)]
#[case::empty("", 0)]
#[case::unicode("日本語", 3)]
#[test]
fn test_char_count(#[case] input: &str, #[case] expected: usize) {
assert_eq!(input.chars().count(), expected);
}Example - Using #[values] for value ranges:
use rstest::rstest;
#[rstest]
fn test_valid_priority(#[values(1, 2, 3, 4, 5)] priority: u8) {
assert!(priority >= 1 && priority <= 5);
}Guidelines:
- Name cases descriptively with
#[case::name(...)]for clear test output - Prefer
#[case]when test cases have different expected behaviors - Prefer
#[values]when testing the same assertion across a range - Don't force rstest on tests that don't benefit from parameterization
- Works with
#[tokio::test]for async tests (place#[rstest]before#[tokio::test])
Beyond parameterization with rstest, follow these patterns for robust test coverage:
Roundtrip Tests (Parse/Serialize Symmetry)
When a type has both serialization (as_str(), to_string()) and deserialization (parse(), from_str()), verify they're inverses:
#[test]
fn reference_kind_roundtrip() {
let variants = [
ReferenceKind::Import,
ReferenceKind::Call,
ReferenceKind::Type,
];
for kind in variants {
assert_eq!(
ReferenceKind::parse(kind.as_str()),
Some(kind),
"roundtrip failed for {kind:?}"
);
}
}Invariant Tests (Contract Verification)
When documentation or API design promises invariants, write tests that verify them:
#[test]
fn status_counts_sum_to_total() {
// Invariant: total issues == sum of issues grouped by status
let stats = storage.stats().expect("stats failed");
let status_sum: usize = stats.by_status.values().sum();
assert_eq!(
stats.total,
status_sum,
"total should equal the sum of per-status counts"
);
}Descriptive Assertions in Tests
Use .expect("descriptive message") instead of .unwrap() in tests for clearer failure output:
// Good - failure message explains context
let result = parser.parse(input).expect("parser should handle valid input");
let issue = storage.get(&id).expect("issue should exist after creation");
// Avoid - failures give no context
let result = parser.parse(input).unwrap();Test Categories to Consider:
- Roundtrip:
parse(serialize(x)) == xfor all serializable types - Invariant: Document promises hold under all conditions
- Boundary: Edge cases (empty, max, special characters)
- Error paths: Invalid inputs return appropriate errors
- State transitions: Operations produce expected state changes
This project uses the tracing crate for structured logging. Always use structured fields instead of string interpolation.
Do this (structured):
tracing::warn!(
error = %reload_err,
issue_id = %id,
"Failed to reload after save error"
);
tracing::info!(
workspace = %path.display(),
issue_count = count,
"Loaded workspace"
);
tracing::debug!(
operation = "update",
issue_id = %id,
fields_changed = ?changed_fields,
"Issue updated"
);Don't do this (string interpolation):
// BAD - loses structured data
tracing::warn!("Failed to reload after save error: {}", reload_err);
tracing::info!("Loaded {} issues from {}", count, path.display());Field formatting:
%value- UseDisplaytrait (for user-friendly output)?value- UseDebugtrait (for developer debugging)value- Use directly if it implementstracing::Value
Log levels:
error!- Unrecoverable errors, operation failedwarn!- Recoverable issues, degraded operationinfo!- Significant events (startup, shutdown, major operations)debug!- Detailed diagnostic informationtrace!- Very verbose, step-by-step execution
When to use logging vs user output:
- Use
tracing::*for internal diagnostics (debugging, monitoring, troubleshooting) - Use
println!/eprintln!for user-facing output (command results, error messages shown to users)