Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overview

RUSVEL’s architecture is guided by 14 Architecture Decision Records (ADRs). These capture the “why” behind key design choices. All were informed by cross-validation with multiple AI systems and real-world constraints of a solo founder product.

ADR-001: Merge Ops + Connect into GoToMarket Engine

Status: Accepted

Context: Original design had 7 engines including separate Ops (CRM, invoicing, SOPs) and Connect (outreach, sequences, follow-ups) engines. Perplexity review flagged that 7 is too many for a solo founder product.

Decision: Merge into single GoToMarket engine. CRM, outreach, pipeline, invoicing all live together.

Consequence: 5 engines instead of 7. GoToMarket engine is larger but cohesive around “find and close business.”

ADR-002: Fold Mission into Forge Engine

Status: Accepted

Context: Mission (goals, daily planning, reviews) was a separate engine. Mission is really “orchestrated agents” – goal-setting, planning, and reviews are all agent tasks.

Decision: Mission becomes a sub-module of the Forge engine.

Consequence: rusvel forge mission today instead of rusvel mission today. Forge is the meta-engine that orchestrates everything.

ADR-003: Single Central Job Queue

Status: Accepted

Context: Original design had AutomationPort (workflows), SchedulePort (cron/triggers), plus Forge (agent orchestration) and per-engine scheduling. This created 4 overlapping workflow substrates.

Decision: Single JobPort with SQLite-backed job queue. All async work goes through one queue. Worker pool routes jobs to engines by JobKind.

Consequence: One place to see all pending/running/completed work. Simpler debugging.

ADR-004: StoragePort with 5 Canonical Stores

Status: Accepted

Context: The original StoragePort was “persist anything” with generic put/get/delete/query. Too broad – would re-invent a typed repository layer.

Decision: StoragePort exposes 5 sub-stores: EventStore (append-only), ObjectStore (CRUD), SessionStore (hierarchy), JobStore (queue semantics), MetricStore (time-series).

Consequence: Clear boundaries. Each store optimized for its access pattern.

ADR-005: Event.kind as String, not Enum

Status: Accepted

Context: The original EventKind was a giant enum forcing rusvel-core to know every event type.

Decision: Event.kind is a String. Engines define their own event kind constants (e.g., forge::AGENT_CREATED).

Consequence: rusvel-core stays minimal. New engines define new event kinds without modifying core.

ADR-006: HarvestPort and PublishPort Are Engine-Internal

Status: Accepted

Context: The original design had HarvestPort and PublishPort as core ports. But scraping and publishing are domain-specific, not cross-cutting concerns.

Decision: Move to engine-internal traits. Harvest engine defines its own source adapters. Content engine defines its own platform adapters.

Consequence: Core ports reduced from 13 to 10 (later grew to 19 with new ports). Cleaner separation.

ADR-007: metadata: serde_json::Value on All Domain Types

Status: Accepted

Context: Schema evolution concern – domain types will iterate frequently.

Decision: All domain types include metadata: serde_json::Value for extensibility without migrations.

Consequence: Base columns + metadata JSON pattern. Huge flexibility for evolution.

ADR-008: Human-in-the-Loop Approval Model

Status: Accepted

Context: For a solo founder, the most valuable pattern is “agent proposes, human approves.” Without this, the system either does too much (scary) or too little (useless).

Decision: ApprovalStatus and ApprovalPolicy are core domain types. Content publishing and outreach require approval by default. Agents can be auto-approved below cost thresholds.

Consequence: JobStatus includes AwaitingApproval. The UI shows an approval queue. Agents pause and present results for review.

ADR-009: Engines Never Call LlmPort Directly

Status: Accepted

Context: Unclear boundaries between LlmPort (raw model access) and AgentPort (orchestration). If engines call both, prompting/retries/tool selection logic gets scattered.

Decision: LlmPort is raw (generate, stream, embed). AgentPort wraps LlmPort + ToolPort + MemoryPort. Engines only use AgentPort.

Consequence: All prompt construction, tool selection, retries, and memory injection happen in one place (rusvel-agent).

ADR-010: Engines Depend Only on rusvel-core Traits

Status: Accepted

Context: Hexagonal architecture rule. Engines must not import concrete adapter types.

Decision: Engines depend on rusvel-core. They receive port implementations via constructor injection. rusvel-app (composition root) wires concrete adapters to engines.

Consequence: Any adapter can be swapped without touching engine code. Engines are testable with mock ports.

ADR-011: Department Registry — Dynamic Departments Replace Hardcoded Routing

Status: Accepted (superseded by ADR-014 for registration mechanism)

Context: Scaling from 5 engines to many departments (the registry now holds 14 apps) with hardcoded routes meant 72+ routes and touching 7 files to add one department.

Decision: DepartmentRegistry holds DepartmentDef structs. 6 parameterized /api/dept/{dept}/* routes replace all per-department routes. Frontend uses a single dynamic [dept] route.

Consequence: Adding a department = adding a DepartmentDef entry. Zero route changes. Three-layer config cascade (Global -> Department -> Session).

ADR-012: shadcn/ui Design Tokens — oklch Color System

Status: Accepted

Context: Frontend needed a consistent design system. Tailwind 4 supports oklch natively.

Decision: Adopt shadcn/ui design tokens with oklch color values. Each department gets a color token from the registry.

Consequence: Consistent theming across all department UIs. Dark mode is a CSS variable swap.

ADR-013: Capability Engine — AI-Driven Entity Creation

Status: Accepted

Context: Users need to extend the system with new agents, skills, rules, MCP servers, hooks, and workflows. Manually configuring each is tedious.

Decision: POST /api/capability/build accepts natural language, uses Claude with WebSearch/WebFetch to discover resources, generates a bundle of entities. Also available via !build <description> in chat.

Consequence: One-shot system extension. “Install a GitHub code review agent” creates agent, skills, hooks, and MCP config in one call.

ADR-014: DepartmentApp Pattern — Departments as Self-Contained Crates

Status: Accepted

Context: The EngineKind enum forced rusvel-core to know about every department, violating the Open/Closed Principle. Adding a department touched 5+ files.

Decision: Introduce DepartmentApp trait and DepartmentManifest struct. Each department lives in its own dept-* crate. EngineKind enum is removed entirely; departments use string IDs. 14 dept-* crates including dept-messaging (registered last at boot).

Consequence: Adding a department = adding a dept-* crate. Zero changes to rusvel-core. Each department declares its own routes, tools, capabilities, and system prompt via DepartmentManifest. Supersedes the registration mechanism of ADR-011.