RUSVEL
The Solo Builder’s AI-Powered Virtual Agency One binary, one human, infinite leverage.
RUSVEL is an AI-powered virtual agency built with Rust + SvelteKit. It gives a single person the leverage of an entire agency through 14 autonomous departments, each powered by AI agents.
Quick Start
git clone https://github.com/mbaneshi/rusvel
cd rusvel
cargo run
# Open http://localhost:3000
What’s Inside
- God Agent — Your AI companion that knows your identity, products, and mission
- 14 Departments — Forge, Code, Content, Harvest, GTM, Finance, Product, Growth, Distribution, Legal, Support, Infra, Flow, Messaging
- Knowledge/RAG — fastembed + lancedb for semantic search over your documents
- Self-Improvement — The app can analyze and improve its own codebase
- 55 workspace members — Hexagonal architecture; 22 port traits in
rusvel-core/src/ports.rs(15 primary + 5*Store+ChannelPort+BrowserPort+RusvelBasePort) - ~645 tests (workspace sum); full
cargo testpasses in a normal dev environment
See Repository status for canonical metrics and links to docs/status/current-state.md on GitHub.
Architecture
God Agent (Chat — full authority + visibility)
├── Forge — Mission planning, goals, reviews
├── Code — Full Claude Code capabilities
├── Content — Draft, adapt, publish across platforms
├── Harvest — Find opportunities, score, propose
├── GTM — CRM, outreach, invoicing, deals
├── Finance — Ledger, runway, tax estimation
├── Product — Roadmap, pricing, feedback
├── Growth — Funnel, cohorts, KPIs
├── Distribution — SEO, marketplace, affiliates
├── Legal — Contracts, compliance, IP
├── Support — Tickets, knowledge base, NPS
├── Infra — Deploy, monitor, incidents
├── Flow — DAG workflow engine
└── Messaging — Notification channels
Each department is autonomous — own config, own chat, own agents, own events. God sees everything and can orchestrate any combination.
Stack
- Rust edition 2024, SQLite WAL, Axum, Clap 4, tokio
- SvelteKit 5, Tailwind CSS 4
- LLM: Claude CLI (Max subscription), Ollama, OpenAI, Claude API
- RAG: fastembed (local ONNX embeddings) + lancedb (vector search)
Getting Started
Get RUSVEL running in under 5 minutes.
- Installation — Build from source or use Docker
- First Run — Start the server and explore the dashboard
- First Mission — Create a session and generate your first daily plan
Installation
Prerequisites
RUSVEL is a Rust + SvelteKit application. You need the following installed:
| Dependency | Version | Required | Purpose |
|---|---|---|---|
| Rust | Edition 2024 (nightly or stable 1.85+) | Yes | Backend, all engines |
| Node.js | 20+ | Yes | Frontend build |
| pnpm | 9+ | Yes | Frontend package manager |
| SQLite | 3.35+ | Bundled | Database (WAL mode) |
| Ollama | Latest | Optional | Local LLM inference |
Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Install Node.js + pnpm
Use your preferred method (nvm, fnm, or direct install):
# With nvm
nvm install 22
nvm use 22
# Install pnpm
corepack enable
corepack prepare pnpm@9.15.4 --activate
# Or: npm install -g pnpm
Install Ollama (optional)
Ollama provides free local LLM inference. RUSVEL auto-detects it on first run.
# macOS
brew install ollama
# Linux
curl -fsSL https://ollama.com/install.sh | sh
# Start the service
ollama serve
If you skip Ollama, you can configure Claude API or OpenAI keys instead.
Clone and Build
git clone https://github.com/mbaneshi/rusvel.git
cd rusvel
cargo build
The workspace has 50 members (see Repository status). First build takes a few minutes; subsequent builds are incremental.
Build the Frontend
cd frontend
pnpm install
pnpm build
The built frontend is served by the Rust binary at runtime. During development you can run pnpm dev for hot-reload on port 5173.
Verify the Installation
Run the full test suite to confirm everything works:
cargo test
You should see on the order of ~399 tests passing (exact count changes over time). Use cargo test exit code 0 as the gate.
test result: ok. … passed; 0 failed
Test individual crates
cargo test -p rusvel-core # Core domain types and port traits
cargo test -p rusvel-db # SQLite stores (41 tests)
cargo test -p forge-engine # Agent orchestration (15 tests)
cargo test -p content-engine # Content creation (7 tests)
cargo test -p harvest-engine # Opportunity discovery (12 tests)
Next Steps
Once installed, proceed to First Run to launch RUSVEL for the first time.
First Run
Start the Server
From the project root, run:
cargo run
RUSVEL starts the API server on port 3000. You will see:
RUSVEL API listening on 0.0.0.0:3000
Open your browser to http://localhost:3000.
Create Your First Session
Sessions are RUSVEL’s workspaces. Everything – goals, events, conversations, agent runs – lives inside a session.
Via the Web UI
The frontend guides you through creating a session on first load. The onboarding checklist tracks your progress:
- Create a session
- Add a goal
- Generate a daily plan
- Chat with a department
- Create an agent
Via the CLI
# Create a session
cargo run -- session create "My Startup"
# Output:
# Session created: My Startup
# ID: a1b2c3d4-... (set as active session)
The session ID is saved to ~/.rusvel/active_session and used by all subsequent CLI commands.
List and Switch Sessions
# List all sessions
cargo run -- session list
# Switch active session
cargo run -- session switch <session-id>
LLM Configuration
RUSVEL supports four LLM providers. On first run, it auto-detects Ollama if running locally.
| Provider | Setup | Best For |
|---|---|---|
| Ollama | Auto-detected if running | Free local inference |
| Claude API | Set ANTHROPIC_API_KEY env var | High-quality reasoning |
| Claude CLI | Install claude CLI tool | Claude Max subscription |
| OpenAI | Set OPENAI_API_KEY env var | GPT models |
You can configure the default model per department in the Settings page or via the API:
curl -X PUT http://localhost:3000/api/config \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-4-20250514"}'
The MCP Server
RUSVEL can also run as an MCP (Model Context Protocol) server for integration with Claude Desktop or other MCP clients:
cargo run -- --mcp
This starts a stdio JSON-RPC server instead of the web server.
Directory Structure
After first run, RUSVEL creates:
~/.rusvel/
├── active_session # UUID of the current session
├── config.toml # Global configuration
└── rusvel.db # SQLite database (WAL mode)
Next Steps
With your session created, head to First Mission to set goals and generate your first daily plan.
First Mission
What is a Mission?
A mission is RUSVEL’s daily planning system, powered by the Forge engine. It reads your active goals, checks the state of all departments, and generates a prioritized task list for the day.
The workflow is: set goals > generate daily plan > execute > review progress.
Set Goals
Goals are high-level objectives with a timeframe. They drive what the daily plan prioritizes.
Via the Web UI
- Navigate to the Forge department from the sidebar
- Click the “Set new goal” quick action
- The Forge agent will ask for context and help you define the goal
Via the CLI
# Add a goal with default month timeframe
cargo run -- forge mission goal add "Launch MVP"
# Add a goal with specific timeframe and description
cargo run -- forge mission goal add "Get 10 beta users" \
--description "Recruit beta testers from HN and Reddit" \
--timeframe week
# List all goals
cargo run -- forge mission goals
Output from goals:
ID TITLE TIMEFRAME STATUS PROGRESS
----------------------------------------------------------------------------------------------------
a1b2c3d4-... Launch MVP Month Active 0%
e5f6g7h8-... Get 10 beta users Week Active 0%
Goal Timeframes
| Timeframe | Planning Horizon |
|---|---|
day | Today only |
week | This week |
month | This month |
quarter | This quarter |
Generate a Daily Plan
The daily plan is an AI-generated prioritized task list based on your goals and current state.
Via the Web UI
- Open the Forge department
- Click “Daily plan” quick action
- The agent generates tasks with priorities and focus areas
Via the CLI
cargo run -- forge mission today
Output:
Generating daily plan...
Daily Plan -- 2026-03-23
==================================================
1. [High] Finalize API endpoint documentation
2. [High] Write integration tests for auth flow
3. [Medium] Draft landing page copy
4. [Medium] Set up CI pipeline
5. [Low] Review dependency updates
Focus areas:
- Ship the authentication feature
- Prepare for beta launch
Notes: Focus on high-priority items first. The auth flow is blocking beta signups.
Review Progress
Periodic reviews summarize what was accomplished and identify blockers.
Via the CLI
# Weekly review (default)
cargo run -- forge mission review
# Monthly review
cargo run -- forge mission review --period month
# Quarterly review
cargo run -- forge mission review --period quarter
Output:
Generating Week review...
Review (Week)
==================================================
Accomplishments:
- Completed API authentication flow
- Deployed staging environment
- Drafted 3 blog posts
Blockers:
- Waiting on SSL certificate for production
- Need feedback on pricing page design
Insights:
- Code velocity increased 40% after adding test automation
- Content pipeline is 2 days behind schedule
Next actions:
- Follow up on SSL certificate
- Schedule design review for pricing page
Explore Other Departments
With your mission set, explore the 14 department apps. Each has its own AI agent specialized for that domain:
- Code – implement features, analyze code, run tests
- Content – write blog posts, adapt for social platforms
- Harvest – find freelance opportunities, draft proposals
- GTM – manage CRM, send outreach, track deals
See the full list in Departments.
Concepts
Core building blocks of the RUSVEL system.
- Sessions — Workspace contexts that scope all data
- Departments — 14 autonomous business functions
- Agents — AI workers that execute tasks
- Skills & Rules — Reusable actions and behavioral constraints
- Workflows — Multi-step orchestrated processes
- Domain Concepts Map — How concepts relate to each other
Sessions
What Are Sessions?
Sessions are RUSVEL’s top-level organizational unit. Think of them as workspaces or projects. All data in RUSVEL – goals, events, conversations, agent runs, content items, opportunities – is scoped to a session.
You always have one active session. All CLI commands and API calls operate on the active session by default.
Session Kinds
Each session has a kind that hints at its purpose:
| Kind | Use Case | Example |
|---|---|---|
Project | A codebase or product you are building | “RUSVEL”, “My SaaS App” |
Lead | A potential client or deal | “Acme Corp Engagement” |
ContentCampaign | A content series or marketing push | “Q1 Blog Series” |
General | Catch-all for anything else | “Scratch Pad” |
The session kind is informational. It does not restrict which departments or features you can use.
Creating Sessions
CLI
cargo run -- session create "My Startup"
This creates a new session and sets it as active. The session ID is stored in ~/.rusvel/active_session.
API
curl -X POST http://localhost:3000/api/sessions \
-H "Content-Type: application/json" \
-d '{"name": "My Startup", "kind": "Project"}'
Listing Sessions
CLI
cargo run -- session list
Output shows all sessions with the active one marked:
ID NAME KIND UPDATED
------------------------------------------------------------------------------------------
a1b2c3d4-e5f6-... My Startup Project 2026-03-23 14:30 *
b2c3d4e5-f6g7-... Blog Series Content.. 2026-03-22 09:15
API
curl http://localhost:3000/api/sessions
Switching Sessions
CLI
cargo run -- session switch <session-id>
API
Load a specific session by ID:
curl http://localhost:3000/api/sessions/<session-id>
Data Stored Per Session
Each session contains:
- Goals – strategic objectives with timeframes and progress tracking
- Events – an immutable, append-only log of everything that happens
- Conversations – chat history with each department agent
- Agent runs – records of every agent execution (input, output, tokens used)
- Content items – blog posts, tweets, proposals in various states
- Opportunities – leads, gigs, and deals in the pipeline
- Contacts – CRM entries tied to the session
- Tasks – generated from daily plans, linked to goals
Session Configuration
Sessions can override global configuration. The config cascade is:
Global config → Department config → Session config
This lets you use different LLM models, approval policies, or tool permissions per session.
Departments
Overview
RUSVEL organizes work into 14 department apps (dept-* crates), each with its own specialized AI agent surface. Together they form a virtual agency that a solo founder can command from a single interface.
Departments follow the DepartmentApp pattern (ADR-014). Each department lives in its own dept-* crate that implements the DepartmentApp trait, declares its capabilities via a DepartmentManifest, and registers with the host at boot. Adding a new department means adding a new dept-* crate – zero changes to rusvel-core.
The DepartmentApp Trait
Every department crate implements this contract:
#![allow(unused)]
fn main() {
pub trait DepartmentApp: Send + Sync {
fn manifest(&self) -> DepartmentManifest;
fn register(&self, ctx: &mut RegistrationContext) -> Result<()>;
}
}
The DepartmentManifest declares the department’s ID, name, icon, color, system prompt, capabilities, tabs, quick actions, routes, tools, and CLI commands. The host collects all manifests to generate the DepartmentRegistry, API routes, CLI subcommands, and frontend navigation.
The 14 dept-* Crates
| Crate | Department | Engine | Focus |
|---|---|---|---|
dept-forge | Forge | Forge | Agent orchestration, goal planning, mission management |
dept-code | Code | Code | Code intelligence, implementation, testing |
dept-content | Content | Content | Content creation, publishing, calendar |
dept-harvest | Harvest | Harvest | Opportunity discovery, proposals, pipeline |
dept-flow | Flow | Flow | DAG workflow engine, visual workflow builder |
dept-gtm | GTM | GoToMarket | CRM, outreach, deals, invoicing |
dept-finance | Finance | Finance | Revenue, expenses, runway, tax |
dept-product | Product | Product | Roadmap, pricing, feedback |
dept-growth | Growth | Growth | Funnels, cohorts, KPIs, retention |
dept-distro | Distro | Distribution | Marketplace, SEO, affiliates |
dept-legal | Legal | Legal | Contracts, compliance, IP |
dept-support | Support | Support | Tickets, knowledge base, NPS |
dept-infra | Infra | Infra | Deployments, monitoring, incidents |
dept-messaging | Messaging | — (shell) | Cross-channel messaging surface |
Department UI Structure
Each department page has two panels:
Chat Panel (right side)
The AI agent for this department. Send messages, get responses, use quick actions. The agent has access to department-specific tools via the ScopedToolRegistry – each department only sees tools relevant to its domain.
Department Panel (left side)
A tabbed panel with up to 9 tabs depending on the department:
| Tab | Purpose |
|---|---|
| Actions | Quick-action buttons for common tasks |
| Agents | Custom agent profiles for this department |
| Workflows | Multi-step agent chains (sequential, parallel, loop, graph) |
| Skills | Reusable prompt templates with variables |
| Rules | Constraints injected into the system prompt |
| MCP | MCP server connections (Code department) |
| Hooks | Event-triggered automations |
| Dirs | Working directories for code operations |
| Events | Event log for this department |
The God Agent
The main Chat page (not tied to any department) runs the God Agent. This agent has full authority and visibility over all departments. Use it for cross-cutting tasks:
- “What is the status across all departments?”
- “Move the proposal from Harvest to Content for editing”
- “Generate a weekly summary of all activity”
Quick Actions
Each department has predefined quick-action buttons that send a prompt to the agent with one click. These are declared in the department’s DepartmentManifest and can be customized.
Department Configuration
Each department inherits global config and can override:
- Model – which LLM to use for this department’s agent
- Effort – low, medium, or high (controls response depth)
- Permission mode – what tools the agent can use
- Working directories – for code-operating departments
Configure via the Settings page or the API:
curl -X PUT http://localhost:3000/api/dept/code/config \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-4-20250514", "effort": "high"}'
Agents
What Are Agents?
Agents are AI personas with specific roles, instructions, tools, and budgets. Each department has a default agent, and you can create custom agents for specialized tasks.
Agents are powered by the AgentRuntime (in rusvel-agent), which wraps LLM access, tool execution, streaming, and memory into a single orchestration layer.
Agent Profile Structure
Every agent has:
name — Display name (e.g., "rust-engine")
role — One-line role description
instructions — System prompt guiding behavior
model — Which LLM to use (provider + model name)
allowed_tools — List of tools the agent can invoke
capabilities — What the agent can do (code_analysis, content_creation, etc.)
budget_limit — Optional spending cap per run
department — Which department this agent belongs to
Seeded Agents
RUSVEL ships with 5 pre-configured agents:
| Agent | Department | Role |
|---|---|---|
| rust-engine | Code | Rust development – writes, tests, and refactors Rust code |
| svelte-ui | Code | Frontend development – SvelteKit components and styling |
| test-writer | Code | Writes unit and integration tests for existing code |
| content-writer | Content | Drafts blog posts, social media content, and documentation |
| proposal-writer | Harvest | Writes proposals for freelance opportunities |
Creating Custom Agents
Via the Web UI
- Navigate to any department
- Open the Agents tab in the department panel
- Click “Add Agent”
- Fill in name, role, instructions, and model
- Save
Via the API
curl -X POST http://localhost:3000/api/agents \
-H "Content-Type: application/json" \
-d '{
"name": "seo-analyst",
"role": "SEO and keyword research specialist",
"instructions": "You analyze websites for SEO performance...",
"model": "claude-sonnet-4-20250514",
"department": "distro",
"capabilities": ["seo"]
}'
Managing Agents
# List all agents
curl http://localhost:3000/api/agents
# Get a specific agent
curl http://localhost:3000/api/agents/<id>
# Update an agent
curl -X PUT http://localhost:3000/api/agents/<id> \
-H "Content-Type: application/json" \
-d '{"instructions": "Updated instructions..."}'
# Delete an agent
curl -X DELETE http://localhost:3000/api/agents/<id>
Model Selection and ModelTier Routing
Agents can use any configured LLM provider:
| Provider | Example Models |
|---|---|
| Ollama | llama3.1, mistral, codellama |
| Claude API | claude-sonnet-4-20250514, claude-opus-4-20250514 |
| Claude CLI | Real streaming via Claude CLI binary |
| OpenAI | gpt-4o, gpt-4o-mini |
The model is set per agent and falls back to the department default, then the global default.
ModelTier routing automatically classifies models into three tiers – Haiku (fast/cheap), Sonnet (balanced), Opus (powerful/expensive). The CostTracker in MetricStore logs token usage and costs per tier, enabling budget-aware model selection.
Tools and ScopedToolRegistry
Agents have access to 22+ tools:
- 10 built-in tools:
read_file,write_file,edit_file,glob,grep,bash,git_status,git_diff,git_log, andtool_search(meta-tool) - 12 engine tools: harvest (5: scan, score, propose, list, pipeline), content (5: draft, adapt, publish, list, approve), code (2: analyze, search)
The ScopedToolRegistry filters tools per department – the Content department sees content tools, the Code department sees code tools. This prevents tool overload and keeps agents focused.
Deferred tool loading via the tool_search meta-tool saves ~85% of tool-description tokens. Instead of loading all tool schemas upfront, agents discover tools on-demand by searching for capabilities they need.
Agent Budgets
Set a budget_limit to cap how much an agent can spend per run. This is useful for expensive operations like long code generation sessions.
When the budget is exceeded, the agent pauses and requests approval to continue.
How Agents Execute
When you chat with a department, here is what happens:
- Your message goes to the department’s agent
- The AgentRuntime loads the system prompt (department prompt + active rules via
load_rules_for_engine()) - The ScopedToolRegistry provides department-specific tools
- The LLM generates a response via
run_streaming(), emitting AgentEvents (text chunks, tool calls, tool results) - Tool calls are executed and results fed back to the LLM in a tool-use loop
- LlmStreamEvent provides character-by-character SSE streaming to the frontend
- An event is logged with tokens used and tools called
Engines never call the LLM directly (ADR-009). They always go through the AgentPort, which handles prompt construction, tool routing, retries, and memory injection.
Frontend Components
The chat interface includes:
- ToolCallCard – renders tool invocations inline in the chat
- ApprovalCard – shows approval requests for sensitive operations
- ApprovalQueue – sidebar badge showing pending approvals across all departments
Skills & Rules
Skills
Skills are reusable prompt templates with variable placeholders. They let you save common prompts and re-invoke them with different inputs.
Skill Structure
name — Display name (e.g., "Write Blog Post")
template — Prompt text with {variable} placeholders
department — Which department this skill belongs to
variables — List of variable names used in the template
Example Skill
Name: Draft Technical Blog Post
Template: |
Write a technical blog post about {topic}.
Target audience: {audience}.
Length: {length} words.
Include code examples in {language}.
Tone: educational but practical.
When invoked, you fill in {topic}, {audience}, {length}, and {language}.
Managing Skills
Via the Web UI
- Navigate to a department
- Open the Skills tab in the department panel
- Click “Add Skill”
- Write the template with
{variable}placeholders - Save
Via the API
# Create a skill
curl -X POST http://localhost:3000/api/skills \
-H "Content-Type: application/json" \
-d '{
"name": "Proposal Template",
"template": "Write a proposal for {gig_title} on {platform}...",
"department": "harvest"
}'
# List all skills
curl http://localhost:3000/api/skills
# Update a skill
curl -X PUT http://localhost:3000/api/skills/<id> \
-H "Content-Type: application/json" \
-d '{"template": "Updated template..."}'
# Delete a skill
curl -X DELETE http://localhost:3000/api/skills/<id>
Rules
Rules are constraints injected into agent system prompts. They enforce policies, coding standards, or business logic across all interactions with a department.
Rule Structure
name — Display name (e.g., "Hexagonal Architecture")
content — The constraint text injected into system prompts
department — Which department this rule applies to
enabled — Whether this rule is currently active
Seeded Rules
RUSVEL ships with 3 pre-configured rules:
| Rule | Department | Purpose |
|---|---|---|
| Hexagonal Architecture | Code | Engines never import adapters. Use port traits only. |
| Human Approval Gate | Content, GTM | All publishing and outreach requires human approval. |
| Crate Size Limit | Code | Each crate must stay under 2000 lines. |
How Rules Work
When a department’s agent processes a message:
- All enabled rules for that department are loaded
- Rule content is appended to the department system prompt
- The combined prompt guides the agent’s behavior
This means rules are “always on” constraints, not one-time instructions.
Managing Rules
Via the Web UI
- Navigate to a department
- Open the Rules tab
- Toggle rules on/off with the enable switch
- Add new rules with “Add Rule”
Via the API
# Create a rule
curl -X POST http://localhost:3000/api/rules \
-H "Content-Type: application/json" \
-d '{
"name": "Always Write Tests",
"content": "Every code change must include corresponding unit tests.",
"department": "code",
"enabled": true
}'
# List all rules
curl http://localhost:3000/api/rules
# Toggle a rule off
curl -X PUT http://localhost:3000/api/rules/<id> \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
Skills vs Rules
| Skills | Rules | |
|---|---|---|
| Purpose | Reusable prompts | Behavioral constraints |
| When used | On demand, when invoked | Always active (if enabled) |
| Contains variables | Yes ({variable}) | No |
| Injected into | Chat as a user message | System prompt |
Workflows
What Are Workflows?
Workflows chain multiple agents together to accomplish multi-step tasks. Instead of manually prompting one agent at a time, you define a flow that routes work between agents automatically.
Workflow Patterns
RUSVEL supports four execution patterns:
Sequential
Agents run one after another. Each agent’s output feeds into the next as input.
Agent A → Agent B → Agent C → Result
Example: Research a topic (Agent A) > write a blog post (Agent B) > adapt for Twitter (Agent C).
Parallel
Multiple agents run simultaneously. Results are collected and merged.
Agent A ─┐
Agent B ─┤→ Merge → Result
Agent C ─┘
Example: Analyze code quality (Agent A), run security scan (Agent B), check dependencies (Agent C) – all at once.
Loop
An agent runs repeatedly until a condition is met or a maximum iteration count is reached.
Agent A → Check → (repeat or done)
Example: Iterate on a draft until it meets quality criteria.
Graph
A directed acyclic graph of agents with conditional branching. The most flexible pattern.
Agent A → [if pass] → Agent B → Agent D
→ [if fail] → Agent C → Agent D
Example: Analyze an opportunity, route to different proposal strategies based on the score.
Creating Workflows
Via the Web UI
- Navigate to a department that supports workflows (Forge, Code, GTM)
- Open the Workflows tab (labeled “Flows”)
- Click “Add Workflow”
- Define the steps, agents, and pattern
- Save
Via the API
# Create a workflow
curl -X POST http://localhost:3000/api/workflows \
-H "Content-Type: application/json" \
-d '{
"name": "Blog Pipeline",
"department": "content",
"pattern": "sequential",
"steps": [
{"agent": "content-writer", "prompt": "Research and outline: {topic}"},
{"agent": "content-writer", "prompt": "Write full draft from outline"},
{"agent": "content-writer", "prompt": "Adapt for Twitter thread"}
]
}'
# List workflows
curl http://localhost:3000/api/workflows
# Get a specific workflow
curl http://localhost:3000/api/workflows/<id>
Running Workflows
Via the API
curl -X POST http://localhost:3000/api/workflows/<id>/run \
-H "Content-Type: application/json" \
-d '{"variables": {"topic": "Hexagonal Architecture in Rust"}}'
The workflow executes each step in order, passing context between agents. Results are streamed back as events.
Workflow Variables
Workflows accept variables at execution time. Use {variable_name} in step prompts:
{
"steps": [
{"prompt": "Analyze the {language} codebase in {directory}"},
{"prompt": "Write tests for any uncovered functions"}
]
}
When running, provide the variables:
{"variables": {"language": "Rust", "directory": "crates/rusvel-core"}}
Managing Workflows
# Update a workflow
curl -X PUT http://localhost:3000/api/workflows/<id> \
-H "Content-Type: application/json" \
-d '{"name": "Updated Pipeline", "steps": [...]}'
# Delete a workflow
curl -X DELETE http://localhost:3000/api/workflows/<id>
RUSVEL Domain Concepts — How Everything Fits Together
The Mental Model
Everything in RUSVEL is scoped to a Department. Each department owns its agents, skills, rules, hooks, and MCP servers. When a user chats with a department, all these pieces compose into a single agent execution. Session is the work boundary that tracks cost, memory, goals, and events across departments. Events are the glue — hooks react to events, enabling cross-department automation without coupling.
The Hierarchy
Session ← your work context (project, lead, campaign)
├── has Goals ← what you're trying to achieve
├── has Budget ← spending limit across all departments
├── scopes Events ← everything that happens is tagged to session
├── scopes Memory ← what the system remembers, per session
│
└── talks to Departments ──────← the 14 organizational units
│
├── owns Agents ← personas (@security-reviewer)
├── owns Skills ← prompt shortcuts (/research topic)
├── owns Rules ← "always do X" injected into every chat
├── owns Hooks ← "when X happens, do Y"
├── owns MCP Servers ← external tool providers
├── owns Config ← model, temperature, per-dept settings
├── owns Chat History ← conversation per department
│
└── can trigger:
├── Workflow ← simple: step1 → step2 → step3
├── Playbook ← rich: Agent → Approval → Flow
└── Flow ← DAG: parallel branches, conditions
JSON Manifest Shape
{
"session": {
"id": "uuid",
"kind": "project",
"goals": ["Launch MVP by April"],
"budget_limit_usd": 50.0,
"departments": {
"forge": {
"agents": [
{ "name": "strategist", "role": "Plan daily missions", "model": "opus" }
],
"skills": [
{ "name": "daily-brief", "template": "Generate executive brief for {{input}}" }
],
"rules": [
{ "name": "concise", "content": "Keep responses under 200 words", "enabled": true }
],
"hooks": [
{ "event": "forge.chat.completed", "type": "http", "action": "https://slack.com/webhook" }
],
"mcp_servers": [
{ "name": "github", "type": "stdio", "command": "mcp-github" }
],
"chat_history": "dept_msg_forge",
"config": { "default_model": "sonnet", "temperature": 0.7 }
},
"content": { "...same shape..." },
"harvest": { "...same shape..." }
},
"workflows": [
{ "name": "publish-pipeline", "steps": ["draft", "review", "publish"] }
],
"playbooks": [
{
"name": "content-from-code",
"steps": [
{ "action": "Agent", "persona": "code-analyst", "prompt": "Analyze {{input}}" },
{ "action": "Approval", "message": "Proceed with draft?" },
{ "action": "Agent", "persona": "writer", "prompt": "Write article from {{last_output}}" }
]
}
],
"flows": [
{
"name": "opportunity-pipeline",
"nodes": ["scan", "score", "decide", "propose"],
"connections": [
{ "scan": "score" },
{ "score": "decide" },
{ "decide[true]": "propose" },
{ "decide[false]": "archive" }
]
}
]
}
}
Visual Diagrams
System Overview
graph TB
subgraph Session["SESSION (work context)"]
Goals["Goals"]
Budget["Budget"]
Memory["Memory (FTS5)"]
EventBus["Event Bus"]
end
subgraph Departments["DEPARTMENTS (14 units)"]
subgraph Forge["Forge"]
FA["Agents"]
FS["Skills"]
FR["Rules"]
FH["Hooks"]
FM["MCP Servers"]
FC["Chat History"]
end
subgraph Content["Content"]
CA["Agents"]
CS["Skills"]
CR["Rules"]
CH["Hooks"]
end
subgraph Harvest["Harvest"]
HA["Agents"]
HS["Skills"]
HR["Rules"]
HH["Hooks"]
end
Other["+ 11 more departments"]
end
subgraph Orchestration["ORCHESTRATION (cross-department)"]
Workflow["Workflow\n(sequential)"]
Playbook["Playbook\n(Agent + Approval + Flow)"]
Flow["Flow\n(DAG with branches)"]
end
Session --> Departments
Departments --> Orchestration
EventBus --> FH & CH & HH
Forge & Content & Harvest --> EventBus
Orchestration --> EventBus
Chat Request Flow
sequenceDiagram
actor User
participant API as Department API
participant Config as Config Cascade
participant Skill as Skill Resolver
participant Agent as Agent Override
participant Rules as Rule Loader
participant RAG as Knowledge/RAG
participant Runtime as AgentRuntime
participant LLM as LLM Provider
participant Hooks as Hook Dispatch
participant Events as Event Bus
User->>API: POST /api/dept/{id}/chat
API->>Config: Load dept config (registry + stored + user)
API->>API: Load conversation history
alt message starts with /skill-name
API->>Skill: resolve_skill(message)
Skill-->>API: expanded template with {{input}}
end
alt message mentions @agent-name
API->>Agent: lookup AgentProfile
Agent-->>API: override system prompt + model
end
API->>Rules: load_rules_for_engine(dept_id)
Rules-->>API: enabled rules appended to system prompt
API->>RAG: embed query + vector search
RAG-->>API: knowledge snippets injected
API->>Runtime: create(AgentConfig) + run_streaming()
loop Tool-use loop
Runtime->>LLM: prompt + tools
LLM-->>Runtime: text or tool_call
Runtime-->>User: SSE: text_delta / tool_call
end
Runtime-->>API: Done (final text)
API->>API: Store assistant message
API->>Events: emit("{dept}.chat.completed")
API->>Hooks: dispatch_hooks(event)
par Hook execution (fire-and-forget)
Hooks->>Hooks: command: sh -c "..."
Hooks->>Hooks: http: POST webhook
Hooks->>Hooks: prompt: claude -p "..."
end
API-->>User: SSE: run_completed
Entity Scoping
graph LR
subgraph Global
Sessions["Sessions"]
Workflows["Workflows"]
Playbooks["Playbooks"]
Flows["Flows"]
end
subgraph "Scoped by metadata.engine"
Agents["Agents"]
Skills["Skills"]
Rules["Rules"]
Hooks["Hooks"]
MCP["MCP Servers"]
Chat["Chat History\n(dept_msg_{id})"]
DeptConfig["Dept Config"]
end
subgraph "Scoped by session_id"
Events["Events"]
Memory["Memory"]
Goals["Goals"]
Runs["Runs"]
Threads["Threads"]
end
Sessions --> Events & Memory & Goals & Runs
Runs --> Threads
Three Orchestration Levels
graph TB
subgraph Workflow["WORKFLOW (simple)"]
direction LR
W1["Step 1:\nagent + prompt"] --> W2["Step 2:\nagent + prompt"] --> W3["Step 3:\nagent + prompt"]
end
subgraph Playbook["PLAYBOOK (rich)"]
direction LR
P1["Agent Step:\ncode-analyst\nanalyzes repo"] --> P2["Approval Step:\nhuman reviews"] --> P3["Agent Step:\nwriter creates\narticle"] --> P4["Flow Step:\npublish pipeline"]
end
subgraph Flow["FLOW (DAG)"]
direction TB
F1["Scan\n(code node)"] --> F2["Score\n(agent node)"]
F2 --> F3{"Score > 80?\n(condition)"}
F3 -->|true| F4["Generate Proposal\n(agent node)"]
F3 -->|false| F5["Archive\n(code node)"]
F4 --> F6["Notify\n(code node)"]
F5 --> F6
end
style Workflow fill:#e8f5e9
style Playbook fill:#e3f2fd
style Flow fill:#fce4ec
Event-Driven Automation
graph LR
subgraph Triggers
ChatDone["chat.completed"]
Published["content.published"]
Scanned["harvest.scan.completed"]
JobDone["job.completed"]
end
subgraph Hooks
H1["Hook: command\nsh -c 'notify.sh'"]
H2["Hook: http\nPOST slack webhook"]
H3["Hook: prompt\nclaude -p 'summarize'"]
end
subgraph Downstream
Slack["Slack notification"]
Flow2["Trigger a Flow"]
Job["Enqueue a Job"]
end
ChatDone --> H1 & H2
Published --> H2 & H3
Scanned --> H1
H1 --> Job
H2 --> Slack
H3 --> Flow2
Storage Map
| Entity | ObjectStore Kind | Scope | Filtered By |
|---|---|---|---|
| Agent | agents | per-dept | metadata.engine |
| Skill | skills | per-dept | metadata.engine |
| Rule | rules | per-dept | metadata.engine |
| Hook | hooks | per-dept | metadata.engine |
| MCP Server | mcp_servers | per-dept | metadata.engine |
| Workflow | workflows | global | — |
| Playbook | in-memory + store | global | — |
| Flow | flow-engine storage | global | — |
| Chat History | dept_msg_{dept} | per-dept | key prefix |
| Dept Config | dept_config | per-dept | key |
| Session | SessionStore | global | — |
| Run | SessionStore | per-session | session_id |
| Thread | SessionStore | per-run | run_id |
| Event | EventStore | per-session | session_id |
| Memory | MemoryPort (FTS5) | per-session | session_id |
| Goal | ObjectStore | per-session | session_id |
| Metric | MetricStore | per-dept | department tag |
When To Use What
| Need | Use | Example |
|---|---|---|
| Reusable prompt shortcut | Skill | /research {{input}} expands to full research prompt |
| Persistent behavior rule | Rule | “Always cite sources” injected into every chat |
| Specialized persona | Agent | @security-reviewer with infosec instructions |
| React to events | Hook | on content.published → POST to Slack |
| Simple ordered steps | Workflow | draft → review → publish |
| Mixed actions with approval | Playbook | AI analyzes → human approves → AI executes |
| Complex branching logic | Flow | DAG with conditions, parallel branches |
| External tool provider | MCP Server | GitHub, Jira, or custom tools via stdio/http |
| Track work context | Session | Project with goals, budget, memory |
| Organize capabilities | Department | 14 units, each with own agents/skills/rules |
The Key Insights
-
Department is the scoping boundary. Agents, skills, rules, hooks, MCP servers, and chat are all scoped by
metadata.enginematching the department ID. Creating an agent in Content means it only appears in Content chat. -
Session is the work boundary. Events, memory, goals, and budget are scoped to a session. You talk to any department within a session, but the session tracks total cost and context.
-
Events are the glue. Hooks react to events. Departments emit events. Jobs emit events. This is how automation chains together without departments knowing about each other.
-
Three orchestration levels exist for a reason:
- Workflow — when you know the exact steps and just need sequential execution
- Playbook — when you need human-in-the-loop approval or mixed AI/Flow steps
- Flow — when you need conditional logic, parallel branches, or complex DAGs
-
Everything composes in the chat handler. A single chat request resolves skills, loads agents, injects rules, searches knowledge, runs the LLM, and dispatches hooks — all in one request/response cycle.
Departments
RUSVEL organizes business functions into 14 departments, each backed by a domain engine and an AI agent.
Wired (6): Forge, Code, Content, Harvest, GTM, Flow
Extended (8): Finance, Product, Growth, Distribution, Legal, Support, Infra, Messaging
Forge Department
Agent orchestration, goal planning, mission management, daily plans, reviews
Overview
The Forge Department is the meta-engine of RUSVEL. It orchestrates AI agents across all departments, manages strategic goals, generates prioritized daily mission plans, and produces periodic reviews. It also houses a catalog of 10 built-in agent personas that can be “hired” into any session, and a safety guard system (budget caps, concurrency slots, circuit breaker) to protect against runaway LLM spend. Forge is the department a solo builder uses to plan their day, track progress, and coordinate work across the entire virtual agency.
Engine (forge-engine)
- Crate:
crates/forge-engine/src/lib.rs - Lines: 483 (lib.rs) + 712 (mission.rs) + 264 (personas.rs) + 23 (events.rs) + submodules
- Status: Wired (real business logic)
Public API
| Method | Signature | Description |
|---|---|---|
new | fn new(agent, events, memory, storage, jobs, session, config) -> Self | Construct with all 7 injected port dependencies |
list_personas | fn list_personas(&self) -> &[AgentProfile] | Return all 10 built-in personas |
get_persona | fn get_persona(&self, name: &str) -> Option<&AgentProfile> | Look up a persona by name (case-insensitive) |
hire_persona | fn hire_persona(&self, name: &str, session_id: &SessionId) -> Result<AgentConfig> | Build a spawn-ready AgentConfig from a named persona |
set_goal | async fn set_goal(&self, session_id, title, description, timeframe) -> Result<Goal> | Create and persist a new goal |
list_goals | async fn list_goals(&self, session_id: &SessionId) -> Result<Vec<Goal>> | List all goals for a session |
mission_today | async fn mission_today(&self, session_id: &SessionId) -> Result<DailyPlan> | Generate a prioritized daily plan via LLM from goals and recent events |
review | async fn review(&self, session_id, period: Timeframe) -> Result<Review> | Generate a periodic review (accomplishments, blockers, next actions) |
generate_brief | async fn generate_brief(&self, session_id: &SessionId) -> Result<ExecutiveBrief> | Cross-department daily digest with strategist summary |
latest_brief | async fn latest_brief(&self, session_id: &SessionId) -> Result<Option<ExecutiveBrief>> | Retrieve the most recently generated executive brief |
run_agent_with_mission_safety | async fn run_agent_with_mission_safety(&self, session_id, config, prompt) -> Result<AgentOutput> | Run an LLM call with circuit breaker, budget check, and concurrency slot |
Internal Structure
PersonaManager(personas.rs) – Manages the catalog of 10 built-in agent personas. Supports lookup by name (case-insensitive), filtering by capability, and runtime additions.SafetyGuard(safety.rs) – Budget enforcement (soft ceiling with 80% warning threshold), concurrency slot limiter, and circuit breaker (opens after repeated failures).- Mission module (
mission.rs) – Goals, daily plans, reviews, and executive briefs. All LLM calls go throughrun_agent_with_mission_safety. - Pipeline module (
pipeline.rs) – Multi-step pipeline orchestration for forge workflows. - Artifacts module (
artifacts.rs) – Persistent artifact records for pipeline outputs. - Events module (
events.rs) – String constants for all emitted event kinds.
Department Wrapper (dept-forge)
- Crate:
crates/dept-forge/src/lib.rs - Lines: 276
- Manifest:
crates/dept-forge/src/manifest.rs
The wrapper creates a ForgeEngine during registration, wires all 7 ports, and registers 5 agent tools. It is a thin delegation layer with no business logic.
Manifest Declaration
System Prompt
You are the Forge department of RUSVEL.
Focus: agent orchestration, goal planning, mission management, daily plans, reviews.
Capabilities
planningorchestration
Quick Actions
| Label | Prompt |
|---|---|
| Daily plan | Generate today’s mission plan based on active goals and priorities. |
| Review progress | Review progress on all active goals. Summarize completed, in-progress, and blocked items. |
| Set new goal | Help me define a new strategic goal. Ask me for context and desired outcome. |
Registered Tools
| Tool Name | Parameters | Description |
|---|---|---|
forge.mission.today | session_id: string (required) | Generate today’s prioritized mission plan from active goals and recent activity |
forge.mission.list_goals | session_id: string (required) | List all goals for a session |
forge.mission.set_goal | session_id: string, title: string, description: string, timeframe: Day/Week/Month/Quarter (all required) | Create a new goal for a session |
forge.mission.review | session_id: string, period: Day/Week/Month/Quarter (required) | Generate a periodic review (accomplishments, blockers, next actions) |
forge.persona.hire | session_id: string, persona_name: string (required) | Build an AgentConfig from a named Forge persona (spawn-ready profile) |
Personas
| Name | Role | Default Model | Allowed Tools | Purpose |
|---|---|---|---|---|
| CodeWriter | code_writer | ollama:llama3.2 | file_write, file_read, shell | Write clean, well-structured code from specifications |
| Reviewer | code_reviewer | ollama:llama3.2 | file_read, search | Examine code for bugs, anti-patterns, performance issues |
| Tester | test_engineer | ollama:llama3.2 | file_write, file_read, shell | Write comprehensive tests, aim for edge cases |
| Debugger | debugger | ollama:llama3.2 | file_read, shell, search | Diagnose failures, trace execution, isolate root causes |
| Architect | architect | ollama:llama3.2 | file_read, search | Design high-level architecture, define module boundaries |
| Documenter | technical_writer | ollama:llama3.2 | file_write, file_read | Produce README files, API docs, architecture guides |
| SecurityAuditor | security_auditor | ollama:llama3.2 | file_read, shell, search | Audit code for vulnerabilities, dependency CVEs |
| Refactorer | refactorer | ollama:llama3.2 | file_write, file_read, shell | Improve code structure without changing behavior |
| ContentWriter | content_writer | ollama:llama3.2 | file_write, web_search | Write blog posts, tweets, documentation, marketing copy |
| Researcher | researcher | ollama:llama3.2 | web_search, file_write | Investigate topics, summarize findings, produce reports |
Skills
| Name | Description | Template |
|---|---|---|
| Daily Standup | Summarize progress and plan for the day | Based on recent activity, generate a standup summary: what was accomplished yesterday, what is planned for today, any blockers |
Rules
| Name | Content | Enabled |
|---|---|---|
| Forge safety – budget | Mission and review flows use ForgeEngine safety: enforce aggregate spend against the configured cost limit before starting LLM work. If the budget would be exceeded, stop and surface a clear error. | Yes |
| Forge safety – concurrency and circuit breaker | Mission LLM runs acquire a concurrency slot and respect the circuit breaker. After repeated failures the circuit opens; no further mission agent runs until reset. When the circuit opens, the engine emits forge.safety.circuit_open. | Yes |
Jobs
No jobs are declared in the Forge manifest. Mission and review work is executed synchronously via run_agent_with_mission_safety.
Events
Produced
| Event Kind | When Emitted |
|---|---|
forge.agent.created | A new agent run is created |
forge.agent.started | An agent run begins executing |
forge.agent.completed | An agent run finishes successfully |
forge.agent.failed | An agent run fails |
forge.mission.plan_generated | mission_today() completes and a DailyPlan is built |
forge.mission.goal_created | set_goal() persists a new goal |
forge.mission.goal_updated | A goal’s status or progress is updated |
forge.mission.review_completed | review() completes and a Review is built |
forge.brief.generated | generate_brief() persists an executive brief |
forge.persona.hired | A persona is hired into a session |
forge.safety.budget_warning | Aggregate spend exceeds 80% of the configured cost limit |
forge.safety.circuit_open | Circuit breaker opens after repeated mission agent failures |
forge.pipeline.started | A forge pipeline orchestration begins |
forge.pipeline.step_started | A pipeline step begins executing |
forge.pipeline.step_completed | A pipeline step completes |
forge.pipeline.completed | A full pipeline orchestration completes |
forge.pipeline.failed | A pipeline orchestration fails |
Consumed
The Forge department does not consume events from other departments. It is triggered by direct API/CLI calls and webhook-dispatched forge.pipeline.requested events (processed by the job worker in rusvel-app).
API Routes
| Method | Path | Description |
|---|---|---|
| GET | /api/sessions/{id}/mission/today | Generate today’s prioritized mission plan for the session |
| GET | /api/sessions/{id}/mission/goals | List goals for the session |
| POST | /api/sessions/{id}/mission/goals | Create a new goal for the session |
| GET | /api/sessions/{id}/events | Query events for the session (includes forge mission and safety events) |
| GET | /api/brief/latest | Return the most recently persisted executive brief for a session |
CLI Commands
rusvel forge mission today # Generate today's daily plan
rusvel forge mission goals # List goals
rusvel forge mission goal <title> # Create a new goal
rusvel forge mission review # Generate a periodic review (default: week)
Entity Auto-Discovery
Agents, skills, rules, hooks, and MCP servers scoped to the Forge department are stored in the object store with metadata.engine = "forge". The shared CRUD API routes (/api/dept/{id}/agents, /api/dept/{id}/skills, etc.) filter by this metadata key, so each department sees only its own entities.
Chat Flow
sequenceDiagram
participant User
participant API as rusvel-api
participant Chat as Chat Handler
participant Config as ConfigPort
participant Skills as Skill Resolver
participant Rules as Rule Loader
participant Agent as AgentRuntime
participant Forge as ForgeEngine
participant Events as EventPort
participant Hooks as Hook Dispatcher
User->>API: POST /api/dept/forge/chat {message}
API->>Chat: route to department chat handler
Chat->>Config: load department config (model, effort, permissions)
Chat->>Skills: resolve_skill() -- check for {{input}} interpolation
Chat->>Chat: check agent override (custom agent for forge)
Chat->>Rules: load_rules_for_engine("forge") -- append to system prompt
Note over Chat: Inject capabilities: planning, orchestration
Note over Chat: System prompt: "You are the Forge department..."
Chat->>Agent: AgentRuntime::run_streaming(config, tools, prompt)
Agent->>Forge: tool calls (forge.mission.today, forge.persona.hire, etc.)
Forge->>Events: emit forge.* events
Agent-->>Chat: SSE stream (AgentEvent chunks)
Chat-->>User: SSE response
Chat->>Events: emit chat completion event
Chat->>Hooks: tokio::spawn hook dispatch
Extending This Department
1. Add a new tool
Register the tool in crates/dept-forge/src/lib.rs inside the register() method using ctx.tools.add("forge", "forge.new_tool", ...). Add a matching ToolContribution entry in forge_engine::mission::mission_tool_contributions_for_manifest() so the manifest stays consistent.
2. Add a new event kind
Add a new pub const in crates/forge-engine/src/events.rs. Emit it from the engine method using self.events.emit(...). Add the event kind string to the events_produced vec in crates/dept-forge/src/manifest.rs.
3. Add a new persona
Add a new profile(...) call in crates/forge-engine/src/personas.rs inside default_personas(). The persona will automatically appear in the manifest via persona_contributions_for_manifest().
4. Add a new skill
Add a SkillContribution entry in the skills vec in crates/dept-forge/src/manifest.rs. Use {{variable}} placeholders in the prompt_template field for runtime interpolation.
5. Add a new API route
Add a RouteContribution entry in forge_engine::mission::forge_route_contributions_for_manifest(). Implement the handler in crates/rusvel-api/src/engine_routes.rs (or a new handler module) and wire the route in crates/rusvel-api/src/lib.rs.
Port Dependencies
| Port | Required | Purpose |
|---|---|---|
| AgentPort | Yes | LLM agent execution for mission planning, reviews, briefs |
| EventPort | Yes | Emit forge.* domain events |
| MemoryPort | Yes | FTS5 session-namespaced memory search |
| StoragePort | Yes | Persist goals, tasks, plans, reviews, briefs via ObjectStore |
| JobPort | Yes | Enqueue background work (pipeline orchestration) |
| SessionPort | Yes | Create and load sessions for mission context |
| ConfigPort | Yes | Read department configuration (budget ceiling, etc.) |
Safety Guard System
The Forge engine includes a SafetyGuard that wraps all LLM calls made through run_agent_with_mission_safety():
Budget Enforcement
- Every mission/review LLM call estimates cost via
config.budget_limit(default: $0.10 per call). - Aggregate spend is tracked across the session lifetime.
- When spend crosses 80% of the configured ceiling, the engine emits
forge.safety.budget_warning. - When spend would exceed the ceiling, the call is rejected with a clear error before any LLM work begins.
Concurrency Slots
- A semaphore limits concurrent mission agent runs (prevents parallel LLM calls from overwhelming providers).
- Each call acquires a slot; the slot is released when the call completes or fails.
Circuit Breaker
- Consecutive failures are tracked. After a threshold of repeated failures, the circuit opens.
- When open, all subsequent mission agent calls are rejected immediately without contacting the LLM.
- The engine emits
forge.safety.circuit_openwhen the circuit opens. - Successful calls reset the failure counter and close the circuit.
Executive Brief System
The generate_brief() method produces a cross-department daily digest:
- Department scans: For each of the 14 department apps, the engine selects an appropriate persona (e.g., CodeWriter for code, Researcher for harvest/finance/legal) and runs a brief-section LLM call asking for status (green/yellow/red), highlights, and metrics.
- Strategist synthesis: An Architect persona processes all department sections and produces a 2-3 sentence executive summary plus 3-5 cross-cutting action items.
- Persistence: The brief is saved to ObjectStore as
executive_briefkind and aforge.brief.generatedevent is emitted. - Retrieval:
latest_brief()returns the most recent brief, sorted bycreated_at.
Department-to-Persona Mapping
| Department(s) | Persona Used |
|---|---|
| code | CodeWriter |
| content, growth, distro, gtm | ContentWriter |
| harvest, finance, legal | Researcher |
| support | Documenter |
| forge, infra, product | Architect |
Pipeline Orchestration
The Forge engine supports multi-step pipeline orchestration triggered by webhook events:
- Webhook registration with
event_kind = "forge.pipeline.requested"enqueuesJobKind::Custom("forge.pipeline"). - The job worker in
rusvel-appcallsForgeEngine::orchestrate_pipeline(). - Pipeline steps emit
forge.pipeline.step_startedandforge.pipeline.step_completedevents. - The full pipeline emits
forge.pipeline.startedandforge.pipeline.completed(orforge.pipeline.failed).
Object Store Kinds
| Kind | Schema | Used By |
|---|---|---|
goal | Goal { id, session_id, title, description, timeframe, status, progress, metadata } | set_goal(), list_goals() |
task | Task { id, session_id, goal_id, title, status, due_at, priority, metadata } | mission_today() |
executive_brief | ExecutiveBriefStored { session_id, brief: ExecutiveBrief } | generate_brief(), latest_brief() |
flow_execution | Pipeline orchestration state | Pipeline module |
UI Integration
The manifest declares a dashboard card and 7 tabs:
- Dashboard card: “Mission Status” (large) – Active goals, today’s plan, recent reviews
- Tabs: actions, engine, agents, workflows, skills, rules, events
Testing
cargo test -p forge-engine # 15 tests (all use mock ports)
Key test scenarios:
- Engine metadata (kind, name, capabilities)
- Engine lifecycle (initialize, health, shutdown)
- Set and list goals (persistence + event emission)
- Mission today generates plan (LLM mock returns JSON, plan parsed)
- Default personas (10 built-in, lookup by name)
- Hire persona creates AgentConfig (session binding, tool list)
- Safety guard initialization (budget check, circuit check)
Code Department
Code intelligence, implementation, debugging, testing, refactoring
Overview
The Code Department provides AI-powered code intelligence for RUSVEL. It parses Rust source files, builds a symbol dependency graph, computes project metrics (file counts, symbol counts, largest function), and exposes BM25-ranked symbol search. It is the department a developer uses to understand a codebase before making changes, and it produces code.analyzed events that the Content department consumes to auto-generate technical blog posts from code snapshots.
Engine (code-engine)
- Crate:
crates/code-engine/src/lib.rs - Lines: 307 (lib.rs) + submodules (parser, graph, metrics, search, error)
- Status: Wired (real business logic)
Public API
| Method | Signature | Description |
|---|---|---|
new | fn new(storage: Arc<dyn StoragePort>, event_port: Arc<dyn EventPort>) -> Self | Construct with storage and event port |
analyze | async fn analyze(&self, repo_path: &Path) -> Result<CodeAnalysis> | Parse directory, build symbol graph, compute metrics, build search index, persist to ObjectStore, emit code.analyzed event |
search | fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchResult>> | BM25-ranked search over previously indexed symbols; returns error if analyze() not yet called |
Internal Structure
parser(parser.rs) – Rust source file parser that extractsSymbolstructs (functions, structs, enums, traits, impls) with name, kind, file path, line number, and body text.graph(graph.rs) –SymbolGraphbuilt from parsed symbols representing call/dependency relationships.metrics(metrics.rs) –count_lines()per file andcompute_project_metrics()aggregate: total files, total symbols, largest function.search(search.rs) –SearchIndexwith BM25 ranking. Built from(name, file_path, body, line)tuples.error(error.rs) – Engine-specific error types.
Data Types
#![allow(unused)]
fn main() {
pub struct CodeAnalysis {
pub snapshot: CodeSnapshotRef, // ID + repo path + analyzed_at timestamp
pub symbols: Vec<Symbol>, // All parsed symbols
pub graph: SymbolGraph, // Dependency graph
pub metrics: ProjectMetrics, // Aggregate metrics
}
}
CodeAnalysis::summary() produces a CodeAnalysisSummary (snapshot_id, repo_path, total_files, total_symbols, top 10 function names, largest function) that the Content engine uses for code-to-content generation.
Department Wrapper (dept-code)
- Crate:
crates/dept-code/src/lib.rs - Lines: 154
- Manifest:
crates/dept-code/src/manifest.rs
The wrapper creates a CodeEngine during registration with StoragePort and EventPort, then registers 2 agent tools. No event handlers or job handlers are registered.
Manifest Declaration
System Prompt
You are the Code department of RUSVEL.
You have full access to Claude Code tools:
- Read, Write, Edit files across all project directories
- Run shell commands (build, test, git, pnpm, cargo, etc.)
- Search codebases with grep and glob
- Fetch web content and search the web
- Spawn sub-agents for parallel work
- Manage background tasks
Focus: code intelligence, implementation, debugging, testing, refactoring. When writing code, follow existing patterns. Be thorough.
Capabilities
code_analysistool_use
Quick Actions
| Label | Prompt |
|---|---|
| Analyze codebase | Analyze the codebase structure, dependencies, and code quality. |
| Run tests | Run cargo test and report results. If any fail, show the errors. |
| Find TODOs | Find all TODO, FIXME, and HACK comments across the codebase. |
| Self-improve | Read docs/status/current-state.md and docs/status/gap-analysis.md. Identify the highest-impact fix you can make right now. Implement it, run tests, and verify. |
| Fix build warnings | Run cargo build and fix any warnings. Then run cargo test to verify nothing broke. |
Registered Tools
| Tool Name | Parameters | Description |
|---|---|---|
code.analyze | path: string (required) | Analyze a codebase directory for symbols, metrics, and dependencies |
code.search | query: string (required), limit: integer (default: 10) | Search previously indexed code symbols |
Personas
| Name | Role | Default Model | Allowed Tools | Purpose |
|---|---|---|---|---|
| code-engineer | Senior software engineer with code intelligence | sonnet | code.analyze, code.search, file_read, file_write, shell | Full-stack code work with engine tools |
Skills
| Name | Description | Template |
|---|---|---|
| Code Review | Analyze code quality and suggest improvements | Analyze the code at: {{path}}. Focus on: code quality, patterns, potential bugs, and improvements. |
Rules
No rules are declared for the Code department.
Jobs
| Job Kind | Description | Requires Approval |
|---|---|---|
code.analyze | Run code analysis on a directory | No |
Events
Produced
| Event Kind | When Emitted |
|---|---|
code.analyzed | analyze() completes successfully. Payload includes snapshot_id, total_symbols, total_files. |
code.searched | A symbol search is executed (declared in manifest). |
Consumed
The Code department does not consume events from other departments.
API Routes
| Method | Path | Description |
|---|---|---|
| POST | /api/dept/code/analyze | Analyze a codebase directory. Body: {"path": "..."}. Returns full CodeAnalysis JSON. |
| GET | /api/dept/code/search | Search indexed symbols. Query params: query, limit. Returns ranked search results. |
CLI Commands
rusvel code analyze [path] # Analyze a codebase (default: current directory)
rusvel code search <query> # Search indexed symbols
Entity Auto-Discovery
Agents, skills, rules, hooks, and MCP servers scoped to the Code department are stored with metadata.engine = "code". The shared CRUD API routes filter by this key so each department sees only its own entities.
Chat Flow
sequenceDiagram
participant User
participant API as rusvel-api
participant Chat as Chat Handler
participant Config as ConfigPort
participant Skills as Skill Resolver
participant Rules as Rule Loader
participant Agent as AgentRuntime
participant Code as CodeEngine
participant Events as EventPort
participant Hooks as Hook Dispatcher
User->>API: POST /api/dept/code/chat {message}
API->>Chat: route to department chat handler
Chat->>Config: load department config (model, effort, add_dirs)
Chat->>Skills: resolve_skill() -- check for {{input}} interpolation
Chat->>Chat: check agent override (custom agent for code)
Chat->>Rules: load_rules_for_engine("code") -- append to system prompt
Note over Chat: Inject capabilities: code_analysis, tool_use
Note over Chat: System prompt: "You are the Code department..."
Note over Chat: Default config: effort=high, add_dirs=["."]
Chat->>Agent: AgentRuntime::run_streaming(config, tools, prompt)
Agent->>Code: tool calls (code.analyze, code.search)
Code->>Events: emit code.analyzed
Agent-->>Chat: SSE stream (AgentEvent chunks)
Chat-->>User: SSE response
Chat->>Events: emit chat completion event
Chat->>Hooks: tokio::spawn hook dispatch
Extending This Department
1. Add a new tool
Register the tool in crates/dept-code/src/lib.rs inside the register() method using ctx.tools.add("code", "code.new_tool", ...). Add a matching ToolContribution entry in crates/dept-code/src/manifest.rs in the tools vec.
2. Add a new event kind
Add a new pub const in the events module inside crates/code-engine/src/lib.rs. Emit it from the engine method. Add the event kind string to events_produced in crates/dept-code/src/manifest.rs.
3. Add a new persona
Add a PersonaContribution entry in the personas vec in crates/dept-code/src/manifest.rs.
4. Add a new skill
Add a SkillContribution entry in the skills vec in crates/dept-code/src/manifest.rs. Use {{variable}} placeholders for runtime interpolation.
5. Add a new API route
Add a RouteContribution entry in the routes vec in crates/dept-code/src/manifest.rs. Implement the handler in crates/rusvel-api/src/engine_routes.rs and wire the route in crates/rusvel-api/src/lib.rs.
Port Dependencies
| Port | Required | Purpose |
|---|---|---|
| StoragePort | Yes | Persist code analysis results via ObjectStore |
| EventPort | Yes | Emit code.analyzed and code.searched events |
Default Configuration
The Code department ships with a non-default LayeredConfig:
effort:"high"– maximizes analysis thoroughnesspermission_mode:"default"add_dirs:["."]– adds the current directory to the agent’s working set
Object Store Kinds
| Kind | Schema | Used By |
|---|---|---|
code_analysis | CodeAnalysis { snapshot, symbols, graph, metrics } | analyze() persists, Content department reads for code-to-content |
Cross-Department Integration
The Code department is a producer in the code-to-content pipeline:
-
Code -> Content: When
analyze()completes, it emitscode.analyzedwithsnapshot_id,total_symbols, andtotal_filesin the payload. The Content department’s event handler receives this and callsdraft_blog_from_code_snapshot()to generate a technical blog post. -
Code -> Forge: The Forge department can use the CodeWriter persona to analyze code during executive brief generation for the code department section.
This is achieved entirely through the event system – the Code engine never imports the Content engine.
Parser Details
The parser module handles Rust source file parsing:
- Scans
.rsfiles in the given directory (recursive) - Extracts symbols: functions (
fn), structs, enums, traits, impl blocks - Each
Symbolincludes: name,SymbolKind, file_path, line number, body text - Used by
SymbolGraph::build()to construct a dependency graph - Used by
SearchIndex::build()to create BM25-searchable index
Symbol Kinds
Function, Struct, Enum, Trait, Impl
Search Index
The BM25 search index is built from tuples of (name, file_path, body, line):
- Index: Built once per
analyze()call, stored in-memory behind aMutex - Query:
search(query, limit)returns rankedSearchResultentries - Error: Returns
RusvelError::Internalif called beforeanalyze()
#![allow(unused)]
fn main() {
pub struct SearchResult {
pub symbol_name: String,
pub file_path: String,
pub line: usize,
pub score: f64,
}
}
Metrics
The metrics module computes:
- Per-file:
count_lines()returnsFileMetrics(total lines, code lines, comment lines, blank lines) - Project-wide:
compute_project_metrics()aggregates across all files:
#![allow(unused)]
fn main() {
pub struct ProjectMetrics {
pub total_files: usize,
pub total_symbols: usize,
pub largest_function: Option<String>,
// ... additional fields
}
}
UI Integration
The manifest declares a dashboard card and 10 tabs:
- Dashboard card: “Code Intelligence” (medium) – Symbol index, metrics, and search
- Tabs: actions, engine, agents, workflows, skills, rules, mcp, hooks, dirs, events
- has_settings: true (supports department-level settings)
Testing
cargo test -p code-engine # Tests in lib.rs
Key test scenarios:
- Analyze a temp directory with Rust files, verify symbol count
- Search after analysis, verify results
- Health returns healthy (with and without index)
- Event emission on analyze
cargo test -p dept-code # Department wrapper tests
Key test scenarios:
- Department creates with correct manifest ID
- Manifest declares 2 routes, 2 tools, 2 events
- Manifest requires StoragePort and EventPort
- Manifest serializes to valid JSON
Content Department
Content creation, platform adaptation, publishing strategy
Overview
The Content Department handles the full content lifecycle for RUSVEL: AI-powered drafting, multi-platform adaptation (LinkedIn, Twitter/X, DEV.to, Substack), calendar scheduling, human-approved publishing, and engagement analytics. It consumes code.analyzed events from the Code department to automatically generate technical blog posts from code snapshots, creating a seamless code-to-content pipeline. Every piece of content must pass a human approval gate (ADR-008) before it can be published.
Engine (content-engine)
- Crate:
crates/content-engine/src/lib.rs - Lines: 424 (lib.rs) + submodules (writer, calendar, analytics, adapters, platform, code_bridge)
- Status: Wired (real business logic)
Public API
| Method | Signature | Description |
|---|---|---|
new | fn new(storage, event_bus, agent, jobs) -> Self | Construct with 4 port dependencies |
register_platform | fn register_platform(&self, adapter: Arc<dyn PlatformAdapter>) | Register a platform adapter for publishing |
draft | async fn draft(&self, session_id, topic, kind: ContentKind) -> Result<ContentItem> | Draft new content via AI agent |
adapt | async fn adapt(&self, session_id, content_id, platform: Platform) -> Result<ContentItem> | Adapt existing content for a target platform |
publish | async fn publish(&self, session_id, content_id, platform) -> Result<PublishResult> | Publish approved content to a platform (requires Approved status) |
schedule | async fn schedule(&self, session_id, content_id, platform, at: DateTime) -> Result<()> | Schedule content for future publication |
schedule_draft | async fn schedule_draft(&self, session_id, draft_id, platform, publish_at) -> Result<()> | Alias for schedule (matches product spec wording) |
approve_content | async fn approve_content(&self, content_id) -> Result<ContentItem> | Mark content as human-approved (ADR-008 gate) |
draft_blog_from_code_snapshot | async fn draft_blog_from_code_snapshot(&self, session_id, snapshot_id) -> Result<ContentItem> | Draft a blog post from a stored code_analysis snapshot |
execute_content_publish_job | async fn execute_content_publish_job(&self, job: Job) -> Result<Value> | Execute a queued ContentPublish job (payload: content_id, platform) |
list_content | async fn list_content(&self, session_id, status_filter: Option<ContentStatus>) -> Result<Vec<ContentItem>> | List content items, optionally filtered by status |
list_scheduled | async fn list_scheduled(&self, session_id) -> Result<Vec<ScheduledPost>> | List all scheduled posts (content calendar) |
list_scheduled_in_range | async fn list_scheduled_in_range(&self, session_id, from, to) -> Result<Vec<ScheduledPost>> | List scheduled posts within a date range |
get_metrics | async fn get_metrics(&self, content_id) -> Result<Vec<(Platform, PostMetrics)>> | Get engagement metrics for a content item |
Internal Structure
ContentWriter(writer.rs) – AI-powered drafting and adaptation. UsesAgentPortto generate content.build_code_prompt()builds prompts fromCodeAnalysisSummaryfor code-to-content.ContentCalendar(calendar.rs) – Scheduling engine. UsesStoragePortfor persistence andJobPortfor scheduling future publish jobs.ContentAnalytics(analytics.rs) – Engagement metrics tracking per content item per platform.PlatformAdaptertrait (platform.rs) – Interface for platform-specific publishing. ReturnsPublishResultwith URL and timestamp. Hasmax_length()for character-limited platforms.- Platform adapters (
adapters/) – Real implementations for LinkedIn, Twitter, and DEV.to. Each reads API credentials fromConfigPort. code_bridge(code_bridge.rs) – Converts storedcode_analysisJSON toCodeAnalysisSummaryfor the writer.
Content Kinds
LongForm, Tweet, Thread, LinkedInPost, Blog, VideoScript, Email, Proposal
Content Statuses
Draft, Adapted, Approved, Published, Cancelled
Supported Platforms
Twitter, LinkedIn, DevTo, Medium, YouTube, Substack, Email, Custom(String)
Department Wrapper (dept-content)
- Crate:
crates/dept-content/src/lib.rs - Lines: 131
- Manifest:
crates/dept-content/src/manifest.rs
The wrapper creates a ContentEngine, registers 3 real platform adapters (LinkedIn, Twitter, DEV.to), registers 2 agent tools, wires an event handler for code.analyzed, and registers a job handler for content.publish.
Registration Details
- Platform adapters: LinkedInAdapter, TwitterAdapter, DevToAdapter (all from content_engine::adapters)
- Tools: content.draft, content.adapt (via tools::register_tools)
- Event handler: "code.analyzed" -> triggers code-to-content pipeline
- Job handler: "content.publish" -> executes content_engine.execute_content_publish_job
Manifest Declaration
System Prompt
You are the Content department of RUSVEL.
Focus: content creation, platform adaptation, publishing strategy. Draft in Markdown. Adapt for LinkedIn, Twitter/X, DEV.to, Substack.
Capabilities
content_creation
Quick Actions
| Label | Prompt |
|---|---|
| Draft blog post | Draft a blog post. Ask me for the topic, audience, and key points. |
| Adapt for Twitter | Adapt the latest content piece into a Twitter/X thread. |
| Content calendar | Show the content calendar for this week with scheduled and draft posts. |
Registered Tools
| Tool Name | Parameters | Description |
|---|---|---|
content.draft | session_id: string (required), topic: string (required), kind: string (enum: LongForm, Tweet, Thread, LinkedInPost, Blog, VideoScript, Email, Proposal) | Draft a blog post or article on a given topic |
content.adapt | session_id: string (required), content_id: string (required), platform: string (enum: twitter, linkedin, devto, medium, youtube, substack, email) | Adapt existing content for a specific platform |
Personas
| Name | Role | Default Model | Allowed Tools | Purpose |
|---|---|---|---|---|
| content-strategist | Content strategist and writer | sonnet | content.draft, content.adapt, web_search | Content planning and execution |
Skills
| Name | Description | Template |
|---|---|---|
| Blog Draft | Draft a blog post from topic and key points | Write a blog post about: {{topic}}. Key points: {{points}}. Audience: {{audience}} |
Rules
| Name | Content | Enabled |
|---|---|---|
| Human Approval Gate | All content must be approved before publishing. Never auto-publish. | Yes |
Jobs
| Job Kind | Description | Requires Approval |
|---|---|---|
content.publish | Publish approved content to target platforms | Yes |
Events
Produced
| Event Kind | When Emitted |
|---|---|
content.drafted | draft() creates a new content item |
content.adapted | adapt() creates a platform-adapted version |
content.scheduled | schedule() schedules content for future publication. Payload includes platform and publish_at. |
content.published | publish() successfully publishes to a platform |
content.reviewed | Content is reviewed (feedback cycle) |
content.cancelled | Content is cancelled |
content.metrics_recorded | Engagement metrics are recorded for a content item |
Consumed
| Event Kind | Source | Action |
|---|---|---|
code.analyzed | Code department | Triggers draft_blog_from_code_snapshot() to auto-generate a blog post from the code analysis |
API Routes
| Method | Path | Description |
|---|---|---|
| POST | /api/dept/content/draft | Draft content from a topic |
| POST | /api/dept/content/from-code | Generate content from a code analysis snapshot |
| PATCH | /api/dept/content/{id}/approve | Approve content for publishing (ADR-008 gate) |
| POST | /api/dept/content/publish | Publish approved content to a platform |
| GET | /api/dept/content/list | List all content items |
CLI Commands
rusvel content draft <topic> # Draft content from a topic
rusvel content from-code # Generate content from code analysis
Entity Auto-Discovery
Agents, skills, rules, hooks, and MCP servers scoped to the Content department are stored with metadata.engine = "content". The shared CRUD API routes filter by this key so each department sees only its own entities.
Chat Flow
sequenceDiagram
participant User
participant API as rusvel-api
participant Chat as Chat Handler
participant Config as ConfigPort
participant Skills as Skill Resolver
participant Rules as Rule Loader
participant Agent as AgentRuntime
participant Content as ContentEngine
participant Events as EventPort
participant Hooks as Hook Dispatcher
User->>API: POST /api/dept/content/chat {message}
API->>Chat: route to department chat handler
Chat->>Config: load department config (model, platform tokens)
Chat->>Skills: resolve_skill() -- check for {{input}} interpolation
Chat->>Chat: check agent override (custom agent for content)
Chat->>Rules: load_rules_for_engine("content") -- append "Human Approval Gate" rule
Note over Chat: Inject capabilities: content_creation
Note over Chat: System prompt: "You are the Content department..."
Chat->>Agent: AgentRuntime::run_streaming(config, tools, prompt)
Agent->>Content: tool calls (content.draft, content.adapt)
Content->>Events: emit content.drafted / content.adapted
Agent-->>Chat: SSE stream (AgentEvent chunks)
Chat-->>User: SSE response
Chat->>Events: emit chat completion event
Chat->>Hooks: tokio::spawn hook dispatch
Code-to-Content Pipeline
sequenceDiagram
participant Code as CodeEngine
participant Events as EventPort
participant ContentHandler as on_code_analyzed handler
participant Content as ContentEngine
Code->>Events: emit code.analyzed {snapshot_id}
Events->>ContentHandler: dispatch event
ContentHandler->>Content: draft_blog_from_code_snapshot(session_id, snapshot_id)
Content->>Content: load code_analysis from ObjectStore
Content->>Content: build CodeAnalysisSummary
Content->>Content: draft(session_id, code_prompt, Blog)
Content->>Events: emit content.drafted
Extending This Department
1. Add a new tool
Register the tool in crates/dept-content/src/tools.rs inside register_tools(). Add a matching ToolContribution entry in crates/dept-content/src/manifest.rs.
2. Add a new event kind
Add a new pub const in the events module inside crates/content-engine/src/lib.rs. Emit it from the engine method. Add the event kind string to events_produced in crates/dept-content/src/manifest.rs.
3. Add a new persona
Add a PersonaContribution entry in the personas vec in crates/dept-content/src/manifest.rs.
4. Add a new skill
Add a SkillContribution entry in the skills vec in crates/dept-content/src/manifest.rs.
5. Add a new API route
Add a RouteContribution entry in the routes vec in crates/dept-content/src/manifest.rs. Implement the handler in crates/rusvel-api/src/engine_routes.rs and wire the route in crates/rusvel-api/src/lib.rs.
6. Add a new platform adapter
Implement the PlatformAdapter trait in a new file under crates/content-engine/src/adapters/. Register it in crates/dept-content/src/lib.rs inside register() with engine.register_platform(Arc::new(NewAdapter::new(ctx.config.clone()))).
Port Dependencies
| Port | Required | Purpose |
|---|---|---|
| AgentPort | Yes | AI-powered content drafting and adaptation via ContentWriter |
| EventPort | Yes | Emit content.* events and receive code.analyzed |
| StoragePort | Yes | Persist content items, scheduled posts, analytics via ObjectStore |
| JobPort | Yes | Enqueue and schedule content.publish jobs |
| ConfigPort | No (optional) | Platform API credentials (devto_api_key, twitter_bearer_token, linkedin_bearer_token) |
Object Store Kinds
| Kind | Schema | Used By |
|---|---|---|
content | ContentItem { id, session_id, title, body_markdown, status, approval, platform_targets, published_at, metadata } | draft(), adapt(), publish(), approve_content(), list_content() |
scheduled_post | ScheduledPost { content_id, platform, publish_at, session_id } | schedule(), list_scheduled() |
content_metrics | Platform engagement metrics per content item | get_metrics() |
Platform Adapter Details
PlatformAdapter Trait
#![allow(unused)]
fn main() {
pub trait PlatformAdapter: Send + Sync + Debug {
fn platform(&self) -> Platform;
fn max_length(&self) -> Option<usize>;
async fn publish(&self, item: &ContentItem) -> Result<PublishResult>;
}
}
Registered Adapters
| Platform | Adapter | Config Keys | Max Length |
|---|---|---|---|
LinkedInAdapter | linkedin_bearer_token | None | |
| Twitter/X | TwitterAdapter | twitter_bearer_token | 280 chars |
| DEV.to | DevToAdapter | devto_api_key | None |
PublishResult
#![allow(unused)]
fn main() {
pub struct PublishResult {
pub url: Option<String>,
pub published_at: DateTime<Utc>,
pub metadata: Value,
}
}
Content Approval Flow (ADR-008)
Content must be explicitly approved before it can be published:
stateDiagram-v2
[*] --> Draft: draft()
Draft --> Adapted: adapt()
Draft --> Approved: approve_content()
Adapted --> Approved: approve_content()
Approved --> Published: publish()
Draft --> Cancelled: cancel
Adapted --> Cancelled: cancel
Attempting to call publish() on content that is not Approved or AutoApproved returns RusvelError::Validation.
Content Calendar
The ContentCalendar manages future publication scheduling:
schedule()creates aScheduledPostand enqueues acontent.publishjob with a scheduled timelist_scheduled()returns all scheduled posts for a sessionlist_scheduled_in_range()filters by date range (inclusive)- Jobs are processed by the job worker, which calls
execute_content_publish_job()
ContentWriter AI Integration
The ContentWriter uses AgentPort for two operations:
- Draft: Given a topic and content kind, generates markdown content via an AI agent
- Adapt: Given existing content and a target platform, rewrites to fit the platform’s format and length constraints
For code-to-content, build_code_prompt() constructs a rich prompt from CodeAnalysisSummary including repository path, symbol counts, top functions, and the largest function name.
UI Integration
The manifest declares a dashboard card and 6 tabs:
- Dashboard card: “Content Pipeline” (medium) – Drafts, scheduled, and published content
- Tabs: actions, engine, agents, skills, rules, events
- has_settings: true (supports platform credential configuration)
Configuration Schema
{
"devto_api_key": "string",
"twitter_bearer_token": "string",
"linkedin_bearer_token": "string",
"default_format": "markdown | html"
}
Testing
cargo test -p content-engine # 7 tests
Key test scenarios:
- Draft creates content item and emits event
- Adapt creates platform-adapted version
- Publish requires approval status
- Calendar scheduling
- Code-to-content pipeline integration
cargo test -p dept-content # Department wrapper tests
Key test scenarios:
- Department creates with correct manifest ID
- Manifest declares 5 routes, 2 tools, 7 events
- Manifest requires AgentPort (non-optional)
- Manifest serializes to valid JSON
Harvest Department
Opportunity discovery, scoring, proposal generation, pipeline management
Overview
The Harvest Department is RUSVEL’s freelance opportunity engine. It scans sources (Upwork, LinkedIn, GitHub, RSS feeds), scores discovered opportunities using keyword matching or LLM-based evaluation, generates tailored proposals via an AI agent, and manages a Kanban pipeline (Cold -> Warm -> Hot -> Won/Lost). It also records deal outcomes (won/lost/withdrawn) that feed back into the scoring model, and supports optional RAG via embedding and vector store for similarity-based scoring hints. Browser-captured data from CDP sessions (Upwork job listings, client profiles) is normalized into opportunities and CRM contacts.
Engine (harvest-engine)
- Crate:
crates/harvest-engine/src/lib.rs - Lines: 1013 (lib.rs) + submodules (source, scorer, proposal, pipeline, events, outcomes, cdp_source)
- Status: Wired (real business logic)
Public API
| Method | Signature | Description |
|---|---|---|
new | fn new(storage: Arc<dyn StoragePort>) -> Self | Construct with storage (minimal; other ports added via builder) |
with_browser | fn with_browser(self, b: Arc<dyn BrowserPort>) -> Self | Attach browser port for CDP capture |
with_events | fn with_events(self, events: Arc<dyn EventPort>) -> Self | Attach event port |
with_agent | fn with_agent(self, agent: Arc<dyn AgentPort>) -> Self | Attach agent port for LLM scoring/proposals |
with_config | fn with_config(self, config: HarvestConfig) -> Self | Set skills and min_budget filter |
configure_rag | fn configure_rag(&self, embedding, vector_store) | Wire embedding + vector store for outcome-based scoring hints |
harvest_skills | fn harvest_skills(&self) -> &[String] | Skills used for scoring and RSS query expansion |
scan | async fn scan(&self, session_id, source: &dyn HarvestSource) -> Result<Vec<Opportunity>> | Scan a source, score results, persist, return opportunities |
score_opportunity | async fn score_opportunity(&self, session_id, opportunity_id) -> Result<OpportunityScoreUpdate> | Re-score an existing opportunity (keyword or LLM path) |
generate_proposal | async fn generate_proposal(&self, session_id, opportunity_id, profile) -> Result<Proposal> | Generate and persist a proposal for a stored opportunity |
get_proposals | async fn get_proposals(&self, session_id) -> Result<Vec<Proposal>> | List persisted proposals for a session |
pipeline | async fn pipeline(&self, session_id) -> Result<PipelineStats> | Get pipeline statistics (total, by_stage counts) |
list_opportunities | async fn list_opportunities(&self, session_id, stage: Option<&OpportunityStage>) -> Result<Vec<Opportunity>> | List opportunities, optionally filtered by stage |
advance_opportunity | async fn advance_opportunity(&self, opportunity_id, new_stage) -> Result<()> | Move an opportunity to a new pipeline stage |
record_opportunity_outcome | async fn record_opportunity_outcome(&self, session_id, opportunity_id, result, notes) -> Result<HarvestOutcomeRecord> | Record won/lost/withdrawn, feed into scorer hints, index in vector store |
list_harvest_outcomes | async fn list_harvest_outcomes(&self, session_id, limit) -> Result<Vec<HarvestOutcomeRecord>> | List recorded outcomes (newest first) |
on_data_captured | async fn on_data_captured(&self, session_id, event: BrowserEvent) -> Result<()> | Normalize CDP-captured browser data into opportunities/contacts |
Internal Structure
HarvestSourcetrait (source.rs) – Interface for scanning opportunity sources. ReturnsVec<RawOpportunity>. IncludesMockSourcefor testing.CdpSource(cdp_source.rs) – CDP/browser-based source for Upwork. Includes default JavaScript extraction script.OpportunityScorer(scorer.rs) – Scores raw opportunities. Two methods:ScoringMethod::Keyword(fast, pattern-based) andScoringMethod::Llm(uses AgentPort for AI evaluation). Accepts outcome hints for calibration.ProposalGenerator(proposal.rs) – Generates proposals using AgentPort. ProducesProposalwith body, estimated value, tone, and metadata.Pipeline(pipeline.rs) – Kanban pipeline management via ObjectStore.add(),list(),advance(),stats().outcomes(outcomes.rs) – Deal outcome recording and retrieval.record_outcome(),list_outcomes(),recent_outcome_prompt_lines()for LLM hints.events(events.rs) – Event kind constants.
Data Types
#![allow(unused)]
fn main() {
pub struct HarvestConfig {
pub skills: Vec<String>, // Skills to match (default: ["rust"])
pub min_budget: Option<f64>, // Minimum budget filter
}
pub struct OpportunityScoreUpdate {
pub score: f64,
pub reasoning: String,
}
pub enum HarvestDealOutcome { Won, Lost, Withdrawn }
pub struct HarvestOutcomeRecord {
pub id: String,
pub session_id: SessionId,
pub opportunity_snapshot: Value,
pub result: HarvestDealOutcome,
pub notes: String,
}
}
RAG Integration
When configure_rag() is called with both an EmbeddingPort and VectorStorePort:
- Scoring hints: Before scoring, the engine embeds the opportunity title+description, searches the vector store for similar past outcomes, and includes the top 5 matches as prompt hints.
- Outcome indexing: When
record_opportunity_outcome()is called, the outcome is embedded and upserted into the vector store withkind: "harvest_outcome"metadata.
Department Wrapper (dept-harvest)
- Crate:
crates/dept-harvest/src/lib.rs - Lines: 99
- Manifest:
crates/dept-harvest/src/manifest.rs
The wrapper creates a HarvestEngine with builder pattern (with_events, with_agent), calls configure_rag() to wire optional embedding and vector store from the registration context, and persists the engine reference.
Manifest Declaration
System Prompt
You are the Harvest department of RUSVEL.
Focus: finding opportunities, scoring gigs, drafting proposals. Sources: Upwork, LinkedIn, GitHub.
Capabilities
opportunity_discovery
Quick Actions
| Label | Prompt |
|---|---|
| Scan opportunities | Scan for new freelance opportunities on Upwork, LinkedIn, and GitHub. |
| Score pipeline | Score all opportunities in the pipeline by fit, budget, and probability. |
| Draft proposal | Draft a proposal for an opportunity. Ask me for the gig details. |
Registered Tools
| Tool Name | Parameters | Description |
|---|---|---|
harvest.scan | session_id: string (required), source: string (optional) | Scan sources for freelance opportunities |
harvest.score | session_id: string (required), opportunity_id: string (required) | Re-score an opportunity |
harvest.proposal | session_id: string (required), opportunity_id: string (required), profile: string (optional) | Generate a proposal for an opportunity |
Personas
| Name | Role | Default Model | Allowed Tools | Purpose |
|---|---|---|---|---|
| opportunity-hunter | Freelance opportunity scout and proposal writer | sonnet | harvest.scan, harvest.score, harvest.proposal, web_search | Discovery and proposal generation |
Skills
| Name | Description | Template |
|---|---|---|
| Proposal Draft | Draft a winning proposal for a freelance opportunity | Draft a proposal for this opportunity: Title: {{title}}. Description: {{description}}. Highlight relevant skills and past experience. |
Rules
| Name | Content | Enabled |
|---|---|---|
| Human Approval Gate | All proposals must be reviewed before submission. Never auto-submit. | Yes |
Jobs
| Job Kind | Description | Requires Approval |
|---|---|---|
harvest.scan | Scan opportunity sources | No |
Events
Produced
| Event Kind | When Emitted |
|---|---|
harvest.scan.started | scan() begins scanning a source. Payload: {source: name}. |
harvest.scan.completed | scan() finishes. Payload: {count: N}. |
harvest.opportunity.discovered | Each opportunity found during scan. Payload: {id, title}. |
harvest.opportunity.scored | score_opportunity() completes re-scoring. Payload: {id, score, reasoning}. |
harvest.proposal.generated | generate_proposal() produces a proposal. Payload: {opportunity_id}. |
harvest.proposal.persisted | Proposal is saved to ObjectStore. Payload: {key, opportunity_id}. |
harvest.pipeline.advanced | advance_opportunity() moves an opportunity to a new stage. |
harvest.outcome.recorded | record_opportunity_outcome() records a deal result. Payload: {outcome_id, opportunity_id, result}. |
harvest.contact.captured | on_data_captured() ingests a client profile from browser. Payload: {contact_id, name}. |
Consumed
The Harvest department does not consume events from other departments. It is triggered by direct API/CLI calls and browser capture events.
API Routes
| Method | Path | Description |
|---|---|---|
| POST | /api/dept/harvest/scan | Scan sources for new opportunities |
| POST | /api/dept/harvest/score | Re-score an existing opportunity |
| POST | /api/dept/harvest/proposal | Generate a proposal for an opportunity |
| GET | /api/dept/harvest/pipeline | Get pipeline statistics (total, by_stage) |
| GET | /api/dept/harvest/list | List opportunities (optional stage filter) |
CLI Commands
rusvel harvest pipeline # Show pipeline statistics
rusvel harvest status # Department status
rusvel harvest list # List items
rusvel harvest events # Show recent events
Entity Auto-Discovery
Agents, skills, rules, hooks, and MCP servers scoped to the Harvest department are stored with metadata.engine = "harvest". The shared CRUD API routes filter by this key so each department sees only its own entities.
Chat Flow
sequenceDiagram
participant User
participant API as rusvel-api
participant Chat as Chat Handler
participant Config as ConfigPort
participant Skills as Skill Resolver
participant Rules as Rule Loader
participant Agent as AgentRuntime
participant Harvest as HarvestEngine
participant Events as EventPort
participant VectorStore as VectorStorePort
participant Hooks as Hook Dispatcher
User->>API: POST /api/dept/harvest/chat {message}
API->>Chat: route to department chat handler
Chat->>Config: load department config (skills, min_budget)
Chat->>Skills: resolve_skill() -- check for {{input}} interpolation
Chat->>Chat: check agent override (custom agent for harvest)
Chat->>Rules: load_rules_for_engine("harvest") -- append "Human Approval Gate" rule
Note over Chat: Inject capabilities: opportunity_discovery
Note over Chat: System prompt: "You are the Harvest department..."
Chat->>Agent: AgentRuntime::run_streaming(config, tools, prompt)
Agent->>Harvest: tool calls (harvest.scan, harvest.score, harvest.proposal)
Harvest->>VectorStore: similarity search for outcome hints (if RAG configured)
Harvest->>Events: emit harvest.* events
Agent-->>Chat: SSE stream (AgentEvent chunks)
Chat-->>User: SSE response
Chat->>Events: emit chat completion event
Chat->>Hooks: tokio::spawn hook dispatch
Extending This Department
1. Add a new tool
Register the tool in crates/dept-harvest/src/lib.rs inside the register() method using ctx.tools.add("harvest", "harvest.new_tool", ...). Add a matching ToolContribution entry in crates/dept-harvest/src/manifest.rs.
2. Add a new event kind
Add a new pub const in crates/harvest-engine/src/events.rs. Emit it from the engine method. Add the event kind string to events_produced in crates/dept-harvest/src/manifest.rs.
3. Add a new persona
Add a PersonaContribution entry in the personas vec in crates/dept-harvest/src/manifest.rs.
4. Add a new skill
Add a SkillContribution entry in the skills vec in crates/dept-harvest/src/manifest.rs.
5. Add a new API route
Add a RouteContribution entry in the routes vec in crates/dept-harvest/src/manifest.rs. Implement the handler in crates/rusvel-api/src/engine_routes.rs and wire the route in crates/rusvel-api/src/lib.rs.
6. Add a new harvest source
Implement the HarvestSource trait in a new file under crates/harvest-engine/src/. Return Vec<RawOpportunity> from scan(). The engine’s scan() method will automatically score and persist results.
Port Dependencies
| Port | Required | Purpose |
|---|---|---|
| StoragePort | Yes | Persist opportunities, proposals, outcomes, contacts via ObjectStore |
| EventPort | Yes | Emit harvest.* domain events |
| AgentPort | Yes | LLM-based scoring and proposal generation |
| ConfigPort | No (optional) | Skills list, min_budget, source configuration |
| BrowserPort | No (optional) | CDP-captured browser data ingestion (Upwork) |
| EmbeddingPort | No (optional) | Text embedding for RAG-based outcome hints |
| VectorStorePort | No (optional) | Vector search for similar past outcomes |
Configuration
{
"skills": ["rust", "axum", "tokio"],
"min_budget": 1000.0
}
skills– Skills to match when scoring opportunities and expanding RSS queries. Default:["rust"].min_budget– Minimum budget filter. Opportunities below this are scored lower.
Object Store Kinds
| Kind | Schema | Used By |
|---|---|---|
opportunity | Opportunity { id, session_id, source, title, url, description, score, stage, value_estimate, metadata } | scan(), score_opportunity(), list_opportunities(), advance_opportunity() |
proposal | StoredProposalRecord { session_id, opportunity_id, proposal } | generate_proposal(), get_proposals() |
harvest_outcome | HarvestOutcomeRecord { id, session_id, opportunity_snapshot, result, notes } | record_opportunity_outcome(), list_harvest_outcomes() |
contact | Contact { id, session_id, name, emails, links, company, ... } | on_data_captured() (Upwork client profiles) |
Scoring System
The OpportunityScorer supports two scoring methods:
Keyword Scoring (ScoringMethod::Keyword)
Fast, pattern-based scoring used when no AgentPort is configured:
- Matches opportunity title and description against configured skills
- Applies budget multiplier based on
min_budgetthreshold - Returns a normalized score (0.0 to 1.0)
LLM Scoring (ScoringMethod::Llm)
AI-powered scoring used when AgentPort is available:
- Sends opportunity details plus configured skills to the agent
- Includes outcome hints (recent outcomes + vector similarity matches) for calibration
- Agent returns a score and reasoning
Outcome-Based Calibration
When RAG is configured, scoring includes:
- Recent outcomes: The last 12
HarvestOutcomeRecordentries for the session, formatted as prompt lines - Vector similarity: The opportunity title+description is embedded and searched against indexed outcomes; top 5 matches with similarity scores are included
This creates a feedback loop: won/lost outcomes improve future scoring accuracy.
Pipeline Stages
Opportunities flow through a Kanban pipeline:
Cold -> Warm -> Hot -> Won
-> Lost
-> Withdrawn
Cold: Newly discovered, not yet evaluatedWarm: Scored positively, under considerationHot: Actively pursuing, proposal sentWon: Deal closed successfullyLost: Opportunity not wonWithdrawn: Withdrawn from consideration
advance_opportunity() moves an opportunity to any valid stage and emits harvest.pipeline.advanced.
Browser Data Capture (CDP)
The on_data_captured() method handles BrowserEvent::DataCaptured payloads from CDP sessions:
Upwork Job Listings (kind: "job_listing")
- Accepts single job or
{jobs: [...]}array - Extracts: title, description, url, budget, skills, posted_at
- Runs through the scorer
- Creates
Opportunityin Cold stage - Emits
harvest.opportunity.discovered
Upwork Client Profiles (kind: "client_profile")
- Extracts: name, profile_url, company
- Creates
Contactin ObjectStore - Emits
harvest.contact.captured
CDP Extract JavaScript
DEFAULT_CDP_EXTRACT_JS provides a default JavaScript snippet for extracting job listing cards from Upwork pages via Chrome DevTools Protocol.
UI Integration
The manifest declares a dashboard card and 6 tabs:
- Dashboard card: “Opportunity Pipeline” (medium) – Cold, warm, and hot opportunities
- Tabs: actions, engine, agents, skills, rules, events
- has_settings: true (supports skills and budget configuration)
Testing
cargo test -p harvest-engine # 12 tests
Key test scenarios:
- Scan with mock source returns 3 opportunities
- Pipeline stores and lists correctly
- Pipeline stats match (all Cold after scan)
- Health returns healthy
- Proposals respect session filter
- Outcome recording and retrieval
cargo test -p dept-harvest # Department wrapper tests
Key test scenarios:
- Department creates with correct manifest ID
- Manifest declares 5 routes, 3 tools, 3 events
- Manifest requires StoragePort, EventPort, AgentPort (non-optional) + ConfigPort (optional)
- Manifest serializes to valid JSON
GoToMarket Department
CRM, outreach sequences, deal management, invoicing
Overview
The GoToMarket (GTM) Department handles the full sales pipeline for RUSVEL: contact management (CRM), multi-step outreach sequences with human approval gates, deal tracking through pipeline stages (Lead -> Qualified -> Proposal -> Negotiation -> Won/Lost), and invoicing with line items and payment tracking. Outreach sequences draft emails via the AI agent, hold them for human approval (ADR-008), then send via SMTP and auto-schedule the next step in the sequence. This is the department a solo builder uses to manage client relationships from first contact through paid invoice.
Engine (gtm-engine)
- Crate:
crates/gtm-engine/src/lib.rs - Lines: 710 (lib.rs) + submodules (crm, outreach, invoice, email)
- Status: Wired (real business logic)
Public API
| Method | Signature | Description |
|---|---|---|
new | fn new(storage, events, agent, jobs) -> Self | Construct with 4 port dependencies; creates CRM, Outreach, and Invoice managers |
crm | fn crm(&self) -> &CrmManager | Access the CRM sub-manager |
outreach | fn outreach(&self) -> &OutreachManager | Access the outreach sub-manager |
invoices | fn invoices(&self) -> &InvoiceManager | Access the invoice sub-manager |
emit_event | async fn emit_event(&self, kind, payload) -> Result<EventId> | Emit a domain event on the event bus |
process_outreach_send_job | async fn process_outreach_send_job(&self, job, email: &dyn EmailAdapter) -> Result<OutreachSendDispatch> | Process one OutreachSend job (draft -> approval -> SMTP send -> schedule next step) |
CrmManager API
| Method | Signature | Description |
|---|---|---|
add_contact | async fn add_contact(&self, session_id, contact: Contact) -> Result<ContactId> | Add a new contact to the CRM |
get_contact | async fn get_contact(&self, id: &ContactId) -> Result<Contact> | Retrieve a contact by ID |
list_contacts | async fn list_contacts(&self, session_id: SessionId) -> Result<Vec<Contact>> | List all contacts for a session |
add_deal | async fn add_deal(&self, session_id, deal: Deal) -> Result<DealId> | Add a new deal to the pipeline |
list_deals | async fn list_deals(&self, session_id, stage: Option<DealStage>) -> Result<Vec<Deal>> | List deals, optionally filtered by stage |
advance_deal | async fn advance_deal(&self, deal_id, new_stage: DealStage) -> Result<()> | Move a deal to a new pipeline stage |
OutreachManager API
| Method | Signature | Description |
|---|---|---|
create_sequence | async fn create_sequence(&self, session_id, name, steps: Vec<SequenceStep>) -> Result<SequenceId> | Create a multi-step outreach sequence |
list_sequences | async fn list_sequences(&self, session_id) -> Result<Vec<OutreachSequence>> | List all sequences for a session |
activate_sequence | async fn activate_sequence(&self, seq_id: &SequenceId) -> Result<()> | Activate a sequence (must be Active to execute) |
execute_sequence | async fn execute_sequence(&self, session_id, seq_id, contact_id) -> Result<JobId> | Enqueue the first step of a sequence as an OutreachSend job |
process_outreach_send_job | async fn process_outreach_send_job(&self, job, events, email) -> Result<OutreachSendDispatch> | Execute one step: draft email, hold for approval or send + schedule next |
InvoiceManager API
| Method | Signature | Description |
|---|---|---|
create_invoice | async fn create_invoice(&self, session_id, contact_id, items, due_date) -> Result<InvoiceId> | Create an invoice with line items |
mark_paid | async fn mark_paid(&self, invoice_id: &InvoiceId) -> Result<()> | Mark an invoice as paid |
total_revenue | async fn total_revenue(&self, session_id) -> Result<f64> | Sum of all paid invoices for a session |
Internal Structure
CrmManager(crm.rs) – Contact and deal CRUD. Deals have stages: Lead, Qualified, Proposal, Negotiation, Won, Lost.OutreachManager(outreach.rs) – Multi-step sequence management. Steps have delay_days, channel, and template. Sequences must be activated before execution. Execution enqueuesJobKind::OutreachSendjobs.InvoiceManager(invoice.rs) – Invoice creation withLineItem(description, quantity, unit_price). Tracks status: Draft, Sent, Paid, Overdue.EmailAdaptertrait (email.rs) – Interface for sending emails. Implementations:SmtpEmailAdapter(usesRUSVEL_SMTP_*env vars) andMockEmailAdapter(for testing).OutreachSendDispatch– Result enum from processing an outreach job:HoldForApproval(result)(first pass, draft held),Complete { result, next }(approved, sent, next step scheduled if any).
Data Types
#![allow(unused)]
fn main() {
pub struct Deal {
pub id: DealId,
pub session_id: SessionId,
pub contact_id: ContactId,
pub title: String,
pub value: f64,
pub stage: DealStage, // Lead | Qualified | Proposal | Negotiation | Won | Lost
pub notes: String,
pub created_at: DateTime<Utc>,
pub metadata: Value,
}
pub struct OutreachSequence {
pub id: SequenceId,
pub session_id: SessionId,
pub name: String,
pub steps: Vec<SequenceStep>,
pub status: SequenceStatus, // Draft | Active | Paused | Completed
pub metadata: Value,
}
pub struct SequenceStep {
pub delay_days: u32,
pub channel: String, // "email", "linkedin", etc.
pub template: String,
}
pub struct Invoice {
pub id: InvoiceId,
pub session_id: SessionId,
pub contact_id: ContactId,
pub items: Vec<LineItem>,
pub status: InvoiceStatus, // Draft | Sent | Paid | Overdue
pub total: f64,
pub due_date: DateTime<Utc>,
pub metadata: Value,
}
}
Department Wrapper (dept-gtm)
- Crate:
crates/dept-gtm/src/lib.rs - Lines: 95
- Manifest:
crates/dept-gtm/src/manifest.rs - Tools:
crates/dept-gtm/src/tools.rs
The wrapper creates a GtmEngine with all 4 ports during registration and registers 5 agent tools via tools::register().
Manifest Declaration
System Prompt
You are the GoToMarket department of RUSVEL.
Focus: CRM, outreach sequences, deal management, invoicing.
Capabilities
outreachcrminvoicing
Quick Actions
| Label | Prompt |
|---|---|
| List contacts | List all contacts in the CRM. Show name, company, status, and last interaction. |
| Draft outreach | Draft a multi-step outreach sequence for a prospect. |
| Deal pipeline | Show the current deal pipeline with stages, values, and next actions. |
| Generate invoice | Generate an invoice. Ask me for client details and line items. |
Registered Tools
| Tool Name | Parameters | Description |
|---|---|---|
gtm.crm.add_contact | session_id: string, name: string, email: string (required); company: string, metadata: object (optional) | Add a new contact to the CRM |
gtm.crm.list_contacts | session_id: string (required) | List all contacts in the CRM for a session |
gtm.crm.add_deal | session_id: string, contact_id: string, title: string, value: number, stage: string (required) | Add a new deal to the CRM pipeline |
gtm.outreach.create_sequence | session_id: string, name: string, steps: array (required) | Create a multi-step outreach sequence |
gtm.invoices.create_invoice | session_id: string, client_name: string, line_items: array, due_date: string (required) | Create an invoice with line items |
Personas
No personas are declared in the GTM manifest. The department uses the default agent configuration.
Skills
No skills are declared in the GTM manifest.
Rules
No rules are declared in the GTM manifest. Outreach approval is enforced at the job processing level (ADR-008 via OutreachSendDispatch::HoldForApproval).
Jobs
No jobs are declared in the GTM manifest. JobKind::OutreachSend is processed by the job worker in rusvel-app which calls GtmEngine::process_outreach_send_job().
Events
Produced
| Event Kind | When Emitted |
|---|---|
gtm.outreach.sent | An outreach email is sent after approval |
gtm.email.sent | An email is sent via the email adapter |
gtm.deal.updated | A deal is advanced to a new stage |
gtm.contact.added | A new contact is added to the CRM |
gtm.invoice.created | A new invoice is created |
gtm.invoice.paid | An invoice is marked as paid |
gtm.sequence.created | A new outreach sequence is created |
Consumed
The GTM department does not consume events from other departments.
API Routes
The GTM manifest declares no route contributions. GTM functionality is accessed through:
- Agent tools: The 5 registered tools are available during chat sessions
- Generic department routes:
/api/dept/gtm/status,/api/dept/gtm/list,/api/dept/gtm/events - Job worker:
JobKind::OutreachSendprocessed in therusvel-appjob worker
CLI Commands
rusvel gtm status # Department status
rusvel gtm list # List items
rusvel gtm events # Show recent events
Outreach Approval Flow
The GTM outreach system enforces human approval (ADR-008) through a two-pass job processing model:
sequenceDiagram
participant User
participant Outreach as OutreachManager
participant Jobs as JobPort
participant Worker as Job Worker
participant GTM as GtmEngine
participant Email as EmailAdapter
participant Events as EventPort
User->>Outreach: execute_sequence(session, seq_id, contact_id)
Outreach->>Jobs: enqueue OutreachSend job (step 0)
Note over Worker: Pass 1: Draft
Worker->>GTM: process_outreach_send_job(job, email)
GTM->>GTM: draft email via AgentPort
GTM-->>Worker: HoldForApproval(result)
Worker->>Jobs: hold_for_approval(job_id, result)
Note over User: User reviews and approves
User->>Jobs: approve(job_id)
Note over Worker: Pass 2: Send
Worker->>GTM: process_outreach_send_job(job_with_approval, email)
GTM->>Email: send(to, subject, body)
GTM->>Events: emit gtm.email.sent
GTM->>Events: emit gtm.outreach.sent
GTM-->>Worker: Complete { result, next: Some(step 1 job) }
Worker->>Jobs: schedule next step (delay_days offset)
Entity Auto-Discovery
Agents, skills, rules, hooks, and MCP servers scoped to the GTM department are stored with metadata.engine = "gtm". The shared CRUD API routes filter by this key so each department sees only its own entities.
Chat Flow
sequenceDiagram
participant User
participant API as rusvel-api
participant Chat as Chat Handler
participant Config as ConfigPort
participant Skills as Skill Resolver
participant Rules as Rule Loader
participant Agent as AgentRuntime
participant GTM as GtmEngine
participant Events as EventPort
participant Hooks as Hook Dispatcher
User->>API: POST /api/dept/gtm/chat {message}
API->>Chat: route to department chat handler
Chat->>Config: load department config
Chat->>Skills: resolve_skill() -- check for {{input}} interpolation
Chat->>Chat: check agent override (custom agent for gtm)
Chat->>Rules: load_rules_for_engine("gtm") -- append to system prompt
Note over Chat: Inject capabilities: outreach, crm, invoicing
Note over Chat: System prompt: "You are the GoToMarket department..."
Chat->>Agent: AgentRuntime::run_streaming(config, tools, prompt)
Agent->>GTM: tool calls (gtm.crm.add_contact, gtm.outreach.create_sequence, etc.)
GTM->>Events: emit gtm.* events
Agent-->>Chat: SSE stream (AgentEvent chunks)
Chat-->>User: SSE response
Chat->>Events: emit chat completion event
Chat->>Hooks: tokio::spawn hook dispatch
Extending This Department
1. Add a new tool
Register the tool in crates/dept-gtm/src/tools.rs inside register(). Add the tool ID to the GTM_TOOL_IDS constant. No manifest ToolContribution is needed since the GTM manifest currently declares an empty tools vec (tools are registered dynamically).
2. Add a new event kind
Add a new pub const in the events module inside crates/gtm-engine/src/lib.rs. Emit it from the engine method. Optionally add it to events_produced in crates/dept-gtm/src/manifest.rs.
3. Add a new persona
Add a PersonaContribution entry in the personas vec in crates/dept-gtm/src/manifest.rs.
4. Add a new skill
Add a SkillContribution entry in the skills vec in crates/dept-gtm/src/manifest.rs.
5. Add a new API route
Add a RouteContribution entry in the routes vec in crates/dept-gtm/src/manifest.rs. Implement the handler in crates/rusvel-api/src/engine_routes.rs and wire the route in crates/rusvel-api/src/lib.rs.
6. Add a new email adapter
Implement the EmailAdapter trait in crates/gtm-engine/src/email.rs. The trait requires a single send() method that takes an EmailMessage and returns Result<()>.
Port Dependencies
| Port | Required | Purpose |
|---|---|---|
| StoragePort | Yes | Persist contacts, deals, sequences, invoices via ObjectStore |
| EventPort | Yes | Emit gtm.* domain events |
| AgentPort | Yes | AI-powered email drafting in outreach sequences |
| JobPort | Yes | Enqueue and process OutreachSend jobs |
Environment Variables
| Variable | Purpose |
|---|---|
RUSVEL_SMTP_HOST | SMTP server host for outreach emails |
RUSVEL_SMTP_PORT | SMTP server port |
RUSVEL_SMTP_USERNAME | SMTP authentication username |
RUSVEL_SMTP_PASSWORD | SMTP authentication password |
RUSVEL_SMTP_FROM | Sender email address |
Finance Department
Revenue tracking, expense management, tax optimization, runway forecasting.
| Field | Value |
|---|---|
| ID | finance |
| Icon | % |
| Color | green |
| Engine crate | finance-engine (~400 lines) |
| Dept crate | dept-finance |
| Status | Skeleton – manager structures with CRUD, minimal business logic |
Overview
The Finance department handles financial operations for a solo SaaS business: ledger transactions (income/expenses), tax estimation, and runway forecasting. The engine provides three manager subsystems, each backed by ObjectStore for persistence.
Current Status: Skeleton
The finance engine has manager structures but minimal business logic (~400 lines total across lib.rs and three manager modules). The managers provide CRUD operations:
- LedgerManager –
record(),balance(),list_transactions(). Records income and expense transactions, computes net balance. - TaxManager –
add_estimate(),total_liability(),list_estimates(). Stores per-category tax estimates and sums liability (estimates minus deductions). - RunwayManager –
calculate(),list_snapshots(). Computes runway from cash and burn rate, persists snapshots.
The department is fully registered and bootable – it appears in the department registry, responds to chat, and has 4 agent tools wired. However, it needs the following to be production-ready:
- P&L report generation (aggregate by category/period)
- Recurring transaction support
- Multi-currency handling
- Budget alerts and threshold notifications
- Integration with real accounting data sources
- Scheduled runway recalculation via job queue
Engine Details
Crate: finance-engine (~400 lines)
Struct: FinanceEngine
Constructor:
#![allow(unused)]
fn main() {
FinanceEngine::new(
storage: Arc<dyn StoragePort>,
events: Arc<dyn EventPort>,
agent: Arc<dyn AgentPort>,
jobs: Arc<dyn JobPort>,
)
}
Managers:
| Manager | Methods | Description |
|---|---|---|
LedgerManager | record(sid, kind, amount, description, category), balance(sid), list_transactions(sid) | Income/expense tracking |
TaxManager | add_estimate(sid, category, amount, period), total_liability(sid), list_estimates(sid) | Tax estimation by category |
RunwayManager | calculate(sid, cash, burn_rate), list_snapshots(sid) | Runway forecasting |
Implements: rusvel_core::engine::Engine trait (kind: "finance", name: "Finance Engine")
Manifest
Declared in dept-finance/src/manifest.rs:
id: "finance"
name: "Finance Department"
description: "Revenue tracking, expense management, tax optimization, runway forecasting, P&L reports, unit economics"
icon: "%"
color: "green"
capabilities: ["ledger", "tax", "runway"]
System Prompt
You are the Finance department of RUSVEL.
Focus: revenue tracking, expense management, tax optimization, runway forecasting, P&L reports, unit economics.
Tools
Tools registered at runtime via dept-finance/src/lib.rs (4 tools):
| Tool | Parameters | Description |
|---|---|---|
finance.ledger.record | session_id, kind (Income/Expense), amount, description, category | Record a transaction |
finance.ledger.balance | session_id | Get current balance (income minus expenses) |
finance.tax.add_estimate | session_id, category (Income/SelfEmployment/Sales/Deduction), amount, period | Add a tax estimate |
finance.tax.total_liability | session_id | Calculate total tax liability |
Personas
None declared in the manifest. The department uses the default system prompt for chat interactions.
Skills
None declared in the manifest.
Rules
None declared in the manifest.
Jobs
None declared in the manifest. Future candidates:
- Scheduled runway recalculation
- Periodic P&L report generation
- Tax deadline reminders
Events
Defined Constants (engine)
| Constant | Value |
|---|---|
INCOME_RECORDED | finance.income.recorded |
EXPENSE_RECORDED | finance.expense.recorded |
RUNWAY_CALCULATED | finance.runway.calculated |
TAX_ESTIMATED | finance.tax.estimated |
These constants are defined in the engine but are not yet emitted automatically by the manager methods (emitting requires calling engine.emit_event() explicitly). They are also not listed in the manifest’s events_produced.
Consumed
None.
API Routes
None declared in the manifest. The department is accessible via the standard parameterized routes:
GET /api/dept/finance/status– department statusPOST /api/dept/finance/chat– SSE chat
CLI Commands
Standard department CLI:
rusvel finance status # One-shot status
rusvel finance list # List items
rusvel finance events # Show events
Entity Auto-Discovery
The standard CRUD subsystems are available at /api/dept/finance/*:
- Agents, Skills, Rules, Hooks, Workflows, MCP Servers
Chat Flow
sequenceDiagram
participant U as User
participant API as /api/dept/finance/chat
participant Agent as AgentRuntime
participant Tool as finance.* tools
participant Engine as FinanceEngine
participant Store as StoragePort
U->>API: POST "Record $5000 income from consulting"
API->>Agent: run_streaming(system_prompt, tools)
Agent->>Tool: finance.ledger.record(kind=Income, amount=5000, ...)
Tool->>Engine: ledger().record(sid, Income, 5000, ...)
Engine->>Store: objects.put("transactions", id, value)
Store-->>Engine: Ok
Engine-->>Tool: transaction_id
Tool-->>Agent: "Transaction recorded: {id}"
Agent->>Tool: finance.ledger.balance(session_id)
Tool->>Engine: ledger().balance(sid)
Engine->>Store: objects.list("transactions")
Store-->>Engine: Vec<Transaction>
Engine-->>Tool: balance
Tool-->>Agent: "5000.00"
Agent-->>API: SSE chunks
API-->>U: "Recorded $5000 income. Current balance: $5000.00"
Extending the Department
Adding P&L reports
- Add a
PnlManagertofinance-enginewithgenerate_report(sid, period)method - Register a
finance.pnl.reporttool indept-finance/src/lib.rs - Add a quick action to the manifest for “P&L report”
- Optionally add a scheduled job kind for periodic report generation
Adding event emission
The event constants are already defined. To wire them:
- Call
self.emit_event(events::INCOME_RECORDED, payload)insideLedgerManager::record()when the transaction kind isIncome - Add the event kinds to the manifest’s
events_producedvector - Other departments can then subscribe via
events_consumed
Adding API routes
- Add route contributions to the manifest in
dept-finance/src/manifest.rs - Implement handler functions in
rusvel-api/src/engine_routes.rs - Wire the routes in
rusvel-api/src/lib.rs
Port Dependencies
| Port | Required | Usage |
|---|---|---|
StoragePort | Yes | Transactions, tax estimates, runway snapshots (via ObjectStore) |
EventPort | Yes | Domain event emission (constants defined, not yet auto-emitted) |
AgentPort | Yes | LLM-powered financial analysis via chat |
JobPort | Yes | Future: scheduled reports and calculations |
Product Department
Product roadmaps, feature prioritization, pricing strategy, user feedback analysis.
| Field | Value |
|---|---|
| ID | product |
| Icon | @ |
| Color | rose |
| Engine crate | product-engine (~388 lines) |
| Dept crate | dept-product |
| Status | Skeleton – manager structures with CRUD, minimal business logic |
Overview
The Product department manages the product lifecycle: roadmap features with priorities and milestones, pricing tier definitions, and user feedback collection. The engine provides three manager subsystems backed by ObjectStore.
Current Status: Skeleton
The product engine has manager structures but minimal business logic (~388 lines total across lib.rs and three manager modules). The managers provide CRUD operations:
- RoadmapManager –
add_feature(sid, title, description, priority, milestone),list_features(sid),mark_feature_done(sid, feature_id). Tracks features withPriority(Critical/High/Medium/Low) andFeatureStatus(Planned/InProgress/Done/Cancelled). - PricingManager –
create_tier(sid, name, price, annual_price, features),list_tiers(sid),update_tier(sid, tier_id, updates). Manages pricing tiers with monthly/annual pricing and feature lists. - FeedbackManager –
add_feedback(sid, source, kind, content),list_feedback(sid),analyze_feedback(sid). Collects feedback categorized byFeedbackKind(FeatureRequest/Bug/Praise/Complaint).
The department is fully registered and bootable – it appears in the department registry, responds to chat, and has 4 agent tools wired. However, it needs the following to be production-ready:
- Feature dependency tracking and critical path analysis
- Milestone timeline visualization data
- Pricing A/B test support
- Feedback sentiment analysis via AgentPort
- Feature voting and prioritization scoring
- Integration with issue trackers (GitHub, Linear)
Engine Details
Crate: product-engine (~388 lines)
Struct: ProductEngine
Constructor:
#![allow(unused)]
fn main() {
ProductEngine::new(
storage: Arc<dyn StoragePort>,
events: Arc<dyn EventPort>,
agent: Arc<dyn AgentPort>,
jobs: Arc<dyn JobPort>,
)
}
Managers:
| Manager | Methods | Description |
|---|---|---|
RoadmapManager | add_feature(), list_features(), mark_feature_done() | Feature tracking with priority and status |
PricingManager | create_tier(), list_tiers(), update_tier() | Pricing tier management |
FeedbackManager | add_feedback(), list_feedback(), analyze_feedback() | User feedback collection |
Implements: rusvel_core::engine::Engine trait (kind: "product", name: "Product Engine")
Manifest
Declared in dept-product/src/manifest.rs:
id: "product"
name: "Product Department"
description: "Product roadmaps, feature prioritization, pricing strategy, user feedback analysis, A/B testing"
icon: "@"
color: "rose"
capabilities: ["roadmap", "pricing", "feedback"]
System Prompt
You are the Product department of RUSVEL.
Focus: product roadmaps, feature prioritization, pricing strategy, user feedback analysis, A/B testing.
Tools
Tools registered at runtime via dept-product/src/lib.rs (4 tools):
| Tool | Parameters | Description |
|---|---|---|
product.roadmap.add_feature | session_id, title, description, priority (Critical/High/Medium/Low), status (optional milestone) | Add a feature to the roadmap |
product.roadmap.list_features | session_id, status (optional filter: Planned/InProgress/Done/Cancelled) | List roadmap features |
product.pricing.create_tier | session_id, name, price, features (array) | Create a pricing tier |
product.feedback.record | session_id, source, sentiment (FeatureRequest/Bug/Praise/Complaint), content | Record user feedback |
Personas
None declared in the manifest.
Skills
None declared in the manifest.
Rules
None declared in the manifest.
Jobs
None declared in the manifest. Future candidates:
- Periodic feedback analysis aggregation
- Roadmap progress reports
- Pricing optimization suggestions
Events
Defined Constants (engine)
| Constant | Value |
|---|---|
FEATURE_CREATED | product.feature.created |
MILESTONE_REACHED | product.milestone.reached |
PRICING_UPDATED | product.pricing.updated |
FEEDBACK_RECEIVED | product.feedback.received |
These constants are defined in the engine but are not yet emitted automatically by the manager methods. They are not listed in the manifest’s events_produced.
Consumed
None.
API Routes
None declared in the manifest. The department is accessible via the standard parameterized routes:
GET /api/dept/product/status– department statusPOST /api/dept/product/chat– SSE chat
CLI Commands
Standard department CLI:
rusvel product status # One-shot status
rusvel product list # List items
rusvel product events # Show events
Entity Auto-Discovery
The standard CRUD subsystems are available at /api/dept/product/*:
- Agents, Skills, Rules, Hooks, Workflows, MCP Servers
Chat Flow
sequenceDiagram
participant U as User
participant API as /api/dept/product/chat
participant Agent as AgentRuntime
participant Tool as product.* tools
participant Engine as ProductEngine
participant Store as StoragePort
U->>API: POST "Add OAuth2 feature, high priority, for v1.0"
API->>Agent: run_streaming(system_prompt, tools)
Agent->>Tool: product.roadmap.add_feature(title="OAuth2", priority="High", status="v1.0")
Tool->>Engine: roadmap().add_feature(sid, "OAuth2", ..., High, Some("v1.0"))
Engine->>Store: objects.put("features", id, value)
Store-->>Engine: Ok
Engine-->>Tool: feature_id
Tool-->>Agent: "Feature created: {id}"
Agent->>Tool: product.roadmap.list_features(session_id)
Tool->>Engine: roadmap().list_features(sid)
Engine->>Store: objects.list("features")
Store-->>Engine: Vec<Feature>
Engine-->>Tool: features JSON
Tool-->>Agent: formatted list
Agent-->>API: SSE chunks
API-->>U: "Added OAuth2 (High) to v1.0 milestone. Roadmap now has N features."
Extending the Department
Adding feature dependency tracking
- Add a
dependencies: Vec<FeatureId>field to theFeaturestruct inproduct-engine/src/roadmap.rs - Add a
critical_path(sid)method toRoadmapManagerthat computes the longest dependency chain - Register a
product.roadmap.critical_pathtool indept-product/src/lib.rs
Adding AI-powered feedback analysis
- Implement
analyze_feedback(sid)inFeedbackManagerto callAgentPortwith all feedback items - The agent can categorize, extract themes, and suggest feature priorities
- Register a
product.feedback.analyzetool
Adding event emission
Wire the defined constants by calling self.emit_event() in the relevant manager methods. Add the event kinds to the manifest’s events_produced vector.
Port Dependencies
| Port | Required | Usage |
|---|---|---|
StoragePort | Yes | Features, pricing tiers, feedback items (via ObjectStore) |
EventPort | Yes | Domain event emission (constants defined, not yet auto-emitted) |
AgentPort | Yes | LLM-powered product analysis via chat, future feedback analysis |
JobPort | Yes | Future: scheduled reports and analysis |
Growth Department
Funnel optimization, conversion tracking, cohort analysis, KPI dashboards.
| Field | Value |
|---|---|
| ID | growth |
| Icon | & |
| Color | orange |
| Engine crate | growth-engine (~375 lines) |
| Dept crate | dept-growth |
| Status | Skeleton – manager structures with CRUD, minimal business logic |
Overview
The Growth department tracks growth metrics for a solo SaaS business: conversion funnels with ordered stages, user cohorts for retention analysis, and KPI time-series recording. The engine provides three manager subsystems backed by ObjectStore.
Current Status: Skeleton
The growth engine has manager structures but minimal business logic (~375 lines total across lib.rs and three manager modules). The managers provide CRUD operations:
- FunnelManager –
add_stage(sid, name, order),list_stages(sid),record_conversion(sid, stage_id, count). Defines ordered funnel stages and records conversion counts per stage. - CohortManager –
create_cohort(sid, name, size),list_cohorts(sid),analyze_retention(sid, cohort_id). Creates named cohorts with initial size for retention tracking. - KpiManager –
record_kpi(sid, name, value, unit),list_kpis(sid),get_kpi_trend(sid, name). Records time-series KPI measurements and computes simple two-point trend (delta between last two readings).
The department is fully registered and bootable – it appears in the department registry, responds to chat, and has 4 agent tools wired. However, it needs the following to be production-ready:
- Funnel drop-off rate calculation and visualization data
- Cohort retention matrix computation
- KPI alerting (threshold-based notifications)
- Churn prediction models via AgentPort
- Historical trend analysis beyond two-point comparison
- Dashboard data aggregation endpoints
Engine Details
Crate: growth-engine (~375 lines)
Struct: GrowthEngine
Constructor:
#![allow(unused)]
fn main() {
GrowthEngine::new(
storage: Arc<dyn StoragePort>,
events: Arc<dyn EventPort>,
agent: Arc<dyn AgentPort>,
jobs: Arc<dyn JobPort>,
)
}
Managers:
| Manager | Methods | Description |
|---|---|---|
FunnelManager | add_stage(), list_stages(), record_conversion() | Conversion funnel tracking |
CohortManager | create_cohort(), list_cohorts(), analyze_retention() | User cohort analysis |
KpiManager | record_kpi(), list_kpis(), get_kpi_trend() | KPI time-series |
Implements: rusvel_core::engine::Engine trait (kind: "growth", name: "Growth Engine")
Manifest
Declared in dept-growth/src/manifest.rs:
id: "growth"
name: "Growth Department"
description: "Funnel optimization, conversion tracking, cohort analysis, churn prediction, retention strategies, KPI dashboards"
icon: "&"
color: "orange"
capabilities: ["funnel", "cohort", "kpi"]
System Prompt
You are the Growth department of RUSVEL.
Focus: funnel optimization, conversion tracking, cohort analysis, churn prediction, retention strategies, KPI dashboards.
Tools
Tools registered at runtime via dept-growth/src/tools.rs (4 tools):
| Tool | Parameters | Description |
|---|---|---|
growth.funnel.add_stage | session_id, name, order | Add a funnel stage for conversion tracking |
growth.cohort.create_cohort | session_id, name, size | Create a user cohort for retention analysis |
growth.kpi.record_kpi | session_id, name, value, unit | Record a KPI measurement |
growth.kpi.get_trend | session_id, kpi_name | Compare last two readings for a named metric (simple trend with delta) |
Personas
None declared in the manifest.
Skills
None declared in the manifest.
Rules
None declared in the manifest.
Jobs
None declared in the manifest. Future candidates:
- Scheduled KPI snapshot collection
- Periodic cohort retention analysis
- Churn detection scans
Events
Defined Constants (engine)
| Constant | Value |
|---|---|
FUNNEL_UPDATED | growth.funnel.updated |
COHORT_ANALYZED | growth.cohort.analyzed |
KPI_RECORDED | growth.kpi.recorded |
CHURN_DETECTED | growth.churn.detected |
These constants are defined in the engine but are not yet emitted automatically by the manager methods. They are not listed in the manifest’s events_produced.
Consumed
None.
API Routes
None declared in the manifest. The department is accessible via the standard parameterized routes:
GET /api/dept/growth/status– department statusPOST /api/dept/growth/chat– SSE chat
CLI Commands
Standard department CLI:
rusvel growth status # One-shot status
rusvel growth list # List items
rusvel growth events # Show events
Entity Auto-Discovery
The standard CRUD subsystems are available at /api/dept/growth/*:
- Agents, Skills, Rules, Hooks, Workflows, MCP Servers
Chat Flow
sequenceDiagram
participant U as User
participant API as /api/dept/growth/chat
participant Agent as AgentRuntime
participant Tool as growth.* tools
participant Engine as GrowthEngine
participant Store as StoragePort
U->>API: POST "Record MRR of $12,000"
API->>Agent: run_streaming(system_prompt, tools)
Agent->>Tool: growth.kpi.record_kpi(name="MRR", value=12000, unit="USD")
Tool->>Engine: kpi().record_kpi(sid, "MRR", 12000.0, "USD")
Engine->>Store: objects.put("kpis", id, value)
Store-->>Engine: Ok
Engine-->>Tool: kpi_id
Tool-->>Agent: {"kpi_id": "..."}
Agent->>Tool: growth.kpi.get_trend(kpi_name="MRR")
Tool->>Engine: kpi().list_kpis(sid)
Engine->>Store: objects.list("kpis")
Store-->>Engine: Vec<KpiEntry>
Tool->>Tool: filter by name, sort, compute delta
Tool-->>Agent: {"previous": 10000, "latest": 12000, "delta": 2000}
Agent-->>API: SSE chunks
API-->>U: "MRR recorded at $12,000. Up $2,000 from last reading."
Extending the Department
Adding funnel drop-off analysis
- Add a
drop_off_rates(sid)method toFunnelManagerthat computes conversion percentages between consecutive stages - Register a
growth.funnel.analyzetool indept-growth/src/tools.rs - Add a quick action to the manifest
Adding churn detection
- Implement a
detect_churn(sid)method inCohortManagerthat identifies cohorts with declining retention - Wire
events::CHURN_DETECTEDemission when thresholds are crossed - Optionally use
AgentPortfor AI-powered churn prediction - Add to the manifest’s
events_produced
Adding dashboard data endpoints
- Add route contributions to the manifest for
/api/dept/growth/dashboard - Implement a handler that aggregates funnel, cohort, and KPI data into a single response
- Wire the route in
rusvel-api
Port Dependencies
| Port | Required | Usage |
|---|---|---|
StoragePort | Yes | Funnel stages, cohorts, KPI entries (via ObjectStore) |
EventPort | Yes | Domain event emission (constants defined, not yet auto-emitted) |
AgentPort | Yes | LLM-powered growth analysis via chat, future churn prediction |
JobPort | Yes | Future: scheduled KPI collection and analysis |
Distribution Department
Marketplace listings, SEO optimization, affiliate programs, partnerships.
| Field | Value |
|---|---|
| ID | distro |
| Icon | ! |
| Color | teal |
| Engine crate | distro-engine (~390 lines) |
| Dept crate | dept-distro |
| Status | Skeleton – manager structures with CRUD, minimal business logic |
Overview
The Distribution department manages distribution channels for a solo SaaS business: marketplace listings across platforms, SEO keyword tracking with position monitoring, and affiliate partner management with commission tracking. The engine provides three manager subsystems backed by ObjectStore.
Current Status: Skeleton
The distro engine has manager structures but minimal business logic (~390 lines total across lib.rs and three manager modules). The managers provide CRUD operations:
- MarketplaceManager –
add_listing(sid, platform, name, url),list_listings(sid),publish_listing(sid, listing_id). Creates listings withListingStatus(Draft/Published/Archived) and tracks revenue per listing. - SeoManager –
add_keyword(sid, keyword, position, volume),list_keywords(sid),track_ranking(sid, keyword_id, new_position). Tracks keyword positions and search volume. - AffiliateManager –
add_partner(sid, name, commission_rate),list_partners(sid),track_commission(sid, partner_id, amount). Manages affiliate partners with commission rates.
The department is fully registered and bootable – it appears in the department registry, responds to chat, and has 4 agent tools wired. However, it needs the following to be production-ready:
- Automated marketplace listing sync with real platforms
- SEO position history and trend analysis
- Affiliate link generation and click tracking
- Revenue attribution across channels
- Automated SEO audit recommendations via AgentPort
- Scheduled ranking checks via job queue
Engine Details
Crate: distro-engine (~390 lines)
Struct: DistroEngine
Constructor:
#![allow(unused)]
fn main() {
DistroEngine::new(
storage: Arc<dyn StoragePort>,
events: Arc<dyn EventPort>,
agent: Arc<dyn AgentPort>,
jobs: Arc<dyn JobPort>,
)
}
Managers:
| Manager | Methods | Description |
|---|---|---|
MarketplaceManager | add_listing(), list_listings(), publish_listing() | Marketplace listing management |
SeoManager | add_keyword(), list_keywords(), track_ranking() | SEO keyword position tracking |
AffiliateManager | add_partner(), list_partners(), track_commission() | Affiliate partner and commission management |
Implements: rusvel_core::engine::Engine trait (kind: "distro", name: "Distribution Engine")
Manifest
Declared in dept-distro/src/manifest.rs:
id: "distro"
name: "Distribution Department"
description: "Marketplace listings, SEO optimization, affiliate programs, partnerships, API distribution channels"
icon: "!"
color: "teal"
capabilities: ["marketplace", "seo", "affiliate"]
System Prompt
You are the Distribution department of RUSVEL.
Focus: marketplace listings, SEO optimization, affiliate programs, partnerships, API distribution channels.
Tools
Tools registered at runtime via dept-distro/src/tools.rs (4 tools):
| Tool | Parameters | Description |
|---|---|---|
distro.seo.analyze | session_id | Summarize tracked SEO keywords (positions, volume, average position) |
distro.marketplace.list | session_id | List all marketplace listings |
distro.affiliate.create_link | session_id, name, commission_rate (0-1) | Register an affiliate partner |
distro.analytics.report | session_id | Aggregate listings, affiliates, and SEO keywords into one JSON report |
Personas
None declared in the manifest.
Skills
None declared in the manifest.
Rules
None declared in the manifest.
Jobs
None declared in the manifest. Future candidates:
- Scheduled SEO position checks
- Marketplace listing status sync
- Affiliate commission payouts
Events
Defined Constants (engine)
| Constant | Value |
|---|---|
LISTING_PUBLISHED | distro.listing.published |
SEO_RANKED | distro.seo.ranked |
AFFILIATE_JOINED | distro.affiliate.joined |
PARTNERSHIP_CREATED | distro.partnership.created |
These constants are defined in the engine but are not yet emitted automatically by the manager methods. They are not listed in the manifest’s events_produced.
Consumed
None.
API Routes
None declared in the manifest. The department is accessible via the standard parameterized routes:
GET /api/dept/distro/status– department statusPOST /api/dept/distro/chat– SSE chat
CLI Commands
Standard department CLI:
rusvel distro status # One-shot status (alias: "distribution")
rusvel distro list # List items
rusvel distro events # Show events
Entity Auto-Discovery
The standard CRUD subsystems are available at /api/dept/distro/*:
- Agents, Skills, Rules, Hooks, Workflows, MCP Servers
Chat Flow
sequenceDiagram
participant U as User
participant API as /api/dept/distro/chat
participant Agent as AgentRuntime
participant Tool as distro.* tools
participant Engine as DistroEngine
participant Store as StoragePort
U->>API: POST "Show me an SEO report"
API->>Agent: run_streaming(system_prompt, tools)
Agent->>Tool: distro.seo.analyze(session_id)
Tool->>Engine: seo().list_keywords(sid)
Engine->>Store: objects.list("keywords")
Store-->>Engine: Vec<Keyword>
Tool->>Tool: compute avg position, aggregate
Tool-->>Agent: {"keyword_count": 5, "avg_position": 12.4, "keywords": [...]}
Agent->>Tool: distro.analytics.report(session_id)
Tool->>Engine: marketplace().list_listings(sid)
Tool->>Engine: affiliate().list_partners(sid)
Tool->>Engine: seo().list_keywords(sid)
Engine-->>Tool: aggregated data
Tool-->>Agent: full analytics report JSON
Agent-->>API: SSE chunks
API-->>U: "SEO Report: 5 keywords tracked, avg position 12.4. ..."
Extending the Department
Adding automated SEO audits
- Add an
audit(sid)method toSeoManagerthat callsAgentPortwith keyword data for AI-powered recommendations - Register a
distro.seo.audittool indept-distro/src/tools.rs - Add a scheduled job kind for periodic audits
- Emit
distro.seo.rankedevents when position changes are detected
Adding marketplace sync
- Implement platform-specific adapters (similar to content-engine’s LinkedIn/Twitter adapters)
- Add a
sync_listing(sid, listing_id)method that fetches live data from the platform - Wire a job kind for periodic sync
Adding revenue attribution
- Extend
MarketplaceManagerto track revenue per listing over time - Extend
AffiliateManagerto compute total commissions earned per partner - Add a
distro.revenue.attributiontool that breaks down revenue by channel - Wire the data into the analytics report tool
Port Dependencies
| Port | Required | Usage |
|---|---|---|
StoragePort | Yes | Listings, keywords, partners (via ObjectStore) |
EventPort | Yes | Domain event emission (constants defined, not yet auto-emitted) |
AgentPort | Yes | LLM-powered distribution analysis via chat, future SEO audits |
JobPort | Yes | Future: scheduled ranking checks and sync |
Legal Department
Contracts, IP protection, terms of service, GDPR compliance, licensing, privacy policies.
| Field | Value |
|---|---|
| ID | legal |
| Icon | § |
| Color | slate |
| Engine crate | legal-engine (~390 lines) |
| Wrapper crate | dept-legal |
| Status | Skeleton |
Overview
The Legal department manages the full contract lifecycle, compliance audits, and intellectual property tracking. It wraps legal-engine via the ADR-014 DepartmentApp pattern and is registered in the composition root alongside the other skeleton departments.
System Prompt
You are the Legal department of RUSVEL.
Focus: contracts, IP protection, terms of service, GDPR compliance, licensing, privacy policies.
Capabilities
| Capability | Description |
|---|---|
contract | Create, list, review, and sign contracts |
compliance | Record and track compliance checks (GDPR, Privacy, Licensing, Tax) |
ip | File and manage intellectual property assets (Patent, Trademark, Copyright, TradeSecret) |
Quick Actions
| Label | Prompt |
|---|---|
| Draft contract | “Draft a contract. Ask me for type, parties, and terms.” |
| Compliance check | “Run a compliance check for GDPR and privacy policy.” |
| IP review | “Review intellectual property assets.” |
Architecture
Engine: legal-engine
Three manager structs compose the engine, each backed by ObjectStore via StoragePort:
| Manager | Domain Type | Object Kind | Methods |
|---|---|---|---|
ContractManager | Contract | legal_contract | create_contract, list_contracts |
ComplianceManager | ComplianceCheck | legal_compliance | add_check, list_checks |
IpManager | IpAsset | legal_ip | file_asset, list_assets |
Domain Types
Contract – ContractId (UUIDv7), ContractStatus enum (Draft, Sent, Signed, Expired, Cancelled), fields: title, counterparty, template, signed_at, expires_at, created_at, metadata.
ComplianceCheck – ComplianceCheckId (UUIDv7), ComplianceArea enum (GDPR, Privacy, Licensing, Tax), fields: description, passed, checked_at, notes, metadata.
IpAsset – IpAssetId (UUIDv7), IpKind enum (Patent, Trademark, Copyright, TradeSecret), fields: name, description, filed_at, status, metadata.
Wrapper: dept-legal
LegalDepartmentstruct withOnceLock<Arc<LegalEngine>>for lazy initializationregister()creates the engine, stores it, and registers agent toolsshutdown()delegates to engine- 2 unit tests (department creation, manifest purity)
Registered Tools
| Tool Name | Description | Parameters |
|---|---|---|
legal.contracts.create | Create a new contract draft | session_id, title, counterparty, template |
legal.contracts.list | List contracts for a session | session_id |
legal.compliance.check | Record a compliance check outcome | session_id, area, description, passed, notes |
legal.ip.register | File an intellectual property asset | session_id, kind, name, description |
Events
| Event Kind | Constant | Description |
|---|---|---|
legal.contract.created | CONTRACT_CREATED | A new contract draft was created |
legal.compliance.checked | COMPLIANCE_CHECKED | A compliance check was recorded |
legal.ip.filed | IP_FILED | An IP asset was filed |
legal.review.completed | REVIEW_COMPLETED | A legal review was completed |
Note: event constants are defined in legal_engine::events but emission is not yet wired into manager methods (skeleton status).
Required Ports
| Port | Optional |
|---|---|
StoragePort | No |
EventPort | No |
AgentPort | No |
JobPort | No |
UI Contribution
Tabs: actions, agents, rules, events
No dashboard cards, settings panel, or custom components.
Chat Flow
sequenceDiagram
participant User
participant Frontend
participant API as /api/dept/legal/chat
participant Agent as AgentPort
participant Engine as LegalEngine
participant Store as ObjectStore
User->>Frontend: "Draft an NDA for Acme Corp"
Frontend->>API: POST (SSE)
API->>Agent: run(system_prompt + user message)
Agent->>Engine: legal.contracts.create(title, counterparty, template)
Engine->>Store: objects().put("legal_contract", ...)
Store-->>Engine: Ok
Engine-->>Agent: { contract_id }
Agent-->>API: SSE token stream
API-->>Frontend: SSE events
Frontend-->>User: "Created contract draft [id]"
CLI Usage
rusvel legal status # Show department status
rusvel legal list # List all legal items
rusvel legal list --kind contract # List contracts only
rusvel legal events # Show recent legal events
Testing
cargo test -p legal-engine # Engine tests (contract CRUD, health check)
cargo test -p dept-legal # Wrapper tests (manifest, department creation)
Current Status: Skeleton
The Legal department is fully registered and bootable within the RUSVEL department registry, but its business logic is minimal. Here is what exists and what remains to be built:
What exists:
- Manager structures with basic CRUD operations (create + list for each domain)
- Domain types with full serialization (Contract, ComplianceCheck, IpAsset)
- 4 agent tools registered in the scoped tool registry
- Event kind constants defined (but not yet emitted from manager methods)
- Engine implements the
Enginetrait with health check - Unit tests for engine and wrapper
What needs to be built for production readiness:
- Wire
emit_event()calls into manager methods so domain events actually fire - Add
review_contract,sign_contractoperations to ContractManager - Add
mark_passedoperation to ComplianceManager - Add
file_ip(full filing workflow with status transitions) to IpManager - Implement AI-assisted contract drafting via AgentPort (template expansion)
- Add job kinds for async legal review workflows (e.g.,
JobKind::Custom("legal.review")) - Build compliance report generation (aggregate check results into a report)
- Add engine-specific API routes (e.g.,
/api/dept/legal/contracts,/api/dept/legal/compliance) - Add engine-specific CLI commands (e.g.,
rusvel legal draft,rusvel legal audit) - Add personas for legal agent specialization
- Add skills and rules for legal workflows
Source Files
| File | Lines | Purpose |
|---|---|---|
crates/legal-engine/src/lib.rs | 390 | Engine struct, capabilities, tests |
crates/legal-engine/src/contract.rs | 112 | Contract domain type + ContractManager |
crates/legal-engine/src/compliance.rs | 108 | ComplianceCheck domain type + ComplianceManager |
crates/legal-engine/src/ip.rs | 107 | IpAsset domain type + IpManager |
crates/dept-legal/src/lib.rs | 89 | DepartmentApp implementation |
crates/dept-legal/src/manifest.rs | 96 | Static manifest definition |
crates/dept-legal/src/tools.rs | 184 | Agent tool registration |
Support Department
Customer support tickets, knowledge base, NPS tracking, auto-triage, customer success.
| Field | Value |
|---|---|
| ID | support |
| Icon | ? |
| Color | yellow |
| Engine crate | support-engine (~391 lines) |
| Wrapper crate | dept-support |
| Status | Skeleton |
Overview
The Support department handles customer-facing operations: ticket management with priority-based triage, a knowledge base for self-service articles, and Net Promoter Score (NPS) tracking for customer satisfaction measurement. It wraps support-engine via the ADR-014 DepartmentApp pattern.
System Prompt
You are the Support department of RUSVEL.
Focus: customer support tickets, knowledge base, NPS tracking, auto-triage, customer success.
Capabilities
| Capability | Description |
|---|---|
ticket | Create, list, resolve, and assign support tickets with priority levels |
knowledge | Manage knowledge base articles (add, list, search) |
nps | Track NPS survey responses and calculate scores |
Quick Actions
| Label | Prompt |
|---|---|
| Open tickets | “Show all open support tickets prioritized by urgency.” |
| Write KB article | “Write a knowledge base article. Ask me for the topic.” |
| NPS survey | “Analyze recent NPS survey results with score breakdown and themes.” |
Architecture
Engine: support-engine
Three manager structs compose the engine, each backed by ObjectStore via StoragePort:
| Manager | Domain Type | Object Kind | Methods |
|---|---|---|---|
TicketManager | Ticket | support_ticket | create_ticket, list_tickets |
KnowledgeManager | Article | support_article | add_article, list_articles |
NpsManager | NpsResponse | support_nps | add_response, list_responses, calculate_nps |
Domain Types
Ticket – TicketId (UUIDv7), TicketStatus enum (Open, InProgress, Resolved, Closed), TicketPriority enum (Low, Medium, High, Urgent), fields: subject, description, requester_email, assignee, created_at, resolved_at, metadata.
Article – ArticleId (UUIDv7), fields: title, content, tags, published, created_at, metadata.
NpsResponse – NpsResponseId (UUIDv7), fields: score (0-10), feedback, respondent, created_at, metadata.
Wrapper: dept-support
SupportDepartmentstruct withOnceLock<Arc<SupportEngine>>for lazy initializationregister()creates the engine, stores it, and registers agent toolsshutdown()delegates to engine- 2 unit tests (department creation, manifest purity)
Registered Tools
| Tool Name | Description | Parameters |
|---|---|---|
support.tickets.create | Create a support ticket | session_id, subject, description, priority, requester_email |
support.tickets.list | List support tickets for a session | session_id |
support.knowledge.search | Search knowledge base articles by keyword | session_id, query |
support.nps.calculate_score | Calculate Net Promoter Score | session_id |
Events
| Event Kind | Constant | Description |
|---|---|---|
support.ticket.created | TICKET_CREATED | A new support ticket was opened |
support.ticket.resolved | TICKET_RESOLVED | A ticket was resolved |
support.article.published | ARTICLE_PUBLISHED | A knowledge base article was published |
support.nps.recorded | NPS_RECORDED | An NPS response was recorded |
Note: event constants are defined in support_engine::events but emission is not yet wired into manager methods (skeleton status).
Required Ports
| Port | Optional |
|---|---|
StoragePort | No |
EventPort | No |
AgentPort | No |
JobPort | No |
UI Contribution
Tabs: actions, agents, skills, rules, events
No dashboard cards, settings panel, or custom components.
Chat Flow
sequenceDiagram
participant User
participant Frontend
participant API as /api/dept/support/chat
participant Agent as AgentPort
participant Engine as SupportEngine
participant Store as ObjectStore
User->>Frontend: "Show open tickets sorted by priority"
Frontend->>API: POST (SSE)
API->>Agent: run(system_prompt + user message)
Agent->>Engine: support.tickets.list(session_id)
Engine->>Store: objects().list("support_ticket", filter)
Store-->>Engine: Vec<Ticket>
Engine-->>Agent: ticket list JSON
Agent-->>API: SSE token stream
API-->>Frontend: SSE events
Frontend-->>User: Formatted ticket list with priorities
CLI Usage
rusvel support status # Show department status
rusvel support list # List all support items
rusvel support list --kind ticket # List tickets only
rusvel support events # Show recent support events
Testing
cargo test -p support-engine # Engine tests (ticket CRUD, health check)
cargo test -p dept-support # Wrapper tests (manifest, department creation)
Current Status: Skeleton
The Support department is fully registered and bootable within the RUSVEL department registry, but its business logic is minimal. Here is what exists and what remains to be built:
What exists:
- Manager structures with basic CRUD operations (create + list for tickets, articles, NPS)
- Domain types with full serialization (Ticket, Article, NpsResponse)
- NPS score calculation logic (promoters minus detractors percentage)
- 4 agent tools registered in the scoped tool registry
- Knowledge base search with keyword filtering (in-memory, case-insensitive)
- Event kind constants defined (but not yet emitted from manager methods)
- Engine implements the
Enginetrait with health check - Unit tests for engine and wrapper
What needs to be built for production readiness:
- Wire
emit_event()calls into manager methods so domain events actually fire - Add
resolve_ticket,assign_ticketoperations to TicketManager - Implement auto-triage: use AgentPort to classify incoming tickets by priority/category
- Add
search_articleswith proper full-text search (currently keyword match only) - Build customer success metrics aggregation (ticket resolution time, first-response time)
- Add job kinds for async support workflows (e.g.,
JobKind::Custom("support.auto_triage")) - Implement AI-assisted KB article generation via AgentPort
- Add engine-specific API routes (e.g.,
/api/dept/support/tickets,/api/dept/support/nps) - Add engine-specific CLI commands (e.g.,
rusvel support triage,rusvel support kb) - Add personas for support agent specialization (triage agent, KB writer)
- Add skills and rules for support workflows
- Build NPS trend analysis and reporting
Source Files
| File | Lines | Purpose |
|---|---|---|
crates/support-engine/src/lib.rs | 391 | Engine struct, capabilities, tests |
crates/support-engine/src/ticket.rs | – | Ticket domain type + TicketManager |
crates/support-engine/src/knowledge.rs | – | Article domain type + KnowledgeManager |
crates/support-engine/src/nps.rs | – | NpsResponse domain type + NpsManager |
crates/dept-support/src/lib.rs | 89 | DepartmentApp implementation |
crates/dept-support/src/manifest.rs | 97 | Static manifest definition |
crates/dept-support/src/tools.rs | 163 | Agent tool registration |
Infra Department
CI/CD pipelines, deployments, monitoring, incident response, performance, cost analysis.
| Field | Value |
|---|---|
| ID | infra |
| Icon | > |
| Color | red |
| Engine crate | infra-engine (~390 lines) |
| Wrapper crate | dept-infra |
| Status | Skeleton |
Overview
The Infra department manages infrastructure operations: deployment tracking, health monitoring, and incident management. It wraps infra-engine via the ADR-014 DepartmentApp pattern and provides tools for recording deployments, running health checks, and managing incidents with severity levels.
System Prompt
You are the Infrastructure department of RUSVEL.
Focus: CI/CD pipelines, deployments, monitoring, incident response, performance, cost analysis.
Capabilities
| Capability | Description |
|---|---|
deploy | Record and track deployments across services and environments |
monitor | Manage health checks with status tracking (Up, Down, Degraded) |
incident | Open, track, and resolve incidents with severity levels (P1-P4) |
Quick Actions
| Label | Prompt |
|---|---|
| Deploy status | “Show current deployment status across all services and environments.” |
| Health check | “Run health checks on all monitored services.” |
| Incident report | “Show open incidents with severity, timeline, and resolution status.” |
Architecture
Engine: infra-engine
Three manager structs compose the engine, each backed by ObjectStore via StoragePort:
| Manager | Domain Type | Object Kind | Methods |
|---|---|---|---|
DeployManager | Deployment | infra_deploy | record_deployment, list_deployments |
MonitorManager | HealthCheck | infra_healthcheck | add_check, list_checks |
IncidentManager | Incident | infra_incident | open_incident, list_incidents |
Domain Types
Deployment – DeploymentId (UUIDv7), DeployStatus enum (Pending, InProgress, Deployed, Failed, RolledBack), fields: service, version, environment, deployed_at, created_at, metadata.
HealthCheck – HealthCheckId (UUIDv7), CheckStatus enum (Up, Down, Degraded), fields: service, endpoint, status, last_checked, response_ms, metadata.
Incident – IncidentId (UUIDv7), IncidentStatus enum (Open, Investigating, Mitigated, Resolved), Severity enum (P1, P2, P3, P4), fields: title, description, severity, status, opened_at, resolved_at, metadata.
Wrapper: dept-infra
InfraDepartmentstruct withOnceLock<Arc<InfraEngine>>for lazy initializationregister()creates the engine, stores it, and registers agent toolsshutdown()delegates to engine- 2 unit tests (department creation, manifest purity)
Registered Tools
| Tool Name | Description | Parameters |
|---|---|---|
infra.deploy.trigger | Record a deployment (starts as Pending) | session_id, service, version, environment |
infra.monitor.status | List health checks with status summary | session_id |
infra.incidents.create | Open an incident | session_id, title, description, severity |
infra.incidents.list | List incidents for a session | session_id |
Events
| Event Kind | Constant | Description |
|---|---|---|
infra.deploy.completed | DEPLOY_COMPLETED | A deployment was completed |
infra.alert.fired | ALERT_FIRED | A monitoring alert was triggered |
infra.incident.opened | INCIDENT_OPENED | A new incident was opened |
infra.incident.resolved | INCIDENT_RESOLVED | An incident was resolved |
Note: event constants are defined in infra_engine::events but emission is not yet wired into manager methods (skeleton status).
Required Ports
| Port | Optional |
|---|---|
StoragePort | No |
EventPort | No |
AgentPort | No |
JobPort | No |
UI Contribution
Tabs: actions, agents, rules, events
No dashboard cards, settings panel, or custom components.
Chat Flow
sequenceDiagram
participant User
participant Frontend
participant API as /api/dept/infra/chat
participant Agent as AgentPort
participant Engine as InfraEngine
participant Store as ObjectStore
User->>Frontend: "Deploy rusvel-api v0.3.0 to production"
Frontend->>API: POST (SSE)
API->>Agent: run(system_prompt + user message)
Agent->>Engine: infra.deploy.trigger(service, version, environment)
Engine->>Store: objects().put("infra_deploy", ...)
Store-->>Engine: Ok
Engine-->>Agent: { deployment_id }
Agent->>Engine: infra.monitor.status(session_id)
Engine->>Store: objects().list("infra_healthcheck", filter)
Store-->>Engine: Vec<HealthCheck>
Engine-->>Agent: health summary JSON
Agent-->>API: SSE token stream
API-->>Frontend: SSE events
Frontend-->>User: "Deployment recorded. All services healthy."
CLI Usage
rusvel infra status # Show department status
rusvel infra list # List all infra items
rusvel infra list --kind deploy # List deployments only
rusvel infra events # Show recent infra events
Testing
cargo test -p infra-engine # Engine tests (deployment CRUD, health check)
cargo test -p dept-infra # Wrapper tests (manifest, department creation)
Current Status: Skeleton
The Infra department is fully registered and bootable within the RUSVEL department registry, but its business logic is minimal. Here is what exists and what remains to be built:
What exists:
- Manager structures with basic CRUD operations (create + list for each domain)
- Domain types with full serialization (Deployment, HealthCheck, Incident)
- Health check status summary tool (counts down/degraded services)
- 4 agent tools registered in the scoped tool registry
- Event kind constants defined (but not yet emitted from manager methods)
- Engine implements the
Enginetrait with health check - Unit tests for engine and wrapper
What needs to be built for production readiness:
- Wire
emit_event()calls into manager methods so domain events actually fire - Add
mark_deployed,rollbackoperations to DeployManager - Add
run_health_check(actual HTTP probe) to MonitorManager - Add
resolve_incident, status transition logic to IncidentManager - Implement deployment pipeline orchestration via AgentPort + JobPort
- Add job kinds for async infra workflows (e.g.,
JobKind::Custom("infra.deploy"),JobKind::Custom("infra.health_scan")) - Build real monitoring: periodic health check scheduling via job queue
- Integrate with
rusvel-deploycrate for actual deployment execution - Add incident timeline tracking (status change history)
- Add engine-specific API routes (e.g.,
/api/dept/infra/deploys,/api/dept/infra/incidents) - Add engine-specific CLI commands (e.g.,
rusvel infra deploy,rusvel infra health) - Add personas for infra agent specialization (SRE agent, incident commander)
- Add skills and rules for incident response runbooks
- Build cost analysis and performance metrics aggregation
Source Files
| File | Lines | Purpose |
|---|---|---|
crates/infra-engine/src/lib.rs | 390 | Engine struct, capabilities, tests |
crates/infra-engine/src/deploy.rs | – | Deployment domain type + DeployManager |
crates/infra-engine/src/monitor.rs | – | HealthCheck domain type + MonitorManager |
crates/infra-engine/src/incident.rs | – | Incident domain type + IncidentManager |
crates/dept-infra/src/lib.rs | 89 | DepartmentApp implementation |
crates/dept-infra/src/manifest.rs | 96 | Static manifest definition |
crates/dept-infra/src/tools.rs | 178 | Agent tool registration |
Flow Department
DAG-based workflow automation – create, execute, and monitor directed acyclic graph workflows.
| Field | Value |
|---|---|
| ID | flow |
| Icon | ~ |
| Color | sky |
| Engine crate | flow-engine (~600 lines) |
| Dept crate | dept-flow |
| Status | Wired – full business logic with DAG execution, checkpointing, and resume |
Overview
The Flow department provides a general-purpose DAG workflow engine built on petgraph. Workflows are defined as directed acyclic graphs of typed nodes (code, condition, agent, browser, parallel) connected by edges. The engine supports:
- Topological execution with parallel branch support
- Condition-based branching (true/false output ports)
- Agent nodes that delegate to
AgentPortfor LLM-driven steps - Browser trigger and action nodes (CDP integration)
- Parallel evaluate nodes for multi-agent comparison
- Checkpoint persistence for resume after failure
- Single-node retry from checkpoint state
Engine Details
Crate: flow-engine (~600 lines in lib.rs, plus submodules for executor, expression, nodes, templates)
Struct: FlowEngine
Constructor:
#![allow(unused)]
fn main() {
FlowEngine::new(
storage: Arc<dyn StoragePort>,
events: Arc<dyn EventPort>,
agent: Arc<dyn AgentPort>,
terminal: Option<Arc<dyn TerminalPort>>,
browser: Option<Arc<dyn BrowserPort>>,
)
}
Public methods:
| Method | Description |
|---|---|
node_types() | List registered node type names |
save_flow(flow) | Persist a FlowDef to storage |
get_flow(id) | Retrieve a flow definition by ID |
list_flows() | List all saved flow definitions |
delete_flow(id) | Remove a flow definition |
run_flow(id, trigger_data) | Execute a flow, return FlowExecution |
resume_flow(execution_id) | Resume from checkpoint (same execution ID) |
retry_node(execution_id, node_id) | Re-run a single node using checkpoint state |
get_checkpoint(execution_id) | Load the current checkpoint for an execution |
get_execution(id) | Get an execution record by ID |
list_executions(flow_id) | List all executions for a flow |
Storage keys: flows, flow_executions, flow_checkpoints
Node Types
| Type | Module | Description |
|---|---|---|
code | nodes::code | Returns a literal value from parameters |
condition | nodes::condition | Branches on a boolean result (true/false output ports) |
agent | nodes::agent | Delegates to AgentPort for LLM execution |
browser_trigger | nodes::browser | Triggers browser automation via CDP |
browser_action | nodes::browser | Executes browser actions via BrowserPort |
parallel_evaluate | nodes::parallel | Multi-agent parallel evaluation (gated by RUSVEL_FLOW_PARALLEL_EVALUATE=1) |
Manifest
Declared in dept-flow/src/manifest.rs:
id: "flow"
name: "Flow Department"
description: "DAG workflow automation -- create, execute, and monitor workflows"
icon: "~"
color: "sky"
capabilities: ["workflow_automation"]
System Prompt
You are the Flow department of RUSVEL.
Focus: DAG-based workflow automation.
Create, execute, and monitor directed acyclic graph workflows
with code, condition, and agent nodes.
Tools
Tools registered at runtime via dept-flow/src/tools.rs (7 tools):
| Tool | Parameters | Description |
|---|---|---|
flow.save | flow (FlowDef JSON) | Save a flow definition |
flow.get | flow_id | Load a flow definition by ID |
flow.list | (none) | List all saved flow definitions |
flow.run | flow_id, trigger_data | Run a flow with trigger payload |
flow.resume | execution_id | Resume a flow from checkpoint |
flow.get_execution | execution_id | Get an execution record by ID |
flow.list_executions | flow_id | List executions for a flow |
Manifest also declares 3 tool contributions for discovery:
flow.create– Create a new DAG workflow definitionflow.execute– Execute a saved workflow by IDflow.list– List all saved workflows
Personas
| Name | Role | Default Model | Allowed Tools |
|---|---|---|---|
workflow-architect | DAG workflow designer and automation expert | sonnet | flow.create, flow.execute, flow.list |
Skills
| Name | Description | Template |
|---|---|---|
| Workflow Design | Design a DAG workflow from requirements | Design a workflow for: {{goal}}\n\nBreak it into discrete steps with conditions and dependencies. |
Rules
None declared in the manifest.
Jobs
| Kind | Description | Requires Approval |
|---|---|---|
flow.execute | Execute a DAG workflow | No |
Events
Produced (manifest)
flow.startedflow.completedflow.failed
Emitted (engine runtime)
flow.execution.completed– emitted afterrun_flow()andresume_flow()with payload{ flow_id, execution_id, status }
Consumed
None.
API Routes
Declared in manifest (7 routes):
| Method | Path | Description |
|---|---|---|
GET | /api/flows | List all flow definitions |
POST | /api/flows | Create a new flow definition |
GET | /api/flows/{id} | Get a flow definition by ID |
PUT | /api/flows/{id} | Update a flow definition |
DELETE | /api/flows/{id} | Delete a flow definition |
POST | /api/flows/{id}/execute | Execute a flow |
GET | /api/flows/{id}/executions | List executions for a flow |
Additional route in rusvel-api (not in manifest):
GET /api/flows/node-types– returns available node types (filtersparallel_evaluateunlessRUSVEL_FLOW_PARALLEL_EVALUATE=1)
CLI Commands
Manifest declares one command:
| Command | Args | Description |
|---|---|---|
execute | flow_id (required) | Execute a flow by ID |
Entity Auto-Discovery
The standard CRUD subsystems are available for the Flow department at the parameterized /api/dept/flow/* routes:
- Agents, Skills, Rules, Hooks, Workflows, MCP Servers
These are auto-discovered from the department registry. No additional engine-specific entities.
Chat Flow
sequenceDiagram
participant U as User
participant API as /api/dept/flow/chat
participant Agent as AgentRuntime
participant Tool as flow.* tools
participant Engine as FlowEngine
participant Store as StoragePort
participant Bus as EventPort
U->>API: POST message (SSE)
API->>Agent: run_streaming(system_prompt, tools)
Agent->>Tool: flow.list()
Tool->>Engine: list_flows()
Engine->>Store: objects.list("flows")
Store-->>Engine: Vec<FlowDef>
Engine-->>Tool: flows
Tool-->>Agent: JSON response
Agent->>Tool: flow.run(flow_id, trigger_data)
Tool->>Engine: run_flow(id, data)
Engine->>Store: get_flow(id)
Engine->>Engine: execute DAG (topological order)
Engine->>Store: put execution record
Engine->>Bus: emit flow.execution.completed
Bus-->>Engine: EventId
Engine-->>Tool: FlowExecution
Tool-->>Agent: execution result
Agent-->>API: SSE chunks
API-->>U: streamed response
Extending the Department
Adding a new node type
- Create a new module under
flow-engine/src/nodes/implementing theFlowNodetrait - Register it in
FlowEngine::new()viaregistry.register(Arc::new(YourNode)) - The node becomes available in
node_types()and usable in flow definitions
Adding new tools
Add tool registrations in dept-flow/src/tools.rs inside the register() function. Follow the existing pattern of cloning the engine Arc and using ctx.tools.add().
Adding events
Define event kind constants in the engine and emit them via self.events.emit(Event { ... }). Add the event kind strings to the manifest’s events_produced vector.
Port Dependencies
| Port | Required | Usage |
|---|---|---|
StoragePort | Yes | Flow definitions, executions, checkpoints (via ObjectStore) |
EventPort | Yes | Emit flow.execution.completed events |
AgentPort | Yes | Agent node execution within flows |
JobPort | Yes | Background flow execution via job queue |
Optional ports (not in manifest requires_ports but accepted by engine constructor):
TerminalPort– PTY management for terminal-based nodesBrowserPort– CDP browser automation for browser trigger/action nodes
Messaging Department
Outbound notifications and channel adapters (e.g. Telegram) when configured.
| Field | Value |
|---|---|
| ID | messaging |
| Icon | @ |
| Color | violet |
| Engine crate | None (wrapper-only) |
| Wrapper crate | dept-messaging (~52 lines) |
| Status | Wrapper-only |
Overview
The Messaging department is a lightweight department shell with no dedicated engine crate. It exists to represent outbound notification capabilities in the department registry and provide a namespace for future multi-channel routing. Channel composition (Telegram, future Slack/Discord/email) is handled at the rusvel-app composition root, not inside an engine.
System Prompt
You are the Messaging department of RUSVEL.
Focus: outbound notifications, channel configuration, delivery status, and integration with external chat platforms when available.
Capabilities
| Capability | Description |
|---|---|
notify | Send outbound notifications via configured channels |
channel | Configure and manage messaging channel adapters |
Quick Actions
| Label | Prompt |
|---|---|
| Notify status | “Summarize configured outbound channels and recent notification delivery health.” |
| Channel setup | “Explain how to configure outbound messaging (e.g. Telegram) via environment and config.” |
Why There Is No Engine
Unlike other departments that wrap a dedicated *-engine crate, Messaging has no messaging-engine. This is an intentional architectural choice:
-
Channel composition belongs at the app level. The
ChannelPorttrait is defined inrusvel-coreand implemented by adapter crates (e.g.,rusvel-channel). The composition root (rusvel-app/src/main.rs) wires the channel adapter intoAppStatebased on environment variables. This follows the hexagonal pattern: the port lives in core, the adapter lives in its own crate, and wiring happens at the composition root. -
No domain logic yet. Sending a notification is a single
send_message()call onChannelPort. There is no complex domain state (no entities to CRUD, no workflows to orchestrate) that would justify an engine crate. -
Registered last.
dept-messagingis the last entry ininstalled_departments()inrusvel-app/src/boot.rs, ensuring all domain departments are registered before the channel shell.
ChannelPort and rusvel-channel
The Port (rusvel-core)
#![allow(unused)]
fn main() {
// crates/rusvel-core/src/ports.rs
#[async_trait]
pub trait ChannelPort: Send + Sync {
fn channel_kind(&self) -> &'static str;
async fn send_message(&self, session_id: &SessionId, payload: serde_json::Value) -> Result<()>;
}
}
ChannelPort is one of the 22 port traits in rusvel-core. It defines a minimal interface: identify the channel kind (e.g., "telegram") and send a message with a JSON payload scoped to a session.
The Adapter: TelegramChannel (rusvel-channel)
rusvel-channel provides TelegramChannel, the first ChannelPort implementation. It sends messages via the Telegram Bot API (sendMessage endpoint).
Configuration (environment variables):
| Variable | Required | Description |
|---|---|---|
RUSVEL_TELEGRAM_BOT_TOKEN | Yes | Telegram Bot API token (from @BotFather) |
RUSVEL_TELEGRAM_CHAT_ID | No | Default destination chat ID; can be overridden per-message in payload |
Initialization: TelegramChannel::from_env() returns Option<Arc<TelegramChannel>>. It returns None when RUSVEL_TELEGRAM_BOT_TOKEN is unset or empty, so the app gracefully degrades when Telegram is not configured.
Payload format:
{
"text": "Message body (required; 'message' is also accepted as alias)",
"chat_id": "Optional override for default chat ID"
}
Message format sent to Telegram: [session <session_id>] <text>
Wiring in the Composition Root
In rusvel-app/src/main.rs, the channel is wired into AppState:
#![allow(unused)]
fn main() {
let channel: Option<Arc<dyn ChannelPort>> = TelegramChannel::from_env()
.map(|t| t as Arc<dyn ChannelPort>);
}
The AppState.channel field is Option<Arc<dyn ChannelPort>>, making it available to API handlers.
The Notify API Endpoint
POST /api/system/notify
Request body:
{
"session_id": "uuid",
"text": "Notification message"
}
Behavior:
- If
state.channelisSome, wrapstextinto{"text": text}and callschannel.send_message() - If
state.channelisNone, returns503 Service Unavailablewith"notify channel not configured" - On Telegram API failure, returns
502 Bad Gatewaywith the error message - On success, returns
204 No Content
Example:
curl -X POST http://localhost:3000/api/system/notify \
-H "Content-Type: application/json" \
-d '{"session_id": "...", "text": "Build completed successfully"}'
Required Ports
| Port | Optional |
|---|---|
StoragePort | No |
EventPort | No |
AgentPort | No |
JobPort | No |
These ports are inherited defaults from the DepartmentManifest structure. The messaging department does not currently use any of them directly – it has no register() logic beyond logging.
UI Contribution
Tabs: actions, agents, rules, events
No dashboard cards, settings panel, or custom components.
Chat Flow
sequenceDiagram
participant User
participant Frontend
participant API as /api/dept/messaging/chat
participant Agent as AgentPort
User->>Frontend: "What channels are configured?"
Frontend->>API: POST (SSE)
API->>Agent: run(system_prompt + user message)
Note over Agent: No engine tools available.<br/>Agent answers from system prompt<br/>and general knowledge.
Agent-->>API: SSE token stream
API-->>Frontend: SSE events
Frontend-->>User: Channel configuration explanation
Note: since no engine tools are registered, the messaging agent operates purely through its system prompt and general knowledge. It cannot currently inspect actual channel configuration at runtime.
Notify Flow (System Endpoint)
sequenceDiagram
participant Caller
participant API as POST /api/system/notify
participant State as AppState
participant Channel as ChannelPort
participant Telegram as Telegram API
Caller->>API: { session_id, text }
API->>State: state.channel?
alt Channel configured
State-->>API: Some(channel)
API->>Channel: send_message(session_id, {text})
Channel->>Telegram: POST /bot<token>/sendMessage
Telegram-->>Channel: 200 OK
Channel-->>API: Ok
API-->>Caller: 204 No Content
else No channel
State-->>API: None
API-->>Caller: 503 Service Unavailable
end
Boot Order
installed_departments() order:
1. forge (core)
2. code (core)
3. content (cross-dept events)
4. harvest (cross-dept events)
5. flow (cross-dept events)
6. gtm (progressive enhancement)
7. finance (progressive enhancement)
8. product (progressive enhancement)
9. growth (progressive enhancement)
10. distro (progressive enhancement)
11. legal (skeleton)
12. support (skeleton)
13. infra (skeleton)
14. messaging <-- registered last
CLI Usage
rusvel messaging status # Show department status (via generic dept handler)
rusvel messaging list # List items (empty -- no domain objects)
rusvel messaging events # Show recent messaging events (none emitted currently)
Testing
cargo test -p dept-messaging # Wrapper test (manifest ID check, port requirements)
No engine tests since there is no engine crate.
Future: Multi-Channel Routing
This is where multi-channel routing would live as the department matures:
- Slack adapter – Implement
ChannelPortfor Slack Webhook/API, register inrusvel-channel - Discord adapter – Implement
ChannelPortfor Discord Webhook - Email adapter – Implement
ChannelPortfor SMTP (complement to theRUSVEL_SMTP_*config already used by GTM outreach) - Channel router – A
MultiChannelRouterthat wraps multipleChannelPortimplementations and routes based on message type, urgency, or user preference - Messaging engine – Once routing logic becomes complex enough, extract a
messaging-enginecrate with delivery queuing, retry policies, and delivery status tracking - Delivery events – Emit
messaging.sent,messaging.failed,messaging.deliveredevents for observability - Department tools – Register tools like
messaging.send,messaging.channels.list,messaging.delivery.status
Source Files
| File | Lines | Purpose |
|---|---|---|
crates/dept-messaging/src/lib.rs | 52 | DepartmentApp implementation (wrapper-only) |
crates/dept-messaging/src/manifest.rs | 101 | Static manifest definition |
crates/rusvel-channel/src/lib.rs | 7 | ChannelPort re-export + module declaration |
crates/rusvel-channel/src/telegram.rs | 107 | TelegramChannel adapter implementation |
crates/rusvel-core/src/ports.rs | (line 542) | ChannelPort trait definition |
crates/rusvel-api/src/system.rs | (line 66) | POST /api/system/notify handler |
Reference
Technical reference documentation for RUSVEL’s interfaces.
- Repository Status — Current metrics and build health
- CLI — Command-line interface reference
- API — REST API endpoints
- Configuration — Settings and environment variables
Repository status
The canonical metrics, feature inventory, and gaps for the RUSVEL codebase live in the main repository under docs/. This page summarizes what shipped as of the last audit and links to the full Markdown (always current on main).
Canonical sources (GitHub)
| Document | Purpose |
|---|---|
| docs/status/current-state.md | Single written source of truth — numbers, what works E2E, gaps, re-verify commands |
| docs/README.md | Documentation index — which folder is truth vs plans vs scratch |
| docs/status/verification-log-2026-03-30.md | Claim → evidence for the latest metrics snapshot |
When this mdBook page and the repo diverge, trust the repo files above and refresh the tables below on the next audit.
Metric definitions (abbrev.)
| Term | Meaning |
|---|---|
| Workspace members | Packages in root Cargo.toml [workspace].members |
| HTTP route chains | Lines with .route( in crates/rusvel-api/src/lib.rs (one line can register get().post()) |
| API modules | *.rs files in rusvel-api/src/ except lib.rs |
Numbers at a glance (2026-03-30)
| Metric | Count |
|---|---|
| Workspace members | 55 |
Rust LOC (crates/*.rs) | ~73,058 |
Rust source files (crates/) | 301 |
Tests (approx., full cargo test) | ~645 |
| HTTP route chains in API router | 153 |
API modules (rusvel-api, excl. lib.rs) | 39 |
Port traits (rusvel-core/src/ports.rs) | 22 |
Departments / dept-* crates | 14 / 14 |
| Engines | 13 (6 wired + 7 skeletons) |
Gaps (explicit)
- GTM / CRM depth — OutreachSend job path is wired (approval-gated,
gtm-engine); more CRM surfaces and polish remain. - Auth — not full API middleware; env/in-memory style for many paths.
- Depth — Several business engines are thinner than Forge/Code/Content/Harvest/Flow; department chat still works via
DepartmentApp.
How to re-verify
cargo build
cargo test
cargo metadata --format-version 1 --no-deps | python3 -c "import json,sys; print(len(json.load(sys.stdin)['workspace_members']))"
find crates -name '*.rs' | wc -l
wc -l $(find crates -name '*.rs') | tail -1
rg '\.route\(' crates/rusvel-api/src/lib.rs | wc -l
Some tests (e.g. terminal PTY) may fail in restricted environments.
CLI
Overview
RUSVEL provides a three-tier CLI using Clap 4. The binary is rusvel (or cargo run -- during development).
rusvel <dept> <action> # Tier 1: One-shot commands (12 depts; 14 apps in API/UI — see below)
rusvel shell # Tier 2: Interactive REPL (reedline, autocomplete, history)
rusvel --tui # Tier 3: TUI dashboard (ratatui, 4-panel layout)
Global Options
| Flag | Description |
|---|---|
--mcp | Start the MCP server (stdio JSON-RPC) instead of the web server |
--tui | Launch the TUI dashboard (ratatui, 4-panel layout) |
--help | Show help information |
--version | Show version |
Starting the Server
rusvel (no subcommand)
Starts the API web server on 0.0.0.0:3000. Serves the REST API and the embedded SvelteKit frontend.
cargo run
rusvel --mcp
Starts the MCP (Model Context Protocol) server over stdio. Used for integration with Claude Desktop and other MCP clients.
cargo run -- --mcp
rusvel --tui
Launches the TUI dashboard with 4 panels: Tasks, Goals, Pipeline, Events. Press q to exit.
cargo run -- --tui
Tier 1: Department One-Shot Commands
These one-shot actions are registered for 12 departments (Tier 1). The web app and API register 14 DepartmentApp instances (flow and messaging are not in this Tier 1 list).
rusvel <dept> status # Show department status summary
rusvel <dept> list [--kind X] # List department items
rusvel <dept> events # Show recent events for department
Tier 1 departments: forge, code, content, harvest, gtm, finance, product, growth, distro, legal, support, infra
Engine-Specific Commands
Some departments have additional commands powered by their wired engines:
# Code department
rusvel code analyze [path] # Analyze code: parser, dependency graph, metrics
rusvel code search <query> # BM25 search across codebase
# Content department
rusvel content draft <topic> # Draft content on a topic
rusvel content from-code # Generate content from code analysis
# Harvest department
rusvel harvest pipeline # Show opportunity pipeline
Forge Commands
rusvel forge mission today # Generate a prioritized daily plan
rusvel forge mission goals # List all goals for active session
rusvel forge mission goal add <title> # Add a new goal
rusvel forge mission review # Generate a periodic review
Options for goal add:
| Flag | Default | Description |
|---|---|---|
--description | "" | Goal description |
--timeframe | month | One of: day, week, month, quarter |
Options for review:
| Flag | Default | Description |
|---|---|---|
--period | week | One of: day, week, month, quarter |
Tier 2: Interactive REPL
rusvel shell
Launches an interactive shell powered by reedline with:
- Tab completion for commands and department names
- Ctrl+R history search
use <dept>to switch department context- All Tier 1 commands available without the
rusvelprefix
Session Management
rusvel session create <name> # Create a new session
rusvel session list # List all sessions
rusvel session switch <id> # Switch active session
The CLI stores the active session ID in ~/.rusvel/active_session. All forge commands operate on this session.
If no active session is set, commands that require one will error:
Error: No active session. Run `rusvel session create <name>` first.
Active Session
The CLI stores the active session ID in ~/.rusvel/active_session. All forge commands operate on this session. Change it with session switch.
API
Overview
RUSVEL exposes a JSON REST API on port 3000 via Axum. All endpoints use the /api/ prefix. CORS is enabled for all origins.
Scale (verify on main): the main router in crates/rusvel-api/src/lib.rs registers 153 .route( chains. Handler logic is split across 39 modules (one *.rs per module, excluding lib.rs). That is not the same as “153 HTTP methods” — a single chain can register get().post().
Router modules: agents, analytics, approvals, auth, browser, build_cmd, capability, chat, config, cron, db_routes, department, engine_routes, flow_routes, help, hook_dispatch, hooks, jobs, kits, knowledge, mcp_servers, pipeline_runner, playbooks, routes, rules, skills, system, terminal, visual_report, webhooks, workflows.
For canonical metrics and gaps, see Repository status.
Core
| Method | Path | Description |
|---|---|---|
GET | /api/health | Health check |
GET | /api/brief | Executive brief |
POST | /api/brief/generate | Generate brief |
GET | /api/config | Global configuration |
PUT | /api/config | Update configuration |
GET | /api/config/models | List LLM models |
GET | /api/config/tools | List tools |
GET | /api/analytics | Analytics dashboard data |
GET | /api/profile | User profile |
PUT | /api/profile | Update profile |
Sessions
| Method | Path | Description |
|---|---|---|
GET | /api/sessions | List sessions |
POST | /api/sessions | Create session |
GET | /api/sessions/{id} | Get session |
GET | /api/sessions/{id}/mission/today | Daily plan |
GET | /api/sessions/{id}/mission/goals | List goals |
POST | /api/sessions/{id}/mission/goals | Create goal |
GET | /api/sessions/{id}/events | Query events |
Chat (God agent)
| Method | Path | Description |
|---|---|---|
POST | /api/chat | Send message (SSE) |
GET | /api/chat/conversations | List conversations |
GET | /api/chat/conversations/{id} | Conversation history |
Departments
Replace {dept} with: forge, code, content, harvest, gtm, finance, product, growth, distro, legal, support, infra, flow, messaging.
| Method | Path | Description |
|---|---|---|
GET | /api/departments | List department definitions |
POST | /api/dept/{dept}/chat | Department chat (SSE) |
GET | /api/dept/{dept}/chat/conversations | List conversations |
GET | /api/dept/{dept}/chat/conversations/{id} | History |
GET | /api/dept/{dept}/config | Department config |
PUT | /api/dept/{dept}/config | Update config |
GET | /api/dept/{dept}/events | Department events |
Engine-specific routes
| Method | Path | Description |
|---|---|---|
POST | /api/dept/code/analyze | Code analysis |
GET | /api/dept/code/search | BM25 search |
POST | /api/dept/content/draft | Draft content |
POST | /api/dept/content/from-code | Content from code analysis |
PATCH | /api/dept/content/{id}/approve | Approve content item |
POST | /api/dept/content/publish | Publish (may require approval) |
GET | /api/dept/content/list | List content items |
POST | /api/dept/harvest/score | Score opportunity |
POST | /api/dept/harvest/scan | Scan sources |
POST | /api/dept/harvest/proposal | Generate proposal |
GET | /api/dept/harvest/pipeline | Pipeline |
GET | /api/dept/harvest/list | List harvest items |
Flow engine (DAG)
| Method | Path | Description |
|---|---|---|
GET / POST | /api/flows | List / create flows |
GET / PUT / DELETE | /api/flows/{id} | Get / update / delete |
POST | /api/flows/{id}/run | Run flow |
GET | /api/flows/{id}/executions | List executions |
GET | /api/flows/{id}/executions/{exec_id}/panes | Execution panes |
GET | /api/flows/executions/{id} | Get execution |
POST | /api/flows/executions/{id}/resume | Resume |
POST | /api/flows/executions/{id}/retry/{node_id} | Retry node |
GET | /api/flows/executions/{id}/checkpoint | Checkpoint |
GET | /api/flows/node-types | List node types |
Playbooks
| Method | Path | Description |
|---|---|---|
GET | /api/playbooks/runs | List runs |
GET | /api/playbooks/runs/{run_id} | Get run |
GET / POST | /api/playbooks | List / create playbook |
GET | /api/playbooks/{id} | Get playbook |
POST | /api/playbooks/{id}/run | Run playbook |
Starter kits
| Method | Path | Description |
|---|---|---|
GET | /api/kits | List kits |
GET | /api/kits/{id} | Get kit |
POST | /api/kits/{id}/install | Install kit |
Knowledge / RAG
| Method | Path | Description |
|---|---|---|
GET | /api/knowledge | List entries |
POST | /api/knowledge/ingest | Ingest |
POST | /api/knowledge/search | Semantic search |
POST | /api/knowledge/hybrid-search | Hybrid search |
GET | /api/knowledge/stats | Stats |
GET | /api/knowledge/related | Related entries |
DELETE | /api/knowledge/{id} | Delete entry |
Database (RusvelBase)
| Method | Path | Description |
|---|---|---|
GET | /api/db/tables | List tables |
GET | /api/db/tables/{table}/schema | Table schema |
GET | /api/db/tables/{table}/rows | Table rows |
POST | /api/db/sql | Run SQL |
Approvals
| Method | Path | Description |
|---|---|---|
GET | /api/approvals | Pending jobs |
POST | /api/approvals/{id}/approve | Approve |
POST | /api/approvals/{id}/reject | Reject |
Agents, skills, rules, workflows, MCP, hooks
Standard REST under /api/agents, /api/skills, /api/rules, /api/workflows, /api/mcp-servers, /api/hooks. Individual resource endpoints include GET /api/mcp-servers/{id} and GET /api/hooks/{id}. Workflows: POST /api/workflows/{id}/run. Hooks: GET /api/hooks/events for event types.
Capability and help
| Method | Path | Description |
|---|---|---|
POST | /api/capability/build | Capability / !build bundle |
POST | /api/help | AI help |
System and visual regression
| Method | Path | Description |
|---|---|---|
POST | /api/system/test | Run tests |
POST | /api/system/build | Run build |
GET | /api/system/status | Status |
POST | /api/system/fix | Self-fix |
POST | /api/system/ingest-docs | Ingest docs |
GET / POST | /api/system/visual-report | Visual reports |
POST | /api/system/visual-report/self-correct | Self-correct from diffs |
POST | /api/system/visual-test | Run visual tests |
Terminal
| Method | Path | Description |
|---|---|---|
GET | /api/terminal/dept/{dept_id} | Dept terminal pane |
GET | /api/terminal/runs/{run_id}/panes | Run panes |
GET | /api/terminal/ws | Terminal WebSocket |
Browser (CDP)
| Method | Path | Description |
|---|---|---|
GET | /api/browser/status | Status |
POST | /api/browser/connect | Connect |
GET | /api/browser/tabs | Tabs |
POST | /api/browser/observe/{tab} | Observe |
GET | /api/browser/captures | Captures |
GET | /api/browser/captures/stream | Capture stream |
POST | /api/browser/act | Action |
Configuration
Directory Structure
RUSVEL stores its configuration and data in ~/.rusvel/:
~/.rusvel/
├── active_session # UUID of the currently active session
├── config.toml # Global configuration
├── profile.toml # User profile (name, email, bio)
└── rusvel.db # SQLite database (WAL mode)
Config Hierarchy
Configuration follows a three-layer cascade. More specific layers override less specific ones:
Global config → Department config → Session config
For example, you can set a default model globally, override it for the Code department to use a more capable model, and further override it for a specific session.
Global Configuration (config.toml)
# Default LLM model for all departments
model = "claude-sonnet-4-20250514"
# Default effort level: "low", "medium", "high"
effort = "medium"
# API server settings
[server]
host = "0.0.0.0"
port = 3000
# LLM provider configuration
[llm]
default_provider = "ollama"
[llm.ollama]
base_url = "http://localhost:11434"
[llm.openai]
# Key loaded from OPENAI_API_KEY env var
model = "gpt-4o"
[llm.claude]
# Key loaded from ANTHROPIC_API_KEY env var
model = "claude-sonnet-4-20250514"
User Profile (profile.toml)
name = "Your Name"
email = "you@example.com"
bio = "Solo founder building products with AI"
company = "Your Company"
The profile is used by agents for context – the Content department knows your name for bylines, the GTM department uses your company name in outreach, etc.
Department Configuration
Department-level config is set via the API or the Settings page in the UI:
# Set Code department to use high effort and a specific model
curl -X PUT http://localhost:3000/api/dept/code/config \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-20250514",
"effort": "high",
"permission_mode": "default",
"add_dirs": ["."]
}'
Department Config Options
| Key | Type | Description |
|---|---|---|
model | string | LLM model to use for this department |
effort | string | Response depth: low, medium, high |
permission_mode | string | Tool permissions: default, restricted |
add_dirs | string[] | Working directories (for Code department) |
system_prompt | string | Override the department system prompt |
max_turns | number | Max conversation turns per request |
Department Registry (DepartmentApp Pattern)
Since ADR-014, departments are no longer configured via TOML files. Each department is a self-contained dept-* crate implementing the DepartmentApp trait, which declares its capabilities via a DepartmentManifest:
#![allow(unused)]
fn main() {
pub struct DepartmentManifest {
pub id: String, // e.g., "code"
pub name: String, // e.g., "Code"
pub title: String, // e.g., "Code Intelligence"
pub icon: String, // e.g., "terminal"
pub color: String, // e.g., "emerald"
pub system_prompt: String, // Department-specific system prompt
pub capabilities: Vec<String>,
pub tabs: Vec<String>,
pub quick_actions: Vec<QuickAction>,
}
}
The host (rusvel-app) collects manifests from all 14 dept-* crates at boot to generate the DepartmentRegistry. Adding a new department means adding a new dept-* crate – zero changes to rusvel-core.
Environment Variables
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY | Claude API authentication |
OPENAI_API_KEY | OpenAI API authentication |
RUSVEL_DB_PATH | Not implemented in Rust binary — DB file is {data_dir}/rusvel.db (default ~/.rusvel/rusvel.db). |
RUSVEL_CONFIG_DIR | Override config directory (default: ~/.rusvel) |
RUST_LOG | Logging level (e.g., info, debug, rusvel_api=debug) |
RUSVEL_RATE_LIMIT | API rate limit in requests/second (default: 100) |
RUSVEL_API_READ_TOKEN | Read-only API bearer token (GET/HEAD/OPTIONS only) |
Database
RUSVEL uses SQLite in WAL (Write-Ahead Logging) mode for concurrent reads. The database file is at ~/.rusvel/rusvel.db by default.
The database stores: sessions, goals, events, conversations, agents, skills, rules, workflows, hooks, MCP server configs, and analytics data. Migrations run automatically on startup.
Architecture
RUSVEL’s hexagonal architecture and design decisions.
- Overview — Ports & adapters, layers, dependency graph
- Decisions (ADRs) — 14 architecture decision records with rationale
Overview
Design Philosophy
RUSVEL follows hexagonal architecture (ports and adapters). The core principle: domain logic never depends on infrastructure. Engines express what they need through port traits; adapters provide the implementations.
Four Layers
┌─────────────────── SURFACES ───────────────────┐
│ CLI (Clap) │ TUI (Ratatui) │ Web (Svelte) │
│ MCP Server │
└──────────────────────┬─────────────────────────┘
│
┌──────────────────────┴─────────────────────────┐
│ DOMAIN ENGINES (13) │
│ │
│ Forge │ Code │ Harvest │
│ (+ Mission) │ │ │
│ │ │ │
│ Content │ GoToMarket │
│ │ (Ops+Connect+Outreach) │
└──────────────────────┬─────────────────────────┘
│ uses (traits only)
┌──────────────────────┴─────────────────────────┐
│ FOUNDATION │
│ │
│ ┌──────────── rusvel-core ──────────────┐ │
│ │ Port Traits + Shared Domain Types │ │
│ └───────────────────────────────────────┘ │
│ │
│ ┌──────────── Adapters ─────────────────┐ │
│ │ rusvel-llm (model providers) │ │
│ │ rusvel-agent (agent runtime) │ │
│ │ rusvel-db (SQLite + 5 stores) │ │
│ │ rusvel-event (event bus + persist) │ │
│ │ rusvel-memory (context + search) │ │
│ │ rusvel-tool (tool registry) │ │
│ │ rusvel-jobs (central job queue) │ │
│ │ rusvel-auth (credentials) │ │
│ │ rusvel-config (settings) │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Layer 1: rusvel-core (Foundation)
The heart of the system. Contains:
- 22 port traits in
ports.rs— includes five*Storesubtraits under the storage model,BrowserPort,ChannelPort,RusvelBasePort, and primary ports (LlmPort, AgentPort, ToolPort, EventPort, StoragePort, MemoryPort, JobPort, SessionPort, AuthPort, ConfigPort, EmbeddingPort, VectorStorePort, DeployPort, TerminalPort).DepartmentAppis defined underdepartment/. - ~114
pub struct/pub enumindomain.rs, plus shared types — Session, Goal, Event, Agent, Content, Opportunity, Contact, Task, DepartmentManifest, etc. (seedocs/status/current-state.md§1). - Zero framework dependencies
Layer 2: Adapters
Concrete implementations of the port traits:
| Crate | Implements | Notes |
|---|---|---|
rusvel-llm | LlmPort | 4 providers: Ollama, OpenAI, Claude API, Claude CLI |
rusvel-agent | AgentPort | Wraps LLM + Tool + Memory into orchestration |
rusvel-db | StoragePort | SQLite WAL with 5 sub-stores, migrations |
rusvel-event | EventPort | Event bus with persistence |
rusvel-memory | MemoryPort | FTS5 session-namespaced search |
rusvel-tool | ToolPort | Tool registry with JSON Schema |
rusvel-jobs | JobPort | Central SQLite job queue |
rusvel-auth | AuthPort | In-memory credential storage from env |
rusvel-config | ConfigPort | TOML config with per-session overrides |
rusvel-embed | EmbeddingPort | Local embeddings via fastembed |
rusvel-vector | VectorStorePort | LanceDB vector store |
rusvel-deploy | DeployPort | Deployment adapter |
rusvel-terminal | TerminalPort | Terminal interaction adapter |
rusvel-builtin-tools | — | Built-in tools for agents (file, shell, git, etc.) + optional tool_search meta-tool |
rusvel-cdp | — | Chrome DevTools Protocol client (Browser/CDP wiring) |
rusvel-engine-tools | — | Engine-specific tool wiring |
rusvel-mcp-client | — | MCP client for external MCP servers |
rusvel-schema | — | Database schema introspection (RusvelBase) |
Layer 3: Engines
Domain logic crates. Each engine depends only on rusvel-core traits:
| Engine | Focus |
|---|---|
forge-engine | Agent orchestration + Mission (goals, planning, reviews) |
code-engine | Code intelligence: parser, dependency graph, BM25 search, metrics |
harvest-engine | Opportunity discovery: scanning, scoring, proposals, pipeline |
content-engine | Content creation: writer, calendar, platform adapters, analytics |
gtm-engine | GoToMarket: CRM, outreach sequences, invoicing, deal stages |
finance-engine | Ledger, runway calculator, tax estimation |
product-engine | Roadmap, pricing analysis, feedback aggregation |
growth-engine | Funnel analysis, cohort tracking, KPI dashboard |
distro-engine | SEO, marketplace listings, affiliate channels |
legal-engine | Contract drafting, compliance checks, IP management |
support-engine | Ticket management, knowledge base, NPS tracking |
infra-engine | Deployment, monitoring, incident response |
flow-engine | DAG workflow engine: petgraph, code/condition/agent nodes |
Each engine also has a corresponding dept-* wrapper crate (e.g. dept-forge, dept-code, dept-finance) that implements the DepartmentApp trait (ADR-014), declaring the department’s manifest, tools, and registration logic. There are 14 dept-* crates (13 engine-backed departments + dept-messaging). Of the 13 engines, 6 are fully wired (Forge, Code, Content, Harvest, GTM, Flow) and 7 are skeletons (Finance, Product, Growth, Distro, Legal, Support, Infra).
Layer 4: Surfaces
User-facing interfaces that wire adapters into engines:
| Surface | Technology | Status |
|---|---|---|
rusvel-api | Axum HTTP | Active |
rusvel-cli | Clap 4 | Active |
rusvel-mcp | stdio JSON-RPC | Active |
rusvel-tui | Ratatui | Active |
frontend/ | SvelteKit 5 + Tailwind 4 | Active |
Composition Root: rusvel-app
The binary entry point. It constructs all adapters, injects them into engines, and starts the chosen surface (web server, CLI, or MCP). This is the only place where concrete types meet.
Workspace Layout
rusvel/
├── crates/ 55 workspace members
│ ├── rusvel-core/ 22 port traits in ports.rs + domain types + DepartmentApp
│ ├── rusvel-db/ SQLite WAL + 5 canonical stores
│ ├── rusvel-llm/ 4 LLM providers
│ ├── rusvel-agent/ Agent runtime (LLM+Tool+Memory)
│ ├── rusvel-event/ Event bus + persistence
│ ├── rusvel-memory/ FTS5 session-namespaced search
│ ├── rusvel-tool/ Tool registry + JSON Schema
│ ├── rusvel-builtin-tools/ Built-in + meta tools for agent execution
│ ├── rusvel-engine-tools/ Engine-specific tool wiring
│ ├── rusvel-mcp-client/ MCP client for external servers
│ ├── rusvel-jobs/ Central job queue
│ ├── rusvel-auth/ Credential storage
│ ├── rusvel-config/ TOML config + overrides
│ ├── rusvel-deploy/ Deployment port adapter
│ ├── rusvel-embed/ Text embedding (fastembed)
│ ├── rusvel-vector/ Vector store (LanceDB)
│ ├── rusvel-schema/ Database schema introspection
│ ├── rusvel-terminal/ Terminal interaction adapter
│ ├── rusvel-cdp/ CDP client (browser automation)
│ ├── forge-engine/ Agent orchestration + Mission
│ ├── code-engine/ Code intelligence
│ ├── harvest-engine/ Opportunity discovery
│ ├── content-engine/ Content creation + publishing
│ ├── gtm-engine/ GoToMarket (CRM + outreach)
│ ├── finance-engine/ Ledger, runway, tax
│ ├── product-engine/ Roadmap, pricing, feedback
│ ├── growth-engine/ Funnel, cohorts, KPIs
│ ├── distro-engine/ SEO, marketplace, affiliates
│ ├── legal-engine/ Contracts, compliance, IP
│ ├── support-engine/ Tickets, knowledge base, NPS
│ ├── infra-engine/ Deploy, monitor, incidents
│ ├── flow-engine/ DAG workflow engine
│ ├── dept-forge/ DepartmentApp wrapper for Forge
│ ├── dept-code/ DepartmentApp wrapper for Code
│ ├── dept-harvest/ ... (14 dept-* wrappers total)
│ ├── dept-flow/ DepartmentApp wrapper for Flow
│ ├── rusvel-api/ Axum HTTP API
│ ├── rusvel-cli/ Clap CLI + REPL
│ ├── rusvel-tui/ Ratatui TUI
│ ├── rusvel-mcp/ MCP server (stdio JSON-RPC)
│ └── rusvel-app/ Binary entry point
├── frontend/ SvelteKit 5 + Tailwind 4
├── Cargo.toml Workspace manifest (55 members)
└── CLAUDE.md Project conventions
Key Rules
- Engines never import adapter crates. They receive port implementations via constructor injection.
- Engines never call LlmPort directly. They use AgentPort, which wraps LLM + Tool + Memory (ADR-009).
- All domain types have
metadata: serde_json::Valuefor schema evolution without migrations (ADR-007). - Event.kind is a String, not an enum. Engines define their own constants (ADR-005).
- Single job queue for all async work. No per-engine scheduling (ADR-003).
- Human approval gates on content publishing and outreach sending (ADR-008).
- Each crate stays under 2000 lines. Single responsibility.
Port traits (rusvel-core/src/ports.rs)
There are 21 pub trait definitions in ports.rs (including the five *Store subtraits and ChannelPort). The table below lists the main contracts; see the source for the exact set.
| Port | Responsibility |
|---|---|
LlmPort | Raw model access: generate, stream, embed |
AgentPort | Agent orchestration: create, run, stop, status |
ToolPort | Tool registry + execution |
EventPort | System-wide typed event bus (append-only) |
StoragePort | 5 canonical sub-stores (see below) |
EventStore | Append-only event log |
ObjectStore | CRUD for domain objects |
SessionStore | Session/Run/Thread hierarchy |
JobStore | Job queue persistence |
MetricStore | Time-series metrics |
MemoryPort | Context, knowledge, semantic search |
JobPort | Central job queue with approval support |
SessionPort | Session hierarchy management |
AuthPort | Opaque credential handles |
ConfigPort | Settings and preferences |
EmbeddingPort | Text embedding (fastembed) |
VectorStorePort | Vector storage and search (LanceDB) |
DeployPort | Deployment operations |
TerminalPort | Terminal interaction and display |
BrowserPort | Browser/CDP observation and actions |
ChannelPort | Outbound notification channels (Telegram, etc.) |
Decisions (ADRs)
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.
Testing
Manual and automated testing guides for every RUSVEL surface.
- Playbook Overview — Testing strategy and surface map
- Setup & Prerequisites — Build, services, env vars, tools
- CLI, REPL & TUI — Terminal interface testing
- API Testing — 130+ REST routes with curl examples
- Frontend Testing — Browser UI and visual regression
- Engine-Specific Testing — Code, Content, Harvest, GTM, Flow
- Smoke Test Checklist — 30-step release gate
Manual Testing Playbook
Complete feature-by-feature testing guide with expected behavior for every RUSVEL surface.
RUSVEL exposes 6 surfaces — each needs manual verification:
| Surface | Entry point | Requires Ollama |
|---|---|---|
| CLI One-Shot | cargo run -- <command> | Some commands |
| REPL Shell | cargo run -- shell | No |
| TUI Dashboard | cargo run -- --tui | No |
| Web API | curl localhost:3000/api/* | Chat endpoints |
| Web Frontend | Browser at localhost:3000 | Chat pages |
| MCP Server | cargo run -- --mcp | Some tools |
Testing Strategy
- Start with the Smoke Test Checklist — 30 steps, covers all surfaces in ~15 minutes
- Deep-dive per surface — Use the dedicated pages for thorough testing
- Engine-specific tests — Code, Content, Harvest, GTM, Flow each have unique endpoints
- Cross-surface verification — Perform an action in one surface, verify it in another:
- Create a session via CLI -> verify via
curl /api/sessions-> verify in browser UI - Chat via web UI -> verify conversation via API -> check events in TUI
- Create a session via CLI -> verify via
Quick Reference
| What | Where |
|---|---|
| Build & prerequisites | Setup |
| CLI, REPL, TUI | CLI Testing |
| REST API (130+ routes) | API Testing |
| Browser UI (30+ pages) | Frontend Testing |
| Code, Content, Harvest, GTM, Flow | Engine Testing |
| 30-step release gate | Smoke Checklist |
Setup & Prerequisites
Build the Binary
cargo build
Expected: Compiles 55 workspace members. Zero errors. Warnings are acceptable.
Run Automated Tests First
cargo test
Expected: ~645 tests pass (workspace sum), 0 failures. Some tests may be ignored (PTY tests in sandboxed environments).
Required Services
| Service | How to start | Required for |
|---|---|---|
| Ollama | ollama serve | LLM-powered features (chat, mission, draft, etc.) |
| Ollama model | ollama pull llama3.2 | Must have at least one model pulled |
Optional Environment Variables
| Variable | Purpose | Default |
|---|---|---|
RUST_LOG | Tracing level | info |
RUSVEL_API_TOKEN | Bearer token for API auth | None (open) |
RUSVEL_RATE_LIMIT | API rate limit (req/sec) | 100 |
RUSVEL_SMTP_HOST | SMTP for outreach emails | Mock adapter |
RUSVEL_SMTP_PORT | SMTP port | – |
RUSVEL_SMTP_USER | SMTP username | – |
RUSVEL_SMTP_PASSWORD | SMTP password | – |
RUSVEL_TELEGRAM_BOT_TOKEN | Telegram notifications | Disabled |
RUSVEL_FLOW_PARALLEL_EVALUATE | Enable parallel flow nodes | 0 |
ANTHROPIC_API_KEY | Claude API provider | Claude CLI fallback |
OPENAI_API_KEY | OpenAI provider | – |
Data Directory
RUSVEL stores data in ~/.rusvel/:
rusvel.db– SQLite databaseconfig.toml– configurationshell_history.txt– REPL historylance/– vector store data
To reset to clean state: rm -rf ~/.rusvel/ (destructive – removes all data).
Docker Alternative
docker compose up
Expected: Starts RUSVEL on :3000 + Ollama on :11434. No local install needed.
Environment Combos
Minimal (no AI)
cargo run
# Test all non-LLM features: CRUD, DB browser, config, etc.
Full Local
ollama serve &
ollama pull llama3.2
cargo run
# All features available
With Claude API
export ANTHROPIC_API_KEY=sk-ant-...
cargo run
# Uses Claude for higher-quality responses
With Notifications
export RUSVEL_TELEGRAM_BOT_TOKEN=...
cargo run
# POST /api/system/notify sends Telegram messages
Testing Tools
| Tool | Use for | Install |
|---|---|---|
curl | API testing | Pre-installed on macOS |
jq | JSON formatting | brew install jq |
websocat | WebSocket testing | brew install websocat |
httpie | Friendlier API calls | brew install httpie |
just | Task runner recipes | brew install just |
watchexec | File-watching re-runs | cargo install watchexec-cli |
httpie Examples (alternative to curl)
# GET
http :3000/api/health
http :3000/api/departments
# POST
http POST :3000/api/sessions name=test kind=General
# SSE streaming
http --stream POST :3000/api/chat message="hello"
CLI, REPL & TUI Testing
CLI One-Shot Commands
Help
cargo run -- --help
Expected: Shows usage, subcommands (session, forge, shell, brief, browser, finance, growth, distro, legal, support, infra, product, code, harvest, content, gtm), and flags (--mcp, --mcp-http, --tui).
Session Management
# Create a session
cargo run -- session create "test-project"
Expected: Prints session ID (UUID). Session is now active.
# List sessions
cargo run -- session list
Expected: Table showing session ID, name, kind, created_at. Should include “test-project”.
# Switch session
cargo run -- session switch <session-id>
Expected: Confirms switch to the specified session.
Forge / Mission
# Daily mission plan (requires Ollama)
cargo run -- forge mission today
Expected: AI-generated daily plan with prioritized tasks. Streams text to stdout. May take 5-30s depending on model.
# List goals
cargo run -- forge mission goals
Expected: Table of goals (may be empty on fresh install).
# Add a goal
cargo run -- forge mission goal add "Launch MVP" \
--description "Ship v1 to production" --timeframe month
Expected: Prints goal ID. Goal appears in subsequent goals listing.
# Periodic review
cargo run -- forge mission review --period week
Expected: AI-generated weekly review (requires Ollama).
Executive Brief
cargo run -- brief
Expected: Generates an executive daily digest summarizing activity across departments.
Department Commands (Generic)
Every department supports status, list, and events:
# Status (repeat for each department)
cargo run -- finance status
cargo run -- growth status
cargo run -- code status
cargo run -- content status
cargo run -- harvest status
cargo run -- gtm status
cargo run -- product status
cargo run -- distro status
cargo run -- legal status
cargo run -- support status
cargo run -- infra status
Expected: Each prints a status summary. May show “No active items” on fresh install.
# List with kind filter
cargo run -- finance list --kind transactions
cargo run -- support list --kind tickets
cargo run -- legal list --kind contracts
cargo run -- growth list --kind funnel_stages
cargo run -- product list --kind features
Expected: Table of domain objects. Empty on fresh install, but no error.
# Events
cargo run -- finance events --limit 5
Expected: Recent events for that department. Empty on fresh install.
Engine-Specific Commands
# Code: analyze current directory
cargo run -- code analyze .
Expected: Prints analysis results (file count, complexity metrics, dependency info). Works without Ollama.
# Code: search symbols
cargo run -- code search "DepartmentApp"
Expected: BM25 search results showing matching symbols/definitions.
# Content: draft a topic (requires Ollama)
cargo run -- content draft "Why Rust is great for CLI tools"
Expected: AI-generated markdown draft. Streams to stdout.
# Harvest: pipeline stats
cargo run -- harvest pipeline
Expected: Pipeline stage counts (Cold, Contacted, Qualified, etc.). All zeros on fresh install.
Browser Commands
# Check browser status (no Chrome needed)
cargo run -- browser status
Expected: Shows “Not connected” or connection info.
# Connect to Chrome (requires Chrome with --remote-debugging-port=9222)
cargo run -- browser connect
Expected: Connects to Chrome DevTools Protocol endpoint.
Interactive REPL Shell
cargo run -- shell
Expected: Launches interactive prompt: rusvel>
REPL Commands
| Command | Expected Behavior |
|---|---|
help | Lists all available commands |
status | Overview across all departments |
use finance | Switches context -> prompt becomes rusvel:finance> |
status (in dept) | Shows finance-specific status |
list transactions | Lists finance transactions |
events | Shows finance events |
back | Returns to top-level rusvel> |
use code | Switches to code department |
session list | Lists all sessions |
session create foo | Creates a new session |
exit or Ctrl+D | Exits the REPL |
REPL Features
| Feature | How to test | Expected |
|---|---|---|
| Tab completion | Type us then press Tab | Completes to use |
| Department completion | Type use then Tab | Shows all 14 departments |
| History | Type a command, exit, re-enter shell | Up arrow recalls previous commands |
| History search | Press Ctrl+R, type partial command | Finds matching history entry |
TUI Dashboard
cargo run -- --tui
Expected: Full-screen terminal dashboard with 4 panels.
TUI Panels
| Panel | Content | Expected |
|---|---|---|
| Tasks (top-left) | Active tasks with priority markers | Shows tasks or “No active tasks” |
| Goals (top-right) | Goals with progress bars | Shows goals or “No goals” |
| Pipeline (bottom-left) | Opportunity counts by stage | Shows stages with counts |
| Events (bottom-right) | Recent system events | Shows recent events or empty |
TUI Keybindings
| Key | Expected |
|---|---|
q or Esc | Exits TUI cleanly, returns to terminal |
t | Toggles terminal pane focus |
| Arrow keys (in terminal mode) | Navigate between terminal panes |
TUI Verification
- No crash on empty data – Fresh install should render all panels without panic
- Resize handling – Resize terminal window; panels should reflow
- Clean exit – Terminal should restore to normal state after
q
API Testing
Start the server first:
cargo run
Expected: Server starts on http://localhost:3000. Logs show boot sequence: database init, department registration (14 departments), tool registration, job worker started.
Health & System
curl -s http://localhost:3000/api/health | jq .
Expected: {"status": "ok"} or similar health response.
curl -s http://localhost:3000/api/system/status | jq .
Expected: JSON with system info (uptime, departments, sessions, etc.).
Sessions
# Create session
curl -s -X POST http://localhost:3000/api/sessions \
-H 'Content-Type: application/json' \
-d '{"name": "api-test", "kind": "General"}' | jq .
Expected: Returns JSON with id (UUID), name, kind, created_at.
# List sessions
curl -s http://localhost:3000/api/sessions | jq .
Expected: Array of session objects.
# Get single session
curl -s http://localhost:3000/api/sessions/<id> | jq .
Expected: Single session object with full details.
Departments
# List all departments
curl -s http://localhost:3000/api/departments | jq .
Expected: Array of 14 department objects, each with id, name, description, icon, color, capabilities, quick_actions.
# Department config
curl -s http://localhost:3000/api/dept/code/config | jq .
Expected: Code department configuration (system prompt, model, etc.).
# Department events
curl -s http://localhost:3000/api/dept/forge/events | jq .
Expected: Array of events (may be empty).
Agents CRUD
# List agents
curl -s http://localhost:3000/api/agents | jq .
Expected: Array with seeded agents (rust-engine, svelte-ui, test-writer, arch-reviewer, etc.).
# Create agent
curl -s -X POST http://localhost:3000/api/agents \
-H 'Content-Type: application/json' \
-d '{
"name": "test-agent",
"role": "Test",
"instructions": "You are a test agent",
"model": "llama3.2"
}' | jq .
Expected: Returns created agent with id.
# Get / Update / Delete
curl -s http://localhost:3000/api/agents/<id> | jq .
curl -s -X PUT http://localhost:3000/api/agents/<id> \
-H 'Content-Type: application/json' \
-d '{"name": "updated-agent", "instructions": "Updated"}' | jq .
curl -s -X DELETE http://localhost:3000/api/agents/<id>
Expected: Standard CRUD responses (200 with entity, 204 on delete).
Skills CRUD
# List skills
curl -s http://localhost:3000/api/skills | jq .
Expected: Array with seeded skills (/analyze-architecture, /check-crate-sizes, etc.).
# Create skill
curl -s -X POST http://localhost:3000/api/skills \
-H 'Content-Type: application/json' \
-d '{
"name": "test-skill",
"description": "A test skill",
"action": "echo {{input}}"
}' | jq .
Expected: Returns created skill with id. The {{input}} placeholder is preserved.
Rules CRUD
curl -s http://localhost:3000/api/rules | jq .
Expected: Array with seeded rules (architecture boundaries, crate size, etc.).
curl -s -X POST http://localhost:3000/api/rules \
-H 'Content-Type: application/json' \
-d '{"name": "test-rule", "description": "Test", "content": "Always test"}' | jq .
Hooks CRUD
curl -s http://localhost:3000/api/hooks | jq .
# Create a command hook
curl -s -X POST http://localhost:3000/api/hooks \
-H 'Content-Type: application/json' \
-d '{
"name": "test-hook",
"event_kind": "code.chat.completed",
"hook_type": "command",
"action": "echo hook fired"
}' | jq .
# List hook events (what events can hooks fire on)
curl -s http://localhost:3000/api/hooks/events | jq .
MCP Servers CRUD
curl -s http://localhost:3000/api/mcp-servers | jq .
curl -s -X POST http://localhost:3000/api/mcp-servers \
-H 'Content-Type: application/json' \
-d '{
"name": "test-mcp",
"command": "node",
"args": ["./mcp-server.js"],
"description": "Test MCP server"
}' | jq .
Workflows CRUD + Execution
# List workflows
curl -s http://localhost:3000/api/workflows | jq .
Expected: Array with seeded self-improvement workflow.
# Create workflow
curl -s -X POST http://localhost:3000/api/workflows \
-H 'Content-Type: application/json' \
-d '{
"name": "test-workflow",
"steps": [
{"name": "step1", "action": "echo step 1"},
{"name": "step2", "action": "echo step 2"}
]
}' | jq .
# Run workflow (requires Ollama for agent steps)
curl -s -X POST http://localhost:3000/api/workflows/<id>/run | jq .
Expected: Returns run status. Job is enqueued.
Chat (God Agent) – SSE Streaming
curl -N http://localhost:3000/api/chat \
-H 'Content-Type: application/json' \
-d '{"message": "Hello, what can you do?"}'
Expected SSE events:
data: {"type":"text_delta","text":"I"}
data: {"type":"text_delta","text":" can"}
data: {"type":"text_delta","text":" help"}
...
data: {"type":"done","output":"I can help you with..."}
Verify: Stream starts within 2-5s. Text deltas arrive incrementally. Stream ends with a done event.
Department Chat – SSE Streaming
curl -N http://localhost:3000/api/dept/code/chat \
-H 'Content-Type: application/json' \
-d '{"message": "What files are in this project?"}'
Expected: SSE stream with department-scoped response. The agent uses code-related tools.
# List conversations
curl -s http://localhost:3000/api/dept/code/chat/conversations | jq .
# Get conversation history
curl -s http://localhost:3000/api/dept/code/chat/conversations/<id> | jq .
Agent Tool-Use Loop
The core loop: LLM generates -> tool call -> execute -> feed result -> repeat.
curl -N http://localhost:3000/api/dept/code/chat \
-H 'Content-Type: application/json' \
-d '{"message": "Read the Cargo.toml and count workspace members"}'
Expected SSE events in order:
text_delta– agent starts respondingtool_call–{"name": "read_file", "input": {"path": "Cargo.toml"}}tool_result– file contentstext_delta– agent interprets resultsdone– final answer mentions “55 workspace members” (ordocs/status/current-state.md)
Scoped Tool Registry
Different departments see different tools:
# Code dept should have code_analyze, code_search
curl -N http://localhost:3000/api/dept/code/chat \
-H 'Content-Type: application/json' \
-d '{"message": "What tools do you have available?"}'
# Content dept should have content_draft, content_publish
curl -N http://localhost:3000/api/dept/content/chat \
-H 'Content-Type: application/json' \
-d '{"message": "What tools do you have available?"}'
Conversation History
# Send first message
curl -N http://localhost:3000/api/dept/code/chat \
-H 'Content-Type: application/json' \
-d '{"message": "Remember: my favorite number is 42"}'
# Get conversation ID
curl -s http://localhost:3000/api/dept/code/chat/conversations | jq '.[0].id'
# Continue conversation
curl -N http://localhost:3000/api/dept/code/chat \
-H 'Content-Type: application/json' \
-d '{"message": "What is my favorite number?", "conversation_id": "<id>"}'
Expected: Agent recalls “42” from conversation history.
Skills Execution
# Create a skill with input interpolation
curl -s -X POST http://localhost:3000/api/skills \
-H 'Content-Type: application/json' \
-d '{"name": "greet", "action": "Say hello to {{input}}"}' | jq .
# Execute via chat
curl -N http://localhost:3000/api/dept/forge/chat \
-H 'Content-Type: application/json' \
-d '{"message": "/greet World"}'
Expected: Agent resolves skill, interpolates “World” into {{input}}, executes.
Rules Injection
# Create a rule for code department
curl -s -X POST http://localhost:3000/api/rules \
-H 'Content-Type: application/json' \
-d '{"name": "always-rust", "department": "code", "content": "Always recommend Rust"}' | jq .
# Chat with code department
curl -N http://localhost:3000/api/dept/code/chat \
-H 'Content-Type: application/json' \
-d '{"message": "What language should I use for a CLI tool?"}'
Expected: Rule is appended to system prompt. Agent recommends Rust.
Mission / Forge API
# Daily plan (requires Ollama + active session)
curl -s http://localhost:3000/api/sessions/<session-id>/mission/today | jq .
# List goals
curl -s http://localhost:3000/api/sessions/<session-id>/mission/goals | jq .
# Create goal
curl -s -X POST http://localhost:3000/api/sessions/<session-id>/mission/goals \
-H 'Content-Type: application/json' \
-d '{"title": "API test goal", "timeframe": "week"}' | jq .
# Generate executive brief
curl -s -X POST http://localhost:3000/api/brief/generate | jq .
# Get latest brief
curl -s http://localhost:3000/api/brief/latest | jq .
Config & Models
curl -s http://localhost:3000/api/config | jq .
curl -s http://localhost:3000/api/config/models | jq .
curl -s http://localhost:3000/api/config/tools | jq .
curl -s -X PUT http://localhost:3000/api/config \
-H 'Content-Type: application/json' \
-d '{"default_model": "llama3.2"}' | jq .
Analytics
curl -s http://localhost:3000/api/analytics | jq .
curl -s http://localhost:3000/api/analytics/dashboard | jq .
curl -s http://localhost:3000/api/analytics/spend | jq .
Jobs & Approvals
# List jobs
curl -s http://localhost:3000/api/jobs | jq .
# List pending approvals
curl -s http://localhost:3000/api/approvals | jq .
# Approve / reject a job
curl -s -X POST http://localhost:3000/api/approvals/<job-id>/approve | jq .
curl -s -X POST http://localhost:3000/api/approvals/<job-id>/reject | jq .
Hooks & Events
Command Hook
# Create hook that fires on chat completion
curl -s -X POST http://localhost:3000/api/hooks \
-H 'Content-Type: application/json' \
-d '{
"name": "log-chat",
"event_kind": "forge.chat.completed",
"hook_type": "command",
"action": "echo HOOK_FIRED >> /tmp/rusvel-hook.log"
}' | jq .
# Trigger by chatting with forge
curl -N http://localhost:3000/api/dept/forge/chat \
-H 'Content-Type: application/json' \
-d '{"message": "hello"}'
# Verify hook fired
cat /tmp/rusvel-hook.log
Expected: File contains “HOOK_FIRED”.
HTTP Hook
curl -s -X POST http://localhost:3000/api/hooks \
-H 'Content-Type: application/json' \
-d '{
"name": "notify-hook",
"event_kind": "content.published",
"hook_type": "http",
"action": "http://localhost:8080/webhook"
}' | jq .
Expected: When content is published, POST request sent to the URL with event payload.
User Profile
curl -s http://localhost:3000/api/profile | jq .
curl -s -X PUT http://localhost:3000/api/profile \
-H 'Content-Type: application/json' \
-d '{"name": "Mehdi", "skills": {"primary": ["rust", "sveltekit"]}}' | jq .
Help (AI-Powered)
curl -s -X POST http://localhost:3000/api/help \
-H 'Content-Type: application/json' \
-d '{"question": "How do I create a content draft?"}' | jq .
Database Browser
# List tables
curl -s http://localhost:3000/api/db/tables | jq .
# Get table schema
curl -s http://localhost:3000/api/db/tables/events/schema | jq .
# Get table rows
curl -s "http://localhost:3000/api/db/tables/events/rows?limit=10" | jq .
# Execute SQL
curl -s -X POST http://localhost:3000/api/db/sql \
-H 'Content-Type: application/json' \
-d '{"sql": "SELECT COUNT(*) as count FROM events"}' | jq .
Security check: Destructive SQL should be rejected or run in read-only mode:
curl -s -X POST http://localhost:3000/api/db/sql \
-H 'Content-Type: application/json' \
-d '{"sql": "DROP TABLE events"}' | jq .
Knowledge / RAG
# Ingest knowledge
curl -s -X POST http://localhost:3000/api/knowledge/ingest \
-H 'Content-Type: application/json' \
-d '{"content": "Rust is a systems programming language.", "source": "manual"}' | jq .
# Search knowledge
curl -s -X POST http://localhost:3000/api/knowledge/search \
-H 'Content-Type: application/json' \
-d '{"query": "programming language safety"}' | jq .
# Hybrid search (keyword + semantic)
curl -s -X POST http://localhost:3000/api/knowledge/hybrid-search \
-H 'Content-Type: application/json' \
-d '{"query": "Rust safety"}' | jq .
# Stats, related, delete
curl -s http://localhost:3000/api/knowledge/stats | jq .
curl -s "http://localhost:3000/api/knowledge/related?id=<id>" | jq .
curl -s -X DELETE http://localhost:3000/api/knowledge/<id>
Webhook & Cron
Webhooks
# Create webhook
curl -s -X POST http://localhost:3000/api/webhooks \
-H 'Content-Type: application/json' \
-d '{"event_kind": "forge.pipeline.requested"}' | jq .
# List / trigger
curl -s http://localhost:3000/api/webhooks | jq .
curl -s -X POST http://localhost:3000/api/webhooks/<id> \
-H 'Content-Type: application/json' \
-d '{"session_id": "<session-id>"}' | jq .
Cron Scheduling
# Create schedule
curl -s -X POST http://localhost:3000/api/cron \
-H 'Content-Type: application/json' \
-d '{"name": "daily-brief", "cron": "0 9 * * *", "job_kind": "Custom", "payload": {"action": "brief"}}' | jq .
# List / tick / get / update / delete
curl -s http://localhost:3000/api/cron | jq .
curl -s -X POST http://localhost:3000/api/cron/tick | jq .
curl -s http://localhost:3000/api/cron/<id> | jq .
curl -s -X PUT http://localhost:3000/api/cron/<id> -H 'Content-Type: application/json' -d '{"cron": "0 10 * * *"}' | jq .
curl -s -X DELETE http://localhost:3000/api/cron/<id>
Playbooks
# List playbooks
curl -s http://localhost:3000/api/playbooks | jq .
# Create playbook
curl -s -X POST http://localhost:3000/api/playbooks \
-H 'Content-Type: application/json' \
-d '{
"name": "code-to-blog",
"steps": [
{"engine": "code", "action": "analyze", "params": {"path": "."}},
{"engine": "content", "action": "from-code", "params": {"topic": "architecture"}}
]
}' | jq .
# Run / check status
curl -s -X POST http://localhost:3000/api/playbooks/<id>/run | jq .
curl -s http://localhost:3000/api/playbooks/runs | jq .
curl -s http://localhost:3000/api/playbooks/runs/<run-id> | jq .
Starter Kits
curl -s http://localhost:3000/api/kits | jq .
curl -s http://localhost:3000/api/kits/<id> | jq .
curl -s -X POST http://localhost:3000/api/kits/<id>/install | jq .
Terminal & Browser Ports
Terminal
# WebSocket terminal connection
websocat ws://localhost:3000/api/terminal/ws
# Get department terminal pane
curl -s http://localhost:3000/api/terminal/dept/code | jq .
Browser (CDP)
Requires Chrome running with --remote-debugging-port=9222.
curl -s http://localhost:3000/api/browser/status | jq .
curl -s -X POST http://localhost:3000/api/browser/connect \
-H 'Content-Type: application/json' \
-d '{"endpoint": "http://localhost:9222"}' | jq .
curl -s http://localhost:3000/api/browser/tabs | jq .
curl -s -X POST http://localhost:3000/api/browser/observe/0 | jq .
curl -s -X POST http://localhost:3000/api/browser/act \
-H 'Content-Type: application/json' \
-d '{"action": "click", "selector": "#submit-button"}' | jq .
Notification System
# Requires RUSVEL_TELEGRAM_BOT_TOKEN
curl -s -X POST http://localhost:3000/api/system/notify \
-H 'Content-Type: application/json' \
-d '{"message": "Test notification from RUSVEL"}' | jq .
MCP Server
Stdio Transport
cargo run -- --mcp
Expected: Server enters stdio JSON-RPC mode. Reads JSON from stdin, writes to stdout.
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}' \
| cargo run -- --mcp
HTTP Transport
cargo run -- --mcp-http
Expected: MCP server starts on HTTP with /mcp (POST) and /mcp/sse (GET) endpoints.
MCP Tools
| Tool | Input | Expected |
|---|---|---|
session_list | {} | Array of sessions |
session_create | {"name": "mcp-test"} | New session with ID |
mission_today | {"session_id": "<id>"} | Daily plan text |
mission_goals | {"session_id": "<id>"} | Array of goals |
mission_add_goal | {"session_id": "<id>", "title": "MCP goal"} | Goal ID |
visual_inspect | {} | Visual test results |
Frontend Testing
Start the server (cargo run) and open http://localhost:3000 in a browser.
Pages to Check
| URL | What to verify |
|---|---|
/ | Dashboard loads. Shows department cards, system status, recent activity |
/chat | Chat interface loads. Can type messages, SSE stream works |
/dept/forge | Forge overview. Shows mission, goals, tasks |
/dept/code | Code department. Shows analysis tools |
/dept/content | Content department. Shows drafting UI |
/dept/harvest | Harvest department. Shows pipeline |
/dept/gtm | GTM department. Shows CRM |
/dept/finance | Finance department. Shows ledger |
/dept/[id]/chat | Department-scoped chat works for each department |
/dept/[id]/agents | Agent CRUD UI – list, create, edit, delete |
/dept/[id]/skills | Skills CRUD UI |
/dept/[id]/rules | Rules CRUD UI |
/dept/[id]/hooks | Hooks CRUD UI |
/dept/[id]/mcp | MCP server management |
/dept/[id]/workflows | Workflow management |
/dept/[id]/config | Department configuration |
/dept/[id]/events | Event log |
/dept/[id]/actions | Quick actions |
/dept/[id]/engine | Engine-specific UI |
/dept/[id]/terminal | Terminal pane |
/dept/content/calendar | Content calendar view |
/dept/harvest/pipeline | Opportunity pipeline view |
/dept/gtm/contacts | CRM contacts |
/dept/gtm/deals | CRM deals |
/dept/gtm/outreach | Outreach sequences |
/dept/gtm/invoices | Invoicing |
/flows | Flow builder – create/edit DAG workflows |
/knowledge | Knowledge base – ingest, search |
/approvals | Approval queue – pending human approvals (sidebar badge) |
/settings | Global settings |
/settings/spend | Cost analytics |
/terminal | Terminal view |
/database/tables | Database browser – list tables |
/database/schema | Schema viewer |
/database/sql | SQL runner |
Feature Checklist
| Feature | How to test | Expected |
|---|---|---|
| Navigation | Click each sidebar item | Page loads without error |
| Department switching | Click different departments | URL updates, content refreshes |
| Dark mode | Toggle theme (if available) | CSS variables swap, no broken colors |
| Chat streaming | Send a message in /chat | Text appears character by character |
| Tool call cards | Chat with code dept, ask about files | ToolCallCard renders with tool name + result |
| Approval cards | Create content draft needing approval | ApprovalCard appears in sidebar badge |
| Department colors | Visit each department | Each has its own oklch color accent |
| Responsive layout | Resize browser window | Layout adapts, no overflow |
| Error handling | Visit /dept/nonexistent | Error page or redirect, no crash |
| Command palette | Keyboard shortcut (Cmd+K or similar) | CommandPalette opens |
| Onboarding | Fresh install, first visit | OnboardingChecklist or ProductTour appears |
Visual Regression Tests
cd frontend
pnpm install
pnpm test:visual
Expected: Playwright takes screenshots and compares against baselines.
# Update baselines
pnpm test:e2e:update
# AI-powered diff analysis
pnpm test:analyze
Expected: Claude Vision analyzes visual diffs and reports issues.
# Via API
curl -s -X POST http://localhost:3000/api/system/visual-test | jq .
# Self-correction loop
curl -s -X POST http://localhost:3000/api/system/visual-report/self-correct | jq .
Expected: Auto-generates fix skills/rules based on visual diff analysis.
Engine-Specific Testing
Each wired engine has dedicated API endpoints and CLI commands beyond the generic department CRUD.
Code Engine
# Analyze codebase
curl -s -X POST http://localhost:3000/api/dept/code/analyze \
-H 'Content-Type: application/json' \
-d '{"path": "."}' | jq .
Expected: Returns analysis with file count, LOC, complexity metrics.
# Search symbols
curl -s "http://localhost:3000/api/dept/code/search?query=DepartmentApp" | jq .
Expected: BM25 ranked results with file paths and line numbers.
# CLI equivalents
cargo run -- code analyze .
cargo run -- code search "DepartmentApp"
Content Engine
# Draft content (requires Ollama)
curl -s -X POST http://localhost:3000/api/dept/content/draft \
-H 'Content-Type: application/json' \
-d '{"topic": "Building CLI tools in Rust"}' | jq .
Expected: Returns markdown draft with title, body, metadata.
# Code-to-content pipeline
curl -s -X POST http://localhost:3000/api/dept/content/from-code \
-H 'Content-Type: application/json' \
-d '{"path": ".", "topic": "architecture"}' | jq .
Expected: Analyzes code first, then generates content based on analysis.
# List content
curl -s http://localhost:3000/api/dept/content/list | jq .
Expected: Array of content items with status (Draft, Adapted, Scheduled, Published).
# Approve content
curl -s -X PATCH http://localhost:3000/api/dept/content/<id>/approve | jq .
Expected: Content status changes. Approval status updated.
# Schedule content
curl -s -X POST http://localhost:3000/api/dept/content/schedule \
-H 'Content-Type: application/json' \
-d '{"content_id": "<id>", "scheduled_at": "2026-04-01T09:00:00Z"}' | jq .
Expected: Content marked as Scheduled with timestamp.
# Publish content
curl -s -X POST http://localhost:3000/api/dept/content/publish \
-H 'Content-Type: application/json' \
-d '{"content_id": "<id>"}' | jq .
Expected: Content published (or job created with approval gate).
# List scheduled content
curl -s http://localhost:3000/api/dept/content/scheduled | jq .
# CLI
cargo run -- content draft "test topic"
Harvest Engine
# Scan for opportunities (requires Ollama)
curl -s -X POST http://localhost:3000/api/dept/harvest/scan | jq .
Expected: Scans configured sources, returns discovered opportunities.
# Score an opportunity
curl -s -X POST http://localhost:3000/api/dept/harvest/score \
-H 'Content-Type: application/json' \
-d '{"opportunity_id": "<id>"}' | jq .
Expected: Returns score (0.0-1.0) with scoring rationale.
# Generate proposal
curl -s -X POST http://localhost:3000/api/dept/harvest/proposal \
-H 'Content-Type: application/json' \
-d '{"opportunity_id": "<id>"}' | jq .
Expected: AI-generated proposal draft (approval-gated).
# Pipeline stats
curl -s http://localhost:3000/api/dept/harvest/pipeline | jq .
Expected: Counts by stage (Cold, Contacted, Qualified, ProposalSent, Won, Lost).
# List opportunities
curl -s http://localhost:3000/api/dept/harvest/list | jq .
# Advance opportunity
curl -s -X POST http://localhost:3000/api/dept/harvest/advance \
-H 'Content-Type: application/json' \
-d '{"opportunity_id": "<id>", "stage": "Contacted"}' | jq .
Expected: Opportunity stage updated.
# Record outcome
curl -s -X POST http://localhost:3000/api/dept/harvest/outcome \
-H 'Content-Type: application/json' \
-d '{"opportunity_id": "<id>", "outcome": "Won", "value": 5000.0}' | jq .
Expected: Outcome recorded with value.
# List outcomes
curl -s http://localhost:3000/api/dept/harvest/outcomes | jq .
# CLI
cargo run -- harvest pipeline
GTM Engine
# Create contact
curl -s -X POST http://localhost:3000/api/dept/gtm/contacts \
-H 'Content-Type: application/json' \
-d '{"name": "Alice", "emails": ["alice@example.com"], "company": "Acme"}' | jq .
Expected: Contact created with ID.
# List contacts
curl -s http://localhost:3000/api/dept/gtm/contacts | jq .
# List deals
curl -s http://localhost:3000/api/dept/gtm/deals | jq .
# Advance deal
curl -s -X POST http://localhost:3000/api/dept/gtm/deals/advance \
-H 'Content-Type: application/json' \
-d '{"deal_id": "<id>", "stage": "Qualified"}' | jq .
Outreach Sequences
# Create sequence
curl -s -X POST http://localhost:3000/api/dept/gtm/outreach/sequences \
-H 'Content-Type: application/json' \
-d '{
"name": "cold-intro",
"steps": [
{"kind": "email", "template": "Hi {{name}}, ..."},
{"kind": "follow_up", "delay_days": 3, "template": "Following up..."}
]
}' | jq .
# List sequences
curl -s http://localhost:3000/api/dept/gtm/outreach/sequences | jq .
# Activate sequence
curl -s -X POST http://localhost:3000/api/dept/gtm/outreach/sequences/<id>/activate | jq .
# Execute outreach (approval-gated)
curl -s -X POST http://localhost:3000/api/dept/gtm/outreach/execute | jq .
Invoicing
# Create invoice
curl -s -X POST http://localhost:3000/api/dept/gtm/invoices \
-H 'Content-Type: application/json' \
-d '{
"contact_id": "<id>",
"amount": 5000.00,
"description": "Consulting services",
"due_date": "2026-04-15"
}' | jq .
# Get invoice
curl -s http://localhost:3000/api/dept/gtm/invoices/<id> | jq .
# List invoices
curl -s http://localhost:3000/api/dept/gtm/invoices | jq .
# Update status
curl -s -X POST http://localhost:3000/api/dept/gtm/invoices/<id>/status \
-H 'Content-Type: application/json' \
-d '{"status": "sent"}' | jq .
Flow Engine (DAG Workflows)
CRUD
# Create a flow
curl -s -X POST http://localhost:3000/api/flows \
-H 'Content-Type: application/json' \
-d '{
"name": "test-flow",
"nodes": [
{"id": "start", "type": "code", "config": {"command": "echo start"}},
{"id": "check", "type": "condition", "config": {"expression": "true"}},
{"id": "end", "type": "code", "config": {"command": "echo done"}}
],
"edges": [
{"from": "start", "to": "check"},
{"from": "check", "to": "end"}
]
}' | jq .
Expected: Flow created with ID. DAG is validated (no cycles).
curl -s http://localhost:3000/api/flows | jq .
curl -s http://localhost:3000/api/flows/<id> | jq .
curl -s -X PUT http://localhost:3000/api/flows/<id> \
-H 'Content-Type: application/json' \
-d '{"name": "updated-flow"}' | jq .
curl -s -X DELETE http://localhost:3000/api/flows/<id>
Execution
# Run a flow
curl -s -X POST http://localhost:3000/api/flows/<id>/run | jq .
Expected: Returns execution ID. Nodes execute in topological order.
# List executions
curl -s http://localhost:3000/api/flows/<id>/executions | jq .
# Get execution details
curl -s http://localhost:3000/api/flows/executions/<exec-id> | jq .
# Get checkpoint
curl -s http://localhost:3000/api/flows/executions/<exec-id>/checkpoint | jq .
# Resume suspended execution
curl -s -X POST http://localhost:3000/api/flows/executions/<exec-id>/resume | jq .
# Retry a failed node
curl -s -X POST http://localhost:3000/api/flows/executions/<exec-id>/retry/<node-id> | jq .
Node Types
curl -s http://localhost:3000/api/flows/node-types | jq .
Expected: ["code", "condition", "agent"]. If RUSVEL_FLOW_PARALLEL_EVALUATE=1, also includes parallel_evaluate.
Templates
curl -s http://localhost:3000/api/flows/templates/cross-engine-handoff | jq .
Expected: Pre-built flow template showing cross-engine data handoff.
Capability Engine (!build)
Via API
curl -s -X POST http://localhost:3000/api/capability/build \
-H 'Content-Type: application/json' \
-d '{"description": "A GitHub PR review agent"}' | jq .
Expected: Returns bundle of created entities (agent, skills, rules, hooks, MCP config).
Via Chat
curl -N http://localhost:3000/api/dept/forge/chat \
-H 'Content-Type: application/json' \
-d '{"message": "!build a daily standup summary agent"}'
Expected: Agent detects !build prefix, invokes capability engine, creates entities, reports what was installed.
Smoke Test Checklist
Quick pass to verify everything works after a build. Run these in order – takes ~15 minutes.
Phase 1: Build & Boot (no Ollama needed)
| # | Command | Pass if |
|---|---|---|
| 1 | cargo build | Compiles without errors |
| 2 | cargo test | ~645 tests pass (workspace sum; see docs/status/current-state.md) |
| 3 | cargo run -- --help | Shows help with all subcommands |
| 4 | cargo run -- session create smoke-test | Prints session UUID |
| 5 | cargo run -- session list | Shows the smoke-test session |
| 6 | cargo run -- finance status | Prints status (no crash) |
| 7 | cargo run -- code analyze . | Prints analysis results |
| 8 | cargo run -- harvest pipeline | Prints pipeline (all zeros OK) |
Phase 2: Web Server (no Ollama needed)
Start cargo run in a terminal, then in another:
| # | Command | Pass if |
|---|---|---|
| 9 | curl -s localhost:3000/api/health | Returns 200 |
| 10 | curl -s localhost:3000/api/departments | jq length | Returns 14 |
| 11 | curl -s localhost:3000/api/agents | jq length | Returns >= 7 (seeded) |
| 12 | curl -s localhost:3000/api/skills | jq length | Returns >= 5 (seeded) |
| 13 | curl -s localhost:3000/api/rules | jq length | Returns >= 5 (seeded) |
| 14 | curl -s localhost:3000/api/config/tools | jq length | Returns >= 22 |
| 15 | curl -s localhost:3000/api/db/tables | jq length | Returns >= 5 |
| 16 | Open http://localhost:3000 in browser | Dashboard renders |
Phase 3: LLM Features (requires Ollama)
| # | Command | Pass if |
|---|---|---|
| 17 | cargo run -- forge mission today | Streams daily plan |
| 18 | curl -N localhost:3000/api/chat -H 'Content-Type: application/json' -d '{"message":"hi"}' | SSE stream with text deltas |
| 19 | curl -N localhost:3000/api/dept/code/chat -H 'Content-Type: application/json' -d '{"message":"list files"}' | SSE stream with tool calls |
| 20 | cargo run -- content draft "test" | Streams content draft |
Phase 4: Interactive Surfaces
| # | Action | Pass if |
|---|---|---|
| 21 | cargo run -- shell -> type help -> type exit | REPL starts, shows help, exits cleanly |
| 22 | cargo run -- shell -> use finance -> status -> back | Context switching works |
| 23 | cargo run -- --tui -> press q | TUI renders 4 panels, exits cleanly |
Phase 5: CRUD Cycle
Pick any entity (agents, skills, rules, hooks, workflows) and verify the full lifecycle:
| # | Action | Pass if |
|---|---|---|
| 24 | POST create | Returns 200 with ID |
| 25 | GET by ID | Returns created entity |
| 26 | GET list | Entity appears in list |
| 27 | PUT update | Returns updated entity |
| 28 | GET by ID again | Shows updated fields |
| 29 | DELETE | Returns 200/204 |
| 30 | GET by ID again | Returns 404 |
Example CRUD cycle (agents):
# Create
ID=$(curl -s -X POST localhost:3000/api/agents \
-H 'Content-Type: application/json' \
-d '{"name":"smoke-agent","role":"Test","instructions":"test"}' | jq -r '.id')
echo "Created: $ID"
# Read
curl -s localhost:3000/api/agents/$ID | jq .name
# Expected: "smoke-agent"
# Update
curl -s -X PUT localhost:3000/api/agents/$ID \
-H 'Content-Type: application/json' \
-d '{"name":"updated-agent","instructions":"updated"}' | jq .name
# Expected: "updated-agent"
# List (verify it's there)
curl -s localhost:3000/api/agents | jq '.[].name' | grep updated-agent
# Expected: "updated-agent"
# Delete
curl -s -X DELETE localhost:3000/api/agents/$ID -w "\n%{http_code}\n"
# Expected: 200 or 204
# Verify gone
curl -s -o /dev/null -w "%{http_code}" localhost:3000/api/agents/$ID
# Expected: 404
Automated Test Suite
For reference, the full automated suite:
# Full workspace
cargo test
# Individual crates
cargo test -p rusvel-core
cargo test -p rusvel-db
cargo test -p rusvel-api
cargo test -p rusvel-llm
cargo test -p forge-engine
cargo test -p content-engine
cargo test -p harvest-engine
cargo test -p code-engine
cargo test -p flow-engine
cargo test -p rusvel-tool
cargo test -p rusvel-agent
# Line coverage
./scripts/coverage.sh
# Benchmark
cargo bench -p rusvel-app --bench boot
Engineering
Internal engineering documentation, patterns, and plans.
- Concept Hierarchy — Domain model relationships
- Design Patterns — Patterns used across the codebase
- Testing Plan — Automated test strategy
- Refactoring Plan — Planned improvements
Concept Hierarchy
The domain model spine – the ontology that every crate, API route, and UI component maps onto. All refactoring decisions trace back to which level of this hierarchy they affect.
Five levels
Level 0: Platform (RUSVEL)
Level 1: Session (workspace scope)
Level 2: Department (bounded context)
Level 3: Domain Entity (per-department)
Level 4: Cross-cutting Primitive (shared infrastructure)
Level 0: Platform
RUSVEL
|- Identity: single binary, single human, infinite leverage
|- Constraint: SQLite WAL, tokio async, Rust + SvelteKit
'- Invariant: every action traceable to a Session
The platform is the composition root (rusvel-app), the port traits (rusvel-core), and the adapter crates. This level changes only when fundamental infrastructure changes.
Level 1: Session
Session
|- Owns: Runs, Threads, Goals, Events, Jobs, Config overrides
|- Scopes: all state is session-namespaced
|- Lifecycle: create -> active -> archived
'- Future: Session becomes Workspace when multi-user
The session is the unit of isolation. All queries, all tool calls, all agent runs are scoped to a session. This is RUSVEL’s equivalent of tenancy.
Level 2: Department
Department (DepartmentApp)
|- Identity: string ID, manifest, icon, color
|- Owns: Engine, Tools, Skills, Rules, Hooks, Agents, Workflows
|- Communicates via: Events (pub/sub), Jobs (async work), ObjectStore
|- Never: imports another department's crate
'- Types:
|- Wired (6): forge, code, harvest, content, gtm, flow
|- Skeleton (7): finance, product, growth, distro, legal, support, infra
'- Shell (1): messaging
Each department is a bounded context. Departments communicate only through cross-cutting primitives (Level 4), never by importing each other’s types.
Level 3: Domain entities
Each department owns its entities. Entities from different departments reference each other by ID, never by owned struct.
| Department | Entities |
|---|---|
| Forge | Goal, Task, Plan, Review, Persona, AgentProfile |
| Code | Repository, SymbolGraph, Symbol, Metric, SearchResult |
| Harvest | Opportunity, Proposal, Pipeline, Source, Score |
| Content | ContentItem, CalendarEntry, PlatformAdapter, PublishResult |
| GTM | Contact, Deal, OutreachSequence, Step, Invoice |
| Flow | Workflow, Node, Edge, Execution, Checkpoint, NodeResult |
| Finance | Ledger, Transaction, TaxEstimate, RunwayForecast |
| Product | Roadmap, Feature, PricingTier, FeedbackItem |
| Growth | Funnel, Cohort, KPI, Experiment |
| Distro | Listing, SEOProfile, AffiliateProgram, Partnership |
| Legal | Contract, ComplianceCheck, IPRecord, LicenseAgreement |
| Support | Ticket, KBArticle, NPSSurvey, AutoTriageRule |
| Infra | Deployment, Monitor, Incident, Pipeline |
Level 4: Cross-cutting primitives
Shared infrastructure that all departments use through port traits:
| Primitive | Purpose | Mutability |
|---|---|---|
| Event | Immutable record of what happened | Append-only |
| Job | Async work item with state machine | Mutable (state transitions) |
| Tool | Registered capability with handler | Immutable after registration |
| Skill | Stored prompt template | Mutable (CRUD) |
| Rule | System prompt fragment | Mutable (CRUD) |
| Hook | Event-triggered automation | Mutable (CRUD) |
| Agent | LLM + tools + persona + memory | Stateful per run |
| Approval | Human gate on job or publishing | Mutable (approve/reject) |
Level 5: Infrastructure ports
The port traits that connect departments to the outside world:
LlmPort -> raw model access (generate, stream, embed)
AgentPort -> orchestrated LLM (tool loop, memory, verification)
StoragePort -> 5 sub-stores (events, objects, sessions, jobs, metrics)
EventPort -> pub/sub + persistence
JobPort -> central async work queue
ToolPort -> tool registry + execution + permission
MemoryPort -> session-scoped context + FTS5 search
ConfigPort -> layered settings (global -> dept -> session)
AuthPort -> opaque credential handles
EmbeddingPort -> text -> dense vectors
VectorStorePort -> similarity search
ChannelPort -> outbound notifications
TerminalPort -> PTY multiplexer
BrowserPort -> Chrome DevTools Protocol
DeployPort -> deployment operations
SessionPort -> session lifecycle
Design rules
- Concept ownership: every concept belongs to exactly one level
- Cross-level references: use IDs (newtypes), never owned structs
- A Department never owns a Session. They reference each other via
SessionIdand department string ID - Shared kernel: types in
rusvel-core/src/domain.rsare the small set all departments agree on - Anti-corruption: if a department needs another department’s data, it goes through ObjectStore or Events – never through direct type imports
Visual map
┌─────────────────────┐
│ RUSVEL (L0) │
│ Single Binary │
└──────────┬──────────┘
│
┌──────────┴──────────┐
│ Session (L1) │
│ Scope + Isolation │
└──────────┬──────────┘
│
┌────────┬───────────┼───────────┬────────┐
│ │ │ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│Forge│ │Code │ │GTM │ │Flow │ │ ... │
│(L2) │ │(L2) │ │(L2) │ │(L2) │ │(L2) │
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └─────┘
│ │ │ │
Goals Symbols Contacts Nodes (L3)
Tasks Metrics Deals Edges
Plans Repos Invoices Checkpoints
│ │ │ │
└───────┴─────┬─────┴──────────┘
│
┌─────────┴─────────┐
│ Cross-cutting │
│ Primitives (L4) │
│ Events, Jobs, │
│ Tools, Skills │
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ Port Traits │
│ (L5 infra) │
│ LLM, Storage, │
│ Agent, Event │
└───────────────────┘
Design Patterns
How RUSVEL applies Gang of Four patterns, SOLID principles, and modern system design – all adapted to Rust’s type system. See also the Architecture overview and ADRs.
GoF patterns in Rust
Rust has no inheritance. Each GoF pattern either translates via traits/generics, collapses into an enum, or becomes unnecessary because ownership solves the underlying problem.
Creational
| Pattern | Rust Translation | RUSVEL Usage |
|---|---|---|
| Builder | Method chaining, typestate for required fields | DepartmentManifest, LlmRequest, AgentConfig |
| Factory Method | Trait with create() method | DepartmentApp::register() |
| Abstract Factory | Composition root returns trait objects | rusvel-app/src/main.rs wires all adapters |
| Singleton | Arc<T> passed at construction (no statics) | All ports are Arc<dyn Port> |
| Prototype | #[derive(Clone)] | DepartmentManifest |
Structural
| Pattern | Rust Translation | RUSVEL Usage |
|---|---|---|
| Adapter | Struct wraps foreign type, implements local trait | Every rusvel-* adapter crate |
| Decorator | tower::Layer wraps Service | CostTrackingLlm, planned: TimeoutLayer, RetryLayer |
| Facade | Struct composes multiple ports | AgentRuntime over LLM + Tool + Memory (ADR-009) |
| Proxy | Same trait, forwarding + filtering | ScopedToolRegistry per-department filtering |
| Composite | Enum with recursive Box<Self> variants | Flow DAG nodes (planned: SubFlowNode) |
| Bridge | Trait separates abstraction from implementation | All port traits in rusvel-core |
| Flyweight | Arc<T> for shared immutable state | Arc<dyn LlmPort> shared across engines |
Behavioral
| Pattern | Rust Translation | RUSVEL Usage |
|---|---|---|
| Strategy | Generic <S: Strategy> (static) or dyn Strategy (dynamic) | Port trait dispatch; ModelTier routing |
| Observer | tokio::sync::broadcast / mpsc channels | AgentEvent stream, EventPort broadcast |
| Command | Enum variants + match dispatch | JobKind in worker, ToolHandler closures |
| Chain of Responsibility | tower::Layer middleware chain | Axum middleware stack |
| State | Enum (runtime) or typestate (compile-time) | JobStatus, FlowExecutionStatus |
| Template Method | Trait with default method calling abstract methods | LlmPort::stream() defaults to generate() |
| Visitor | Trait with visit_* methods, separate traversal | Planned: SymbolVisitor in code-engine |
| Mediator | Central event bus or registry | EventPort as cross-department mediator |
| Iterator | Iterator trait (first-class in Rust) | Standard throughout |
When NOT to use GoF in Rust
- Singleton: Never use
static Mutex<T>. PassArc<T>via constructors. - Trait objects for closed sets: If all variants are known, use an enum – faster (no vtable), exhaustively matchable.
- Observer with callback vectors: Use async channels instead – they compose with tokio’s scheduler.
- Visitor: Unnecessary when
matchon exhaustive enums covers all cases.
SOLID in Rust
Single Responsibility
Each crate has one reason to change. The <2000 lines per crate rule enforces this. Crates that grow too large are split.
Open/Closed
Traits are Rust’s extension mechanism. Adding a new LlmPort implementation requires zero changes to rusvel-core. The DepartmentApp pattern (ADR-014) is the purest OCP expression – adding a department means adding a dept-* crate, not modifying existing code.
Liskov Substitution
Any Arc<dyn LlmPort> must behave according to the LlmPort contract. Rust enforces signature compatibility; integration tests validate semantic contracts.
Interface Segregation
StoragePort splits into 5 focused sub-stores (ADR-004):
StoragePort
|- EventStore (append-only events)
|- ObjectStore (CRUD domain objects)
|- SessionStore (session lifecycle)
|- JobStore (job queue semantics)
'- MetricStore (time-series metrics)
Engines depend only on the sub-stores they use.
Dependency Inversion
The hexagonal architecture IS dependency inversion. Engines depend on abstractions (rusvel-core traits), not on concrete adapters. rusvel-app is the only place that depends on concrete types. Verified via just check-boundaries.
Rust-specific patterns
Typestate
Encode operation ordering in the type system. The build() method is only available when required fields are set:
#![allow(unused)]
fn main() {
pub struct ManifestBuilder<Id, Prompt> {
id: Id,
prompt: Prompt,
// ...
}
// Can't call build() until both id and system_prompt are set
impl ManifestBuilder<HasId, HasPrompt> {
pub fn build(self) -> DepartmentManifest { /* ... */ }
}
}
Used for: DepartmentManifest, AgentConfig, LlmRequest. See Typestate Pattern – Cliffle.
Newtype
Prevents mixing IDs at compile time:
#![allow(unused)]
fn main() {
pub struct SessionId(Uuid); // Cannot be confused with...
pub struct AgentId(Uuid); // ...this at the call site
}
Used throughout rusvel-core for all domain identifiers.
Tower Service
The foundational middleware abstraction in the Rust async ecosystem. poll_ready() implements backpressure – the caller must check readiness before sending:
#![allow(unused)]
fn main() {
let service = ServiceBuilder::new()
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(RateLimitLayer::new(10, Duration::from_secs(1)))
.service(provider);
}
Used in: rusvel-api (Axum middleware). Planned for: LlmPort, AgentPort, ToolPort.
Modern system design patterns
DepartmentApp as Microkernel
RUSVEL’s DepartmentApp pattern is a microkernel at the application level:
- Kernel:
rusvel-app+rusvel-coreport traits + composition root - Plugins: Each
dept-*crate is a self-contained service - Message passing:
EventPort(domain events),JobPort(async work) - Capability declaration:
DepartmentManifestdeclares what each plugin provides
Event-Driven Architecture
Three EDA patterns in use:
- Event Notification – engines emit events, listeners react (hook dispatch)
- Central Job Queue – orchestration-style async work (ADR-003)
- Event Persistence – append-only
EventStorefor audit trail (ADR-005)
Planned additions:
- Saga pattern – compensation edges in flow-engine DAGs
- Projections – denormalized read views rebuilt from event streams
Agent Orchestration Hierarchy
ForgeEngine (supervisor)
|-- DelegateAgentTool --> ContentEngine agent
|-- DelegateAgentTool --> HarvestEngine agent
'-- DelegateAgentTool --> CodeEngine agent
Delegation is depth-limited (max 3 levels) with per-department tool scoping.
Self-Improving System
The feedback loop substrate:
- Agent runs –> emit success/failure events
- Failure reflection agent analyzes transcript
- Generates skill/rule suggestions –> stored as drafts
- Human approves –> skill/rule promoted to active
- Successful tool sequences mined as reusable templates
References
- GoF in Rust – fadeevab/design-patterns-rust
- Rust Design Patterns – rust-unofficial
- Hexagonal Architecture in Rust – howtocodeit.com
- Tower Service – Tokio blog
- Typestate – Cliffle
- CQRS in Rust – doc.rust-cqrs.org
- Refactoring.guru – Rust patterns
Testing Plan
RUSVEL targets ~1200+ tests across Rust and frontend, with staged workspace coverage toward 55-65%. This page summarizes the strategy; the full checklist lives in docs/plans/comprehensive-testing-plan.md.
Current state
| Area | Count | Notes |
|---|---|---|
| Rust tests (workspace sum) | ~645 | Sum of running N tests from cargo test --workspace; see docs/status/current-state.md |
| Rust integration tests | ~92 | Strong API smoke tests, engine round-trips |
| Benchmarks | 2 | Criterion: DB open + registry load |
| Frontend visual tests | 27 routes | Playwright screenshot comparison |
| Frontend unit tests | 0 | No Vitest setup yet |
| Property-based tests | 0 | No proptest yet |
| Fuzz tests | 0 | No cargo-fuzz targets yet |
| Snapshot tests | 0 | No insta snapshots yet |
CI coverage floor: 42% (cargo-llvm-cov, --fail-under-lines).
Testing pyramid
/\
/ \ E2E (Playwright)
/ 27 \ Visual regression + interaction flows
/--------\
/ \ Integration Tests
/ ~150 \ API handlers, engine pipelines, cross-crate
/--------------\
/ \ Unit Tests
/ ~800+ \ Domain types, port impls, utils, components
/--------------------\
/ \ Property + Fuzz
/ ~50+ \ Parsers, serialization, scoring
/__________________________\
Most tests at the unit level. Integration tests for cross-boundary flows. E2E for critical user journeys only.
Sprint overview
| Sprint | Theme | New Tests |
|---|---|---|
| 0 | Baseline capture | 0 (measurement) |
| 1 | Rust unit tests (rusvel-core to 90%, all crates > 50%) | ~200 |
| 2 | Frontend Vitest setup + api.ts + stores | ~100 |
| 3 | API negative path tests (400/401/404/405 for all routes) | ~120 |
| 4 | Rust engine contract tests (13 engines) | ~80 |
| 5 | Snapshot tests (insta) + property-based tests (proptest) | ~70 |
| 6 | Frontend component tests (@testing-library/svelte) | ~60 |
| 7 | Benchmarks (Criterion) + CI hardening | ~15 |
| 8 | Fuzz testing (cargo-fuzz, nightly) | ~8 |
Coverage targets by layer
These align with docs/testing/coverage-strategy.md:
| Layer | Current | Target |
|---|---|---|
| rusvel-core | ~60% | 85-95% |
| Engines (13) | ~50% | 70-90% |
| Adapters | ~35% | 60-80% |
| rusvel-api | ~45% | 50-70% |
| rusvel-app | ~25% | 30-50% |
| Frontend (lib/) | 0% | 50% |
| Workspace total | ~42% | ~55-65% |
Test infrastructure
Rust
- Test harness:
crates/rusvel-api/tests/common/mod.rsprovidesTestHarnesswith temp SQLite, stub LLM, mock platform adapters - Mock pattern: Port-based mocking via
FakeStorage,RecordingEvents,StubLlmstructs - Framework:
tokioasync runtime,tempfilefor isolation,criterionfor benchmarks - Coverage:
cargo-llvm-covwith HTML reports via./scripts/coverage.sh
Frontend
- Visual regression: Playwright + Claude Vision analysis (27 routes)
- Unit tests (planned): Vitest + @testing-library/svelte
- Test data: Global setup seeds sessions and goals via API
Key principles
- Port-based mocking – engines receive
Arc<dyn Trait>mocks, never real adapters in unit tests - No mocking the database in integration tests – use temp SQLite for realistic behavior
- Negative paths are required – every API endpoint needs 400, 401, 404, 405 tests
- Property tests for parsers – proptest on code-engine parser, harvest scoring, cron expressions
- Snapshots for serialization – insta catches accidental field renames in API responses
Running tests
# Rust
cargo test # Full workspace
cargo test -p rusvel-core # Single crate
cargo bench -p rusvel-app --bench boot # Benchmarks
./scripts/coverage.sh # HTML coverage report
# Frontend
cd frontend
pnpm test # Vitest unit tests (planned)
pnpm test:visual # Playwright visual regression
pnpm test:e2e # All Playwright tests
pnpm test:coverage # Vitest coverage (planned)
Refactoring Plan
Structural refactoring for design pattern alignment, extensibility, and scalability. The full checklist lives in docs/plans/comprehensive-refactoring-plan.md.
Strategic themes
- Type safety over stringly-typed – replace magic strings (event kinds, job kinds, tool names) with compile-time-safe constants and validated registries
- GoF patterns as Rust idioms – Builder (typestate), Strategy (generics), Observer (typed channels), Decorator (tower layers), Command (typed enums), Visitor (AST traversal)
- Scalability substrate – saga orchestration, association graphs, self-improvement loops, Tower Service composability
What is NOT changing: hexagonal port/adapter boundary, DepartmentApp trait, composition root, single-binary constraint, SQLite WAL, or any ADR decisions.
Current patterns (verified)
| Pattern | Where | Quality |
|---|---|---|
| Strategy | Port traits (LlmPort, AgentPort, etc.) | Excellent |
| Abstract Factory | Composition root (rusvel-app) | Excellent |
| Facade | AgentRuntime over LLM+Tool+Memory | Excellent |
| Adapter | Every rusvel-* crate | Excellent |
| Observer | AgentEvent over mpsc, EventPort broadcast | Good |
| Command | JobKind dispatch, ToolHandler closures | Good |
| Builder | DepartmentManifest::new() | Partial |
| Proxy | ScopedToolRegistry | Good |
Gaps to address
| Pattern | Gap | Sprint |
|---|---|---|
| Typestate Builder | No compile-time field enforcement | 2 |
| Decorator (Tower) | No composable middleware on ports | 3 |
| Visitor | code-engine has no visitor trait | 4 |
| Typed Observer | Event subscriptions use string matching | 4 |
| State (Typestate) | Job/flow status transitions unchecked | 4 |
| Composite | Flow nodes are flat (no nesting) | 8 |
| Mediator | No cross-department agent delegation | 9 |
| Saga | No compensation logic in multi-step flows | 6 |
Sprint overview
| Sprint | Theme | Key Deliverables |
|---|---|---|
| 1 | Type safety | Event kind constants, tool collision detection, job registry |
| 2 | Builders & factories | Typestate DepartmentManifest, PlatformFactory trait, TestFactory |
| 3 | Structural | AppState decomposition (25 fields -> 5 sub-states), Tower layers on LlmPort |
| 4 | Behavioral | TypedEvent system, JobCommand trait, SymbolVisitor, DelegateAgentTool |
| 5 | DDD | AggregateRoot trait, ObjectStore associations, value object enforcement |
| 6 | Events | Saga compensation, lightweight projections, event replay |
| 7 | Plugins | Manifest validation, port requirement checks, capability tokens |
| 8 | Workflows | Checkpoint/resume, SubFlow/Parallel/Loop nodes, per-node retry |
| 9 | Agents | Hierarchical delegation, supervisor pattern, blackboard cross-engine |
| 10 | Scalability | Magic number extraction, graceful shutdown, data-driven CLI |
| 11 | Frontend | Typed API client, command/query stores, error boundaries |
| 12 | Self-improvement | Failure reflection loop, skill accumulation, experience replay |
Structural issues being fixed
| Issue | Severity | Sprint |
|---|---|---|
| Monolithic AppState (~25 fields) | High | 3 |
| Tool name collisions silently overwrite | High | 1 |
| Event kinds are string literals | Medium | 1 |
| Job results stored in untyped metadata | Medium | 4 |
| No association graph between domain objects | Medium | 5 |
| Magic numbers scattered in code | Low | 10 |
| Job worker has no graceful shutdown | Medium | 10 |
| CLI department variants repeated 9x | Low | 10 |