diff --git a/.github/skills/datafusion/README.md b/.agents/skills/datafusion/README.md
similarity index 100%
rename from .github/skills/datafusion/README.md
rename to .agents/skills/datafusion/README.md
diff --git a/.github/skills/datafusion/SKILL.md b/.agents/skills/datafusion/SKILL.md
similarity index 100%
rename from .github/skills/datafusion/SKILL.md
rename to .agents/skills/datafusion/SKILL.md
diff --git a/.github/skills/mvcc/README.md b/.agents/skills/mvcc/README.md
similarity index 100%
rename from .github/skills/mvcc/README.md
rename to .agents/skills/mvcc/README.md
diff --git a/.github/skills/mvcc/SKILL.md b/.agents/skills/mvcc/SKILL.md
similarity index 100%
rename from .github/skills/mvcc/SKILL.md
rename to .agents/skills/mvcc/SKILL.md
diff --git a/.github/skills/raft/README.md b/.agents/skills/raft/README.md
similarity index 100%
rename from .github/skills/raft/README.md
rename to .agents/skills/raft/README.md
diff --git a/.github/skills/raft/SKILL.md b/.agents/skills/raft/SKILL.md
similarity index 100%
rename from .github/skills/raft/SKILL.md
rename to .agents/skills/raft/SKILL.md
diff --git a/.agents/skills/react/SKILL.md b/.agents/skills/react/SKILL.md
new file mode 100644
index 000000000..bb9ff03e1
--- /dev/null
+++ b/.agents/skills/react/SKILL.md
@@ -0,0 +1,722 @@
+# React Best Practices for KalamDB
+
+This skill contains comprehensive React performance optimization guidelines adapted from [Vercel's React Best Practices](https://vercel.com/blog/introducing-react-best-practices). Apply these patterns when building, reviewing, or refactoring React code in the KalamDB UI.
+
+## When to Apply
+
+Reference these guidelines when:
+- Writing new React components for the KalamDB admin UI
+- Implementing data fetching (client or server-side)
+- Reviewing code for performance issues
+- Refactoring existing React code in the UI
+- Optimizing bundle size or load times
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Description |
+|----------|----------|--------|-------------|
+| 1 | Eliminating Waterfalls | CRITICAL | Avoid sequential async operations that should run in parallel |
+| 2 | Bundle Size Optimization | CRITICAL | Minimize JavaScript bundle size and lazy-load heavy components |
+| 3 | Server-Side Performance | HIGH | Optimize server-rendered content and API response times |
+| 4 | Client-Side Data Fetching | MEDIUM-HIGH | Efficient data fetching patterns (SWR, React Query, suspense) |
+| 5 | Re-render Optimization | MEDIUM | Prevent unnecessary component re-renders |
+| 6 | Rendering Performance | MEDIUM | Optimize DOM rendering and layout operations |
+| 7 | JavaScript Performance | LOW-MEDIUM | Micro-optimizations for hot paths |
+| 8 | Advanced Patterns | LOW | Advanced patterns for specific edge cases |
+
+## 1. Eliminating Waterfalls (CRITICAL)
+
+Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
+
+### 1.1 Defer Await Until Needed
+
+Move `await` statements into branches where they're actually used, not before.
+
+**Incorrect (blocks both branches):**
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ const userData = await fetchUserData(userId)
+
+ if (skipProcessing) {
+ // Data loaded but not used
+ return { skipped: true }
+ }
+
+ return processUserData(userData)
+}
+```
+
+**Correct (only blocks when needed):**
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ if (skipProcessing) {
+ return { skipped: true }
+ }
+
+ const userData = await fetchUserData(userId)
+ return processUserData(userData)
+}
+```
+
+### 1.2 Dependency-Based Parallelization
+
+When fetching multiple data sources, start promises early and await them together.
+
+**Incorrect (sequential):**
+```typescript
+const user = await fetchUser(id)
+const posts = await fetchPosts(user.id)
+const comments = await fetchComments(posts[0].id)
+```
+
+**Correct (parallel when independent):**
+```typescript
+const userPromise = fetchUser(id)
+const user = await userPromise
+
+// These don't depend on each other
+const [posts, profile] = await Promise.all([
+ fetchPosts(user.id),
+ fetchProfile(user.id)
+])
+
+// Only this depends on posts
+const comments = await fetchComments(posts[0].id)
+```
+
+### 1.3 Promise.all() for Independent Operations
+
+Use `Promise.all()` to run multiple operations in parallel instead of sequentially.
+
+**Incorrect:**
+```typescript
+const metadata = await fetchMetadata()
+const analytics = await fetchAnalytics()
+const settings = await fetchSettings()
+```
+
+**Correct:**
+```typescript
+const [metadata, analytics, settings] = await Promise.all([
+ fetchMetadata(),
+ fetchAnalytics(),
+ fetchSettings()
+])
+```
+
+### 1.4 Strategic Suspense Boundaries
+
+Use Suspense to stream content as data arrives, don't wait for all queries.
+
+**Incorrect (blocking):**
+```tsx
+async function Page() {
+ const data1 = await heavyQuery1()
+ const data2 = await heavyQuery2()
+ return
+}
+```
+
+**Correct (streaming):**
+```tsx
+function Page() {
+ return (
+ <>
+ }>
+
+
+ }>
+
+
+ >
+ )
+}
+```
+
+## 2. Bundle Size Optimization (CRITICAL)
+
+Large bundles slow down initial load. Every KB matters for time-to-interactive.
+
+### 2.1 Avoid Barrel File Imports
+
+Import directly from modules instead of re-exporting through barrel files, allowing tree-shaking.
+
+**Incorrect (prevents tree-shaking):**
+```typescript
+import { Button, Modal, Input } from '@/components'
+```
+
+**Correct (specific imports):**
+```typescript
+import { Button } from '@/components/button'
+import { Modal } from '@/components/modal'
+import { Input } from '@/components/input'
+```
+
+### 2.2 Lazy Load Heavy Components
+
+Use `React.lazy()` and `next/dynamic` for components you don't need immediately.
+
+**Incorrect (loads with main bundle):**
+```tsx
+import { ChartComponent } from '@/components/charts'
+
+export default function Dashboard() {
+ return
+}
+```
+
+**Correct (code-split):**
+```tsx
+import dynamic from 'next/dynamic'
+
+const ChartComponent = dynamic(
+ () => import('@/components/charts').then(m => m.ChartComponent),
+ { loading: () =>
Loading...
}
+)
+
+export default function Dashboard() {
+ return
+}
+```
+
+### 2.3 Conditional Module Loading
+
+Load modules only when a feature is actually activated.
+
+**Incorrect:**
+```typescript
+import { analytics } from '@/lib/analytics'
+
+// Only used if user has premium
+const trackPremiumEvent = () => analytics.track('premium_event')
+```
+
+**Correct:**
+```typescript
+const trackPremiumEvent = async () => {
+ const { analytics } = await import('@/lib/analytics')
+ analytics.track('premium_event')
+}
+```
+
+### 2.4 Defer Third-Party Scripts
+
+Load analytics, tracking, and non-critical libraries after hydration.
+
+**Incorrect:**
+```tsx
+import { Analytics } from '@/lib/analytics'
+
+export function RootLayout() {
+ return (
+ <>
+ ...
+
+ >
+ )
+}
+```
+
+**Correct:**
+```tsx
+import { useEffect } from 'react'
+
+export function RootLayout() {
+ useEffect(() => {
+ // Load after hydration
+ import('@/lib/analytics').then(m => m.init())
+ }, [])
+
+ return ...
+}
+```
+
+## 3. Server-Side Performance (HIGH)
+
+Optimize what runs on the server to improve time-to-first-byte and reduce client work.
+
+### 3.1 RSC (React Server Components) for Data Fetching
+
+Fetch data on the server, not in browser JavaScript.
+
+**Incorrect (client-side fetch):**
+```tsx
+'use client'
+
+export default function UserList() {
+ const [users, setUsers] = useState([])
+
+ useEffect(() => {
+ fetch('/api/users').then(r => r.json()).then(setUsers)
+ }, [])
+
+ return {users.map(u => )}
+}
+```
+
+**Correct (server-side fetch):**
+```tsx
+// Server Component by default
+async function UserList() {
+ const users = await db.users.findAll()
+ return {users.map(u => )}
+}
+```
+
+### 3.2 Stream Content Early
+
+Start rendering as soon as possible, fetch missing pieces in parallel.
+
+**Incorrect (blocking):**
+```tsx
+async function Page() {
+ const criticalData = await fetchCritical()
+ const nonCriticalData = await fetchNonCritical()
+ return
+}
+```
+
+**Correct (stream critical, load rest in parallel):**
+```tsx
+async function Page() {
+ const criticalData = await fetchCritical()
+ return (
+
+
+
+
+
+ )
+}
+```
+
+## 4. Client-Side Data Fetching (MEDIUM-HIGH)
+
+When fetching on the client, use deduplication, caching, and background updates.
+
+### 4.1 Use Deduplication & Caching
+
+Avoid duplicate requests for the same data.
+
+**Incorrect (duplicated fetches):**
+```tsx
+function UserProfile() {
+ const [user, setUser] = useState(null)
+
+ useEffect(() => {
+ fetch(`/api/user/${id}`).then(r => r.json()).then(setUser)
+ }, [id])
+
+ return {user?.name}
+}
+
+function UserSettings() {
+ const [user, setUser] = useState(null)
+
+ useEffect(() => {
+ fetch(`/api/user/${id}`).then(r => r.json()).then(setUser)
+ }, [id])
+
+ return {user?.email}
+}
+```
+
+**Correct (deduplicated with SWR/React Query):**
+```tsx
+function UserProfile() {
+ const { data: user } = useSWR(`/api/user/${id}`)
+ return {user?.name}
+}
+
+function UserSettings() {
+ const { data: user } = useSWR(`/api/user/${id}`) // Same request, cached
+ return {user?.email}
+}
+```
+
+### 4.2 Fetch on User Interaction
+
+Don't fetch data until the user needs it.
+
+**Incorrect (eager fetching):**
+```tsx
+function Modal() {
+ const [details, setDetails] = useState(null)
+
+ useEffect(() => {
+ // Fetches even if modal never opens
+ fetchDetails(itemId).then(setDetails)
+ }, [itemId])
+
+ return {details?.content}
+}
+```
+
+**Correct (lazy fetch on open):**
+```tsx
+function Modal({ isOpen, itemId }) {
+ const { data: details } = useSWR(
+ isOpen ? `/api/details/${itemId}` : null
+ )
+
+ return {details?.content}
+}
+```
+
+## 5. Re-render Optimization (MEDIUM)
+
+Prevent unnecessary re-renders that slow down the app.
+
+### 5.1 Use Primitive Dependencies in Effects
+
+Specify primitive values instead of objects in effect dependencies.
+
+**Incorrect (re-runs on any user field):**
+```typescript
+useEffect(() => {
+ console.log(user.id)
+}, [user]) // Re-runs if ANY field changes
+```
+
+**Correct (re-runs only when id changes):**
+```typescript
+useEffect(() => {
+ console.log(user.id)
+}, [user.id]) // Re-runs only if id changes
+```
+
+### 5.2 Extract Expensive Work to Memoized Components
+
+Split large components into smaller memoized pieces.
+
+**Incorrect (whole component re-renders):**
+```tsx
+function Dashboard({ user, expensiveData }) {
+ return (
+
+
+ {/* Re-renders even if data unchanged */}
+
+ )
+}
+```
+
+**Correct (memoized expensive component):**
+```tsx
+const MemoizedChart = memo(function ExpensiveChart({ data }) {
+ // Only re-renders if data reference changes
+ return {/* expensive rendering */}
+})
+
+function Dashboard({ user, expensiveData }) {
+ return (
+
+
+
+
+ )
+}
+```
+
+### 5.3 Avoid useMemo for Simple Expressions
+
+Don't wrap simple primitive values in useMemo—the overhead isn't worth it.
+
+**Incorrect (useless memoization):**
+```typescript
+const isActive = useMemo(() => status === 'active', [status])
+const count = useMemo(() => items.length, [items])
+```
+
+**Correct (compute directly):**
+```typescript
+const isActive = status === 'active'
+const count = items.length
+```
+
+### 5.4 Use Functional setState for Stable Callbacks
+
+When setState depends on previous state, use functional updates.
+
+**Incorrect (recreates callback on each render):**
+```tsx
+const [count, setCount] = useState(0)
+
+// Callback recreated every render
+const increment = () => setCount(count + 1)
+
+return +
+```
+
+**Correct (stable callback):**
+```tsx
+const [count, setCount] = useState(0)
+
+// Callback never changes
+const increment = () => setCount(c => c + 1)
+
+return +
+```
+
+### 5.5 Lazy State Initialization
+
+For expensive initial state, pass a function to useState.
+
+**Incorrect (parse on every render):**
+```typescript
+const [config, setConfig] = useState(JSON.parse(localStorage.getItem('config')))
+```
+
+**Correct (parse only on mount):**
+```typescript
+const [config, setConfig] = useState(() =>
+ JSON.parse(localStorage.getItem('config') || '{}')
+)
+```
+
+## 6. Rendering Performance (MEDIUM)
+
+Optimize how components render to the DOM.
+
+### 6.1 Use content-visibility for Long Lists
+
+CSS property that skips rendering off-screen content.
+
+**Before:**
+```tsx
+function LongList({ items }) {
+ return (
+
+ {items.map(item => )}
+
+ )
+}
+```
+
+**After:**
+```tsx
+function LongList({ items }) {
+ return (
+
+ {items.map(item => (
+
+
+
+ ))}
+
+ )
+}
+```
+
+### 6.2 Use Ternary Instead of &&
+
+The `&&` operator can cause rendering glitches; use ternary for conditionals.
+
+**Incorrect:**
+```tsx
+{isLoading && }
+```
+
+**Correct:**
+```tsx
+{isLoading ? : null}
+```
+
+### 6.3 Hoist Static JSX
+
+Extract JSX that doesn't depend on props outside the component.
+
+**Incorrect (recreated every render):**
+```tsx
+function Layout() {
+ const header = // Recreated every render
+ return {header}...
+}
+```
+
+**Correct (defined once):**
+```tsx
+const HEADER =
+
+function Layout() {
+ return {HEADER}...
+}
+```
+
+## 7. JavaScript Performance (LOW-MEDIUM)
+
+Micro-optimizations for hot code paths.
+
+### 7.1 Combine Multiple Loops into One
+
+One loop scanning data multiple times ≠ several loops.
+
+**Incorrect (scans list 3 times):**
+```typescript
+const total = items.reduce((sum, item) => sum + item.price, 0)
+const count = items.filter(item => item.active).length
+const expensive = items.find(item => item.cost > 1000)
+```
+
+**Correct (single pass):**
+```typescript
+let total = 0
+let count = 0
+let expensive = null
+
+for (let i = 0; i < items.length; i++) {
+ total += items[i].price
+ if (items[i].active) count++
+ if (!expensive && items[i].cost > 1000) expensive = items[i]
+}
+```
+
+### 7.2 Cache Repeated Lookups
+
+Store repeated object property or Map lookups in a variable.
+
+**Incorrect (redundant access):**
+```typescript
+for (let i = 0; i < users.length; i++) {
+ console.log(users[i].profile.settings.theme) // Three lookups per iteration
+}
+```
+
+**Correct (cache properties):**
+```typescript
+for (let i = 0; i < users.length; i++) {
+ const user = users[i]
+ const settings = user.profile.settings
+ console.log(settings.theme)
+}
+```
+
+### 7.3 Use Set/Map for O(1) Lookups
+
+Don't iterate arrays for membership checks; use Set or Map.
+
+**Incorrect (O(n) lookup):**
+```typescript
+const adminIds = [1, 2, 3, 4]
+
+if (adminIds.includes(userId)) {
+ // Permission granted
+}
+```
+
+**Correct (O(1) lookup):**
+```typescript
+const adminIds = new Set([1, 2, 3, 4])
+
+if (adminIds.has(userId)) {
+ // Permission granted
+}
+```
+
+### 7.4 Hoist RegExp Outside Render
+
+Don't create RegExp inside render/loops; define once at module scope.
+
+**Incorrect:**
+```tsx
+function Formatter({ text, pattern }: Props) {
+ const regex = new RegExp(pattern, 'g') // New object every render
+ return {text.replace(regex, '...')}
+}
+```
+
+**Correct:**
+```tsx
+function Formatter({ text, pattern }: Props) {
+ const regex = useMemo(
+ () => new RegExp(pattern, 'g'),
+ [pattern]
+ )
+ return {text.replace(regex, '...')}
+}
+```
+
+## 8. Advanced Patterns (LOW)
+
+Advanced techniques for specific use cases.
+
+### 8.1 useEffectEvent (React 19+)
+
+Stable callback references that don't trigger dependency re-runs.
+
+**Incorrect (recreates on dependency change):**
+```tsx
+function Page({ userId }) {
+ const [data, setData] = useState(null)
+
+ const handleSuccess = useCallback((result) => {
+ setData(result)
+ }, [userId]) // Recreates if userId changes
+
+ useEffect(() => {
+ fetch(`/api/user/${userId}`).then(handleSuccess)
+ }, [userId, handleSuccess])
+}
+```
+
+**Correct (stable callback):**
+```tsx
+function Page({ userId }) {
+ const [data, setData] = useState(null)
+
+ const handleSuccess = useEffectEvent((result) => {
+ setData(result)
+ })
+
+ useEffect(() => {
+ fetch(`/api/user/${userId}`).then(handleSuccess)
+ }, [userId])
+}
+```
+
+### 8.2 Move Interaction Logic to Event Handlers
+
+Don't put interaction logic in effects; use event handlers.
+
+**Incorrect (effect for user action):**
+```tsx
+function Input({ value, onChange }) {
+ useEffect(() => {
+ if (value.length > 10) {
+ logEvent('input_too_long')
+ }
+ }, [value])
+
+ return
+}
+```
+
+**Correct (event handler):**
+```tsx
+function Input({ value, onChange }) {
+ const handleChange = (e) => {
+ if (e.target.value.length > 10) {
+ logEvent('input_too_long')
+ }
+ onChange(e)
+ }
+
+ return
+}
+```
+
+## References
+
+- [Vercel React Best Practices Blog](https://vercel.com/blog/introducing-react-best-practices)
+- [React Documentation](https://react.dev)
+- [Next.js Framework](https://nextjs.org)
+- [SWR (Data Fetching)](https://swr.vercel.app)
+- [React Query](https://tanstack.com/query/latest)
+
+## Additional Resources
+
+For KalamDB-specific patterns:
+- Review existing components in `/ui/src/components/` for examples
+- Follow TypeScript patterns defined in the project
+- Use the component library established in the UI codebase
diff --git a/.agents/skills/rust-skills/AGENTS.md b/.agents/skills/rust-skills/AGENTS.md
new file mode 100644
index 000000000..c0f008d2d
--- /dev/null
+++ b/.agents/skills/rust-skills/AGENTS.md
@@ -0,0 +1,335 @@
+---
+name: rust-skills
+description: >
+ Comprehensive Rust coding guidelines with 179 rules across 14 categories.
+ Use when writing, reviewing, or refactoring Rust code. Covers ownership,
+ error handling, async patterns, API design, memory optimization, performance,
+ testing, and common anti-patterns. Invoke with /rust-skills.
+license: MIT
+metadata:
+ author: leonardomso
+ version: "1.0.0"
+ sources:
+ - Rust API Guidelines
+ - Rust Performance Book
+ - ripgrep, tokio, serde, polars codebases
+---
+
+# Rust Best Practices
+
+Comprehensive guide for writing high-quality, idiomatic, and highly optimized Rust code. Contains 179 rules across 14 categories, prioritized by impact to guide LLMs in code generation and refactoring.
+
+## When to Apply
+
+Reference these guidelines when:
+- Writing new Rust functions, structs, or modules
+- Implementing error handling or async code
+- Designing public APIs for libraries
+- Reviewing code for ownership/borrowing issues
+- Optimizing memory usage or reducing allocations
+- Tuning performance for hot paths
+- Refactoring existing Rust code
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix | Rules |
+|----------|----------|--------|--------|-------|
+| 1 | Ownership & Borrowing | CRITICAL | `own-` | 12 |
+| 2 | Error Handling | CRITICAL | `err-` | 12 |
+| 3 | Memory Optimization | CRITICAL | `mem-` | 15 |
+| 4 | API Design | HIGH | `api-` | 15 |
+| 5 | Async/Await | HIGH | `async-` | 15 |
+| 6 | Compiler Optimization | HIGH | `opt-` | 12 |
+| 7 | Naming Conventions | MEDIUM | `name-` | 16 |
+| 8 | Type Safety | MEDIUM | `type-` | 10 |
+| 9 | Testing | MEDIUM | `test-` | 13 |
+| 10 | Documentation | MEDIUM | `doc-` | 11 |
+| 11 | Performance Patterns | MEDIUM | `perf-` | 11 |
+| 12 | Project Structure | LOW | `proj-` | 11 |
+| 13 | Clippy & Linting | LOW | `lint-` | 11 |
+| 14 | Anti-patterns | REFERENCE | `anti-` | 15 |
+
+---
+
+## Quick Reference
+
+### 1. Ownership & Borrowing (CRITICAL)
+
+- [`own-borrow-over-clone`](rules/own-borrow-over-clone.md) - Prefer `&T` borrowing over `.clone()`
+- [`own-slice-over-vec`](rules/own-slice-over-vec.md) - Accept `&[T]` not `&Vec`, `&str` not `&String`
+- [`own-cow-conditional`](rules/own-cow-conditional.md) - Use `Cow<'a, T>` for conditional ownership
+- [`own-arc-shared`](rules/own-arc-shared.md) - Use `Arc` for thread-safe shared ownership
+- [`own-rc-single-thread`](rules/own-rc-single-thread.md) - Use `Rc` for single-threaded sharing
+- [`own-refcell-interior`](rules/own-refcell-interior.md) - Use `RefCell` for interior mutability (single-thread)
+- [`own-mutex-interior`](rules/own-mutex-interior.md) - Use `Mutex` for interior mutability (multi-thread)
+- [`own-rwlock-readers`](rules/own-rwlock-readers.md) - Use `RwLock` when reads dominate writes
+- [`own-copy-small`](rules/own-copy-small.md) - Derive `Copy` for small, trivial types
+- [`own-clone-explicit`](rules/own-clone-explicit.md) - Make `Clone` explicit, avoid implicit copies
+- [`own-move-large`](rules/own-move-large.md) - Move large data instead of cloning
+- [`own-lifetime-elision`](rules/own-lifetime-elision.md) - Rely on lifetime elision when possible
+
+### 2. Error Handling (CRITICAL)
+
+- [`err-thiserror-lib`](rules/err-thiserror-lib.md) - Use `thiserror` for library error types
+- [`err-anyhow-app`](rules/err-anyhow-app.md) - Use `anyhow` for application error handling
+- [`err-result-over-panic`](rules/err-result-over-panic.md) - Return `Result`, don't panic on expected errors
+- [`err-context-chain`](rules/err-context-chain.md) - Add context with `.context()` or `.with_context()`
+- [`err-no-unwrap-prod`](rules/err-no-unwrap-prod.md) - Never use `.unwrap()` in production code
+- [`err-expect-bugs-only`](rules/err-expect-bugs-only.md) - Use `.expect()` only for programming errors
+- [`err-question-mark`](rules/err-question-mark.md) - Use `?` operator for clean propagation
+- [`err-from-impl`](rules/err-from-impl.md) - Use `#[from]` for automatic error conversion
+- [`err-source-chain`](rules/err-source-chain.md) - Use `#[source]` to chain underlying errors
+- [`err-lowercase-msg`](rules/err-lowercase-msg.md) - Error messages: lowercase, no trailing punctuation
+- [`err-doc-errors`](rules/err-doc-errors.md) - Document errors with `# Errors` section
+- [`err-custom-type`](rules/err-custom-type.md) - Create custom error types, not `Box`
+
+### 3. Memory Optimization (CRITICAL)
+
+- [`mem-with-capacity`](rules/mem-with-capacity.md) - Use `with_capacity()` when size is known
+- [`mem-smallvec`](rules/mem-smallvec.md) - Use `SmallVec` for usually-small collections
+- [`mem-arrayvec`](rules/mem-arrayvec.md) - Use `ArrayVec` for bounded-size collections
+- [`mem-box-large-variant`](rules/mem-box-large-variant.md) - Box large enum variants to reduce type size
+- [`mem-boxed-slice`](rules/mem-boxed-slice.md) - Use `Box<[T]>` instead of `Vec` when fixed
+- [`mem-thinvec`](rules/mem-thinvec.md) - Use `ThinVec` for often-empty vectors
+- [`mem-clone-from`](rules/mem-clone-from.md) - Use `clone_from()` to reuse allocations
+- [`mem-reuse-collections`](rules/mem-reuse-collections.md) - Reuse collections with `clear()` in loops
+- [`mem-avoid-format`](rules/mem-avoid-format.md) - Avoid `format!()` when string literals work
+- [`mem-write-over-format`](rules/mem-write-over-format.md) - Use `write!()` instead of `format!()`
+- [`mem-arena-allocator`](rules/mem-arena-allocator.md) - Use arena allocators for batch allocations
+- [`mem-zero-copy`](rules/mem-zero-copy.md) - Use zero-copy patterns with slices and `Bytes`
+- [`mem-compact-string`](rules/mem-compact-string.md) - Use `CompactString` for small string optimization
+- [`mem-smaller-integers`](rules/mem-smaller-integers.md) - Use smallest integer type that fits
+- [`mem-assert-type-size`](rules/mem-assert-type-size.md) - Assert hot type sizes to prevent regressions
+
+### 4. API Design (HIGH)
+
+- [`api-builder-pattern`](rules/api-builder-pattern.md) - Use Builder pattern for complex construction
+- [`api-builder-must-use`](rules/api-builder-must-use.md) - Add `#[must_use]` to builder types
+- [`api-newtype-safety`](rules/api-newtype-safety.md) - Use newtypes for type-safe distinctions
+- [`api-typestate`](rules/api-typestate.md) - Use typestate for compile-time state machines
+- [`api-sealed-trait`](rules/api-sealed-trait.md) - Seal traits to prevent external implementations
+- [`api-extension-trait`](rules/api-extension-trait.md) - Use extension traits to add methods to foreign types
+- [`api-parse-dont-validate`](rules/api-parse-dont-validate.md) - Parse into validated types at boundaries
+- [`api-impl-into`](rules/api-impl-into.md) - Accept `impl Into` for flexible string inputs
+- [`api-impl-asref`](rules/api-impl-asref.md) - Accept `impl AsRef` for borrowed inputs
+- [`api-must-use`](rules/api-must-use.md) - Add `#[must_use]` to `Result` returning functions
+- [`api-non-exhaustive`](rules/api-non-exhaustive.md) - Use `#[non_exhaustive]` for future-proof enums/structs
+- [`api-from-not-into`](rules/api-from-not-into.md) - Implement `From`, not `Into` (auto-derived)
+- [`api-default-impl`](rules/api-default-impl.md) - Implement `Default` for sensible defaults
+- [`api-common-traits`](rules/api-common-traits.md) - Implement `Debug`, `Clone`, `PartialEq` eagerly
+- [`api-serde-optional`](rules/api-serde-optional.md) - Gate `Serialize`/`Deserialize` behind feature flag
+
+### 5. Async/Await (HIGH)
+
+- [`async-tokio-runtime`](rules/async-tokio-runtime.md) - Use Tokio for production async runtime
+- [`async-no-lock-await`](rules/async-no-lock-await.md) - Never hold `Mutex`/`RwLock` across `.await`
+- [`async-spawn-blocking`](rules/async-spawn-blocking.md) - Use `spawn_blocking` for CPU-intensive work
+- [`async-tokio-fs`](rules/async-tokio-fs.md) - Use `tokio::fs` not `std::fs` in async code
+- [`async-cancellation-token`](rules/async-cancellation-token.md) - Use `CancellationToken` for graceful shutdown
+- [`async-join-parallel`](rules/async-join-parallel.md) - Use `tokio::join!` for parallel operations
+- [`async-try-join`](rules/async-try-join.md) - Use `tokio::try_join!` for fallible parallel ops
+- [`async-select-racing`](rules/async-select-racing.md) - Use `tokio::select!` for racing/timeouts
+- [`async-bounded-channel`](rules/async-bounded-channel.md) - Use bounded channels for backpressure
+- [`async-mpsc-queue`](rules/async-mpsc-queue.md) - Use `mpsc` for work queues
+- [`async-broadcast-pubsub`](rules/async-broadcast-pubsub.md) - Use `broadcast` for pub/sub patterns
+- [`async-watch-latest`](rules/async-watch-latest.md) - Use `watch` for latest-value sharing
+- [`async-oneshot-response`](rules/async-oneshot-response.md) - Use `oneshot` for request/response
+- [`async-joinset-structured`](rules/async-joinset-structured.md) - Use `JoinSet` for dynamic task groups
+- [`async-clone-before-await`](rules/async-clone-before-await.md) - Clone data before await, release locks
+
+### 6. Compiler Optimization (HIGH)
+
+- [`opt-inline-small`](rules/opt-inline-small.md) - Use `#[inline]` for small hot functions
+- [`opt-inline-always-rare`](rules/opt-inline-always-rare.md) - Use `#[inline(always)]` sparingly
+- [`opt-inline-never-cold`](rules/opt-inline-never-cold.md) - Use `#[inline(never)]` for cold paths
+- [`opt-cold-unlikely`](rules/opt-cold-unlikely.md) - Use `#[cold]` for error/unlikely paths
+- [`opt-likely-hint`](rules/opt-likely-hint.md) - Use `likely()`/`unlikely()` for branch hints
+- [`opt-lto-release`](rules/opt-lto-release.md) - Enable LTO in release builds
+- [`opt-codegen-units`](rules/opt-codegen-units.md) - Use `codegen-units = 1` for max optimization
+- [`opt-pgo-profile`](rules/opt-pgo-profile.md) - Use PGO for production builds
+- [`opt-target-cpu`](rules/opt-target-cpu.md) - Set `target-cpu=native` for local builds
+- [`opt-bounds-check`](rules/opt-bounds-check.md) - Use iterators to avoid bounds checks
+- [`opt-simd-portable`](rules/opt-simd-portable.md) - Use portable SIMD for data-parallel ops
+- [`opt-cache-friendly`](rules/opt-cache-friendly.md) - Design cache-friendly data layouts (SoA)
+
+### 7. Naming Conventions (MEDIUM)
+
+- [`name-types-camel`](rules/name-types-camel.md) - Use `UpperCamelCase` for types, traits, enums
+- [`name-variants-camel`](rules/name-variants-camel.md) - Use `UpperCamelCase` for enum variants
+- [`name-funcs-snake`](rules/name-funcs-snake.md) - Use `snake_case` for functions, methods, modules
+- [`name-consts-screaming`](rules/name-consts-screaming.md) - Use `SCREAMING_SNAKE_CASE` for constants/statics
+- [`name-lifetime-short`](rules/name-lifetime-short.md) - Use short lowercase lifetimes: `'a`, `'de`, `'src`
+- [`name-type-param-single`](rules/name-type-param-single.md) - Use single uppercase for type params: `T`, `E`, `K`, `V`
+- [`name-as-free`](rules/name-as-free.md) - `as_` prefix: free reference conversion
+- [`name-to-expensive`](rules/name-to-expensive.md) - `to_` prefix: expensive conversion
+- [`name-into-ownership`](rules/name-into-ownership.md) - `into_` prefix: ownership transfer
+- [`name-no-get-prefix`](rules/name-no-get-prefix.md) - No `get_` prefix for simple getters
+- [`name-is-has-bool`](rules/name-is-has-bool.md) - Use `is_`, `has_`, `can_` for boolean methods
+- [`name-iter-convention`](rules/name-iter-convention.md) - Use `iter`/`iter_mut`/`into_iter` for iterators
+- [`name-iter-method`](rules/name-iter-method.md) - Name iterator methods consistently
+- [`name-iter-type-match`](rules/name-iter-type-match.md) - Iterator type names match method
+- [`name-acronym-word`](rules/name-acronym-word.md) - Treat acronyms as words: `Uuid` not `UUID`
+- [`name-crate-no-rs`](rules/name-crate-no-rs.md) - Crate names: no `-rs` suffix
+
+### 8. Type Safety (MEDIUM)
+
+- [`type-newtype-ids`](rules/type-newtype-ids.md) - Wrap IDs in newtypes: `UserId(u64)`
+- [`type-newtype-validated`](rules/type-newtype-validated.md) - Newtypes for validated data: `Email`, `Url`
+- [`type-enum-states`](rules/type-enum-states.md) - Use enums for mutually exclusive states
+- [`type-option-nullable`](rules/type-option-nullable.md) - Use `Option` for nullable values
+- [`type-result-fallible`](rules/type-result-fallible.md) - Use `Result` for fallible operations
+- [`type-phantom-marker`](rules/type-phantom-marker.md) - Use `PhantomData` for type-level markers
+- [`type-never-diverge`](rules/type-never-diverge.md) - Use `!` type for functions that never return
+- [`type-generic-bounds`](rules/type-generic-bounds.md) - Add trait bounds only where needed
+- [`type-no-stringly`](rules/type-no-stringly.md) - Avoid stringly-typed APIs, use enums/newtypes
+- [`type-repr-transparent`](rules/type-repr-transparent.md) - Use `#[repr(transparent)]` for FFI newtypes
+
+### 9. Testing (MEDIUM)
+
+- [`test-cfg-test-module`](rules/test-cfg-test-module.md) - Use `#[cfg(test)] mod tests { }`
+- [`test-use-super`](rules/test-use-super.md) - Use `use super::*;` in test modules
+- [`test-integration-dir`](rules/test-integration-dir.md) - Put integration tests in `tests/` directory
+- [`test-descriptive-names`](rules/test-descriptive-names.md) - Use descriptive test names
+- [`test-arrange-act-assert`](rules/test-arrange-act-assert.md) - Structure tests as arrange/act/assert
+- [`test-proptest-properties`](rules/test-proptest-properties.md) - Use `proptest` for property-based testing
+- [`test-mockall-mocking`](rules/test-mockall-mocking.md) - Use `mockall` for trait mocking
+- [`test-mock-traits`](rules/test-mock-traits.md) - Use traits for dependencies to enable mocking
+- [`test-fixture-raii`](rules/test-fixture-raii.md) - Use RAII pattern (Drop) for test cleanup
+- [`test-tokio-async`](rules/test-tokio-async.md) - Use `#[tokio::test]` for async tests
+- [`test-should-panic`](rules/test-should-panic.md) - Use `#[should_panic]` for panic tests
+- [`test-criterion-bench`](rules/test-criterion-bench.md) - Use `criterion` for benchmarking
+- [`test-doctest-examples`](rules/test-doctest-examples.md) - Keep doc examples as executable tests
+
+### 10. Documentation (MEDIUM)
+
+- [`doc-all-public`](rules/doc-all-public.md) - Document all public items with `///`
+- [`doc-module-inner`](rules/doc-module-inner.md) - Use `//!` for module-level documentation
+- [`doc-examples-section`](rules/doc-examples-section.md) - Include `# Examples` with runnable code
+- [`doc-errors-section`](rules/doc-errors-section.md) - Include `# Errors` for fallible functions
+- [`doc-panics-section`](rules/doc-panics-section.md) - Include `# Panics` for panicking functions
+- [`doc-safety-section`](rules/doc-safety-section.md) - Include `# Safety` for unsafe functions
+- [`doc-question-mark`](rules/doc-question-mark.md) - Use `?` in examples, not `.unwrap()`
+- [`doc-hidden-setup`](rules/doc-hidden-setup.md) - Use `# ` prefix to hide example setup code
+- [`doc-intra-links`](rules/doc-intra-links.md) - Use intra-doc links: `[Vec]`
+- [`doc-link-types`](rules/doc-link-types.md) - Link related types and functions in docs
+- [`doc-cargo-metadata`](rules/doc-cargo-metadata.md) - Fill `Cargo.toml` metadata
+
+### 11. Performance Patterns (MEDIUM)
+
+- [`perf-iter-over-index`](rules/perf-iter-over-index.md) - Prefer iterators over manual indexing
+- [`perf-iter-lazy`](rules/perf-iter-lazy.md) - Keep iterators lazy, collect() only when needed
+- [`perf-collect-once`](rules/perf-collect-once.md) - Don't `collect()` intermediate iterators
+- [`perf-entry-api`](rules/perf-entry-api.md) - Use `entry()` API for map insert-or-update
+- [`perf-drain-reuse`](rules/perf-drain-reuse.md) - Use `drain()` to reuse allocations
+- [`perf-extend-batch`](rules/perf-extend-batch.md) - Use `extend()` for batch insertions
+- [`perf-chain-avoid`](rules/perf-chain-avoid.md) - Avoid `chain()` in hot loops
+- [`perf-collect-into`](rules/perf-collect-into.md) - Use `collect_into()` for reusing containers
+- [`perf-black-box-bench`](rules/perf-black-box-bench.md) - Use `black_box()` in benchmarks
+- [`perf-release-profile`](rules/perf-release-profile.md) - Optimize release profile settings
+- [`perf-profile-first`](rules/perf-profile-first.md) - Profile before optimizing
+
+### 12. Project Structure (LOW)
+
+- [`proj-lib-main-split`](rules/proj-lib-main-split.md) - Keep `main.rs` minimal, logic in `lib.rs`
+- [`proj-mod-by-feature`](rules/proj-mod-by-feature.md) - Organize modules by feature, not type
+- [`proj-flat-small`](rules/proj-flat-small.md) - Keep small projects flat
+- [`proj-mod-rs-dir`](rules/proj-mod-rs-dir.md) - Use `mod.rs` for multi-file modules
+- [`proj-pub-crate-internal`](rules/proj-pub-crate-internal.md) - Use `pub(crate)` for internal APIs
+- [`proj-pub-super-parent`](rules/proj-pub-super-parent.md) - Use `pub(super)` for parent-only visibility
+- [`proj-pub-use-reexport`](rules/proj-pub-use-reexport.md) - Use `pub use` for clean public API
+- [`proj-prelude-module`](rules/proj-prelude-module.md) - Create `prelude` module for common imports
+- [`proj-bin-dir`](rules/proj-bin-dir.md) - Put multiple binaries in `src/bin/`
+- [`proj-workspace-large`](rules/proj-workspace-large.md) - Use workspaces for large projects
+- [`proj-workspace-deps`](rules/proj-workspace-deps.md) - Use workspace dependency inheritance
+
+### 13. Clippy & Linting (LOW)
+
+- [`lint-deny-correctness`](rules/lint-deny-correctness.md) - `#![deny(clippy::correctness)]`
+- [`lint-warn-suspicious`](rules/lint-warn-suspicious.md) - `#![warn(clippy::suspicious)]`
+- [`lint-warn-style`](rules/lint-warn-style.md) - `#![warn(clippy::style)]`
+- [`lint-warn-complexity`](rules/lint-warn-complexity.md) - `#![warn(clippy::complexity)]`
+- [`lint-warn-perf`](rules/lint-warn-perf.md) - `#![warn(clippy::perf)]`
+- [`lint-pedantic-selective`](rules/lint-pedantic-selective.md) - Enable `clippy::pedantic` selectively
+- [`lint-missing-docs`](rules/lint-missing-docs.md) - `#![warn(missing_docs)]`
+- [`lint-unsafe-doc`](rules/lint-unsafe-doc.md) - `#![warn(clippy::undocumented_unsafe_blocks)]`
+- [`lint-cargo-metadata`](rules/lint-cargo-metadata.md) - `#![warn(clippy::cargo)]` for published crates
+- [`lint-rustfmt-check`](rules/lint-rustfmt-check.md) - Run `cargo fmt --check` in CI
+- [`lint-workspace-lints`](rules/lint-workspace-lints.md) - Configure lints at workspace level
+
+### 14. Anti-patterns (REFERENCE)
+
+- [`anti-unwrap-abuse`](rules/anti-unwrap-abuse.md) - Don't use `.unwrap()` in production code
+- [`anti-expect-lazy`](rules/anti-expect-lazy.md) - Don't use `.expect()` for recoverable errors
+- [`anti-clone-excessive`](rules/anti-clone-excessive.md) - Don't clone when borrowing works
+- [`anti-lock-across-await`](rules/anti-lock-across-await.md) - Don't hold locks across `.await`
+- [`anti-string-for-str`](rules/anti-string-for-str.md) - Don't accept `&String` when `&str` works
+- [`anti-vec-for-slice`](rules/anti-vec-for-slice.md) - Don't accept `&Vec` when `&[T]` works
+- [`anti-index-over-iter`](rules/anti-index-over-iter.md) - Don't use indexing when iterators work
+- [`anti-panic-expected`](rules/anti-panic-expected.md) - Don't panic on expected/recoverable errors
+- [`anti-empty-catch`](rules/anti-empty-catch.md) - Don't use empty `if let Err(_) = ...` blocks
+- [`anti-over-abstraction`](rules/anti-over-abstraction.md) - Don't over-abstract with excessive generics
+- [`anti-premature-optimize`](rules/anti-premature-optimize.md) - Don't optimize before profiling
+- [`anti-type-erasure`](rules/anti-type-erasure.md) - Don't use `Box` when `impl Trait` works
+- [`anti-format-hot-path`](rules/anti-format-hot-path.md) - Don't use `format!()` in hot paths
+- [`anti-collect-intermediate`](rules/anti-collect-intermediate.md) - Don't `collect()` intermediate iterators
+- [`anti-stringly-typed`](rules/anti-stringly-typed.md) - Don't use strings for structured data
+
+---
+
+## Recommended Cargo.toml Settings
+
+```toml
+[profile.release]
+opt-level = 3
+lto = "fat"
+codegen-units = 1
+panic = "abort"
+strip = true
+
+[profile.bench]
+inherits = "release"
+debug = true
+strip = false
+
+[profile.dev]
+opt-level = 0
+debug = true
+
+[profile.dev.package."*"]
+opt-level = 3 # Optimize dependencies in dev
+```
+
+---
+
+## How to Use
+
+This skill provides rule identifiers for quick reference. When generating or reviewing Rust code:
+
+1. **Check relevant category** based on task type
+2. **Apply rules** with matching prefix
+3. **Prioritize** CRITICAL > HIGH > MEDIUM > LOW
+4. **Read rule files** in `rules/` for detailed examples
+
+### Rule Application by Task
+
+| Task | Primary Categories |
+|------|-------------------|
+| New function | `own-`, `err-`, `name-` |
+| New struct/API | `api-`, `type-`, `doc-` |
+| Async code | `async-`, `own-` |
+| Error handling | `err-`, `api-` |
+| Memory optimization | `mem-`, `own-`, `perf-` |
+| Performance tuning | `opt-`, `mem-`, `perf-` |
+| Code review | `anti-`, `lint-` |
+
+---
+
+## Sources
+
+This skill synthesizes best practices from:
+- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
+- [Rust Performance Book](https://nnethercote.github.io/perf-book/)
+- [Rust Design Patterns](https://rust-unofficial.github.io/patterns/)
+- Production codebases: ripgrep, tokio, serde, polars, axum, deno
+- Clippy lint documentation
+- Community conventions (2024-2025)
diff --git a/.agents/skills/rust-skills/CLAUDE.md b/.agents/skills/rust-skills/CLAUDE.md
new file mode 100644
index 000000000..c0f008d2d
--- /dev/null
+++ b/.agents/skills/rust-skills/CLAUDE.md
@@ -0,0 +1,335 @@
+---
+name: rust-skills
+description: >
+ Comprehensive Rust coding guidelines with 179 rules across 14 categories.
+ Use when writing, reviewing, or refactoring Rust code. Covers ownership,
+ error handling, async patterns, API design, memory optimization, performance,
+ testing, and common anti-patterns. Invoke with /rust-skills.
+license: MIT
+metadata:
+ author: leonardomso
+ version: "1.0.0"
+ sources:
+ - Rust API Guidelines
+ - Rust Performance Book
+ - ripgrep, tokio, serde, polars codebases
+---
+
+# Rust Best Practices
+
+Comprehensive guide for writing high-quality, idiomatic, and highly optimized Rust code. Contains 179 rules across 14 categories, prioritized by impact to guide LLMs in code generation and refactoring.
+
+## When to Apply
+
+Reference these guidelines when:
+- Writing new Rust functions, structs, or modules
+- Implementing error handling or async code
+- Designing public APIs for libraries
+- Reviewing code for ownership/borrowing issues
+- Optimizing memory usage or reducing allocations
+- Tuning performance for hot paths
+- Refactoring existing Rust code
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix | Rules |
+|----------|----------|--------|--------|-------|
+| 1 | Ownership & Borrowing | CRITICAL | `own-` | 12 |
+| 2 | Error Handling | CRITICAL | `err-` | 12 |
+| 3 | Memory Optimization | CRITICAL | `mem-` | 15 |
+| 4 | API Design | HIGH | `api-` | 15 |
+| 5 | Async/Await | HIGH | `async-` | 15 |
+| 6 | Compiler Optimization | HIGH | `opt-` | 12 |
+| 7 | Naming Conventions | MEDIUM | `name-` | 16 |
+| 8 | Type Safety | MEDIUM | `type-` | 10 |
+| 9 | Testing | MEDIUM | `test-` | 13 |
+| 10 | Documentation | MEDIUM | `doc-` | 11 |
+| 11 | Performance Patterns | MEDIUM | `perf-` | 11 |
+| 12 | Project Structure | LOW | `proj-` | 11 |
+| 13 | Clippy & Linting | LOW | `lint-` | 11 |
+| 14 | Anti-patterns | REFERENCE | `anti-` | 15 |
+
+---
+
+## Quick Reference
+
+### 1. Ownership & Borrowing (CRITICAL)
+
+- [`own-borrow-over-clone`](rules/own-borrow-over-clone.md) - Prefer `&T` borrowing over `.clone()`
+- [`own-slice-over-vec`](rules/own-slice-over-vec.md) - Accept `&[T]` not `&Vec`, `&str` not `&String`
+- [`own-cow-conditional`](rules/own-cow-conditional.md) - Use `Cow<'a, T>` for conditional ownership
+- [`own-arc-shared`](rules/own-arc-shared.md) - Use `Arc` for thread-safe shared ownership
+- [`own-rc-single-thread`](rules/own-rc-single-thread.md) - Use `Rc` for single-threaded sharing
+- [`own-refcell-interior`](rules/own-refcell-interior.md) - Use `RefCell` for interior mutability (single-thread)
+- [`own-mutex-interior`](rules/own-mutex-interior.md) - Use `Mutex` for interior mutability (multi-thread)
+- [`own-rwlock-readers`](rules/own-rwlock-readers.md) - Use `RwLock` when reads dominate writes
+- [`own-copy-small`](rules/own-copy-small.md) - Derive `Copy` for small, trivial types
+- [`own-clone-explicit`](rules/own-clone-explicit.md) - Make `Clone` explicit, avoid implicit copies
+- [`own-move-large`](rules/own-move-large.md) - Move large data instead of cloning
+- [`own-lifetime-elision`](rules/own-lifetime-elision.md) - Rely on lifetime elision when possible
+
+### 2. Error Handling (CRITICAL)
+
+- [`err-thiserror-lib`](rules/err-thiserror-lib.md) - Use `thiserror` for library error types
+- [`err-anyhow-app`](rules/err-anyhow-app.md) - Use `anyhow` for application error handling
+- [`err-result-over-panic`](rules/err-result-over-panic.md) - Return `Result`, don't panic on expected errors
+- [`err-context-chain`](rules/err-context-chain.md) - Add context with `.context()` or `.with_context()`
+- [`err-no-unwrap-prod`](rules/err-no-unwrap-prod.md) - Never use `.unwrap()` in production code
+- [`err-expect-bugs-only`](rules/err-expect-bugs-only.md) - Use `.expect()` only for programming errors
+- [`err-question-mark`](rules/err-question-mark.md) - Use `?` operator for clean propagation
+- [`err-from-impl`](rules/err-from-impl.md) - Use `#[from]` for automatic error conversion
+- [`err-source-chain`](rules/err-source-chain.md) - Use `#[source]` to chain underlying errors
+- [`err-lowercase-msg`](rules/err-lowercase-msg.md) - Error messages: lowercase, no trailing punctuation
+- [`err-doc-errors`](rules/err-doc-errors.md) - Document errors with `# Errors` section
+- [`err-custom-type`](rules/err-custom-type.md) - Create custom error types, not `Box`
+
+### 3. Memory Optimization (CRITICAL)
+
+- [`mem-with-capacity`](rules/mem-with-capacity.md) - Use `with_capacity()` when size is known
+- [`mem-smallvec`](rules/mem-smallvec.md) - Use `SmallVec` for usually-small collections
+- [`mem-arrayvec`](rules/mem-arrayvec.md) - Use `ArrayVec` for bounded-size collections
+- [`mem-box-large-variant`](rules/mem-box-large-variant.md) - Box large enum variants to reduce type size
+- [`mem-boxed-slice`](rules/mem-boxed-slice.md) - Use `Box<[T]>` instead of `Vec` when fixed
+- [`mem-thinvec`](rules/mem-thinvec.md) - Use `ThinVec` for often-empty vectors
+- [`mem-clone-from`](rules/mem-clone-from.md) - Use `clone_from()` to reuse allocations
+- [`mem-reuse-collections`](rules/mem-reuse-collections.md) - Reuse collections with `clear()` in loops
+- [`mem-avoid-format`](rules/mem-avoid-format.md) - Avoid `format!()` when string literals work
+- [`mem-write-over-format`](rules/mem-write-over-format.md) - Use `write!()` instead of `format!()`
+- [`mem-arena-allocator`](rules/mem-arena-allocator.md) - Use arena allocators for batch allocations
+- [`mem-zero-copy`](rules/mem-zero-copy.md) - Use zero-copy patterns with slices and `Bytes`
+- [`mem-compact-string`](rules/mem-compact-string.md) - Use `CompactString` for small string optimization
+- [`mem-smaller-integers`](rules/mem-smaller-integers.md) - Use smallest integer type that fits
+- [`mem-assert-type-size`](rules/mem-assert-type-size.md) - Assert hot type sizes to prevent regressions
+
+### 4. API Design (HIGH)
+
+- [`api-builder-pattern`](rules/api-builder-pattern.md) - Use Builder pattern for complex construction
+- [`api-builder-must-use`](rules/api-builder-must-use.md) - Add `#[must_use]` to builder types
+- [`api-newtype-safety`](rules/api-newtype-safety.md) - Use newtypes for type-safe distinctions
+- [`api-typestate`](rules/api-typestate.md) - Use typestate for compile-time state machines
+- [`api-sealed-trait`](rules/api-sealed-trait.md) - Seal traits to prevent external implementations
+- [`api-extension-trait`](rules/api-extension-trait.md) - Use extension traits to add methods to foreign types
+- [`api-parse-dont-validate`](rules/api-parse-dont-validate.md) - Parse into validated types at boundaries
+- [`api-impl-into`](rules/api-impl-into.md) - Accept `impl Into` for flexible string inputs
+- [`api-impl-asref`](rules/api-impl-asref.md) - Accept `impl AsRef` for borrowed inputs
+- [`api-must-use`](rules/api-must-use.md) - Add `#[must_use]` to `Result` returning functions
+- [`api-non-exhaustive`](rules/api-non-exhaustive.md) - Use `#[non_exhaustive]` for future-proof enums/structs
+- [`api-from-not-into`](rules/api-from-not-into.md) - Implement `From`, not `Into` (auto-derived)
+- [`api-default-impl`](rules/api-default-impl.md) - Implement `Default` for sensible defaults
+- [`api-common-traits`](rules/api-common-traits.md) - Implement `Debug`, `Clone`, `PartialEq` eagerly
+- [`api-serde-optional`](rules/api-serde-optional.md) - Gate `Serialize`/`Deserialize` behind feature flag
+
+### 5. Async/Await (HIGH)
+
+- [`async-tokio-runtime`](rules/async-tokio-runtime.md) - Use Tokio for production async runtime
+- [`async-no-lock-await`](rules/async-no-lock-await.md) - Never hold `Mutex`/`RwLock` across `.await`
+- [`async-spawn-blocking`](rules/async-spawn-blocking.md) - Use `spawn_blocking` for CPU-intensive work
+- [`async-tokio-fs`](rules/async-tokio-fs.md) - Use `tokio::fs` not `std::fs` in async code
+- [`async-cancellation-token`](rules/async-cancellation-token.md) - Use `CancellationToken` for graceful shutdown
+- [`async-join-parallel`](rules/async-join-parallel.md) - Use `tokio::join!` for parallel operations
+- [`async-try-join`](rules/async-try-join.md) - Use `tokio::try_join!` for fallible parallel ops
+- [`async-select-racing`](rules/async-select-racing.md) - Use `tokio::select!` for racing/timeouts
+- [`async-bounded-channel`](rules/async-bounded-channel.md) - Use bounded channels for backpressure
+- [`async-mpsc-queue`](rules/async-mpsc-queue.md) - Use `mpsc` for work queues
+- [`async-broadcast-pubsub`](rules/async-broadcast-pubsub.md) - Use `broadcast` for pub/sub patterns
+- [`async-watch-latest`](rules/async-watch-latest.md) - Use `watch` for latest-value sharing
+- [`async-oneshot-response`](rules/async-oneshot-response.md) - Use `oneshot` for request/response
+- [`async-joinset-structured`](rules/async-joinset-structured.md) - Use `JoinSet` for dynamic task groups
+- [`async-clone-before-await`](rules/async-clone-before-await.md) - Clone data before await, release locks
+
+### 6. Compiler Optimization (HIGH)
+
+- [`opt-inline-small`](rules/opt-inline-small.md) - Use `#[inline]` for small hot functions
+- [`opt-inline-always-rare`](rules/opt-inline-always-rare.md) - Use `#[inline(always)]` sparingly
+- [`opt-inline-never-cold`](rules/opt-inline-never-cold.md) - Use `#[inline(never)]` for cold paths
+- [`opt-cold-unlikely`](rules/opt-cold-unlikely.md) - Use `#[cold]` for error/unlikely paths
+- [`opt-likely-hint`](rules/opt-likely-hint.md) - Use `likely()`/`unlikely()` for branch hints
+- [`opt-lto-release`](rules/opt-lto-release.md) - Enable LTO in release builds
+- [`opt-codegen-units`](rules/opt-codegen-units.md) - Use `codegen-units = 1` for max optimization
+- [`opt-pgo-profile`](rules/opt-pgo-profile.md) - Use PGO for production builds
+- [`opt-target-cpu`](rules/opt-target-cpu.md) - Set `target-cpu=native` for local builds
+- [`opt-bounds-check`](rules/opt-bounds-check.md) - Use iterators to avoid bounds checks
+- [`opt-simd-portable`](rules/opt-simd-portable.md) - Use portable SIMD for data-parallel ops
+- [`opt-cache-friendly`](rules/opt-cache-friendly.md) - Design cache-friendly data layouts (SoA)
+
+### 7. Naming Conventions (MEDIUM)
+
+- [`name-types-camel`](rules/name-types-camel.md) - Use `UpperCamelCase` for types, traits, enums
+- [`name-variants-camel`](rules/name-variants-camel.md) - Use `UpperCamelCase` for enum variants
+- [`name-funcs-snake`](rules/name-funcs-snake.md) - Use `snake_case` for functions, methods, modules
+- [`name-consts-screaming`](rules/name-consts-screaming.md) - Use `SCREAMING_SNAKE_CASE` for constants/statics
+- [`name-lifetime-short`](rules/name-lifetime-short.md) - Use short lowercase lifetimes: `'a`, `'de`, `'src`
+- [`name-type-param-single`](rules/name-type-param-single.md) - Use single uppercase for type params: `T`, `E`, `K`, `V`
+- [`name-as-free`](rules/name-as-free.md) - `as_` prefix: free reference conversion
+- [`name-to-expensive`](rules/name-to-expensive.md) - `to_` prefix: expensive conversion
+- [`name-into-ownership`](rules/name-into-ownership.md) - `into_` prefix: ownership transfer
+- [`name-no-get-prefix`](rules/name-no-get-prefix.md) - No `get_` prefix for simple getters
+- [`name-is-has-bool`](rules/name-is-has-bool.md) - Use `is_`, `has_`, `can_` for boolean methods
+- [`name-iter-convention`](rules/name-iter-convention.md) - Use `iter`/`iter_mut`/`into_iter` for iterators
+- [`name-iter-method`](rules/name-iter-method.md) - Name iterator methods consistently
+- [`name-iter-type-match`](rules/name-iter-type-match.md) - Iterator type names match method
+- [`name-acronym-word`](rules/name-acronym-word.md) - Treat acronyms as words: `Uuid` not `UUID`
+- [`name-crate-no-rs`](rules/name-crate-no-rs.md) - Crate names: no `-rs` suffix
+
+### 8. Type Safety (MEDIUM)
+
+- [`type-newtype-ids`](rules/type-newtype-ids.md) - Wrap IDs in newtypes: `UserId(u64)`
+- [`type-newtype-validated`](rules/type-newtype-validated.md) - Newtypes for validated data: `Email`, `Url`
+- [`type-enum-states`](rules/type-enum-states.md) - Use enums for mutually exclusive states
+- [`type-option-nullable`](rules/type-option-nullable.md) - Use `Option` for nullable values
+- [`type-result-fallible`](rules/type-result-fallible.md) - Use `Result` for fallible operations
+- [`type-phantom-marker`](rules/type-phantom-marker.md) - Use `PhantomData` for type-level markers
+- [`type-never-diverge`](rules/type-never-diverge.md) - Use `!` type for functions that never return
+- [`type-generic-bounds`](rules/type-generic-bounds.md) - Add trait bounds only where needed
+- [`type-no-stringly`](rules/type-no-stringly.md) - Avoid stringly-typed APIs, use enums/newtypes
+- [`type-repr-transparent`](rules/type-repr-transparent.md) - Use `#[repr(transparent)]` for FFI newtypes
+
+### 9. Testing (MEDIUM)
+
+- [`test-cfg-test-module`](rules/test-cfg-test-module.md) - Use `#[cfg(test)] mod tests { }`
+- [`test-use-super`](rules/test-use-super.md) - Use `use super::*;` in test modules
+- [`test-integration-dir`](rules/test-integration-dir.md) - Put integration tests in `tests/` directory
+- [`test-descriptive-names`](rules/test-descriptive-names.md) - Use descriptive test names
+- [`test-arrange-act-assert`](rules/test-arrange-act-assert.md) - Structure tests as arrange/act/assert
+- [`test-proptest-properties`](rules/test-proptest-properties.md) - Use `proptest` for property-based testing
+- [`test-mockall-mocking`](rules/test-mockall-mocking.md) - Use `mockall` for trait mocking
+- [`test-mock-traits`](rules/test-mock-traits.md) - Use traits for dependencies to enable mocking
+- [`test-fixture-raii`](rules/test-fixture-raii.md) - Use RAII pattern (Drop) for test cleanup
+- [`test-tokio-async`](rules/test-tokio-async.md) - Use `#[tokio::test]` for async tests
+- [`test-should-panic`](rules/test-should-panic.md) - Use `#[should_panic]` for panic tests
+- [`test-criterion-bench`](rules/test-criterion-bench.md) - Use `criterion` for benchmarking
+- [`test-doctest-examples`](rules/test-doctest-examples.md) - Keep doc examples as executable tests
+
+### 10. Documentation (MEDIUM)
+
+- [`doc-all-public`](rules/doc-all-public.md) - Document all public items with `///`
+- [`doc-module-inner`](rules/doc-module-inner.md) - Use `//!` for module-level documentation
+- [`doc-examples-section`](rules/doc-examples-section.md) - Include `# Examples` with runnable code
+- [`doc-errors-section`](rules/doc-errors-section.md) - Include `# Errors` for fallible functions
+- [`doc-panics-section`](rules/doc-panics-section.md) - Include `# Panics` for panicking functions
+- [`doc-safety-section`](rules/doc-safety-section.md) - Include `# Safety` for unsafe functions
+- [`doc-question-mark`](rules/doc-question-mark.md) - Use `?` in examples, not `.unwrap()`
+- [`doc-hidden-setup`](rules/doc-hidden-setup.md) - Use `# ` prefix to hide example setup code
+- [`doc-intra-links`](rules/doc-intra-links.md) - Use intra-doc links: `[Vec]`
+- [`doc-link-types`](rules/doc-link-types.md) - Link related types and functions in docs
+- [`doc-cargo-metadata`](rules/doc-cargo-metadata.md) - Fill `Cargo.toml` metadata
+
+### 11. Performance Patterns (MEDIUM)
+
+- [`perf-iter-over-index`](rules/perf-iter-over-index.md) - Prefer iterators over manual indexing
+- [`perf-iter-lazy`](rules/perf-iter-lazy.md) - Keep iterators lazy, collect() only when needed
+- [`perf-collect-once`](rules/perf-collect-once.md) - Don't `collect()` intermediate iterators
+- [`perf-entry-api`](rules/perf-entry-api.md) - Use `entry()` API for map insert-or-update
+- [`perf-drain-reuse`](rules/perf-drain-reuse.md) - Use `drain()` to reuse allocations
+- [`perf-extend-batch`](rules/perf-extend-batch.md) - Use `extend()` for batch insertions
+- [`perf-chain-avoid`](rules/perf-chain-avoid.md) - Avoid `chain()` in hot loops
+- [`perf-collect-into`](rules/perf-collect-into.md) - Use `collect_into()` for reusing containers
+- [`perf-black-box-bench`](rules/perf-black-box-bench.md) - Use `black_box()` in benchmarks
+- [`perf-release-profile`](rules/perf-release-profile.md) - Optimize release profile settings
+- [`perf-profile-first`](rules/perf-profile-first.md) - Profile before optimizing
+
+### 12. Project Structure (LOW)
+
+- [`proj-lib-main-split`](rules/proj-lib-main-split.md) - Keep `main.rs` minimal, logic in `lib.rs`
+- [`proj-mod-by-feature`](rules/proj-mod-by-feature.md) - Organize modules by feature, not type
+- [`proj-flat-small`](rules/proj-flat-small.md) - Keep small projects flat
+- [`proj-mod-rs-dir`](rules/proj-mod-rs-dir.md) - Use `mod.rs` for multi-file modules
+- [`proj-pub-crate-internal`](rules/proj-pub-crate-internal.md) - Use `pub(crate)` for internal APIs
+- [`proj-pub-super-parent`](rules/proj-pub-super-parent.md) - Use `pub(super)` for parent-only visibility
+- [`proj-pub-use-reexport`](rules/proj-pub-use-reexport.md) - Use `pub use` for clean public API
+- [`proj-prelude-module`](rules/proj-prelude-module.md) - Create `prelude` module for common imports
+- [`proj-bin-dir`](rules/proj-bin-dir.md) - Put multiple binaries in `src/bin/`
+- [`proj-workspace-large`](rules/proj-workspace-large.md) - Use workspaces for large projects
+- [`proj-workspace-deps`](rules/proj-workspace-deps.md) - Use workspace dependency inheritance
+
+### 13. Clippy & Linting (LOW)
+
+- [`lint-deny-correctness`](rules/lint-deny-correctness.md) - `#![deny(clippy::correctness)]`
+- [`lint-warn-suspicious`](rules/lint-warn-suspicious.md) - `#![warn(clippy::suspicious)]`
+- [`lint-warn-style`](rules/lint-warn-style.md) - `#![warn(clippy::style)]`
+- [`lint-warn-complexity`](rules/lint-warn-complexity.md) - `#![warn(clippy::complexity)]`
+- [`lint-warn-perf`](rules/lint-warn-perf.md) - `#![warn(clippy::perf)]`
+- [`lint-pedantic-selective`](rules/lint-pedantic-selective.md) - Enable `clippy::pedantic` selectively
+- [`lint-missing-docs`](rules/lint-missing-docs.md) - `#![warn(missing_docs)]`
+- [`lint-unsafe-doc`](rules/lint-unsafe-doc.md) - `#![warn(clippy::undocumented_unsafe_blocks)]`
+- [`lint-cargo-metadata`](rules/lint-cargo-metadata.md) - `#![warn(clippy::cargo)]` for published crates
+- [`lint-rustfmt-check`](rules/lint-rustfmt-check.md) - Run `cargo fmt --check` in CI
+- [`lint-workspace-lints`](rules/lint-workspace-lints.md) - Configure lints at workspace level
+
+### 14. Anti-patterns (REFERENCE)
+
+- [`anti-unwrap-abuse`](rules/anti-unwrap-abuse.md) - Don't use `.unwrap()` in production code
+- [`anti-expect-lazy`](rules/anti-expect-lazy.md) - Don't use `.expect()` for recoverable errors
+- [`anti-clone-excessive`](rules/anti-clone-excessive.md) - Don't clone when borrowing works
+- [`anti-lock-across-await`](rules/anti-lock-across-await.md) - Don't hold locks across `.await`
+- [`anti-string-for-str`](rules/anti-string-for-str.md) - Don't accept `&String` when `&str` works
+- [`anti-vec-for-slice`](rules/anti-vec-for-slice.md) - Don't accept `&Vec` when `&[T]` works
+- [`anti-index-over-iter`](rules/anti-index-over-iter.md) - Don't use indexing when iterators work
+- [`anti-panic-expected`](rules/anti-panic-expected.md) - Don't panic on expected/recoverable errors
+- [`anti-empty-catch`](rules/anti-empty-catch.md) - Don't use empty `if let Err(_) = ...` blocks
+- [`anti-over-abstraction`](rules/anti-over-abstraction.md) - Don't over-abstract with excessive generics
+- [`anti-premature-optimize`](rules/anti-premature-optimize.md) - Don't optimize before profiling
+- [`anti-type-erasure`](rules/anti-type-erasure.md) - Don't use `Box` when `impl Trait` works
+- [`anti-format-hot-path`](rules/anti-format-hot-path.md) - Don't use `format!()` in hot paths
+- [`anti-collect-intermediate`](rules/anti-collect-intermediate.md) - Don't `collect()` intermediate iterators
+- [`anti-stringly-typed`](rules/anti-stringly-typed.md) - Don't use strings for structured data
+
+---
+
+## Recommended Cargo.toml Settings
+
+```toml
+[profile.release]
+opt-level = 3
+lto = "fat"
+codegen-units = 1
+panic = "abort"
+strip = true
+
+[profile.bench]
+inherits = "release"
+debug = true
+strip = false
+
+[profile.dev]
+opt-level = 0
+debug = true
+
+[profile.dev.package."*"]
+opt-level = 3 # Optimize dependencies in dev
+```
+
+---
+
+## How to Use
+
+This skill provides rule identifiers for quick reference. When generating or reviewing Rust code:
+
+1. **Check relevant category** based on task type
+2. **Apply rules** with matching prefix
+3. **Prioritize** CRITICAL > HIGH > MEDIUM > LOW
+4. **Read rule files** in `rules/` for detailed examples
+
+### Rule Application by Task
+
+| Task | Primary Categories |
+|------|-------------------|
+| New function | `own-`, `err-`, `name-` |
+| New struct/API | `api-`, `type-`, `doc-` |
+| Async code | `async-`, `own-` |
+| Error handling | `err-`, `api-` |
+| Memory optimization | `mem-`, `own-`, `perf-` |
+| Performance tuning | `opt-`, `mem-`, `perf-` |
+| Code review | `anti-`, `lint-` |
+
+---
+
+## Sources
+
+This skill synthesizes best practices from:
+- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
+- [Rust Performance Book](https://nnethercote.github.io/perf-book/)
+- [Rust Design Patterns](https://rust-unofficial.github.io/patterns/)
+- Production codebases: ripgrep, tokio, serde, polars, axum, deno
+- Clippy lint documentation
+- Community conventions (2024-2025)
diff --git a/.agents/skills/rust-skills/LICENSE b/.agents/skills/rust-skills/LICENSE
new file mode 100644
index 000000000..3f2707070
--- /dev/null
+++ b/.agents/skills/rust-skills/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Leonardo Maldonado
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/.agents/skills/rust-skills/SKILL.md b/.agents/skills/rust-skills/SKILL.md
new file mode 100644
index 000000000..c0f008d2d
--- /dev/null
+++ b/.agents/skills/rust-skills/SKILL.md
@@ -0,0 +1,335 @@
+---
+name: rust-skills
+description: >
+ Comprehensive Rust coding guidelines with 179 rules across 14 categories.
+ Use when writing, reviewing, or refactoring Rust code. Covers ownership,
+ error handling, async patterns, API design, memory optimization, performance,
+ testing, and common anti-patterns. Invoke with /rust-skills.
+license: MIT
+metadata:
+ author: leonardomso
+ version: "1.0.0"
+ sources:
+ - Rust API Guidelines
+ - Rust Performance Book
+ - ripgrep, tokio, serde, polars codebases
+---
+
+# Rust Best Practices
+
+Comprehensive guide for writing high-quality, idiomatic, and highly optimized Rust code. Contains 179 rules across 14 categories, prioritized by impact to guide LLMs in code generation and refactoring.
+
+## When to Apply
+
+Reference these guidelines when:
+- Writing new Rust functions, structs, or modules
+- Implementing error handling or async code
+- Designing public APIs for libraries
+- Reviewing code for ownership/borrowing issues
+- Optimizing memory usage or reducing allocations
+- Tuning performance for hot paths
+- Refactoring existing Rust code
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix | Rules |
+|----------|----------|--------|--------|-------|
+| 1 | Ownership & Borrowing | CRITICAL | `own-` | 12 |
+| 2 | Error Handling | CRITICAL | `err-` | 12 |
+| 3 | Memory Optimization | CRITICAL | `mem-` | 15 |
+| 4 | API Design | HIGH | `api-` | 15 |
+| 5 | Async/Await | HIGH | `async-` | 15 |
+| 6 | Compiler Optimization | HIGH | `opt-` | 12 |
+| 7 | Naming Conventions | MEDIUM | `name-` | 16 |
+| 8 | Type Safety | MEDIUM | `type-` | 10 |
+| 9 | Testing | MEDIUM | `test-` | 13 |
+| 10 | Documentation | MEDIUM | `doc-` | 11 |
+| 11 | Performance Patterns | MEDIUM | `perf-` | 11 |
+| 12 | Project Structure | LOW | `proj-` | 11 |
+| 13 | Clippy & Linting | LOW | `lint-` | 11 |
+| 14 | Anti-patterns | REFERENCE | `anti-` | 15 |
+
+---
+
+## Quick Reference
+
+### 1. Ownership & Borrowing (CRITICAL)
+
+- [`own-borrow-over-clone`](rules/own-borrow-over-clone.md) - Prefer `&T` borrowing over `.clone()`
+- [`own-slice-over-vec`](rules/own-slice-over-vec.md) - Accept `&[T]` not `&Vec`, `&str` not `&String`
+- [`own-cow-conditional`](rules/own-cow-conditional.md) - Use `Cow<'a, T>` for conditional ownership
+- [`own-arc-shared`](rules/own-arc-shared.md) - Use `Arc` for thread-safe shared ownership
+- [`own-rc-single-thread`](rules/own-rc-single-thread.md) - Use `Rc` for single-threaded sharing
+- [`own-refcell-interior`](rules/own-refcell-interior.md) - Use `RefCell` for interior mutability (single-thread)
+- [`own-mutex-interior`](rules/own-mutex-interior.md) - Use `Mutex` for interior mutability (multi-thread)
+- [`own-rwlock-readers`](rules/own-rwlock-readers.md) - Use `RwLock` when reads dominate writes
+- [`own-copy-small`](rules/own-copy-small.md) - Derive `Copy` for small, trivial types
+- [`own-clone-explicit`](rules/own-clone-explicit.md) - Make `Clone` explicit, avoid implicit copies
+- [`own-move-large`](rules/own-move-large.md) - Move large data instead of cloning
+- [`own-lifetime-elision`](rules/own-lifetime-elision.md) - Rely on lifetime elision when possible
+
+### 2. Error Handling (CRITICAL)
+
+- [`err-thiserror-lib`](rules/err-thiserror-lib.md) - Use `thiserror` for library error types
+- [`err-anyhow-app`](rules/err-anyhow-app.md) - Use `anyhow` for application error handling
+- [`err-result-over-panic`](rules/err-result-over-panic.md) - Return `Result`, don't panic on expected errors
+- [`err-context-chain`](rules/err-context-chain.md) - Add context with `.context()` or `.with_context()`
+- [`err-no-unwrap-prod`](rules/err-no-unwrap-prod.md) - Never use `.unwrap()` in production code
+- [`err-expect-bugs-only`](rules/err-expect-bugs-only.md) - Use `.expect()` only for programming errors
+- [`err-question-mark`](rules/err-question-mark.md) - Use `?` operator for clean propagation
+- [`err-from-impl`](rules/err-from-impl.md) - Use `#[from]` for automatic error conversion
+- [`err-source-chain`](rules/err-source-chain.md) - Use `#[source]` to chain underlying errors
+- [`err-lowercase-msg`](rules/err-lowercase-msg.md) - Error messages: lowercase, no trailing punctuation
+- [`err-doc-errors`](rules/err-doc-errors.md) - Document errors with `# Errors` section
+- [`err-custom-type`](rules/err-custom-type.md) - Create custom error types, not `Box`
+
+### 3. Memory Optimization (CRITICAL)
+
+- [`mem-with-capacity`](rules/mem-with-capacity.md) - Use `with_capacity()` when size is known
+- [`mem-smallvec`](rules/mem-smallvec.md) - Use `SmallVec` for usually-small collections
+- [`mem-arrayvec`](rules/mem-arrayvec.md) - Use `ArrayVec` for bounded-size collections
+- [`mem-box-large-variant`](rules/mem-box-large-variant.md) - Box large enum variants to reduce type size
+- [`mem-boxed-slice`](rules/mem-boxed-slice.md) - Use `Box<[T]>` instead of `Vec` when fixed
+- [`mem-thinvec`](rules/mem-thinvec.md) - Use `ThinVec` for often-empty vectors
+- [`mem-clone-from`](rules/mem-clone-from.md) - Use `clone_from()` to reuse allocations
+- [`mem-reuse-collections`](rules/mem-reuse-collections.md) - Reuse collections with `clear()` in loops
+- [`mem-avoid-format`](rules/mem-avoid-format.md) - Avoid `format!()` when string literals work
+- [`mem-write-over-format`](rules/mem-write-over-format.md) - Use `write!()` instead of `format!()`
+- [`mem-arena-allocator`](rules/mem-arena-allocator.md) - Use arena allocators for batch allocations
+- [`mem-zero-copy`](rules/mem-zero-copy.md) - Use zero-copy patterns with slices and `Bytes`
+- [`mem-compact-string`](rules/mem-compact-string.md) - Use `CompactString` for small string optimization
+- [`mem-smaller-integers`](rules/mem-smaller-integers.md) - Use smallest integer type that fits
+- [`mem-assert-type-size`](rules/mem-assert-type-size.md) - Assert hot type sizes to prevent regressions
+
+### 4. API Design (HIGH)
+
+- [`api-builder-pattern`](rules/api-builder-pattern.md) - Use Builder pattern for complex construction
+- [`api-builder-must-use`](rules/api-builder-must-use.md) - Add `#[must_use]` to builder types
+- [`api-newtype-safety`](rules/api-newtype-safety.md) - Use newtypes for type-safe distinctions
+- [`api-typestate`](rules/api-typestate.md) - Use typestate for compile-time state machines
+- [`api-sealed-trait`](rules/api-sealed-trait.md) - Seal traits to prevent external implementations
+- [`api-extension-trait`](rules/api-extension-trait.md) - Use extension traits to add methods to foreign types
+- [`api-parse-dont-validate`](rules/api-parse-dont-validate.md) - Parse into validated types at boundaries
+- [`api-impl-into`](rules/api-impl-into.md) - Accept `impl Into` for flexible string inputs
+- [`api-impl-asref`](rules/api-impl-asref.md) - Accept `impl AsRef` for borrowed inputs
+- [`api-must-use`](rules/api-must-use.md) - Add `#[must_use]` to `Result` returning functions
+- [`api-non-exhaustive`](rules/api-non-exhaustive.md) - Use `#[non_exhaustive]` for future-proof enums/structs
+- [`api-from-not-into`](rules/api-from-not-into.md) - Implement `From`, not `Into` (auto-derived)
+- [`api-default-impl`](rules/api-default-impl.md) - Implement `Default` for sensible defaults
+- [`api-common-traits`](rules/api-common-traits.md) - Implement `Debug`, `Clone`, `PartialEq` eagerly
+- [`api-serde-optional`](rules/api-serde-optional.md) - Gate `Serialize`/`Deserialize` behind feature flag
+
+### 5. Async/Await (HIGH)
+
+- [`async-tokio-runtime`](rules/async-tokio-runtime.md) - Use Tokio for production async runtime
+- [`async-no-lock-await`](rules/async-no-lock-await.md) - Never hold `Mutex`/`RwLock` across `.await`
+- [`async-spawn-blocking`](rules/async-spawn-blocking.md) - Use `spawn_blocking` for CPU-intensive work
+- [`async-tokio-fs`](rules/async-tokio-fs.md) - Use `tokio::fs` not `std::fs` in async code
+- [`async-cancellation-token`](rules/async-cancellation-token.md) - Use `CancellationToken` for graceful shutdown
+- [`async-join-parallel`](rules/async-join-parallel.md) - Use `tokio::join!` for parallel operations
+- [`async-try-join`](rules/async-try-join.md) - Use `tokio::try_join!` for fallible parallel ops
+- [`async-select-racing`](rules/async-select-racing.md) - Use `tokio::select!` for racing/timeouts
+- [`async-bounded-channel`](rules/async-bounded-channel.md) - Use bounded channels for backpressure
+- [`async-mpsc-queue`](rules/async-mpsc-queue.md) - Use `mpsc` for work queues
+- [`async-broadcast-pubsub`](rules/async-broadcast-pubsub.md) - Use `broadcast` for pub/sub patterns
+- [`async-watch-latest`](rules/async-watch-latest.md) - Use `watch` for latest-value sharing
+- [`async-oneshot-response`](rules/async-oneshot-response.md) - Use `oneshot` for request/response
+- [`async-joinset-structured`](rules/async-joinset-structured.md) - Use `JoinSet` for dynamic task groups
+- [`async-clone-before-await`](rules/async-clone-before-await.md) - Clone data before await, release locks
+
+### 6. Compiler Optimization (HIGH)
+
+- [`opt-inline-small`](rules/opt-inline-small.md) - Use `#[inline]` for small hot functions
+- [`opt-inline-always-rare`](rules/opt-inline-always-rare.md) - Use `#[inline(always)]` sparingly
+- [`opt-inline-never-cold`](rules/opt-inline-never-cold.md) - Use `#[inline(never)]` for cold paths
+- [`opt-cold-unlikely`](rules/opt-cold-unlikely.md) - Use `#[cold]` for error/unlikely paths
+- [`opt-likely-hint`](rules/opt-likely-hint.md) - Use `likely()`/`unlikely()` for branch hints
+- [`opt-lto-release`](rules/opt-lto-release.md) - Enable LTO in release builds
+- [`opt-codegen-units`](rules/opt-codegen-units.md) - Use `codegen-units = 1` for max optimization
+- [`opt-pgo-profile`](rules/opt-pgo-profile.md) - Use PGO for production builds
+- [`opt-target-cpu`](rules/opt-target-cpu.md) - Set `target-cpu=native` for local builds
+- [`opt-bounds-check`](rules/opt-bounds-check.md) - Use iterators to avoid bounds checks
+- [`opt-simd-portable`](rules/opt-simd-portable.md) - Use portable SIMD for data-parallel ops
+- [`opt-cache-friendly`](rules/opt-cache-friendly.md) - Design cache-friendly data layouts (SoA)
+
+### 7. Naming Conventions (MEDIUM)
+
+- [`name-types-camel`](rules/name-types-camel.md) - Use `UpperCamelCase` for types, traits, enums
+- [`name-variants-camel`](rules/name-variants-camel.md) - Use `UpperCamelCase` for enum variants
+- [`name-funcs-snake`](rules/name-funcs-snake.md) - Use `snake_case` for functions, methods, modules
+- [`name-consts-screaming`](rules/name-consts-screaming.md) - Use `SCREAMING_SNAKE_CASE` for constants/statics
+- [`name-lifetime-short`](rules/name-lifetime-short.md) - Use short lowercase lifetimes: `'a`, `'de`, `'src`
+- [`name-type-param-single`](rules/name-type-param-single.md) - Use single uppercase for type params: `T`, `E`, `K`, `V`
+- [`name-as-free`](rules/name-as-free.md) - `as_` prefix: free reference conversion
+- [`name-to-expensive`](rules/name-to-expensive.md) - `to_` prefix: expensive conversion
+- [`name-into-ownership`](rules/name-into-ownership.md) - `into_` prefix: ownership transfer
+- [`name-no-get-prefix`](rules/name-no-get-prefix.md) - No `get_` prefix for simple getters
+- [`name-is-has-bool`](rules/name-is-has-bool.md) - Use `is_`, `has_`, `can_` for boolean methods
+- [`name-iter-convention`](rules/name-iter-convention.md) - Use `iter`/`iter_mut`/`into_iter` for iterators
+- [`name-iter-method`](rules/name-iter-method.md) - Name iterator methods consistently
+- [`name-iter-type-match`](rules/name-iter-type-match.md) - Iterator type names match method
+- [`name-acronym-word`](rules/name-acronym-word.md) - Treat acronyms as words: `Uuid` not `UUID`
+- [`name-crate-no-rs`](rules/name-crate-no-rs.md) - Crate names: no `-rs` suffix
+
+### 8. Type Safety (MEDIUM)
+
+- [`type-newtype-ids`](rules/type-newtype-ids.md) - Wrap IDs in newtypes: `UserId(u64)`
+- [`type-newtype-validated`](rules/type-newtype-validated.md) - Newtypes for validated data: `Email`, `Url`
+- [`type-enum-states`](rules/type-enum-states.md) - Use enums for mutually exclusive states
+- [`type-option-nullable`](rules/type-option-nullable.md) - Use `Option` for nullable values
+- [`type-result-fallible`](rules/type-result-fallible.md) - Use `Result` for fallible operations
+- [`type-phantom-marker`](rules/type-phantom-marker.md) - Use `PhantomData` for type-level markers
+- [`type-never-diverge`](rules/type-never-diverge.md) - Use `!` type for functions that never return
+- [`type-generic-bounds`](rules/type-generic-bounds.md) - Add trait bounds only where needed
+- [`type-no-stringly`](rules/type-no-stringly.md) - Avoid stringly-typed APIs, use enums/newtypes
+- [`type-repr-transparent`](rules/type-repr-transparent.md) - Use `#[repr(transparent)]` for FFI newtypes
+
+### 9. Testing (MEDIUM)
+
+- [`test-cfg-test-module`](rules/test-cfg-test-module.md) - Use `#[cfg(test)] mod tests { }`
+- [`test-use-super`](rules/test-use-super.md) - Use `use super::*;` in test modules
+- [`test-integration-dir`](rules/test-integration-dir.md) - Put integration tests in `tests/` directory
+- [`test-descriptive-names`](rules/test-descriptive-names.md) - Use descriptive test names
+- [`test-arrange-act-assert`](rules/test-arrange-act-assert.md) - Structure tests as arrange/act/assert
+- [`test-proptest-properties`](rules/test-proptest-properties.md) - Use `proptest` for property-based testing
+- [`test-mockall-mocking`](rules/test-mockall-mocking.md) - Use `mockall` for trait mocking
+- [`test-mock-traits`](rules/test-mock-traits.md) - Use traits for dependencies to enable mocking
+- [`test-fixture-raii`](rules/test-fixture-raii.md) - Use RAII pattern (Drop) for test cleanup
+- [`test-tokio-async`](rules/test-tokio-async.md) - Use `#[tokio::test]` for async tests
+- [`test-should-panic`](rules/test-should-panic.md) - Use `#[should_panic]` for panic tests
+- [`test-criterion-bench`](rules/test-criterion-bench.md) - Use `criterion` for benchmarking
+- [`test-doctest-examples`](rules/test-doctest-examples.md) - Keep doc examples as executable tests
+
+### 10. Documentation (MEDIUM)
+
+- [`doc-all-public`](rules/doc-all-public.md) - Document all public items with `///`
+- [`doc-module-inner`](rules/doc-module-inner.md) - Use `//!` for module-level documentation
+- [`doc-examples-section`](rules/doc-examples-section.md) - Include `# Examples` with runnable code
+- [`doc-errors-section`](rules/doc-errors-section.md) - Include `# Errors` for fallible functions
+- [`doc-panics-section`](rules/doc-panics-section.md) - Include `# Panics` for panicking functions
+- [`doc-safety-section`](rules/doc-safety-section.md) - Include `# Safety` for unsafe functions
+- [`doc-question-mark`](rules/doc-question-mark.md) - Use `?` in examples, not `.unwrap()`
+- [`doc-hidden-setup`](rules/doc-hidden-setup.md) - Use `# ` prefix to hide example setup code
+- [`doc-intra-links`](rules/doc-intra-links.md) - Use intra-doc links: `[Vec]`
+- [`doc-link-types`](rules/doc-link-types.md) - Link related types and functions in docs
+- [`doc-cargo-metadata`](rules/doc-cargo-metadata.md) - Fill `Cargo.toml` metadata
+
+### 11. Performance Patterns (MEDIUM)
+
+- [`perf-iter-over-index`](rules/perf-iter-over-index.md) - Prefer iterators over manual indexing
+- [`perf-iter-lazy`](rules/perf-iter-lazy.md) - Keep iterators lazy, collect() only when needed
+- [`perf-collect-once`](rules/perf-collect-once.md) - Don't `collect()` intermediate iterators
+- [`perf-entry-api`](rules/perf-entry-api.md) - Use `entry()` API for map insert-or-update
+- [`perf-drain-reuse`](rules/perf-drain-reuse.md) - Use `drain()` to reuse allocations
+- [`perf-extend-batch`](rules/perf-extend-batch.md) - Use `extend()` for batch insertions
+- [`perf-chain-avoid`](rules/perf-chain-avoid.md) - Avoid `chain()` in hot loops
+- [`perf-collect-into`](rules/perf-collect-into.md) - Use `collect_into()` for reusing containers
+- [`perf-black-box-bench`](rules/perf-black-box-bench.md) - Use `black_box()` in benchmarks
+- [`perf-release-profile`](rules/perf-release-profile.md) - Optimize release profile settings
+- [`perf-profile-first`](rules/perf-profile-first.md) - Profile before optimizing
+
+### 12. Project Structure (LOW)
+
+- [`proj-lib-main-split`](rules/proj-lib-main-split.md) - Keep `main.rs` minimal, logic in `lib.rs`
+- [`proj-mod-by-feature`](rules/proj-mod-by-feature.md) - Organize modules by feature, not type
+- [`proj-flat-small`](rules/proj-flat-small.md) - Keep small projects flat
+- [`proj-mod-rs-dir`](rules/proj-mod-rs-dir.md) - Use `mod.rs` for multi-file modules
+- [`proj-pub-crate-internal`](rules/proj-pub-crate-internal.md) - Use `pub(crate)` for internal APIs
+- [`proj-pub-super-parent`](rules/proj-pub-super-parent.md) - Use `pub(super)` for parent-only visibility
+- [`proj-pub-use-reexport`](rules/proj-pub-use-reexport.md) - Use `pub use` for clean public API
+- [`proj-prelude-module`](rules/proj-prelude-module.md) - Create `prelude` module for common imports
+- [`proj-bin-dir`](rules/proj-bin-dir.md) - Put multiple binaries in `src/bin/`
+- [`proj-workspace-large`](rules/proj-workspace-large.md) - Use workspaces for large projects
+- [`proj-workspace-deps`](rules/proj-workspace-deps.md) - Use workspace dependency inheritance
+
+### 13. Clippy & Linting (LOW)
+
+- [`lint-deny-correctness`](rules/lint-deny-correctness.md) - `#![deny(clippy::correctness)]`
+- [`lint-warn-suspicious`](rules/lint-warn-suspicious.md) - `#![warn(clippy::suspicious)]`
+- [`lint-warn-style`](rules/lint-warn-style.md) - `#![warn(clippy::style)]`
+- [`lint-warn-complexity`](rules/lint-warn-complexity.md) - `#![warn(clippy::complexity)]`
+- [`lint-warn-perf`](rules/lint-warn-perf.md) - `#![warn(clippy::perf)]`
+- [`lint-pedantic-selective`](rules/lint-pedantic-selective.md) - Enable `clippy::pedantic` selectively
+- [`lint-missing-docs`](rules/lint-missing-docs.md) - `#![warn(missing_docs)]`
+- [`lint-unsafe-doc`](rules/lint-unsafe-doc.md) - `#![warn(clippy::undocumented_unsafe_blocks)]`
+- [`lint-cargo-metadata`](rules/lint-cargo-metadata.md) - `#![warn(clippy::cargo)]` for published crates
+- [`lint-rustfmt-check`](rules/lint-rustfmt-check.md) - Run `cargo fmt --check` in CI
+- [`lint-workspace-lints`](rules/lint-workspace-lints.md) - Configure lints at workspace level
+
+### 14. Anti-patterns (REFERENCE)
+
+- [`anti-unwrap-abuse`](rules/anti-unwrap-abuse.md) - Don't use `.unwrap()` in production code
+- [`anti-expect-lazy`](rules/anti-expect-lazy.md) - Don't use `.expect()` for recoverable errors
+- [`anti-clone-excessive`](rules/anti-clone-excessive.md) - Don't clone when borrowing works
+- [`anti-lock-across-await`](rules/anti-lock-across-await.md) - Don't hold locks across `.await`
+- [`anti-string-for-str`](rules/anti-string-for-str.md) - Don't accept `&String` when `&str` works
+- [`anti-vec-for-slice`](rules/anti-vec-for-slice.md) - Don't accept `&Vec` when `&[T]` works
+- [`anti-index-over-iter`](rules/anti-index-over-iter.md) - Don't use indexing when iterators work
+- [`anti-panic-expected`](rules/anti-panic-expected.md) - Don't panic on expected/recoverable errors
+- [`anti-empty-catch`](rules/anti-empty-catch.md) - Don't use empty `if let Err(_) = ...` blocks
+- [`anti-over-abstraction`](rules/anti-over-abstraction.md) - Don't over-abstract with excessive generics
+- [`anti-premature-optimize`](rules/anti-premature-optimize.md) - Don't optimize before profiling
+- [`anti-type-erasure`](rules/anti-type-erasure.md) - Don't use `Box` when `impl Trait` works
+- [`anti-format-hot-path`](rules/anti-format-hot-path.md) - Don't use `format!()` in hot paths
+- [`anti-collect-intermediate`](rules/anti-collect-intermediate.md) - Don't `collect()` intermediate iterators
+- [`anti-stringly-typed`](rules/anti-stringly-typed.md) - Don't use strings for structured data
+
+---
+
+## Recommended Cargo.toml Settings
+
+```toml
+[profile.release]
+opt-level = 3
+lto = "fat"
+codegen-units = 1
+panic = "abort"
+strip = true
+
+[profile.bench]
+inherits = "release"
+debug = true
+strip = false
+
+[profile.dev]
+opt-level = 0
+debug = true
+
+[profile.dev.package."*"]
+opt-level = 3 # Optimize dependencies in dev
+```
+
+---
+
+## How to Use
+
+This skill provides rule identifiers for quick reference. When generating or reviewing Rust code:
+
+1. **Check relevant category** based on task type
+2. **Apply rules** with matching prefix
+3. **Prioritize** CRITICAL > HIGH > MEDIUM > LOW
+4. **Read rule files** in `rules/` for detailed examples
+
+### Rule Application by Task
+
+| Task | Primary Categories |
+|------|-------------------|
+| New function | `own-`, `err-`, `name-` |
+| New struct/API | `api-`, `type-`, `doc-` |
+| Async code | `async-`, `own-` |
+| Error handling | `err-`, `api-` |
+| Memory optimization | `mem-`, `own-`, `perf-` |
+| Performance tuning | `opt-`, `mem-`, `perf-` |
+| Code review | `anti-`, `lint-` |
+
+---
+
+## Sources
+
+This skill synthesizes best practices from:
+- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
+- [Rust Performance Book](https://nnethercote.github.io/perf-book/)
+- [Rust Design Patterns](https://rust-unofficial.github.io/patterns/)
+- Production codebases: ripgrep, tokio, serde, polars, axum, deno
+- Clippy lint documentation
+- Community conventions (2024-2025)
diff --git a/.agents/skills/rust-skills/rules/anti-clone-excessive.md b/.agents/skills/rust-skills/rules/anti-clone-excessive.md
new file mode 100644
index 000000000..539dac2bc
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-clone-excessive.md
@@ -0,0 +1,124 @@
+# anti-clone-excessive
+
+> Don't clone when borrowing works
+
+## Why It Matters
+
+`.clone()` allocates memory and copies data. When you only need to read data, borrowing (`&T`) is free. Excessive cloning wastes memory, CPU cycles, and often indicates misunderstanding of ownership.
+
+## Bad
+
+```rust
+// Cloning to pass to a function that only reads
+fn print_name(name: String) { // Takes ownership
+ println!("{}", name);
+}
+let name = "Alice".to_string();
+print_name(name.clone()); // Unnecessary clone
+print_name(name); // Could have just done this
+
+// Cloning in a loop
+for item in items.clone() { // Clones entire Vec
+ process(&item);
+}
+
+// Cloning for comparison
+if input.clone() == expected { // Pointless clone
+ // ...
+}
+
+// Cloning struct fields
+fn get_name(&self) -> String {
+ self.name.clone() // Caller might not need ownership
+}
+```
+
+## Good
+
+```rust
+// Accept reference if only reading
+fn print_name(name: &str) {
+ println!("{}", name);
+}
+let name = "Alice".to_string();
+print_name(&name); // Borrow, no clone
+
+// Iterate by reference
+for item in &items {
+ process(item);
+}
+
+// Compare by reference
+if input == expected {
+ // ...
+}
+
+// Return reference when possible
+fn get_name(&self) -> &str {
+ &self.name
+}
+```
+
+## When to Clone
+
+```rust
+// Need owned data for async move
+let name = name.clone();
+tokio::spawn(async move {
+ process(name).await;
+});
+
+// Storing in a new struct
+struct Cache {
+ data: String,
+}
+impl Cache {
+ fn store(&mut self, data: &str) {
+ self.data = data.to_string(); // Must own
+ }
+}
+
+// Multiple owners (use Arc instead if frequent)
+let shared = data.clone();
+thread::spawn(move || use_data(shared));
+```
+
+## Alternatives to Clone
+
+| Instead of | Use |
+|------------|-----|
+| `s.clone()` for reading | `&s` |
+| `vec.clone()` for iteration | `&vec` or `vec.iter()` |
+| `Clone` for shared ownership | `Arc` |
+| Clone in hot loop | Move outside loop |
+| `s.to_string()` from `&str` | Accept `&str` if possible |
+
+## Pattern: Clone on Write
+
+```rust
+use std::borrow::Cow;
+
+fn process(input: Cow) -> Cow {
+ if needs_modification(&input) {
+ Cow::Owned(modify(&input)) // Clone only if needed
+ } else {
+ input // No clone
+ }
+}
+```
+
+## Detecting Excessive Clones
+
+```toml
+# Cargo.toml
+[lints.clippy]
+clone_on_copy = "warn"
+clone_on_ref_ptr = "warn"
+redundant_clone = "warn"
+```
+
+## See Also
+
+- [own-borrow-over-clone](./own-borrow-over-clone.md) - Borrowing patterns
+- [own-cow-conditional](./own-cow-conditional.md) - Clone on write
+- [own-arc-shared](./own-arc-shared.md) - Shared ownership
diff --git a/.agents/skills/rust-skills/rules/anti-collect-intermediate.md b/.agents/skills/rust-skills/rules/anti-collect-intermediate.md
new file mode 100644
index 000000000..b83a2fb20
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-collect-intermediate.md
@@ -0,0 +1,131 @@
+# anti-collect-intermediate
+
+> Don't collect intermediate iterators
+
+## Why It Matters
+
+Each `.collect()` allocates a new collection. Collecting intermediate results in a chain creates unnecessary allocations and prevents iterator fusion. Keep the chain lazy; collect only at the end.
+
+## Bad
+
+```rust
+// Three allocations, three passes
+fn process(data: Vec) -> Vec {
+ let step1: Vec<_> = data.into_iter()
+ .filter(|x| *x > 0)
+ .collect();
+
+ let step2: Vec<_> = step1.into_iter()
+ .map(|x| x * 2)
+ .collect();
+
+ step2.into_iter()
+ .filter(|x| *x < 100)
+ .collect()
+}
+
+// Collecting just to check length
+fn has_valid_items(items: &[Item]) -> bool {
+ let valid: Vec<_> = items.iter()
+ .filter(|i| i.is_valid())
+ .collect();
+ !valid.is_empty()
+}
+
+// Collecting to iterate again
+fn sum_valid(items: &[Item]) -> i64 {
+ let valid: Vec<_> = items.iter()
+ .filter(|i| i.is_valid())
+ .collect();
+ valid.iter().map(|i| i.value).sum()
+}
+```
+
+## Good
+
+```rust
+// Single allocation, single pass
+fn process(data: Vec) -> Vec {
+ data.into_iter()
+ .filter(|x| *x > 0)
+ .map(|x| x * 2)
+ .filter(|x| *x < 100)
+ .collect()
+}
+
+// No allocation - iterator short-circuits
+fn has_valid_items(items: &[Item]) -> bool {
+ items.iter().any(|i| i.is_valid())
+}
+
+// No intermediate allocation
+fn sum_valid(items: &[Item]) -> i64 {
+ items.iter()
+ .filter(|i| i.is_valid())
+ .map(|i| i.value)
+ .sum()
+}
+```
+
+## When Collection Is Needed
+
+```rust
+// Need to iterate twice
+let valid: Vec<_> = items.iter()
+ .filter(|i| i.is_valid())
+ .collect();
+let count = valid.len();
+for item in &valid {
+ process(item);
+}
+
+// Need to sort (requires concrete collection)
+let mut sorted: Vec<_> = items.iter()
+ .filter(|i| i.is_active())
+ .collect();
+sorted.sort_by_key(|i| i.priority);
+
+// Need random access
+let indexed: Vec<_> = items.iter().collect();
+let middle = indexed.get(indexed.len() / 2);
+```
+
+## Iterator Methods That Avoid Collection
+
+| Instead of Collecting to... | Use |
+|-----------------------------|-----|
+| Check if empty | `.any(|_| true)` or `.next().is_some()` |
+| Check if any match | `.any(predicate)` |
+| Check if all match | `.all(predicate)` |
+| Count elements | `.count()` |
+| Sum elements | `.sum()` |
+| Find first | `.find(predicate)` |
+| Get first | `.next()` |
+| Get last | `.last()` |
+
+## Pattern: Deferred Collection
+
+```rust
+// Return iterator, let caller collect if needed
+fn valid_items(items: &[Item]) -> impl Iterator- {
+ items.iter().filter(|i| i.is_valid())
+}
+
+// Caller decides
+let count = valid_items(&items).count(); // No collection
+let vec: Vec<_> = valid_items(&items).collect(); // Collection when needed
+```
+
+## Comparison
+
+| Pattern | Allocations | Passes |
+|---------|-------------|--------|
+| `.collect()` each step | N | N |
+| Single chain, one `.collect()` | 1 | 1 |
+| No collection (streaming) | 0 | 1 |
+
+## See Also
+
+- [perf-collect-once](./perf-collect-once.md) - Single collect
+- [perf-iter-lazy](./perf-iter-lazy.md) - Lazy evaluation
+- [perf-iter-over-index](./perf-iter-over-index.md) - Iterator patterns
diff --git a/.agents/skills/rust-skills/rules/anti-empty-catch.md b/.agents/skills/rust-skills/rules/anti-empty-catch.md
new file mode 100644
index 000000000..b2b63f213
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-empty-catch.md
@@ -0,0 +1,132 @@
+# anti-empty-catch
+
+> Don't silently ignore errors
+
+## Why It Matters
+
+Empty error handling (`if let Err(_) = ...`, `let _ = result`, `.ok()`) silently discards errors. Failures go unnoticed, bugs hide, and debugging becomes impossible. Every error deserves acknowledgment—even if just logging.
+
+## Bad
+
+```rust
+// Silently ignores errors
+let _ = write_to_file(data);
+
+// Discards error completely
+if let Err(_) = send_notification() {
+ // Nothing - error vanishes
+}
+
+// Converts Result to Option, losing error info
+let value = risky_operation().ok();
+
+// Match with empty arm
+match database.save(record) {
+ Ok(_) => println!("saved"),
+ Err(_) => {} // Silent failure
+}
+
+// Ignored in loop
+for item in items {
+ let _ = process(item); // Failures unnoticed
+}
+```
+
+## Good
+
+```rust
+// Log the error
+if let Err(e) = write_to_file(data) {
+ error!("failed to write file: {}", e);
+}
+
+// Propagate if possible
+send_notification()?;
+
+// Or handle explicitly
+match send_notification() {
+ Ok(_) => info!("notification sent"),
+ Err(e) => warn!("notification failed: {}", e),
+}
+
+// Collect errors in batch operations
+let (successes, failures): (Vec<_>, Vec<_>) = items
+ .into_iter()
+ .map(process)
+ .partition(Result::is_ok);
+
+if !failures.is_empty() {
+ warn!("{} items failed to process", failures.len());
+}
+
+// Explicit documentation when ignoring
+// Intentionally ignored: cleanup failure is not critical
+let _ = cleanup_temp_file(); // Add comment explaining why
+```
+
+## Acceptable Ignoring (Documented)
+
+```rust
+// Close errors often ignored, but document it
+// INTENTIONAL: TCP close errors are not actionable
+let _ = stream.shutdown(Shutdown::Both);
+
+// Mutex poisoning recovery
+// INTENTIONAL: We'll reset the state anyway
+let guard = mutex.lock().unwrap_or_else(|e| e.into_inner());
+```
+
+## Pattern: Collect and Report
+
+```rust
+fn process_batch(items: Vec
- ) -> BatchResult {
+ let mut errors = Vec::new();
+
+ for item in items {
+ if let Err(e) = process_item(&item) {
+ errors.push((item.id, e));
+ }
+ }
+
+ if errors.is_empty() {
+ BatchResult::AllSucceeded
+ } else {
+ BatchResult::PartialFailure(errors)
+ }
+}
+```
+
+## Pattern: Best-Effort Operations
+
+```rust
+// Metrics/telemetry can fail without affecting main flow
+fn report_metric(name: &str, value: f64) {
+ if let Err(e) = metrics_client.record(name, value) {
+ // Log but don't propagate - metrics are not critical
+ debug!("failed to record metric {}: {}", name, e);
+ }
+}
+```
+
+## Clippy Lint
+
+```toml
+[lints.clippy]
+let_underscore_drop = "warn"
+ignored_unit_patterns = "warn"
+```
+
+## Decision Guide
+
+| Situation | Action |
+|-----------|--------|
+| Critical operation | `?` or handle explicitly |
+| Non-critical, debugging needed | Log the error |
+| Truly ignorable (rare) | `let _ =` with comment |
+| Batch operation | Collect errors, report |
+
+## See Also
+
+- [err-result-over-panic](./err-result-over-panic.md) - Proper error handling
+- [err-context-chain](./err-context-chain.md) - Adding context
+- [anti-unwrap-abuse](./anti-unwrap-abuse.md) - Unwrap issues
diff --git a/.agents/skills/rust-skills/rules/anti-expect-lazy.md b/.agents/skills/rust-skills/rules/anti-expect-lazy.md
new file mode 100644
index 000000000..24e2b3dbe
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-expect-lazy.md
@@ -0,0 +1,95 @@
+# anti-expect-lazy
+
+> Don't use expect for recoverable errors
+
+## Why It Matters
+
+`.expect()` panics with a custom message, but it's still a panic. Using it for errors that could reasonably occur in production (network failures, file not found, invalid input) crashes the program instead of handling the error gracefully.
+
+Reserve `.expect()` for programming errors where panic is appropriate.
+
+## Bad
+
+```rust
+// Network failures are expected - don't panic
+let response = client.get(url).await.expect("failed to fetch");
+
+// Files might not exist
+let config = fs::read_to_string("config.toml").expect("config not found");
+
+// User input can be invalid
+let age: u32 = input.parse().expect("invalid age");
+
+// Database queries can fail
+let user = db.find_user(id).await.expect("user not found");
+```
+
+## Good
+
+```rust
+// Handle recoverable errors properly
+let response = client.get(url).await
+ .context("failed to fetch URL")?;
+
+// Return error if file doesn't exist
+let config = fs::read_to_string("config.toml")
+ .context("failed to read config file")?;
+
+// Validate and return error
+let age: u32 = input.parse()
+ .map_err(|_| Error::InvalidInput("age must be a number"))?;
+
+// Handle missing data
+let user = db.find_user(id).await?
+ .ok_or(Error::NotFound("user"))?;
+```
+
+## When expect() Is Appropriate
+
+Use `.expect()` for invariants that indicate bugs:
+
+```rust
+// Mutex poisoning indicates a bug elsewhere
+let guard = mutex.lock().expect("mutex poisoned");
+
+// Regex is known valid at compile time
+let re = Regex::new(r"^\d{4}$").expect("invalid regex");
+
+// Thread spawn failure is unrecoverable
+let handle = thread::spawn(|| work()).expect("failed to spawn thread");
+
+// Static data that must be valid
+let config: Config = toml::from_str(EMBEDDED_CONFIG)
+ .expect("embedded config is invalid");
+```
+
+## Pattern: expect() vs unwrap()
+
+```rust
+// unwrap: no context, hard to debug
+let x = option.unwrap();
+
+// expect: gives context, still panics
+let x = option.expect("value should exist after validation");
+
+// ?: proper error handling
+let x = option.ok_or(Error::MissingValue)?;
+```
+
+## Decision Guide
+
+| Situation | Use |
+|-----------|-----|
+| User input | `?` with error |
+| File/network I/O | `?` with error |
+| Database operations | `?` with error |
+| Parsed constants | `.expect()` |
+| Thread/mutex operations | `.expect()` |
+| After validation check | `.expect()` with explanation |
+| Never expected to fail | `.expect()` documenting invariant |
+
+## See Also
+
+- [err-expect-bugs-only](./err-expect-bugs-only.md) - When to use expect
+- [err-no-unwrap-prod](./err-no-unwrap-prod.md) - Avoiding unwrap
+- [anti-unwrap-abuse](./anti-unwrap-abuse.md) - Unwrap anti-pattern
diff --git a/.agents/skills/rust-skills/rules/anti-format-hot-path.md b/.agents/skills/rust-skills/rules/anti-format-hot-path.md
new file mode 100644
index 000000000..368627f6f
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-format-hot-path.md
@@ -0,0 +1,141 @@
+# anti-format-hot-path
+
+> Don't use format! in hot paths
+
+## Why It Matters
+
+`format!()` allocates a new `String` every call. In hot paths (loops, frequently called functions), this creates allocation churn that impacts performance. Pre-allocate, reuse buffers, or use `write!()` to an existing buffer.
+
+## Bad
+
+```rust
+// format! in loop - allocates every iteration
+fn log_events(events: &[Event]) {
+ for event in events {
+ let message = format!("[{}] {}: {}", event.level, event.source, event.message);
+ logger.log(&message);
+ }
+}
+
+// format! for building parts
+fn build_url(base: &str, path: &str, params: &[(&str, &str)]) -> String {
+ let mut url = format!("{}{}", base, path);
+ for (key, value) in params {
+ url = format!("{}{}={}&", url, key, value); // New allocation each time
+ }
+ url
+}
+
+// format! for simple concatenation
+fn greet(name: &str) -> String {
+ format!("Hello, {}!", name) // Fine for one-off, bad if called 1M times
+}
+```
+
+## Good
+
+```rust
+use std::fmt::Write;
+
+// Reuse buffer across iterations
+fn log_events(events: &[Event]) {
+ let mut buffer = String::with_capacity(256);
+ for event in events {
+ buffer.clear();
+ write!(buffer, "[{}] {}: {}", event.level, event.source, event.message).unwrap();
+ logger.log(&buffer);
+ }
+}
+
+// Build incrementally in single buffer
+fn build_url(base: &str, path: &str, params: &[(&str, &str)]) -> String {
+ let mut url = String::with_capacity(base.len() + path.len() + params.len() * 20);
+ url.push_str(base);
+ url.push_str(path);
+ for (key, value) in params {
+ write!(url, "{}={}&", key, value).unwrap();
+ }
+ url
+}
+
+// For truly hot paths, avoid allocation entirely
+fn greet_to_buf(name: &str, buffer: &mut String) {
+ buffer.clear();
+ buffer.push_str("Hello, ");
+ buffer.push_str(name);
+ buffer.push('!');
+}
+```
+
+## Comparison
+
+| Approach | Allocations | Performance |
+|----------|-------------|-------------|
+| `format!()` in loop | N | Slow |
+| `write!()` to reused buffer | 1 | Fast |
+| `push_str()` + `push()` | 1 | Fastest |
+| Pre-sized `String::with_capacity()` | 1 (no realloc) | Fast |
+
+## When format! Is Fine
+
+```rust
+// One-time initialization
+let config_path = format!("{}/config.toml", home_dir);
+
+// Error messages (not hot path)
+return Err(format!("invalid input: {}", input));
+
+// Debug output
+println!("Debug: {:?}", value);
+```
+
+## Pattern: Formatter Buffer Pool
+
+```rust
+use std::cell::RefCell;
+
+thread_local! {
+ static BUFFER: RefCell
= RefCell::new(String::with_capacity(256));
+}
+
+fn format_event(event: &Event) -> String {
+ BUFFER.with(|buf| {
+ let mut buf = buf.borrow_mut();
+ buf.clear();
+ write!(buf, "[{}] {}", event.level, event.message).unwrap();
+ buf.clone() // Still one allocation per call, but no parsing
+ })
+}
+```
+
+## Pattern: Display Implementation
+
+```rust
+struct Event {
+ level: Level,
+ message: String,
+}
+
+impl std::fmt::Display for Event {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "[{}] {}", self.level, self.message)
+ }
+}
+
+// Caller controls allocation
+let mut buf = String::new();
+write!(buf, "{}", event)?;
+```
+
+## Clippy Lint
+
+```toml
+[lints.clippy]
+format_in_format_args = "warn"
+```
+
+## See Also
+
+- [mem-avoid-format](./mem-avoid-format.md) - Avoiding format
+- [mem-write-over-format](./mem-write-over-format.md) - Using write!
+- [mem-reuse-collections](./mem-reuse-collections.md) - Buffer reuse
diff --git a/.agents/skills/rust-skills/rules/anti-index-over-iter.md b/.agents/skills/rust-skills/rules/anti-index-over-iter.md
new file mode 100644
index 000000000..c22d85f0a
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-index-over-iter.md
@@ -0,0 +1,125 @@
+# anti-index-over-iter
+
+> Don't use indexing when iterators work
+
+## Why It Matters
+
+Manual indexing (`for i in 0..len`) requires bounds checks on every access, prevents SIMD optimization, and introduces off-by-one error risks. Iterators eliminate these issues and are more idiomatic Rust.
+
+## Bad
+
+```rust
+// Manual indexing - bounds checked every access
+fn sum_squares(data: &[i32]) -> i64 {
+ let mut result = 0i64;
+ for i in 0..data.len() {
+ result += (data[i] as i64) * (data[i] as i64);
+ }
+ result
+}
+
+// Index-based with multiple arrays
+fn dot_product(a: &[f64], b: &[f64]) -> f64 {
+ let mut sum = 0.0;
+ for i in 0..a.len().min(b.len()) {
+ sum += a[i] * b[i];
+ }
+ sum
+}
+
+// Mutation with indices
+fn normalize(data: &mut [f64]) {
+ let max = data.iter().cloned().fold(0.0, f64::max);
+ for i in 0..data.len() {
+ data[i] /= max;
+ }
+}
+```
+
+## Good
+
+```rust
+// Iterator - no bounds checks, SIMD-friendly
+fn sum_squares(data: &[i32]) -> i64 {
+ data.iter()
+ .map(|&x| (x as i64) * (x as i64))
+ .sum()
+}
+
+// Zip - handles length mismatch automatically
+fn dot_product(a: &[f64], b: &[f64]) -> f64 {
+ a.iter()
+ .zip(b.iter())
+ .map(|(&x, &y)| x * y)
+ .sum()
+}
+
+// Mutable iteration
+fn normalize(data: &mut [f64]) {
+ let max = data.iter().cloned().fold(0.0, f64::max);
+ for x in data.iter_mut() {
+ *x /= max;
+ }
+}
+```
+
+## When Indices Are Needed
+
+Sometimes you genuinely need indices:
+
+```rust
+// Need index in output
+for (i, item) in items.iter().enumerate() {
+ println!("{}: {}", i, item);
+}
+
+// Non-sequential access
+for i in (0..len).step_by(2) {
+ swap(&mut data[i], &mut data[i + 1]);
+}
+
+// Multi-dimensional iteration
+for i in 0..rows {
+ for j in 0..cols {
+ matrix[i][j] = i * cols + j;
+ }
+}
+```
+
+## Comparison
+
+| Pattern | Bounds Checks | SIMD | Safety |
+|---------|---------------|------|--------|
+| `for i in 0..len { data[i] }` | Every access | Limited | Off-by-one risk |
+| `for x in &data` | None | Good | Safe |
+| `for x in data.iter()` | None | Good | Safe |
+| `data.iter().enumerate()` | None | Good | Safe |
+
+## Common Conversions
+
+| Index Pattern | Iterator Pattern |
+|---------------|------------------|
+| `for i in 0..v.len()` | `for x in &v` |
+| `v[0]` | `v.first()` |
+| `v[v.len()-1]` | `v.last()` |
+| `for i in 0..a.len() { a[i] + b[i] }` | `a.iter().zip(&b)` |
+| `for i in 0..v.len() { v[i] *= 2 }` | `for x in &mut v { *x *= 2 }` |
+
+## Performance Note
+
+```rust
+// Iterator version can auto-vectorize
+let sum: i32 = data.iter().sum();
+
+// Manual indexing prevents vectorization
+let mut sum = 0;
+for i in 0..data.len() {
+ sum += data[i];
+}
+```
+
+## See Also
+
+- [perf-iter-over-index](./perf-iter-over-index.md) - Performance details
+- [opt-bounds-check](./opt-bounds-check.md) - Bounds check elimination
+- [perf-iter-lazy](./perf-iter-lazy.md) - Lazy iterators
diff --git a/.agents/skills/rust-skills/rules/anti-lock-across-await.md b/.agents/skills/rust-skills/rules/anti-lock-across-await.md
new file mode 100644
index 000000000..20742d82e
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-lock-across-await.md
@@ -0,0 +1,127 @@
+# anti-lock-across-await
+
+> Don't hold locks across await points
+
+## Why It Matters
+
+Holding a `Mutex` or `RwLock` guard across an `.await` causes the lock to be held while the task is suspended. Other tasks waiting for the lock block indefinitely. With `std::sync::Mutex`, this is even worse—it can deadlock the entire runtime.
+
+## Bad
+
+```rust
+use std::sync::Mutex;
+use tokio::sync::Mutex as AsyncMutex;
+
+// DEADLOCK RISK: std::sync::Mutex held across await
+async fn bad_std_mutex(data: &Mutex>) {
+ let mut guard = data.lock().unwrap();
+ do_async_work().await; // Lock held during await!
+ guard.push(42);
+}
+
+// BLOCKS OTHER TASKS: tokio Mutex held across await
+async fn bad_async_mutex(data: &AsyncMutex>) {
+ let mut guard = data.lock().await;
+ slow_network_call().await; // Lock held for entire call!
+ guard.push(42);
+}
+```
+
+## Good
+
+```rust
+use std::sync::Mutex;
+use tokio::sync::Mutex as AsyncMutex;
+
+// Release lock before await
+async fn good_approach(data: &Mutex>) {
+ let value = {
+ let guard = data.lock().unwrap();
+ guard.last().copied() // Extract what you need
+ }; // Lock released here
+
+ let result = do_async_work(value).await;
+
+ {
+ let mut guard = data.lock().unwrap();
+ guard.push(result);
+ }
+}
+
+// Minimize lock scope with async mutex
+async fn good_async_mutex(data: &AsyncMutex>, item: i32) {
+ // Quick lock, quick release
+ data.lock().await.push(item);
+
+ // Async work without lock
+ let result = slow_network_call().await;
+
+ // Quick lock again
+ data.lock().await.push(result);
+}
+```
+
+## Pattern: Clone Before Await
+
+```rust
+async fn process(data: &AsyncMutex) -> Result<()> {
+ // Clone inside lock scope
+ let config = data.lock().await.clone();
+
+ // Now use config freely across awaits
+ let result = fetch_data(&config.url).await?;
+ process_result(&config, result).await?;
+
+ Ok(())
+}
+```
+
+## Pattern: Restructure to Avoid Lock
+
+```rust
+// Instead of locking a shared map
+struct Service {
+ data: AsyncMutex>,
+}
+
+// Use channels or owned data
+struct BetterService {
+ // Each task owns its data via channels
+ sender: mpsc::Sender,
+}
+
+impl BetterService {
+ async fn request(&self, key: String) -> Data {
+ let (tx, rx) = oneshot::channel();
+ self.sender.send(Request { key, respond: tx }).await?;
+ rx.await?
+ }
+}
+```
+
+## What Can Cross Await
+
+| Type | Safe Across Await? |
+|------|--------------------|
+| `std::sync::Mutex` guard | **NO** - can deadlock |
+| `std::sync::RwLock` guard | **NO** - can deadlock |
+| `tokio::sync::Mutex` guard | Allowed but blocks tasks |
+| `tokio::sync::RwLock` guard | Allowed but blocks tasks |
+| Owned values | Yes |
+| `Arc` | Yes |
+| References | Depends on lifetime |
+
+## Detection
+
+```toml
+# Cargo.toml
+[lints.clippy]
+await_holding_lock = "deny"
+await_holding_refcell_ref = "deny"
+```
+
+## See Also
+
+- [async-no-lock-await](./async-no-lock-await.md) - Async lock patterns
+- [async-clone-before-await](./async-clone-before-await.md) - Clone pattern
+- [own-mutex-interior](./own-mutex-interior.md) - Mutex usage
diff --git a/.agents/skills/rust-skills/rules/anti-over-abstraction.md b/.agents/skills/rust-skills/rules/anti-over-abstraction.md
new file mode 100644
index 000000000..44a12f02a
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-over-abstraction.md
@@ -0,0 +1,120 @@
+# anti-over-abstraction
+
+> Don't over-abstract with excessive generics
+
+## Why It Matters
+
+Generics and traits are powerful but come at a cost: compile times, binary size, and cognitive load. Over-abstraction—making everything generic "for flexibility"—often adds complexity without benefit. Start concrete; generalize when you have real use cases.
+
+## Bad
+
+```rust
+// Overly generic for a simple function
+fn add(a: T, b: U) -> R
+where
+ T: Into,
+ U: Into,
+ R: std::ops::Add,
+{
+ a.into() + b.into()
+}
+
+// Just call add(1, 2) - why make it this complex?
+
+// Trait explosion
+trait Readable {}
+trait Writable {}
+trait ReadWritable: Readable + Writable {}
+trait AsyncReadable {}
+trait AsyncWritable {}
+trait AsyncReadWritable: AsyncReadable + AsyncWritable {}
+
+// Abstract factory pattern (Java flashback)
+trait Factory {
+ fn create(&self) -> T;
+}
+trait FactoryFactory, T> {
+ fn create_factory(&self) -> F;
+}
+```
+
+## Good
+
+```rust
+// Concrete implementation - clear and simple
+fn add_i32(a: i32, b: i32) -> i32 {
+ a + b
+}
+
+// Generic when actually needed (e.g., library code)
+fn add>(a: T, b: T) -> T {
+ a + b
+}
+
+// Simple traits for actual polymorphism needs
+trait Storage {
+ fn save(&self, key: &str, value: &[u8]) -> Result<(), Error>;
+ fn load(&self, key: &str) -> Result, Error>;
+}
+
+// Concrete types first
+struct FileStorage { path: PathBuf }
+struct MemoryStorage { data: HashMap> }
+```
+
+## Signs of Over-Abstraction
+
+| Sign | Symptom |
+|------|---------|
+| Single implementation | Generic trait with only one impl |
+| Type parameter soup | `T, U, V, W` everywhere |
+| Marker traits | Traits with no methods |
+| Deep trait bounds | `where T: A + B + C + D + E` |
+| Phantom generics | Type parameters not used meaningfully |
+
+## When to Generalize
+
+Generalize when:
+- You have 2+ concrete types that share behavior
+- You're writing library code for public consumption
+- Performance requires static dispatch
+- The abstraction simplifies the API
+
+Don't generalize when:
+- You "might need it later" (YAGNI)
+- Only one type will ever implement it
+- It makes code harder to understand
+
+## Rule of Three
+
+Wait until you have three similar concrete implementations before abstracting:
+
+```rust
+// Version 1: Just FileStorage
+struct FileStorage { /* ... */ }
+
+// Version 2: Added MemoryStorage, similar interface
+struct MemoryStorage { /* ... */ }
+
+// Version 3: Now Redis too - time to abstract
+trait Storage {
+ fn save(&self, key: &str, value: &[u8]) -> Result<()>;
+ fn load(&self, key: &str) -> Result>;
+}
+```
+
+## Prefer Concrete Types in Private Code
+
+```rust
+// Internal function - concrete type is fine
+fn process_orders(db: &PostgresDb, orders: Vec) { }
+
+// Public API - might benefit from abstraction
+pub fn process_orders(storage: &S, orders: Vec) { }
+```
+
+## See Also
+
+- [type-generic-bounds](./type-generic-bounds.md) - Minimal bounds
+- [api-sealed-trait](./api-sealed-trait.md) - Controlled extension
+- [anti-type-erasure](./anti-type-erasure.md) - When Box is wrong
diff --git a/.agents/skills/rust-skills/rules/anti-panic-expected.md b/.agents/skills/rust-skills/rules/anti-panic-expected.md
new file mode 100644
index 000000000..cecb34cb9
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-panic-expected.md
@@ -0,0 +1,131 @@
+# anti-panic-expected
+
+> Don't panic on expected or recoverable errors
+
+## Why It Matters
+
+Panics crash the program. They're for unrecoverable situations—bugs, corrupted state, invariant violations. Using panic for expected conditions (network failures, file not found, invalid input) makes programs fragile and forces callers to catch panics or die.
+
+Use `Result` for recoverable errors.
+
+## Bad
+
+```rust
+// Network failures are expected
+fn fetch_data(url: &str) -> Data {
+ let response = reqwest::blocking::get(url)
+ .expect("network error"); // Crashes on timeout
+ response.json().expect("invalid json") // Crashes on bad response
+}
+
+// User input is often invalid
+fn parse_config(input: &str) -> Config {
+ toml::from_str(input).expect("invalid config") // Crashes on typo
+}
+
+// Files may not exist
+fn load_settings() -> Settings {
+ let content = fs::read_to_string("settings.json")
+ .expect("settings not found"); // Crashes if missing
+ serde_json::from_str(&content).expect("invalid settings")
+}
+
+// Custom panic for validation
+fn process_age(age: i32) {
+ if age < 0 {
+ panic!("age cannot be negative"); // Should return error
+ }
+}
+```
+
+## Good
+
+```rust
+// Return errors for expected failures
+fn fetch_data(url: &str) -> Result {
+ let response = reqwest::blocking::get(url)
+ .context("failed to connect")?;
+ let data = response.json()
+ .context("failed to parse response")?;
+ Ok(data)
+}
+
+// Validate and return Result
+fn parse_config(input: &str) -> Result {
+ toml::from_str(input).map_err(ConfigError::Parse)
+}
+
+// Handle missing files gracefully
+fn load_settings() -> Result {
+ let content = fs::read_to_string("settings.json")?;
+ let settings = serde_json::from_str(&content)?;
+ Ok(settings)
+}
+
+// Return error for validation failure
+fn process_age(age: i32) -> Result<(), ValidationError> {
+ if age < 0 {
+ return Err(ValidationError::NegativeAge);
+ }
+ Ok(())
+}
+```
+
+## When to Panic
+
+Panic IS appropriate for:
+
+```rust
+// Bug detection - invariant violated
+fn get_unchecked(&self, index: usize) -> &T {
+ assert!(index < self.len(), "index out of bounds - this is a bug");
+ unsafe { self.data.get_unchecked(index) }
+}
+
+// Unrecoverable state
+fn init() {
+ if !CAN_PROCEED {
+ panic!("system requirements not met");
+ }
+}
+
+// Tests
+#[test]
+fn test_fails() {
+ panic!("expected panic in test");
+}
+```
+
+## Decision Guide
+
+| Condition | Action |
+|-----------|--------|
+| Invalid user input | Return `Err` |
+| Network failure | Return `Err` |
+| File not found | Return `Err` |
+| Malformed data | Return `Err` |
+| Bug/impossible state | `panic!` or `unreachable!` |
+| Failed assertion in test | `panic!` |
+| Unrecoverable init failure | `panic!` |
+
+## Anti-pattern: panic! for Control Flow
+
+```rust
+// BAD: Using panic for control flow
+fn find_or_die(items: &[Item], id: u64) -> &Item {
+ items.iter()
+ .find(|i| i.id == id)
+ .unwrap_or_else(|| panic!("item {} not found", id))
+}
+
+// GOOD: Return Option or Result
+fn find(items: &[Item], id: u64) -> Option<&Item> {
+ items.iter().find(|i| i.id == id)
+}
+```
+
+## See Also
+
+- [err-result-over-panic](./err-result-over-panic.md) - Use Result
+- [anti-unwrap-abuse](./anti-unwrap-abuse.md) - Unwrap anti-pattern
+- [err-expect-bugs-only](./err-expect-bugs-only.md) - When to expect
diff --git a/.agents/skills/rust-skills/rules/anti-premature-optimize.md b/.agents/skills/rust-skills/rules/anti-premature-optimize.md
new file mode 100644
index 000000000..646007ecc
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-premature-optimize.md
@@ -0,0 +1,156 @@
+# anti-premature-optimize
+
+> Don't optimize before profiling
+
+## Why It Matters
+
+Premature optimization wastes time, complicates code, and often targets the wrong bottlenecks. Most code isn't performance-critical; the hot 10% matters. Profile first, then optimize the actual bottlenecks with data-driven decisions.
+
+## Bad
+
+```rust
+// "Optimizing" without measurement
+fn sum(data: &[i32]) -> i32 {
+ // Using unsafe "for performance" without profiling
+ unsafe {
+ let mut sum = 0;
+ for i in 0..data.len() {
+ sum += *data.get_unchecked(i);
+ }
+ sum
+ }
+}
+
+// Complex caching with no evidence it's needed
+lazy_static! {
+ static ref CACHE: RwLock>> =
+ RwLock::new(HashMap::new());
+}
+
+// Hand-rolled data structures "for speed"
+struct MyVec {
+ ptr: *mut T,
+ len: usize,
+ cap: usize,
+}
+```
+
+## Good
+
+```rust
+// Simple, idiomatic - let compiler optimize
+fn sum(data: &[i32]) -> i32 {
+ data.iter().sum()
+}
+
+// Profile, then optimize if needed
+fn sum_optimized(data: &[i32]) -> i32 {
+ // After profiling showed this is a bottleneck,
+ // we measured that manual SIMD gives 3x speedup
+ #[cfg(target_arch = "x86_64")]
+ {
+ // SIMD implementation with benchmark data
+ }
+ #[cfg(not(target_arch = "x86_64"))]
+ {
+ data.iter().sum()
+ }
+}
+
+// Use standard library - it's well-optimized
+let cache: HashMap = HashMap::new();
+```
+
+## Profiling Workflow
+
+```bash
+# 1. Write correct code first
+cargo build --release
+
+# 2. Profile with real workloads
+cargo flamegraph --bin my_app -- --real-args
+# or
+cargo bench
+
+# 3. Identify hotspots (top 10% of time)
+
+# 4. Measure before optimizing
+# 5. Optimize ONE thing
+# 6. Measure after - verify improvement
+# 7. Repeat if still slow
+```
+
+## Optimization Principles
+
+| Do | Don't |
+|----|-------|
+| Profile first | Guess at bottlenecks |
+| Optimize hotspots | Optimize everything |
+| Measure improvement | Assume it's faster |
+| Keep it simple | Add complexity speculatively |
+| Trust the compiler | Outsmart the compiler |
+
+## When to Optimize
+
+```rust
+// AFTER profiling shows this is 40% of runtime
+#[inline]
+fn hot_function(data: &[u8]) -> u64 {
+ // Optimized implementation justified by benchmarks
+}
+
+// Clear, measurable benefit documented
+/// Pre-allocated buffer for repeated formatting.
+/// Benchmarks show 3x speedup for >1000 calls/sec workloads.
+struct FormatterPool {
+ buffers: Vec,
+}
+```
+
+## Common Premature Optimizations
+
+| Premature | Reality |
+|-----------|---------|
+| `#[inline(always)]` everywhere | Compiler usually knows better |
+| `unsafe` for bounds check removal | Iterator does this safely |
+| Custom allocator | Default is usually fine |
+| Object pooling | Allocator is fast enough |
+| Manual SIMD | Auto-vectorization works |
+
+## Profile Tools
+
+```bash
+# Sampling profiler
+perf record ./target/release/app && perf report
+
+# Flamegraph
+cargo install flamegraph
+cargo flamegraph
+
+# Criterion benchmarks
+cargo bench
+
+# Memory profiling
+valgrind --tool=massif ./target/release/app
+```
+
+## Document Optimizations
+
+```rust
+/// Lookup table for fast character classification.
+///
+/// # Performance
+///
+/// Benchmarked with criterion (benchmarks/char_class.rs):
+/// - Table lookup: 2.3ns/op
+/// - Match statement: 8.7ns/op
+///
+/// Justified for hot path in parser (called 10M+ times).
+static CHAR_CLASS: [CharClass; 256] = [/* ... */];
+```
+
+## See Also
+
+- [perf-profile-first](./perf-profile-first.md) - Profile before optimize
+- [test-criterion-bench](./test-criterion-bench.md) - Benchmarking
+- [opt-inline-small](./opt-inline-small.md) - Inline guidelines
diff --git a/.agents/skills/rust-skills/rules/anti-string-for-str.md b/.agents/skills/rust-skills/rules/anti-string-for-str.md
new file mode 100644
index 000000000..16de14149
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-string-for-str.md
@@ -0,0 +1,122 @@
+# anti-string-for-str
+
+> Don't accept &String when &str works
+
+## Why It Matters
+
+`&String` is strictly less flexible than `&str`. A `&str` can be created from `String`, `&str`, literals, and slices. A `&String` requires exactly a `String`. This forces callers to allocate when they might not need to.
+
+## Bad
+
+```rust
+// Forces callers to have a String
+fn greet(name: &String) {
+ println!("Hello, {}", name);
+}
+
+// Caller must allocate
+greet(&"Alice".to_string()); // Unnecessary allocation
+greet(&name); // Only works if name is String
+
+// In struct
+struct Config {
+ name: String,
+}
+
+impl Config {
+ fn set_name(&mut self, name: &String) { // Too restrictive
+ self.name = name.clone();
+ }
+}
+```
+
+## Good
+
+```rust
+// Accept &str - works with String, &str, literals
+fn greet(name: &str) {
+ println!("Hello, {}", name);
+}
+
+// All these work
+greet("Alice"); // String literal
+greet(&name); // &String coerces to &str
+greet(name.as_str()); // Explicit &str
+
+// In struct
+impl Config {
+ fn set_name(&mut self, name: &str) {
+ self.name = name.to_string();
+ }
+
+ // Or accept owned String if caller usually has one
+ fn set_name_owned(&mut self, name: String) {
+ self.name = name;
+ }
+
+ // Or be generic
+ fn set_name_into(&mut self, name: impl Into) {
+ self.name = name.into();
+ }
+}
+```
+
+## Deref Coercion
+
+`String` implements `Deref`, so `&String` automatically coerces to `&str`:
+
+```rust
+fn takes_str(s: &str) { }
+
+let owned = String::from("hello");
+takes_str(&owned); // &String -> &str via Deref
+```
+
+## When to Accept &String
+
+Rarely. Maybe if you need `String`-specific methods:
+
+```rust
+fn needs_capacity(s: &String) -> usize {
+ s.capacity() // Only String has capacity()
+}
+```
+
+But usually you'd take `&str` and let the caller manage the `String`.
+
+## Pattern: Flexible APIs
+
+```rust
+// Most flexible: accept anything that can become &str
+fn process(input: impl AsRef) {
+ let s: &str = input.as_ref();
+ // ...
+}
+
+process("literal");
+process(String::from("owned"));
+process(&some_string);
+```
+
+## Similar Anti-patterns
+
+| Anti-pattern | Better |
+|--------------|--------|
+| `&String` | `&str` |
+| `&Vec` | `&[T]` |
+| `&Box` | `&T` |
+| `&PathBuf` | `&Path` |
+| `&OsString` | `&OsStr` |
+
+## Clippy Detection
+
+```toml
+[lints.clippy]
+ptr_arg = "warn" # Catches &String, &Vec, &PathBuf
+```
+
+## See Also
+
+- [anti-vec-for-slice](./anti-vec-for-slice.md) - Similar pattern for Vec
+- [own-slice-over-vec](./own-slice-over-vec.md) - Slice patterns
+- [api-impl-asref](./api-impl-asref.md) - AsRef pattern
diff --git a/.agents/skills/rust-skills/rules/anti-stringly-typed.md b/.agents/skills/rust-skills/rules/anti-stringly-typed.md
new file mode 100644
index 000000000..fac957963
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-stringly-typed.md
@@ -0,0 +1,167 @@
+# anti-stringly-typed
+
+> Don't use strings where enums or newtypes would provide type safety
+
+## Why It Matters
+
+Strings are the most primitive way to represent data—they accept any value, provide no validation, and offer no IDE support. When you have a fixed set of valid values or a semantic type, use enums or newtypes. The compiler catches mistakes at compile time instead of runtime.
+
+## Bad
+
+```rust
+fn process_order(status: &str, priority: &str) {
+ // What are valid statuses? "pending"? "Pending"? "PENDING"?
+ // What are valid priorities? "high"? "1"? "urgent"?
+ match status {
+ "pending" => { ... }
+ "completed" => { ... }
+ _ => panic!("unknown status"), // Runtime error
+ }
+}
+
+struct User {
+ email: String, // Any string, even "not an email"
+ phone: String, // Any string, even "hello"
+ user_id: String, // Could be confused with other string IDs
+}
+
+// Easy to make mistakes
+process_order("complete", "high"); // Typo: "complete" vs "completed"
+process_order("high", "pending"); // Swapped arguments - compiles!
+```
+
+## Good
+
+```rust
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum OrderStatus {
+ Pending,
+ Processing,
+ Completed,
+ Cancelled,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+enum Priority {
+ Low,
+ Medium,
+ High,
+ Critical,
+}
+
+fn process_order(status: OrderStatus, priority: Priority) {
+ match status {
+ OrderStatus::Pending => { ... }
+ OrderStatus::Processing => { ... }
+ OrderStatus::Completed => { ... }
+ OrderStatus::Cancelled => { ... }
+ } // Exhaustive - compiler checks all cases
+}
+
+// Validated newtypes
+struct Email(String);
+struct PhoneNumber(String);
+struct UserId(u64);
+
+impl Email {
+ pub fn new(s: &str) -> Result {
+ if is_valid_email(s) {
+ Ok(Email(s.to_string()))
+ } else {
+ Err(ValidationError::InvalidEmail)
+ }
+ }
+}
+
+struct User {
+ email: Email, // Must be valid email
+ phone: PhoneNumber, // Must be valid phone
+ user_id: UserId, // Can't confuse with other IDs
+}
+
+// Compile errors catch mistakes
+process_order(OrderStatus::Completed, Priority::High); // Clear and correct
+process_order(Priority::High, OrderStatus::Pending); // Compile error!
+```
+
+## Parsing Strings to Types
+
+```rust
+use std::str::FromStr;
+
+#[derive(Debug, Clone, Copy)]
+enum OrderStatus {
+ Pending,
+ Processing,
+ Completed,
+ Cancelled,
+}
+
+impl FromStr for OrderStatus {
+ type Err = ParseError;
+
+ fn from_str(s: &str) -> Result {
+ match s.to_lowercase().as_str() {
+ "pending" => Ok(OrderStatus::Pending),
+ "processing" => Ok(OrderStatus::Processing),
+ "completed" => Ok(OrderStatus::Completed),
+ "cancelled" | "canceled" => Ok(OrderStatus::Cancelled),
+ _ => Err(ParseError::UnknownStatus(s.to_string())),
+ }
+ }
+}
+
+// Parse at boundary, use types internally
+fn handle_request(status_str: &str) -> Result<(), Error> {
+ let status: OrderStatus = status_str.parse()?; // Validate once
+ process_order(status); // Type-safe from here
+ Ok(())
+}
+```
+
+## With Serde
+
+```rust
+use serde::{Serialize, Deserialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+enum Status {
+ Pending,
+ InProgress,
+ Completed,
+}
+
+// JSON: {"status": "in_progress"}
+// Deserialization validates automatically
+```
+
+## Error Messages
+
+```rust
+#[derive(Debug, Clone, Copy)]
+enum Color {
+ Red,
+ Green,
+ Blue,
+}
+
+impl std::fmt::Display for Color {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Color::Red => write!(f, "red"),
+ Color::Green => write!(f, "green"),
+ Color::Blue => write!(f, "blue"),
+ }
+ }
+}
+
+// Type-safe and displayable
+println!("Selected color: {}", Color::Red);
+```
+
+## See Also
+
+- [api-newtype-safety](./api-newtype-safety.md) - Newtype pattern
+- [api-parse-dont-validate](./api-parse-dont-validate.md) - Parse at boundaries
+- [type-newtype-ids](./type-newtype-ids.md) - Type-safe IDs
diff --git a/.agents/skills/rust-skills/rules/anti-type-erasure.md b/.agents/skills/rust-skills/rules/anti-type-erasure.md
new file mode 100644
index 000000000..b1d1535b1
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-type-erasure.md
@@ -0,0 +1,134 @@
+# anti-type-erasure
+
+> Don't use Box when impl Trait works
+
+## Why It Matters
+
+`Box` (type erasure) introduces heap allocation and dynamic dispatch overhead. When you have a single concrete type or can use generics, `impl Trait` provides the same flexibility with zero overhead through monomorphization.
+
+## Bad
+
+```rust
+// Unnecessary type erasure
+fn get_iterator() -> Box> {
+ Box::new((0..10).map(|x| x * 2))
+}
+
+// Boxing for no reason
+fn make_handler() -> Box i32> {
+ Box::new(|x| x + 1)
+}
+
+// Vec of boxed trait objects when one type would do
+fn get_validators() -> Vec> {
+ vec![
+ Box::new(LengthValidator),
+ Box::new(RegexValidator),
+ ]
+}
+```
+
+## Good
+
+```rust
+// impl Trait - zero overhead, inlined
+fn get_iterator() -> impl Iterator- {
+ (0..10).map(|x| x * 2)
+}
+
+// impl Fn - no boxing
+fn make_handler() -> impl Fn(i32) -> i32 {
+ |x| x + 1
+}
+
+// When mixed types are genuinely needed, Box is OK
+fn get_validators() -> Vec
> {
+ // Actually different types at runtime - Box is appropriate
+ config.validators.iter()
+ .map(|v| v.create_validator())
+ .collect()
+}
+```
+
+## When to Use Box
+
+Type erasure IS appropriate when:
+
+```rust
+// Heterogeneous collection of different types
+let handlers: Vec> = vec![
+ Box::new(LogHandler),
+ Box::new(MetricsHandler),
+ Box::new(AuthHandler),
+];
+
+// Type cannot be known at compile time
+fn create_from_config(config: &Config) -> Box {
+ match config.db_type {
+ DbType::Postgres => Box::new(PostgresDb::new()),
+ DbType::Sqlite => Box::new(SqliteDb::new()),
+ }
+}
+
+// Recursive types
+struct Node {
+ value: i32,
+ children: Vec>,
+}
+
+// Breaking cycles in complex ownership
+struct EventLoop {
+ handlers: Vec>,
+}
+```
+
+## Comparison
+
+| Approach | Allocation | Dispatch | Binary Size |
+|----------|------------|----------|-------------|
+| `impl Trait` | Stack/inline | Static | Larger (monomorphization) |
+| `Box` | Heap | Dynamic | Smaller |
+| Generics `` | Stack/inline | Static | Larger |
+
+## impl Trait Positions
+
+```rust
+// Return position - caller doesn't need to know concrete type
+fn process() -> impl Future { }
+
+// Argument position - like generics but simpler
+fn handle(handler: impl Handler) { }
+
+// Can't use in trait definitions (use associated types instead)
+trait Processor {
+ type Output: Display; // Not impl Display
+ fn process(&self) -> Self::Output;
+}
+```
+
+## Pattern: Enum Instead of dyn
+
+```rust
+// Instead of Box
+enum Shape {
+ Circle { radius: f64 },
+ Rectangle { width: f64, height: f64 },
+ Triangle { base: f64, height: f64 },
+}
+
+impl Shape {
+ fn area(&self) -> f64 {
+ match self {
+ Shape::Circle { radius } => PI * radius * radius,
+ Shape::Rectangle { width, height } => width * height,
+ Shape::Triangle { base, height } => 0.5 * base * height,
+ }
+ }
+}
+```
+
+## See Also
+
+- [anti-over-abstraction](./anti-over-abstraction.md) - Excessive generics
+- [type-generic-bounds](./type-generic-bounds.md) - Generic constraints
+- [mem-box-large-variant](./mem-box-large-variant.md) - Boxing enum variants
diff --git a/.agents/skills/rust-skills/rules/anti-unwrap-abuse.md b/.agents/skills/rust-skills/rules/anti-unwrap-abuse.md
new file mode 100644
index 000000000..ca3326122
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-unwrap-abuse.md
@@ -0,0 +1,143 @@
+# anti-unwrap-abuse
+
+> Don't use `.unwrap()` in production code
+
+## Why It Matters
+
+`.unwrap()` panics on `None` or `Err`, crashing your program. In production, this means lost data, failed requests, and unhappy users. It also makes debugging harder since panic messages often lack context.
+
+## Bad
+
+```rust
+// Crashes if file doesn't exist
+let content = std::fs::read_to_string("config.toml").unwrap();
+
+// Crashes on invalid input
+let num: i32 = user_input.parse().unwrap();
+
+// Crashes if key missing
+let value = map.get("key").unwrap();
+
+// Crashes if channel closed
+let msg = receiver.recv().unwrap();
+```
+
+## Good
+
+```rust
+// Propagate with ?
+fn load_config() -> Result {
+ let content = std::fs::read_to_string("config.toml")?;
+ Ok(toml::from_str(&content)?)
+}
+
+// Provide default
+let num: i32 = user_input.parse().unwrap_or(0);
+
+// Handle missing key
+let value = map.get("key").ok_or(Error::MissingKey)?;
+
+// Or use if-let
+if let Some(value) = map.get("key") {
+ process(value);
+}
+
+// Channel with proper handling
+match receiver.recv() {
+ Ok(msg) => handle(msg),
+ Err(_) => break, // Channel closed
+}
+```
+
+## When unwrap() Is Acceptable
+
+```rust
+// 1. Tests - panics are expected failures
+#[test]
+fn test_parse() {
+ let result = parse("valid").unwrap(); // OK in tests
+ assert_eq!(result, expected);
+}
+
+// 2. Const/static initialization (compile-time guaranteed)
+static REGEX: Lazy = Lazy::new(|| {
+ Regex::new(r"^\d+$").unwrap() // Known-valid pattern
+});
+
+// 3. After a check that guarantees success
+if map.contains_key("key") {
+ let value = map.get("key").unwrap(); // Just checked
+}
+// Better: use if-let or entry API instead
+
+// 4. Truly impossible cases with proof comment
+let last = vec.pop().unwrap();
+// OK only if you just checked !vec.is_empty()
+// Better: use last() or pattern match
+```
+
+## Alternatives to unwrap()
+
+```rust
+// unwrap_or - provide default
+let x = opt.unwrap_or(default);
+
+// unwrap_or_default - use Default trait
+let x = opt.unwrap_or_default();
+
+// unwrap_or_else - compute default lazily
+let x = opt.unwrap_or_else(|| expensive_default());
+
+// ? operator - propagate errors
+let x = opt.ok_or(Error::Missing)?;
+
+// if let - handle Some/Ok case
+if let Some(x) = opt {
+ use_x(x);
+}
+
+// match - handle all cases
+match opt {
+ Some(x) => use_x(x),
+ None => handle_none(),
+}
+
+// map - transform if present
+let y = opt.map(|x| x + 1);
+
+// and_then - chain fallible operations
+let z = opt.and_then(|x| x.checked_add(1));
+```
+
+## expect() Is Slightly Better
+
+```rust
+// unwrap() - no context
+let file = File::open(path).unwrap();
+// Panics with: "called `Result::unwrap()` on an `Err` value: Os { code: 2, ... }"
+
+// expect() - adds context
+let file = File::open(path)
+ .expect("config file should exist at startup");
+// Panics with: "config file should exist at startup: Os { code: 2, ... }"
+
+// But still use only for invariants, not error handling
+```
+
+## Clippy Lint
+
+```rust
+// Enable these lints to catch unwrap usage:
+#![warn(clippy::unwrap_used)]
+#![warn(clippy::expect_used)] // Stricter
+
+// Or per-function:
+#[allow(clippy::unwrap_used)]
+fn tests_only() { }
+```
+
+## See Also
+
+- [err-question-mark](err-question-mark.md) - Use ? for propagation
+- [err-result-over-panic](err-result-over-panic.md) - Return Result instead of panicking
+- [anti-expect-lazy](anti-expect-lazy.md) - Don't use expect for recoverable errors
diff --git a/.agents/skills/rust-skills/rules/anti-vec-for-slice.md b/.agents/skills/rust-skills/rules/anti-vec-for-slice.md
new file mode 100644
index 000000000..ed50a038c
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/anti-vec-for-slice.md
@@ -0,0 +1,121 @@
+# anti-vec-for-slice
+
+> Don't accept &Vec when &[T] works
+
+## Why It Matters
+
+`&Vec` is strictly less flexible than `&[T]`. A slice can be created from `Vec`, arrays, and other slice-like types. Accepting `&Vec` forces callers to have exactly a `Vec`, preventing them from using arrays, slices, or other collections.
+
+## Bad
+
+```rust
+// Forces callers to have a Vec
+fn sum(numbers: &Vec) -> i32 {
+ numbers.iter().sum()
+}
+
+// Caller must allocate
+let arr = [1, 2, 3, 4, 5];
+sum(&arr.to_vec()); // Unnecessary allocation
+
+// Slice won't work
+let slice: &[i32] = &[1, 2, 3];
+// sum(slice); // Error: expected &Vec
+```
+
+## Good
+
+```rust
+// Accept slice - works with Vec, arrays, slices
+fn sum(numbers: &[i32]) -> i32 {
+ numbers.iter().sum()
+}
+
+// All these work
+sum(&[1, 2, 3, 4, 5]); // Array
+sum(&vec![1, 2, 3]); // Vec
+sum(&numbers[1..3]); // Slice of slice
+sum(numbers.as_slice()); // Explicit slice
+```
+
+## Deref Coercion
+
+`Vec` implements `Deref`, so `&Vec` automatically coerces to `&[T]`:
+
+```rust
+fn takes_slice(s: &[i32]) { }
+
+let vec = vec![1, 2, 3];
+takes_slice(&vec); // &Vec -> &[i32] via Deref
+```
+
+## Mutable Slices
+
+Same applies to `&mut`:
+
+```rust
+// Bad
+fn double(numbers: &mut Vec) {
+ for n in numbers.iter_mut() {
+ *n *= 2;
+ }
+}
+
+// Good
+fn double(numbers: &mut [i32]) {
+ for n in numbers.iter_mut() {
+ *n *= 2;
+ }
+}
+```
+
+## When to Accept &Vec
+
+Rarely. Only when you need Vec-specific operations:
+
+```rust
+fn needs_capacity(v: &Vec) -> usize {
+ v.capacity() // Only Vec has capacity
+}
+
+fn might_grow(v: &mut Vec) {
+ v.push(42); // Slice can't push
+}
+```
+
+## Pattern: Accepting Multiple Types
+
+```rust
+// Accept anything that can be viewed as a slice
+fn process>(data: T) {
+ let bytes: &[u8] = data.as_ref();
+ // ...
+}
+
+process(&[1u8, 2, 3]); // Array
+process(vec![1u8, 2, 3]); // Vec
+process(&some_vec); // &Vec
+process(b"bytes"); // Byte string
+```
+
+## Similar Anti-patterns
+
+| Anti-pattern | Better |
+|--------------|--------|
+| `&Vec` | `&[T]` |
+| `&String` | `&str` |
+| `&PathBuf` | `&Path` |
+| `&Box` | `&T` |
+
+## Clippy Detection
+
+```toml
+[lints.clippy]
+ptr_arg = "warn" # Catches &Vec, &String, &PathBuf
+```
+
+## See Also
+
+- [anti-string-for-str](./anti-string-for-str.md) - Similar for String
+- [own-slice-over-vec](./own-slice-over-vec.md) - Slice patterns
+- [api-impl-asref](./api-impl-asref.md) - AsRef pattern
diff --git a/.agents/skills/rust-skills/rules/api-builder-must-use.md b/.agents/skills/rust-skills/rules/api-builder-must-use.md
new file mode 100644
index 000000000..988849dcc
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-builder-must-use.md
@@ -0,0 +1,143 @@
+# api-builder-must-use
+
+> Mark builder methods with `#[must_use]` to prevent silent drops
+
+## Why It Matters
+
+Builder pattern methods return a modified builder. Without `#[must_use]`, calling a builder method and ignoring the return value silently does nothing—the builder is dropped, and the configuration is lost. This creates confusing bugs where code appears correct but has no effect.
+
+## Bad
+
+```rust
+struct RequestBuilder {
+ url: String,
+ timeout: Option,
+ headers: Vec<(String, String)>,
+}
+
+impl RequestBuilder {
+ fn timeout(mut self, duration: Duration) -> Self {
+ self.timeout = Some(duration);
+ self
+ }
+
+ fn header(mut self, key: &str, value: &str) -> Self {
+ self.headers.push((key.to_string(), value.to_string()));
+ self
+ }
+}
+
+// Bug: builder methods are ignored - no warning!
+let request = RequestBuilder::new("https://api.example.com");
+request.timeout(Duration::from_secs(30)); // Dropped silently!
+request.header("Authorization", "Bearer token"); // Dropped silently!
+let response = request.send(); // Sends with no timeout or headers
+```
+
+## Good
+
+```rust
+struct RequestBuilder {
+ url: String,
+ timeout: Option,
+ headers: Vec<(String, String)>,
+}
+
+impl RequestBuilder {
+ #[must_use = "builder methods return modified builder - chain or assign"]
+ fn timeout(mut self, duration: Duration) -> Self {
+ self.timeout = Some(duration);
+ self
+ }
+
+ #[must_use = "builder methods return modified builder - chain or assign"]
+ fn header(mut self, key: &str, value: &str) -> Self {
+ self.headers.push((key.to_string(), value.to_string()));
+ self
+ }
+}
+
+// Now warns: unused return value that must be used
+let request = RequestBuilder::new("https://api.example.com");
+request.timeout(Duration::from_secs(30)); // Warning!
+
+// Correct: chain methods
+let response = RequestBuilder::new("https://api.example.com")
+ .timeout(Duration::from_secs(30))
+ .header("Authorization", "Bearer token")
+ .send();
+```
+
+## Apply to Entire Type
+
+```rust
+#[must_use = "builders do nothing unless consumed"]
+struct ConfigBuilder {
+ log_level: Level,
+ max_connections: usize,
+}
+
+// Now all methods returning Self warn if ignored
+impl ConfigBuilder {
+ fn log_level(mut self, level: Level) -> Self {
+ self.log_level = level;
+ self
+ }
+
+ fn max_connections(mut self, n: usize) -> Self {
+ self.max_connections = n;
+ self
+ }
+
+ fn build(self) -> Config {
+ Config {
+ log_level: self.log_level,
+ max_connections: self.max_connections,
+ }
+ }
+}
+```
+
+## Message Guidelines
+
+```rust
+// Descriptive message helps users understand
+#[must_use = "builder methods return modified builder"]
+fn with_foo(self, foo: Foo) -> Self { ... }
+
+#[must_use = "this creates a new String and does not modify the original"]
+fn to_uppercase(&self) -> String { ... }
+
+#[must_use = "iterator adaptors are lazy - use .collect() to consume"]
+fn map(self, f: F) -> Map { ... }
+```
+
+## Clippy Lint
+
+```toml
+[lints.clippy]
+must_use_candidate = "warn" # Suggests where #[must_use] would help
+return_self_not_must_use = "warn" # Specifically for -> Self methods
+```
+
+## Standard Library Examples
+
+```rust
+// std::Option - must_use on map, and, or
+let x: Option = Some(5);
+x.map(|v| v * 2); // Warning: unused return value
+
+// std::Result - must_use on the type itself
+#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
+pub enum Result { ... }
+
+// Iterator adaptors
+let v = vec![1, 2, 3];
+v.iter().map(|x| x * 2); // Warning: iterators are lazy
+```
+
+## See Also
+
+- [api-builder-pattern](./api-builder-pattern.md) - Builder pattern best practices
+- [api-must-use](./api-must-use.md) - General must_use guidelines
+- [err-result-over-panic](./err-result-over-panic.md) - Result types are must_use
diff --git a/.agents/skills/rust-skills/rules/api-builder-pattern.md b/.agents/skills/rust-skills/rules/api-builder-pattern.md
new file mode 100644
index 000000000..f825d1dd2
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-builder-pattern.md
@@ -0,0 +1,187 @@
+# api-builder-pattern
+
+> Use Builder pattern for complex construction
+
+## Why It Matters
+
+When a type has many optional parameters or complex initialization, the Builder pattern provides a clear, flexible API. It avoids constructors with many parameters (which are error-prone) and makes the code self-documenting.
+
+## Bad
+
+```rust
+// Constructor with many parameters - hard to read, easy to get wrong
+let client = Client::new(
+ "https://api.example.com", // Which is which?
+ 30, // Timeout? Retries?
+ true, // What does this mean?
+ None,
+ Some("auth_token"),
+ false,
+);
+
+// Or many Option fields
+struct Client {
+ url: String,
+ timeout: Option,
+ retries: Option,
+ // ... 10 more optional fields
+}
+```
+
+## Good
+
+```rust
+#[derive(Default)]
+#[must_use = "builders do nothing unless you call build()"]
+pub struct ClientBuilder {
+ base_url: Option,
+ timeout: Option,
+ max_retries: u32,
+ auth_token: Option,
+}
+
+impl ClientBuilder {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Sets the base URL for all requests.
+ pub fn base_url(mut self, url: impl Into) -> Self {
+ self.base_url = Some(url.into());
+ self
+ }
+
+ /// Sets the request timeout. Default is 30 seconds.
+ pub fn timeout(mut self, timeout: Duration) -> Self {
+ self.timeout = Some(timeout);
+ self
+ }
+
+ /// Sets the maximum number of retries. Default is 3.
+ pub fn max_retries(mut self, n: u32) -> Self {
+ self.max_retries = n;
+ self
+ }
+
+ /// Sets the authentication token.
+ pub fn auth_token(mut self, token: impl Into) -> Self {
+ self.auth_token = Some(token.into());
+ self
+ }
+
+ /// Builds the client with the configured options.
+ pub fn build(self) -> Result {
+ let base_url = self.base_url
+ .ok_or(BuilderError::MissingBaseUrl)?;
+
+ Ok(Client {
+ base_url,
+ timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
+ max_retries: self.max_retries,
+ auth_token: self.auth_token,
+ })
+ }
+}
+
+// Usage - clear and self-documenting
+let client = ClientBuilder::new()
+ .base_url("https://api.example.com")
+ .timeout(Duration::from_secs(10))
+ .max_retries(5)
+ .auth_token("secret")
+ .build()?;
+```
+
+## Builder Variations
+
+```rust
+// 1. Infallible builder (build() returns T, not Result)
+impl WidgetBuilder {
+ pub fn build(self) -> Widget {
+ Widget {
+ color: self.color.unwrap_or(Color::Black),
+ size: self.size.unwrap_or(Size::Medium),
+ }
+ }
+}
+
+// 2. Typestate builder (compile-time required field checking)
+pub struct ClientBuilder {
+ url: Url,
+ timeout: Option,
+}
+
+pub struct NoUrl;
+pub struct HasUrl(String);
+
+impl ClientBuilder {
+ pub fn new() -> Self {
+ Self { url: NoUrl, timeout: None }
+ }
+
+ pub fn url(self, url: String) -> ClientBuilder {
+ ClientBuilder { url: HasUrl(url), timeout: self.timeout }
+ }
+}
+
+impl ClientBuilder {
+ pub fn build(self) -> Client {
+ // url is guaranteed to be set
+ Client { url: self.url.0, timeout: self.timeout }
+ }
+}
+
+// 3. Consuming vs borrowing (consuming is more common)
+// Consuming (takes self)
+pub fn timeout(mut self, t: Duration) -> Self { ... }
+
+// Borrowing (takes &mut self, allows reuse)
+pub fn timeout(&mut self, t: Duration) -> &mut Self { ... }
+```
+
+## Evidence from reqwest
+
+```rust
+// https://github.com/seanmonstar/reqwest/blob/master/src/async_impl/client.rs
+
+#[must_use]
+pub struct ClientBuilder {
+ config: Config,
+}
+
+impl ClientBuilder {
+ pub fn new() -> ClientBuilder {
+ ClientBuilder {
+ config: Config::default(),
+ }
+ }
+
+ pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
+ self.config.timeout = Some(timeout);
+ self
+ }
+
+ pub fn build(self) -> Result {
+ // Validation and construction
+ }
+}
+```
+
+## Key Attributes
+
+```rust
+#[derive(Default)] // Enables MyBuilder::default()
+#[must_use = "builders do nothing unless you call build()"]
+pub struct MyBuilder { ... }
+
+impl MyBuilder {
+ #[must_use] // Each method should have this
+ pub fn option(mut self, value: T) -> Self { ... }
+}
+```
+
+## See Also
+
+- [api-builder-must-use](api-builder-must-use.md) - Add #[must_use] to builders
+- [api-typestate](api-typestate.md) - Compile-time state machines
+- [api-impl-into](api-impl-into.md) - Accept impl Into for flexibility
diff --git a/.agents/skills/rust-skills/rules/api-common-traits.md b/.agents/skills/rust-skills/rules/api-common-traits.md
new file mode 100644
index 000000000..64a61ae06
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-common-traits.md
@@ -0,0 +1,165 @@
+# api-common-traits
+
+> Implement standard traits (Debug, Clone, PartialEq, etc.) for public types
+
+## Why It Matters
+
+Standard traits make your types interoperable with the Rust ecosystem. `Debug` enables `println!("{:?}")` and error messages. `Clone` allows explicit duplication. `PartialEq` enables `==`. Without these, users can't use your types in common patterns like testing, collections, or debugging.
+
+## Bad
+
+```rust
+// Bare struct - severely limited usability
+pub struct Point {
+ pub x: f64,
+ pub y: f64,
+}
+
+// Can't debug
+println!("{:?}", point); // Error: Debug not implemented
+
+// Can't compare
+if point1 == point2 { } // Error: PartialEq not implemented
+
+// Can't use in HashMap
+let mut map: HashMap = HashMap::new(); // Error: Hash not implemented
+
+// Can't clone
+let copy = point.clone(); // Error: Clone not implemented
+```
+
+## Good
+
+```rust
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Point {
+ pub x: f64,
+ pub y: f64,
+}
+
+// Now everything works
+println!("{:?}", point);
+assert_eq!(point1, point2);
+let copy = point; // Copy, not just Clone
+
+// For hashable types
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct UserId(u64);
+
+let mut map: HashMap = HashMap::new();
+```
+
+## Trait Derivation Guide
+
+| Trait | Derive When | Requirements |
+|-------|-------------|--------------|
+| `Debug` | Always for public types | All fields implement Debug |
+| `Clone` | Type can be duplicated | All fields implement Clone |
+| `Copy` | Small, simple types | All fields implement Copy, no Drop |
+| `PartialEq` | Comparison makes sense | All fields implement PartialEq |
+| `Eq` | Total equality | PartialEq, no floating-point fields |
+| `Hash` | Used as HashMap/HashSet key | Eq, consistent with PartialEq |
+| `Default` | Sensible default exists | All fields implement Default |
+| `PartialOrd` | Ordering makes sense | PartialEq, all fields implement PartialOrd |
+| `Ord` | Total ordering | Eq + PartialOrd, no floating-point |
+
+## Common Trait Bundles
+
+```rust
+// ID types
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct EntityId(u64);
+
+// Value types
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub struct Vector2 { x: f32, y: f32 }
+
+// Configuration
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct Config {
+ name: String,
+ options: HashMap,
+}
+
+// Error types
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ParseError {
+ InvalidSyntax(String),
+ UnexpectedToken(Token),
+}
+```
+
+## Manual Implementations
+
+```rust
+// When derive doesn't do what you want
+struct CaseInsensitiveString(String);
+
+impl PartialEq for CaseInsensitiveString {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.to_lowercase() == other.0.to_lowercase()
+ }
+}
+
+impl Eq for CaseInsensitiveString {}
+
+impl Hash for CaseInsensitiveString {
+ fn hash(&self, state: &mut H) {
+ // Must be consistent with PartialEq
+ self.0.to_lowercase().hash(state);
+ }
+}
+
+// Custom Debug for sensitive data
+struct Password(String);
+
+impl Debug for Password {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "Password([REDACTED])")
+ }
+}
+```
+
+## Serde Traits
+
+```rust
+use serde::{Serialize, Deserialize};
+
+// For serializable types
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct ApiResponse {
+ pub status: String,
+ pub data: Vec- ,
+}
+
+// With custom serialization
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Config {
+ #[serde(default)]
+ pub verbose: bool,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub api_key: Option
,
+}
+```
+
+## Minimum Recommended
+
+```rust
+// At minimum, public types should have:
+#[derive(Debug, Clone, PartialEq)]
+pub struct MyType { ... }
+
+// Add based on use case:
+// + Eq, Hash → for HashMap keys
+// + Ord, PartialOrd → for BTreeMap, sorting
+// + Default → for Option::unwrap_or_default()
+// + Copy → for small value types
+// + Serialize → for serialization
+```
+
+## See Also
+
+- [own-copy-small](./own-copy-small.md) - When to implement Copy
+- [api-default-impl](./api-default-impl.md) - Implementing Default
+- [doc-examples-section](./doc-examples-section.md) - Documenting trait implementations
diff --git a/.agents/skills/rust-skills/rules/api-default-impl.md b/.agents/skills/rust-skills/rules/api-default-impl.md
new file mode 100644
index 000000000..be82a02a7
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-default-impl.md
@@ -0,0 +1,177 @@
+# api-default-impl
+
+> Implement `Default` for types with sensible default values
+
+## Why It Matters
+
+`Default` is a standard trait that provides a canonical way to create a default instance. It integrates with many ecosystem patterns: `Option::unwrap_or_default()`, `#[derive(Default)]`, struct update syntax `..Default::default()`, and generic code that requires `T: Default`. Implementing it makes your types more ergonomic.
+
+## Bad
+
+```rust
+struct Config {
+ timeout: Duration,
+ retries: u32,
+ verbose: bool,
+}
+
+impl Config {
+ // Custom constructor - works but non-standard
+ fn new() -> Self {
+ Config {
+ timeout: Duration::from_secs(30),
+ retries: 3,
+ verbose: false,
+ }
+ }
+}
+
+// Can't use with standard patterns
+let config: Config = Default::default(); // Error: Default not implemented
+let timeout = settings.get("timeout").unwrap_or_default(); // Won't work
+```
+
+## Good
+
+```rust
+#[derive(Default)]
+struct Config {
+ #[default = Duration::from_secs(30)] // Nightly, or implement manually
+ timeout: Duration,
+ retries: u32, // Defaults to 0 with derive
+ verbose: bool, // Defaults to false with derive
+}
+
+// Or implement manually for custom defaults
+impl Default for Config {
+ fn default() -> Self {
+ Config {
+ timeout: Duration::from_secs(30),
+ retries: 3,
+ verbose: false,
+ }
+ }
+}
+
+// Now works with all standard patterns
+let config = Config::default();
+let config = Config { retries: 5, ..Default::default() };
+let value = map.get("key").cloned().unwrap_or_default();
+```
+
+## Derive vs Manual
+
+```rust
+// Derive: all fields use their own Default
+#[derive(Default)]
+struct Simple {
+ count: u32, // 0
+ name: String, // ""
+ items: Vec, // []
+}
+
+// Manual: when you need custom defaults
+struct Connection {
+ host: String,
+ port: u16,
+ timeout: Duration,
+}
+
+impl Default for Connection {
+ fn default() -> Self {
+ Connection {
+ host: "localhost".to_string(),
+ port: 8080,
+ timeout: Duration::from_secs(30),
+ }
+ }
+}
+```
+
+## Builder with Default
+
+```rust
+#[derive(Default)]
+struct ServerBuilder {
+ host: String,
+ port: u16,
+ workers: usize,
+}
+
+impl ServerBuilder {
+ fn host(mut self, host: impl Into) -> Self {
+ self.host = host.into();
+ self
+ }
+
+ fn port(mut self, port: u16) -> Self {
+ self.port = port;
+ self
+ }
+}
+
+// Clean initialization
+let server = ServerBuilder::default()
+ .host("0.0.0.0")
+ .port(3000)
+ .build();
+```
+
+## Default with Required Fields
+
+```rust
+// When some fields have no sensible default, don't implement Default
+struct User {
+ id: UserId, // No sensible default
+ name: String, // Could default to ""
+}
+
+// Instead, provide a constructor
+impl User {
+ fn new(id: UserId, name: impl Into) -> Self {
+ User { id, name: name.into() }
+ }
+}
+
+// Or use builder with required fields
+struct UserBuilder {
+ id: Option,
+ name: String,
+}
+
+impl Default for UserBuilder {
+ fn default() -> Self {
+ UserBuilder {
+ id: None,
+ name: String::new(),
+ }
+ }
+}
+```
+
+## Generic Default
+
+```rust
+// Require Default in generic bounds when needed
+fn create_or_default(opt: Option) -> T {
+ opt.unwrap_or_default()
+}
+
+// PhantomData is Default regardless of T
+use std::marker::PhantomData;
+struct Wrapper {
+ _marker: PhantomData,
+}
+
+impl Default for Wrapper {
+ fn default() -> Self {
+ Wrapper { _marker: PhantomData }
+ }
+}
+```
+
+## See Also
+
+- [api-builder-pattern](./api-builder-pattern.md) - Building complex types
+- [api-common-traits](./api-common-traits.md) - Other common traits to implement
+- [api-from-not-into](./api-from-not-into.md) - Conversion traits
diff --git a/.agents/skills/rust-skills/rules/api-extension-trait.md b/.agents/skills/rust-skills/rules/api-extension-trait.md
new file mode 100644
index 000000000..92e8dd90a
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-extension-trait.md
@@ -0,0 +1,163 @@
+# api-extension-trait
+
+> Use extension traits to add methods to external types
+
+## Why It Matters
+
+Rust's orphan rules prevent implementing external traits on external types. Extension traits provide a workaround: define a new trait with your methods, then implement it for the external type. This pattern is used extensively in the ecosystem (e.g., `itertools::Itertools`, `tokio::AsyncReadExt`).
+
+## Bad
+
+```rust
+// Can't add methods directly to external types
+impl Vec {
+ fn as_hex(&self) -> String {
+ // Error: cannot define inherent impl for a type outside this crate
+ }
+}
+
+// Can't implement external trait for external type
+impl SomeExternalTrait for Vec {
+ // Error: orphan rules violation
+}
+```
+
+## Good
+
+```rust
+// Define an extension trait
+pub trait ByteSliceExt {
+ fn as_hex(&self) -> String;
+ fn is_ascii_printable(&self) -> bool;
+}
+
+// Implement for the external type
+impl ByteSliceExt for [u8] {
+ fn as_hex(&self) -> String {
+ self.iter()
+ .map(|b| format!("{:02x}", b))
+ .collect()
+ }
+
+ fn is_ascii_printable(&self) -> bool {
+ self.iter().all(|b| b.is_ascii_graphic() || b.is_ascii_whitespace())
+ }
+}
+
+// Usage: import the trait to use the methods
+use my_crate::ByteSliceExt;
+
+let data: &[u8] = b"hello";
+println!("{}", data.as_hex()); // "68656c6c6f"
+```
+
+## Convention: Ext Suffix
+
+```rust
+// Standard naming: TypeExt for extending Type
+pub trait OptionExt {
+ fn unwrap_or_log(self, msg: &str) -> Option;
+}
+
+impl OptionExt for Option {
+ fn unwrap_or_log(self, msg: &str) -> Option {
+ if self.is_none() {
+ log::warn!("{}", msg);
+ }
+ self
+ }
+}
+
+// For generic extensions
+pub trait ResultExt {
+ fn log_err(self) -> Self;
+}
+
+impl ResultExt for Result {
+ fn log_err(self) -> Self {
+ if let Err(ref e) = self {
+ log::error!("{}", e);
+ }
+ self
+ }
+}
+```
+
+## Ecosystem Examples
+
+```rust
+// itertools::Itertools
+use itertools::Itertools;
+let groups = vec![1, 1, 2, 2, 3].into_iter().group_by(|x| *x);
+
+// futures::StreamExt
+use futures::StreamExt;
+let next = stream.next().await;
+
+// tokio::io::AsyncReadExt
+use tokio::io::AsyncReadExt;
+let mut buf = [0u8; 1024];
+reader.read(&mut buf).await?;
+
+// anyhow::Context
+use anyhow::Context;
+let content = std::fs::read_to_string(path)
+ .with_context(|| format!("failed to read {}", path))?;
+```
+
+## Scoped Extensions
+
+```rust
+// Extension only visible where imported
+mod string_utils {
+ pub trait StringExt {
+ fn truncate_ellipsis(&self, max_len: usize) -> String;
+ }
+
+ impl StringExt for str {
+ fn truncate_ellipsis(&self, max_len: usize) -> String {
+ if self.len() <= max_len {
+ self.to_string()
+ } else {
+ format!("{}...", &self[..max_len.saturating_sub(3)])
+ }
+ }
+ }
+}
+
+// Only available when explicitly imported
+use string_utils::StringExt;
+let short = "very long string".truncate_ellipsis(10);
+```
+
+## Generic Extensions with Bounds
+
+```rust
+pub trait VecExt {
+ fn push_if_unique(&mut self, item: T)
+ where
+ T: PartialEq;
+}
+
+impl VecExt for Vec {
+ fn push_if_unique(&mut self, item: T)
+ where
+ T: PartialEq,
+ {
+ if !self.contains(&item) {
+ self.push(item);
+ }
+ }
+}
+
+// Works with any T: PartialEq
+let mut v = vec![1, 2, 3];
+v.push_if_unique(2); // No-op
+v.push_if_unique(4); // Adds 4
+```
+
+## See Also
+
+- [api-sealed-trait](./api-sealed-trait.md) - Controlling trait implementations
+- [api-impl-into](./api-impl-into.md) - Using standard conversion traits
+- [name-as-free](./name-as-free.md) - Naming conventions for conversions
diff --git a/.agents/skills/rust-skills/rules/api-from-not-into.md b/.agents/skills/rust-skills/rules/api-from-not-into.md
new file mode 100644
index 000000000..8500a805d
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-from-not-into.md
@@ -0,0 +1,146 @@
+# api-from-not-into
+
+> Implement `From`, not `Into` - From gives you Into for free
+
+## Why It Matters
+
+The standard library has a blanket implementation: `impl Into for T where U: From`. This means implementing `From for U` automatically gives you `Into for T`. Implementing `Into` directly bypasses this and is considered non-idiomatic. Always implement `From`.
+
+## Bad
+
+```rust
+struct UserId(u64);
+
+// Non-idiomatic: implementing Into directly
+impl Into for u64 {
+ fn into(self) -> UserId {
+ UserId(self)
+ }
+}
+
+// Works, but now you can't use From syntax
+let id = UserId::from(42); // Error: From not implemented
+let id: UserId = 42.into(); // Works, but limited
+```
+
+## Good
+
+```rust
+struct UserId(u64);
+
+// Idiomatic: implement From
+impl From for UserId {
+ fn from(id: u64) -> Self {
+ UserId(id)
+ }
+}
+
+// Now both work automatically
+let id = UserId::from(42); // From syntax
+let id: UserId = 42.into(); // Into syntax (via blanket impl)
+
+// And Into bound works in generics
+fn process(id: impl Into) {
+ let id: UserId = id.into();
+}
+process(42u64); // Works!
+```
+
+## Blanket Implementation
+
+```rust
+// This is in std, you don't write it
+impl Into for T
+where
+ U: From,
+{
+ fn into(self) -> U {
+ U::from(self)
+ }
+}
+
+// So when you implement From:
+impl From for MyType { ... }
+
+// You automatically get:
+// impl Into for String { ... }
+```
+
+## Multiple From Implementations
+
+```rust
+struct Email(String);
+
+impl From for Email {
+ fn from(s: String) -> Self {
+ Email(s)
+ }
+}
+
+impl From<&str> for Email {
+ fn from(s: &str) -> Self {
+ Email(s.to_string())
+ }
+}
+
+// All of these work
+let e1 = Email::from("test@example.com");
+let e2 = Email::from(String::from("test@example.com"));
+let e3: Email = "test@example.com".into();
+let e4: Email = String::from("test@example.com").into();
+```
+
+## TryFrom for Fallible Conversions
+
+```rust
+use std::convert::TryFrom;
+
+struct PositiveInt(u32);
+
+// Fallible conversion
+impl TryFrom for PositiveInt {
+ type Error = &'static str;
+
+ fn try_from(value: i32) -> Result {
+ if value > 0 {
+ Ok(PositiveInt(value as u32))
+ } else {
+ Err("value must be positive")
+ }
+ }
+}
+
+// Usage
+let pos = PositiveInt::try_from(42)?; // From-style
+let pos: PositiveInt = 42.try_into()?; // Into-style (via blanket)
+```
+
+## Clippy Lint
+
+```toml
+[lints.clippy]
+from_over_into = "warn" # Warns when implementing Into instead of From
+```
+
+```rust
+// Clippy will warn:
+impl Into for Foo { // Warning: prefer From
+ fn into(self) -> Bar { ... }
+}
+```
+
+## When Into IS Needed (Rare)
+
+```rust
+// Only when implementing for external types in specific trait bounds
+// This is very rare and usually indicates a design issue
+
+// Example: you can't implement From for ExternalB
+// because of orphan rules. But you usually shouldn't need to.
+```
+
+## See Also
+
+- [api-impl-into](./api-impl-into.md) - Using Into in function parameters
+- [err-from-impl](./err-from-impl.md) - From for error types
+- [api-newtype-safety](./api-newtype-safety.md) - Newtype conversions
diff --git a/.agents/skills/rust-skills/rules/api-impl-asref.md b/.agents/skills/rust-skills/rules/api-impl-asref.md
new file mode 100644
index 000000000..688f8cc02
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-impl-asref.md
@@ -0,0 +1,142 @@
+# api-impl-asref
+
+> Use `AsRef` when you only need to borrow the inner data
+
+## Why It Matters
+
+`AsRef` provides a cheap borrowed view of data without taking ownership or copying. Functions accepting `impl AsRef` can work with multiple types that contain or represent `T`, making APIs flexible while avoiding unnecessary allocations. Use `AsRef` when you only need to read, `Into` when you need to own.
+
+## Bad
+
+```rust
+// Forces callers to provide exact types
+fn process_text(text: &str) { ... }
+fn read_file(path: &Path) { ... }
+
+// Can't call directly with owned types
+let s = String::from("hello");
+process_text(&s); // Works but verbose
+
+let p = PathBuf::from("/file");
+read_file(&p); // Works but verbose
+read_file("/file"); // Error! &str != &Path
+```
+
+## Good
+
+```rust
+// Accept anything that can be viewed as the target type
+fn process_text(text: impl AsRef) {
+ let s: &str = text.as_ref();
+ println!("{}", s);
+}
+
+fn read_file(path: impl AsRef) -> io::Result> {
+ std::fs::read(path.as_ref())
+}
+
+// All of these work:
+process_text("literal"); // &str
+process_text(String::from("owned")); // String
+process_text(Cow::from("cow")); // Cow
+
+read_file("/path/to/file"); // &str
+read_file(Path::new("/path")); // &Path
+read_file(PathBuf::from("/path")); // PathBuf
+read_file(OsStr::new("/path")); // &OsStr
+```
+
+## AsRef vs Into vs Borrow
+
+```rust
+// AsRef: cheap borrow, no ownership transfer
+fn read(p: impl AsRef) {
+ let path: &Path = p.as_ref();
+}
+
+// Into: ownership transfer, may allocate
+fn store(p: impl Into) {
+ let owned: PathBuf = p.into();
+}
+
+// Borrow: like AsRef but with Eq/Hash consistency guarantee
+use std::borrow::Borrow;
+fn lookup(map: &HashMap, key: &Q) -> Option<&V>
+where
+ String: Borrow,
+ Q: Hash + Eq,
+{
+ map.get(key)
+}
+```
+
+## Implement AsRef for Custom Types
+
+```rust
+struct Name(String);
+
+impl AsRef for Name {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl AsRef<[u8]> for Name {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_bytes()
+ }
+}
+
+// Now Name works with functions expecting AsRef
+fn greet(name: impl AsRef) {
+ println!("Hello, {}!", name.as_ref());
+}
+
+greet(Name("Alice".into()));
+```
+
+## Common AsRef Implementations
+
+```rust
+// Standard library provides many
+impl AsRef for String { ... }
+impl AsRef for str { ... }
+impl AsRef<[u8]> for str { ... }
+impl AsRef<[u8]> for String { ... }
+impl AsRef<[u8]> for Vec { ... }
+impl AsRef for str { ... }
+impl AsRef for String { ... }
+impl AsRef for PathBuf { ... }
+impl AsRef for OsStr { ... }
+impl AsRef for str { ... }
+```
+
+## When to Use Which
+
+| Trait | Use When |
+|-------|----------|
+| `&T` | Single type, simple API |
+| `AsRef` | Read-only access, multiple input types |
+| `Into` | Need to store/own the value |
+| `Borrow` | HashMap/HashSet keys, Eq/Hash needed |
+| `Deref` | Smart pointer semantics |
+
+## Pattern: Optional AsRef Bound
+
+```rust
+// When T itself might be passed
+fn process, U>(value: T) {
+ let inner: &U = value.as_ref();
+}
+
+// More flexible: accept T or &T
+fn process + ?Sized, U: ?Sized>(value: &T) {
+ let inner: &U = value.as_ref();
+}
+```
+
+## See Also
+
+- [api-impl-into](./api-impl-into.md) - When to use Into instead
+- [own-slice-over-vec](./own-slice-over-vec.md) - Using slices for flexibility
+- [own-borrow-over-clone](./own-borrow-over-clone.md) - Preferring borrows
diff --git a/.agents/skills/rust-skills/rules/api-impl-into.md b/.agents/skills/rust-skills/rules/api-impl-into.md
new file mode 100644
index 000000000..9d6707523
--- /dev/null
+++ b/.agents/skills/rust-skills/rules/api-impl-into.md
@@ -0,0 +1,160 @@
+# api-impl-into
+
+> Accept `impl Into` for flexible APIs, implement `From` for conversions
+
+## Why It Matters
+
+APIs that accept `impl Into` are ergonomic—callers can pass the target type directly or any type that converts to it. This reduces boilerplate `.into()` calls at call sites. Implement `From` rather than `Into` because `From` implies `Into` through a blanket implementation.
+
+## Bad
+
+```rust
+// Requires exact type - forces callers to convert
+fn process_path(path: PathBuf) { ... }
+fn set_name(name: String) { ... }
+
+// Caller must convert explicitly
+process_path(PathBuf::from("/path/to/file"));
+process_path("/path/to/file".to_path_buf()); // Verbose
+process_path("/path/to/file".into()); // Explicit
+
+set_name(String::from("Alice"));
+set_name("Alice".to_string()); // Verbose
+```
+
+## Good
+
+```rust
+// Accept anything that converts to the target type
+fn process_path(path: impl Into) {
+ let path = path.into(); // Convert once inside
+ // ...
+}
+
+fn set_name(name: impl Into) {
+ let name = name.into();
+ // ...
+}
+
+// Callers are ergonomic
+process_path("/path/to/file"); // &str converts automatically
+process_path(PathBuf::from(".")); // PathBuf works too
+
+set_name("Alice"); // &str
+set_name(String::from("Alice")); // String
+set_name(format!("User-{}", id)); // String from format!
+```
+
+## Implement From, Not Into
+
+```rust
+struct UserId(u64);
+
+// ✅ Implement From
+impl From for UserId {
+ fn from(id: u64) -> Self {
+ UserId(id)
+ }
+}
+
+// Into is automatically provided by blanket impl
+let id: UserId = 42u64.into(); // Works!
+
+// ❌ Don't implement Into directly
+impl Into for u64 {
+ fn into(self) -> UserId {
+ UserId(self) // This works but is non-idiomatic
+ }
+}
+```
+
+## Common Conversions
+
+```rust
+// String-like types
+fn log_message(msg: impl Into) { ... }
+log_message("literal"); // &str
+log_message(String::from("own")); // String
+log_message(Cow::from("cow")); // Cow
+
+// Path-like types
+fn read_file(path: impl AsRef) { ... } // AsRef for borrowed access
+fn write_file(path: impl Into) { ... } // Into when storing
+
+// Duration
+fn set_timeout(duration: impl Into) { ... }
+set_timeout(Duration::from_secs(5));
+// Note: no blanket impl for integers, would need custom wrapper
+```
+
+## AsRef vs Into
+
+```rust
+// AsRef: borrow as &T, no conversion cost
+fn count_bytes(data: impl AsRef<[u8]>) -> usize {
+ data.as_ref().len() // Just borrows, no allocation
+}
+count_bytes("hello"); // &str -> &[u8]
+count_bytes(b"hello"); // &[u8] -> &[u8]
+count_bytes(vec![1, 2, 3]); // &Vec -> &[u8]
+
+// Into: convert to owned T, may allocate
+fn store_data(data: impl Into>) {
+ let owned: Vec = data.into(); // Takes ownership
+ // ...
+}
+```
+
+## When NOT to Use impl Into
+
+```rust
+// ❌ Trait objects need Sized
+fn process(handler: impl Into>) { }
+// Better: just take Box directly
+
+// ❌ Recursive types
+struct Node {
+ children: Vec>, // Error: impl Trait not allowed here
+}
+
+// ❌ Performance-critical hot paths (minor overhead of trait dispatch)
+fn hot_path(value: impl Into) {
+ // Consider taking u64 directly if called billions of times
+}
+
+// ❌ When you need to name the type
+fn returns_impl() -> impl Into { } // Opaque, hard to use
+```
+
+## Builder Pattern with Into
+
+```rust
+struct Config {
+ name: String,
+ path: PathBuf,
+}
+
+impl Config {
+ fn new(name: impl Into