huskies: merge 858

This commit is contained in:
dave
2026-04-29 10:41:32 +00:00
parent be5db846cc
commit 11d111360d
79 changed files with 265 additions and 0 deletions
+9
View File
@@ -1,12 +1,19 @@
//! Agent subsystem — types, configuration, and orchestration for coding agents.
/// Acceptance-gate checks (clippy, tests, doc coverage).
pub mod gates;
/// Agent start/stop and pipeline advancement on completion.
pub mod lifecycle;
/// Constructs the system prompt sent to coding agents.
pub mod local_prompt;
/// Merge-conflict resolution helpers.
pub mod merge;
pub(crate) mod pool;
pub(crate) mod pty;
/// Runtime backends (Claude Code, OpenAI, Gemini) that execute agent sessions.
pub mod runtime;
/// Persistent session-ID storage for agent resume support.
pub mod session_store;
/// Token-usage tracking and budget estimation.
pub mod token_usage;
use crate::config::AgentConfig;
@@ -74,6 +81,7 @@ pub enum AgentEvent {
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
/// Lifecycle state of an agent session.
pub enum AgentStatus {
Pending,
Running,
@@ -212,6 +220,7 @@ impl TokenUsage {
}
#[derive(Debug, Serialize, Clone)]
/// Snapshot of a running or completed agent, exposed via the HTTP API.
pub struct AgentInfo {
pub story_id: String,
pub agent_name: String,
+2
View File
@@ -48,6 +48,7 @@ pub struct AgentPool {
}
impl AgentPool {
/// Create a new agent pool bound to the given HTTP port and event channel.
pub fn new(port: u16, watcher_tx: broadcast::Sender<WatcherEvent>) -> Self {
let pool = Self {
agents: Arc::new(Mutex::new(HashMap::new())),
@@ -98,6 +99,7 @@ impl AgentPool {
pool
}
/// Return the HTTP port this pool's agents connect to.
pub fn port(&self) -> u16 {
self.port
}
+1
View File
@@ -22,6 +22,7 @@ pub struct ClaudeCodeRuntime {
}
impl ClaudeCodeRuntime {
/// Create a new Claude Code runtime with shared child-killer registry and event channel.
pub fn new(
child_killers: Arc<Mutex<HashMap<String, Box<dyn ChildKiller + Send + Sync>>>>,
watcher_tx: broadcast::Sender<WatcherEvent>,
+1
View File
@@ -43,6 +43,7 @@ pub struct GeminiRuntime {
}
impl GeminiRuntime {
/// Create a new Gemini runtime instance.
pub fn new() -> Self {
Self {
cancelled: Arc::new(AtomicBool::new(false)),
+1
View File
@@ -30,6 +30,7 @@ pub struct OpenAiRuntime {
}
impl OpenAiRuntime {
/// Create a new OpenAI runtime instance.
pub fn new() -> Self {
Self {
cancelled: Arc::new(AtomicBool::new(false)),
+5
View File
@@ -4,13 +4,18 @@
//! sending and editing messages, allowing the bot logic (commands, htop,
//! notifications) to work against any chat platform — Matrix, WhatsApp, etc.
/// Bot command registry and dispatch — parses and routes incoming chat messages.
pub mod commands;
/// Chat history utilities — loading and serialising conversation history.
pub mod history;
pub(crate) mod lookup;
#[cfg(test)]
pub(crate) mod test_helpers;
/// Rate-limit retry timers — stores and fires scheduled retry reminders.
pub mod timer;
/// Platform transports — pluggable backends (Matrix, Slack, WhatsApp, Discord).
pub mod transport;
/// Chat utility functions — shared helpers for message formatting and bot logic.
pub mod util;
use async_trait::async_trait;
@@ -25,6 +25,7 @@ pub struct DiscordTransport {
}
impl DiscordTransport {
/// Creates a new `DiscordTransport` authenticated with the given bot token.
pub fn new(bot_token: String) -> Self {
Self {
bot_token,
+5
View File
@@ -7,10 +7,15 @@
//! receives `MESSAGE_CREATE` events, and dispatches commands.
//! - [`DiscordContext`] — shared context for the bot.
/// Discord bot command handlers — parses and dispatches bot commands from Discord messages.
pub mod commands;
/// Discord message formatter — converts markdown to Discord-compatible markup.
pub mod format;
/// Discord Gateway WebSocket — connects to the Discord Gateway and handles MESSAGE_CREATE events.
pub mod gateway;
/// Discord conversation history — loads prior chat history for context.
pub mod history;
/// DiscordTransport — `ChatTransport` implementation for the Discord Bot API.
pub mod meta;
pub use commands::DiscordContext;
@@ -1,10 +1,17 @@
//! Matrix bot — sub-modules for the Matrix chat bot implementation.
/// Bot context — shared state passed to Matrix bot command handlers.
pub mod context;
/// Matrix message formatter — converts markdown to Matrix HTML.
pub mod format;
/// Conversation history — loads and saves per-room chat history.
pub mod history;
/// Mention detection — identifies messages that mention the bot user.
pub mod mentions;
/// Message handlers — processes incoming Matrix room messages.
pub mod messages;
/// Bot run loop — the main async task that drives the Matrix sync loop.
pub mod run;
/// Device verification — handles Matrix cross-signing and emoji verification flows.
pub mod verification;
// Re-export all public types so existing import paths continue to work.
+9
View File
@@ -15,16 +15,25 @@
//! Multi-room support: configure `room_ids = ["!room1:…", "!room2:…"]` in
//! `bot.toml`. Each room maintains its own independent conversation history.
/// Auto-assign handler — listens for pipeline events and assigns stories to free agents.
pub mod assign;
mod bot;
/// Matrix bot command handlers — parses and routes bot commands from Matrix messages.
pub mod commands;
pub(crate) mod config;
/// Story deletion command — handles `!delete` bot commands to remove work items.
pub mod delete;
/// htop-style agent monitor command — renders a live process table in Matrix.
pub mod htop;
/// Rebuild command — triggers a server rebuild/restart via a bot command.
pub mod rebuild;
/// Reset command — handles `!reset` bot commands to restart the server state.
pub mod reset;
/// rmtree command — handles `!rmtree` bot commands to remove worktrees.
pub mod rmtree;
/// Start command — handles `!start` bot commands to launch agents on stories.
pub mod start;
/// Matrix `ChatTransport` implementation wrapping the Matrix SDK client.
pub mod transport_impl;
pub use bot::{ConversationEntry, ConversationRole, RoomConversation};
@@ -20,6 +20,7 @@ pub struct MatrixTransport {
}
impl MatrixTransport {
/// Creates a new `MatrixTransport` wrapping the given Matrix SDK `Client`.
pub fn new(client: Client) -> Self {
Self { client }
}
+4
View File
@@ -1,5 +1,9 @@
//! Chat transports — pluggable backends (Matrix, Slack, WhatsApp, Discord) for bot messaging.
/// Discord bot transport — sends and receives messages via the Discord REST/Gateway APIs.
pub mod discord;
/// Matrix bot transport — sends messages via the Matrix SDK and runs the sync loop.
pub mod matrix;
/// Slack bot transport — sends messages via the Slack Web API and handles webhook events.
pub mod slack;
/// WhatsApp transport — sends messages via Meta Cloud API and Twilio API.
pub mod whatsapp;
+1
View File
@@ -24,6 +24,7 @@ pub struct SlackTransport {
}
impl SlackTransport {
/// Creates a new `SlackTransport` authenticated with the given bot token.
pub fn new(bot_token: String) -> Self {
Self {
bot_token,
+6
View File
@@ -6,10 +6,15 @@
//! - [`webhook_receive`] — Poem handler for the Slack Events API webhook
//! (POST incoming events including URL verification challenge).
/// Slack bot command handlers — parses and dispatches bot commands from Slack messages.
pub mod commands;
/// Slack message formatter — converts markdown to Slack mrkdwn syntax.
pub mod format;
/// Slack conversation history — loads prior chat history for context.
pub mod history;
/// SlackTransport — `ChatTransport` implementation for the Slack Bot API.
pub mod meta;
/// Slack request signature verification — validates HMAC-SHA256 signatures on incoming webhooks.
pub mod verify;
pub use commands::SlackWebhookContext;
@@ -38,6 +43,7 @@ pub struct SlackEventEnvelope {
pub event: Option<SlackEvent>,
}
/// A single Slack event payload (message, reaction, etc.) nested inside an event callback.
#[derive(Deserialize, Debug)]
pub struct SlackEvent {
pub r#type: Option<String>,
@@ -40,6 +40,7 @@ pub struct WhatsAppTransport {
}
impl WhatsAppTransport {
/// Creates a new `WhatsAppTransport` authenticated with the given Meta Cloud API credentials.
pub fn new(
phone_number_id: String,
access_token: String,
@@ -29,6 +29,7 @@ pub struct TwilioWhatsAppTransport {
}
impl TwilioWhatsAppTransport {
/// Creates a new `TwilioWhatsAppTransport` authenticated with the given Twilio credentials.
pub fn new(account_sid: String, auth_token: String, from_number: String) -> Self {
Self {
account_sid,
+3
View File
@@ -4,6 +4,7 @@ use serde::Deserialize;
use std::collections::HashSet;
use std::path::Path;
/// Top-level project configuration loaded from `.huskies/project.toml`.
#[derive(Debug, Clone, Deserialize)]
pub struct ProjectConfig {
#[serde(default)]
@@ -195,6 +196,7 @@ fn default_max_mesh_peers() -> usize {
3
}
/// Configuration for a project component (name, path, setup/teardown commands).
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct ComponentConfig {
@@ -207,6 +209,7 @@ pub struct ComponentConfig {
pub teardown: Vec<String>,
}
/// Configuration for a single agent definition from `[[agent]]` in `project.toml`.
#[derive(Debug, Clone, Deserialize)]
pub struct AgentConfig {
#[serde(default = "default_agent_name")]
+1
View File
@@ -62,6 +62,7 @@ pub(crate) use state::{ALL_OPS, VECTOR_CLOCK};
/// Hex-encode a byte slice (no external dep needed).
pub(crate) mod hex {
/// Encode `bytes` as a lowercase hexadecimal string.
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
+2
View File
@@ -43,6 +43,7 @@ pub struct GatewayConfigCrdt {
pub active_project: LwwRegisterCrdt<String>,
}
/// Top-level CRDT document holding all replicated pipeline state (items, nodes, jobs, etc.).
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct PipelineDoc {
@@ -57,6 +58,7 @@ pub struct PipelineDoc {
pub gateway_config: GatewayConfigCrdt,
}
/// CRDT sub-document representing a single pipeline work item with LWW fields for stage, agent, etc.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct PipelineItemCrdt {
+1
View File
@@ -200,6 +200,7 @@ fn map_svc_error(err: svc::Error) -> poem::Error {
}
}
/// OpenAPI endpoint group for agent management (start, stop, list, inspect).
pub struct AgentsApi {
pub ctx: Arc<AppContext>,
}
+2
View File
@@ -15,11 +15,13 @@ enum AnthropicTags {
Anthropic,
}
/// OpenAPI endpoint group for Anthropic API key and model operations.
pub struct AnthropicApi {
ctx: Arc<AppContext>,
}
impl AnthropicApi {
/// Create a new `AnthropicApi` bound to the given application context.
pub fn new(ctx: Arc<AppContext>) -> Self {
Self { ctx }
}
+1
View File
@@ -36,6 +36,7 @@ struct BotCommandResponse {
response: String,
}
/// OpenAPI endpoint group for bot slash-command execution.
pub struct BotCommandApi {
pub ctx: Arc<AppContext>,
}
+1
View File
@@ -22,6 +22,7 @@ struct BotConfigPayload {
pub slack_channel_ids: Option<Vec<String>>,
}
/// OpenAPI endpoint group for reading and writing bot configuration.
pub struct BotConfigApi {
pub ctx: Arc<AppContext>,
}
+1
View File
@@ -9,6 +9,7 @@ enum ChatTags {
Chat,
}
/// OpenAPI endpoint group for the LLM-powered chat interface.
pub struct ChatApi {
pub ctx: Arc<AppContext>,
}
+5
View File
@@ -34,6 +34,7 @@ pub struct PermissionForward {
}
#[derive(Clone)]
/// Shared application state threaded through all HTTP handlers via Poem's `Data` extractor.
pub struct AppContext {
pub state: Arc<SessionState>,
pub store: Arc<JsonFileStore>,
@@ -78,6 +79,7 @@ pub struct AppContext {
#[cfg(test)]
impl AppContext {
/// Build a minimal `AppContext` for unit tests with an in-memory store.
pub fn new_test(project_root: std::path::PathBuf) -> Self {
use crate::agents::AgentPool;
let state = SessionState::default();
@@ -119,12 +121,15 @@ impl AppContext {
}
}
/// Alias for `poem::Result<T>` used by OpenAPI handler return types.
pub type OpenApiResult<T> = poem::Result<T>;
/// Return a 400 Bad Request error with the given message.
pub fn bad_request(message: String) -> poem::Error {
poem::Error::from_string(message, StatusCode::BAD_REQUEST)
}
/// Return a 404 Not Found error with the given message.
pub fn not_found(message: String) -> poem::Error {
poem::Error::from_string(message, StatusCode::NOT_FOUND)
}
+1
View File
@@ -37,6 +37,7 @@ struct ExecShellPayload {
pub args: Vec<String>,
}
/// OpenAPI endpoint group for filesystem I/O operations (read, write, list, search).
pub struct IoApi {
pub ctx: Arc<AppContext>,
}
+9
View File
@@ -9,14 +9,23 @@ use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::sync::Arc;
/// MCP tools for agent start, stop, wait, list, and inspect.
pub mod agent_tools;
/// MCP tools for server logs, CRDT dump, version, and story movement.
pub mod diagnostics;
/// MCP tools for git operations scoped to agent worktrees.
pub mod git_tools;
/// MCP tools for merge status and merge-to-master operations.
pub mod merge_tools;
/// MCP tools for QA request, approve, and reject workflows.
pub mod qa_tools;
/// MCP tools for running shell commands and test suites.
pub mod shell_tools;
/// MCP tools for pipeline status, story todos, and triage dump.
pub mod status_tools;
/// MCP tools for creating, updating, and managing stories and bugs.
pub mod story_tools;
/// MCP tools for the project setup wizard.
pub mod wizard_tools;
mod dispatch;
+25
View File
@@ -1,26 +1,46 @@
//! HTTP server — module declarations for all REST, MCP, WebSocket, and SSE endpoints.
/// Agent management HTTP endpoints.
pub mod agents;
/// Server-sent event stream for real-time agent output.
pub mod agents_sse;
/// Anthropic API key management endpoints.
pub mod anthropic;
/// Static asset serving (embedded frontend files).
pub mod assets;
/// Bot slash-command HTTP endpoint.
pub mod bot_command;
/// Bot configuration read/write endpoints.
pub mod bot_config;
/// Chat session HTTP endpoints.
pub mod chat;
/// Shared application context threaded through handlers.
pub mod context;
/// Server-sent event stream for pipeline/watcher events.
pub mod events;
/// Node identity endpoint (public key, node ID).
pub mod identity;
/// Filesystem I/O HTTP endpoints (read, write, list, search).
pub mod io;
/// Model Context Protocol (MCP) HTTP endpoint and tool modules.
pub mod mcp;
/// LLM model selection and listing endpoints.
pub mod model;
/// OAuth 2.0 PKCE flow endpoints for Anthropic authentication.
pub mod oauth;
/// Project settings HTTP endpoints.
pub mod settings;
#[cfg(test)]
pub(crate) mod test_helpers;
/// Workflow helpers for story/bug file operations.
pub mod workflow;
/// Gateway-mode HTTP endpoints for multi-project proxy.
pub mod gateway;
/// Project open/close/list HTTP endpoints.
pub mod project;
/// Setup wizard HTTP endpoints.
pub mod wizard;
/// WebSocket handler for real-time frontend communication.
pub mod ws;
use agents::AgentsApi;
@@ -44,26 +64,31 @@ use crate::chat::transport::whatsapp::WhatsAppWebhookContext;
const DEFAULT_PORT: u16 = 3001;
/// Parse an optional port string, falling back to the default (3001).
pub fn parse_port(value: Option<String>) -> u16 {
value
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(DEFAULT_PORT)
}
/// Read the server port from the `HUSKIES_PORT` env var, or use the default.
pub fn resolve_port() -> u16 {
parse_port(std::env::var("HUSKIES_PORT").ok())
}
/// Write a `.huskies_port` file so other processes can discover the port.
pub fn write_port_file(dir: &Path, port: u16) -> Option<PathBuf> {
let path = dir.join(".huskies_port");
std::fs::write(&path, port.to_string()).ok()?;
Some(path)
}
/// Delete the `.huskies_port` file on shutdown.
pub fn remove_port_file(path: &Path) {
let _ = std::fs::remove_file(path);
}
/// Assemble the full Poem route tree (API, WebSocket, MCP, OAuth, assets).
pub fn build_routes(
ctx: AppContext,
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
+1
View File
@@ -16,6 +16,7 @@ struct ModelPayload {
model: String,
}
/// OpenAPI endpoint group for LLM model selection and listing.
pub struct ModelApi {
pub ctx: Arc<AppContext>,
}
+1
View File
@@ -27,6 +27,7 @@ fn map_project_error(e: ProjectError) -> poem::Error {
}
}
/// OpenAPI endpoint group for project open, close, and listing operations.
pub struct ProjectApi {
pub ctx: Arc<AppContext>,
}
+1
View File
@@ -55,6 +55,7 @@ struct OpenFileResponse {
success: bool,
}
/// OpenAPI endpoint group for user preferences and editor configuration.
pub struct SettingsApi {
pub ctx: Arc<AppContext>,
}
+1
View File
@@ -68,6 +68,7 @@ fn parse_step(step_str: &str) -> Result<WizardStep, poem::Error> {
.map_err(|_| not_found(format!("Unknown wizard step: {step_str}")))
}
/// OpenAPI endpoint group for the multi-step project setup wizard.
pub struct WizardApi {
pub ctx: Arc<AppContext>,
}
@@ -6,6 +6,7 @@ use super::super::{
slugify_name, story_stage, write_story_content,
};
/// Write a new story file to the CRDT content store and return the generated story ID.
pub fn create_story_file(
root: &std::path::Path,
name: &str,
@@ -8,6 +8,7 @@ use super::super::{
slugify_name, story_stage, write_story_content,
};
/// Toggle an acceptance criterion checkbox (`- [ ]` → `- [x]`) by its 0-based index among unchecked items.
pub fn check_criterion_in_file(
project_root: &Path,
story_id: &str,
+1
View File
@@ -39,6 +39,7 @@ pub async fn write_file(path: String, content: String, state: &SessionState) ->
}
#[derive(Serialize, Debug, poem_openapi::Object)]
/// A directory listing entry with its name and kind (file or directory).
pub struct FileEntry {
pub name: String,
pub kind: String,
+5
View File
@@ -1,8 +1,13 @@
//! Filesystem I/O — module declarations and re-exports for file operations.
/// File read/write/list operations.
pub mod files;
/// Path resolution and project-root discovery.
pub mod paths;
/// User model-preference storage.
pub mod preferences;
/// Project open/close/list lifecycle.
pub mod project;
/// `.huskies/` directory scaffolding for new projects.
pub mod scaffold;
pub use files::{
+1
View File
@@ -29,6 +29,7 @@ pub fn find_story_kit_root(start: &Path) -> Option<PathBuf> {
}
}
/// Return the current user's home directory as a string.
pub fn get_home_directory() -> Result<String, String> {
let home = homedir::my_home()
.map_err(|e| format!("Failed to resolve home directory: {e}"))?
+2
View File
@@ -4,6 +4,7 @@ use serde_json::json;
const KEY_SELECTED_MODEL: &str = "selected_model";
/// Read the user's selected LLM model name from the store.
pub fn get_model_preference(store: &dyn StoreOps) -> Result<Option<String>, String> {
if let Some(model) = store
.get(KEY_SELECTED_MODEL)
@@ -15,6 +16,7 @@ pub fn get_model_preference(store: &dyn StoreOps) -> Result<Option<String>, Stri
Ok(None)
}
/// Persist the user's selected LLM model name to the store.
pub fn set_model_preference(model: String, store: &dyn StoreOps) -> Result<(), String> {
store.set(KEY_SELECTED_MODEL, json!(model));
store.save()?;
+4
View File
@@ -84,6 +84,7 @@ pub async fn open_project(
Ok(path)
}
/// Close the active project by clearing the in-memory root and stored path.
#[allow(dead_code)]
pub fn close_project(state: &SessionState, store: &dyn StoreOps) -> Result<(), String> {
{
@@ -99,6 +100,7 @@ pub fn close_project(state: &SessionState, store: &dyn StoreOps) -> Result<(), S
Ok(())
}
/// Return the active project path, restoring it from the store if needed.
#[allow(dead_code)]
pub fn get_current_project(
state: &SessionState,
@@ -133,6 +135,7 @@ pub fn get_current_project(
Ok(None)
}
/// List all previously-opened project paths from the store.
#[allow(dead_code)]
pub fn get_known_projects(store: &dyn StoreOps) -> Result<Vec<String>, String> {
let projects = store
@@ -146,6 +149,7 @@ pub fn get_known_projects(store: &dyn StoreOps) -> Result<Vec<String>, String> {
Ok(projects)
}
/// Remove a project path from the known-projects list in the store.
#[allow(dead_code)]
pub fn forget_known_project(path: String, store: &dyn StoreOps) -> Result<(), String> {
let mut known_projects = get_known_projects(store)?;
+7
View File
@@ -1,10 +1,17 @@
//! I/O subsystem — filesystem, shell, search, onboarding, and story metadata operations.
/// Filesystem helpers — file/directory read, write, list, path resolution, and project scaffolding.
pub mod fs;
/// Onboarding — guides new projects through spec setup and initial configuration.
pub mod onboarding;
/// Code search — full-text search across project files using the `ignore` crate.
pub mod search;
/// Shell command execution — runs sandboxed commands in the project directory.
pub mod shell;
/// Story metadata — parses and serialises front-matter from story files.
pub mod story_metadata;
#[cfg(test)]
pub(crate) mod test_helpers;
/// Filesystem watcher — detects config changes and pipeline file events for hot-reload.
pub mod watcher;
/// Project wizard — multi-step state machine for guided project initialisation.
pub mod wizard;
+1
View File
@@ -7,6 +7,7 @@ use std::fs;
use std::path::PathBuf;
#[derive(Serialize, Debug, poem_openapi::Object)]
/// A single file that matched a text search, with its match count.
pub struct SearchResult {
pub path: String,
pub matches: usize,
+1
View File
@@ -4,6 +4,7 @@ use serde::Serialize;
use std::path::PathBuf;
use std::process::Command;
/// Output captured from a shell command: stdout, stderr, and exit code.
#[derive(Serialize, Debug, poem_openapi::Object)]
pub struct CommandOutput {
pub stdout: String,
+3
View File
@@ -24,6 +24,7 @@ impl QaMode {
}
}
/// Return the lowercase string representation of this QA mode.
pub fn as_str(&self) -> &'static str {
match self {
Self::Server => "server",
@@ -39,6 +40,7 @@ impl std::fmt::Display for QaMode {
}
}
/// Parsed YAML front-matter fields from a story markdown file.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StoryMetadata {
pub name: Option<String>,
@@ -71,6 +73,7 @@ pub struct StoryMetadata {
pub mergemaster_attempted: Option<bool>,
}
/// Errors that can occur when parsing story front-matter metadata.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StoryMetaError {
MissingFrontMatter,
+5
View File
@@ -1,6 +1,11 @@
//! LLM subsystem — chat orchestration, prompts, OAuth, and provider integrations.
/// Chat session orchestration — manages multi-turn LLM conversations with streaming.
pub mod chat;
/// OAuth credential flow for LLM API access (e.g. Anthropic OAuth PKCE).
pub mod oauth;
/// System prompt templates for agent and onboarding sessions.
pub mod prompts;
/// LLM provider implementations (Anthropic, Claude Code, Ollama).
pub mod providers;
/// Core LLM data types: `Message`, `Role`, `ToolCall`, and `ModelProvider`.
pub mod types;
+3
View File
@@ -1,4 +1,6 @@
//! System prompts — static prompt templates for the LLM chat and onboarding flows.
/// The default system prompt injected at the start of every agent chat session.
pub const SYSTEM_PROMPT: &str = r#"You are an AI Agent with direct access to the user's filesystem and development environment.
CRITICAL INSTRUCTIONS:
@@ -91,6 +93,7 @@ REMEMBER:
Remember: You are an autonomous agent that can both explain concepts and take action. Choose appropriately based on the user's request.
"#;
/// System prompt override used when a project is newly scaffolded and needs onboarding.
pub const ONBOARDING_PROMPT: &str = r#"ONBOARDING MODE ACTIVE — This is a newly scaffolded project. The spec files still contain placeholder content and must be replaced with real project information before any stories can be written.
Guide the user through each step below. Ask ONE category of questions at a time do not overwhelm the user with everything at once.
@@ -36,9 +36,11 @@ mod parse;
use events::{handle_stream_event, process_json_event};
/// Orchestrates Claude Code CLI sessions via a PTY for streaming agent chat.
pub struct ClaudeCodeProvider;
impl ClaudeCodeProvider {
/// Creates a new `ClaudeCodeProvider`.
pub fn new() -> Self {
Self
}
+3
View File
@@ -1,4 +1,7 @@
//! LLM providers — module declarations for Anthropic, Claude Code, and Ollama backends.
/// Anthropic API provider — drives chat completions via the Anthropic Messages API.
pub mod anthropic;
/// Claude Code CLI provider — runs `claude` in a PTY and parses structured NDJSON output.
pub mod claude_code;
/// Ollama provider — streaming completion client for locally-hosted Ollama models.
pub mod ollama;
+2
View File
@@ -7,11 +7,13 @@ use futures::StreamExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Ollama HTTP/streaming client that connects to a local Ollama server.
pub struct OllamaProvider {
base_url: String,
}
impl OllamaProvider {
/// Creates a new `OllamaProvider` pointing at the given Ollama server base URL.
pub fn new(base_url: String) -> Self {
Self { base_url }
}
+8
View File
@@ -3,6 +3,7 @@ use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// The role of a message participant in an LLM conversation.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
@@ -12,6 +13,7 @@ pub enum Role {
Tool,
}
/// A single message in an LLM conversation, including optional tool call attachments.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Message {
pub role: Role,
@@ -24,6 +26,7 @@ pub struct Message {
pub tool_call_id: Option<String>,
}
/// A tool invocation requested by the LLM, containing the call ID and function details.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolCall {
pub id: Option<String>,
@@ -32,12 +35,14 @@ pub struct ToolCall {
pub kind: String,
}
/// The function name and JSON-encoded arguments for a tool call.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
/// A tool definition passed to the LLM describing an available function and its schema.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolDefinition {
#[serde(rename = "type")]
@@ -45,6 +50,7 @@ pub struct ToolDefinition {
pub function: ToolFunctionDefinition,
}
/// The name, description, and JSON schema for a single tool function.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolFunctionDefinition {
pub name: String,
@@ -52,6 +58,7 @@ pub struct ToolFunctionDefinition {
pub parameters: serde_json::Value,
}
/// The response from an LLM completion request, containing text and/or tool calls.
#[derive(Debug, Serialize, Deserialize)]
pub struct CompletionResponse {
pub content: Option<String>,
@@ -61,6 +68,7 @@ pub struct CompletionResponse {
pub session_id: Option<String>,
}
/// Trait for LLM backends; implementations drive chat completions with optional tool use.
#[async_trait]
#[allow(dead_code)]
pub trait ModelProvider: Send + Sync {
+2
View File
@@ -24,6 +24,7 @@ pub enum LogLevel {
}
impl LogLevel {
/// Return the uppercase string label for this level (`"ERROR"`, `"WARN"`, or `"INFO"`).
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Error => "ERROR",
@@ -75,6 +76,7 @@ impl LogEntry {
}
}
/// Bounded in-memory ring buffer holding recent log entries and a broadcast channel for live streaming.
pub struct LogBuffer {
entries: Mutex<VecDeque<LogEntry>>,
log_file: Mutex<Option<PathBuf>>,
+10
View File
@@ -9,22 +9,32 @@ mod agent_mode;
mod agents;
mod chat;
mod config;
/// CRDT snapshot — serialisation and restore of the full pipeline CRDT state.
pub mod crdt_snapshot;
/// CRDT state — in-memory pipeline state machine backed by a distributed CRDT.
pub mod crdt_state;
/// CRDT sync — WebSocket-based peer synchronisation for distributed nodes.
pub mod crdt_sync;
/// CRDT wire format — on-wire message types for the crdt-sync protocol.
pub mod crdt_wire;
mod db;
/// Gateway mode — multi-project reverse proxy that fronts multiple project containers.
pub mod gateway;
mod gateway_relay;
mod http;
mod io;
mod llm;
/// Log buffer — in-memory ring buffer for recent server-side log lines.
pub mod log_buffer;
/// Mesh — peer discovery and multi-hop CRDT replication over WebSocket.
pub mod mesh;
/// Node identity — Ed25519 keypair generation and stable node ID management.
pub mod node_identity;
pub(crate) mod pipeline_state;
/// Rebuild — process restart and shutdown coordination.
pub mod rebuild;
mod service;
/// Services — shared service bundle injected into HTTP handlers and bot tasks.
pub mod services;
mod startup;
mod state;
+4
View File
@@ -21,21 +21,25 @@ pub trait TransitionSubscriber: Send + Sync {
fn on_transition(&self, fired: &TransitionFired);
}
/// Collects [`TransitionSubscriber`]s and dispatches [`TransitionFired`] events to each.
pub struct EventBus {
subscribers: Vec<Box<dyn TransitionSubscriber>>,
}
impl EventBus {
/// Create an empty event bus with no subscribers.
pub fn new() -> Self {
Self {
subscribers: Vec::new(),
}
}
/// Register a subscriber to receive all future transition events.
pub fn subscribe<S: TransitionSubscriber + 'static>(&mut self, subscriber: S) {
self.subscribers.push(Box::new(subscriber));
}
/// Fire a transition event, calling every registered subscriber in order.
pub fn fire(&self, event: TransitionFired) {
for sub in &self.subscribers {
sub.on_transition(&event);
+5
View File
@@ -10,6 +10,7 @@ use super::{event_label, stage_dir_name, stage_label};
// These are ready to wire into the event bus but not yet connected to the
// actual subsystems. Suppress dead_code until consumers are migrated.
/// Subscriber that logs pipeline transitions to the Matrix bot channel.
#[allow(dead_code)]
pub struct MatrixBotSubscriber;
#[allow(dead_code)]
@@ -27,6 +28,7 @@ impl TransitionSubscriber for MatrixBotSubscriber {
}
}
/// Subscriber that re-renders the filesystem `work/` directory on stage transitions.
#[allow(dead_code)]
pub struct FileRendererSubscriber;
#[allow(dead_code)]
@@ -43,6 +45,7 @@ impl TransitionSubscriber for FileRendererSubscriber {
}
}
/// Subscriber that writes stage updates to the pipeline-items data store.
#[allow(dead_code)]
pub struct PipelineItemsSubscriber;
#[allow(dead_code)]
@@ -59,6 +62,7 @@ impl TransitionSubscriber for PipelineItemsSubscriber {
}
}
/// Subscriber that promotes eligible backlog items when a story completes or is archived.
#[allow(dead_code)]
pub struct AutoAssignSubscriber;
#[allow(dead_code)]
@@ -77,6 +81,7 @@ impl TransitionSubscriber for AutoAssignSubscriber {
}
}
/// Subscriber that broadcasts stage transitions to all connected WebSocket clients.
#[allow(dead_code)]
pub struct WebUiBroadcastSubscriber;
#[allow(dead_code)]
+1
View File
@@ -51,6 +51,7 @@ pub enum PipelineEvent {
// ── Per-node execution events ───────────────────────────────────────────────
/// Events that drive per-node [`ExecutionState`] transitions (agent lifecycle).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ExecutionEvent {
SpawnRequested { agent: AgentName },
+6
View File
@@ -7,18 +7,23 @@ use std::num::NonZeroU32;
// ── Newtypes ────────────────────────────────────────────────────────────────
/// Unique identifier for a pipeline work item (story, bug, spike, or refactor).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StoryId(pub String);
/// Git branch name associated with a work item.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BranchName(pub String);
/// A Git commit SHA (typically the merge commit written when a story lands on master).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GitSha(pub String);
/// The name of the agent process handling a work item on a given node.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AgentName(pub String);
/// Ed25519 public key (32 bytes) that uniquely identifies a huskies node in the mesh.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodePubkey(pub [u8; 32]);
@@ -191,6 +196,7 @@ pub struct PipelineItem {
// ── Transition errors ───────────────────────────────────────────────────────
/// Error returned when a pipeline event is not valid for the current stage.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransitionError {
InvalidTransition { from_stage: String, event: String },
+1
View File
@@ -31,6 +31,7 @@ pub struct BotShutdownNotifier {
}
impl BotShutdownNotifier {
/// Create a notifier that will send shutdown/startup messages via `transport` to the given `channels`.
pub fn new(transport: Arc<dyn ChatTransport>, channels: Vec<String>, bot_name: String) -> Self {
Self {
transport,
+2
View File
@@ -7,7 +7,9 @@
//!
//! Conventions: `docs/architecture/service-modules.md`
mod io;
/// Agent selection heuristics — pick the best agent for a story.
pub mod selection;
/// Token usage tracking and budget enforcement.
pub mod token;
use crate::agents::AgentInfo;
+1
View File
@@ -11,6 +11,7 @@
//! - `io.rs` — all side-effectful calls (transport handlers, stores, agent pool)
pub(super) mod io;
/// Pure argument parsing for bot slash commands.
pub mod parse;
use crate::agents::AgentPool;
+1
View File
@@ -3,4 +3,5 @@
//! All sub-modules here are pure (no I/O, no side effects). Any helper that
//! duplicates logic across two or more service modules belongs here; anything
//! used by only one service stays in that service.
/// Story/bug/spike ID extraction and formatting helpers.
pub mod item_id;
+2
View File
@@ -6,7 +6,9 @@
//! - `io.rs` — the ONLY place that performs side effects (filesystem reads/writes)
//! - `permission.rs` — pure permission-rule generation and wildcard checks
/// Side-effectful diagnostics I/O — log reads, CRDT dumps, filesystem writes.
pub mod io;
/// Pure permission-rule generation and wildcard matching.
pub mod permission;
#[allow(unused_imports)]
+1
View File
@@ -6,6 +6,7 @@
//!
//! Conventions: `docs/architecture/service-modules.md`
/// Bounded in-memory event ring buffer for SSE streaming.
pub mod buffer;
pub(super) mod io;
+3
View File
@@ -7,9 +7,12 @@
//! - `aggregation.rs` — pure cross-project pipeline formatting
//! - `polling.rs` — pure notification event formatting
/// Cross-project pipeline status aggregation and formatting.
pub mod aggregation;
/// Gateway configuration types and TOML parsing.
pub mod config;
pub(crate) mod io;
/// Notification event polling for gateway-level broadcasts.
pub mod polling;
pub use aggregation::format_aggregate_status_compact;
+3
View File
@@ -7,8 +7,11 @@
//! - `path_guard.rs` — pure path-prefix safety checks
//! - `porcelain.rs` — pure git porcelain output parsers
/// Side-effectful git command execution (add, commit, diff, log, status).
pub mod io;
/// Pure worktree path-prefix safety checks.
pub mod path_guard;
/// Pure git porcelain output parsers.
pub mod porcelain;
#[allow(unused_imports)]
+2
View File
@@ -6,7 +6,9 @@
//! - `io.rs` — the ONLY place that performs side effects
//! - `status.rs` — pure merge-status message formatting
/// Side-effectful merge I/O — rebase, cherry-pick, and branch operations.
pub mod io;
/// Pure merge-status message formatting.
pub mod status;
#[allow(unused_imports)]
+22
View File
@@ -5,25 +5,47 @@
//! - `mod.rs` orchestrates and owns the typed `Error` type
//! - `io.rs` is the only file that performs side effects
//! - Topic-named pure files contain branching logic with no I/O
/// Agent management — start, stop, inspect, and list agents.
pub mod agents;
/// Anthropic API key storage and model listing.
pub mod anthropic;
/// Bot command dispatch — parses and executes slash commands.
pub mod bot_command;
/// Shared pure helpers used across service modules.
pub mod common;
/// Diagnostics — server logs, CRDT dump, and permission management.
pub mod diagnostics;
/// Pipeline event buffer for SSE streaming.
pub mod events;
/// File I/O — path validation, read, write, and listing.
pub mod file_io;
/// Gateway — multi-project proxy domain logic.
pub mod gateway;
/// Git operations — worktree-scoped git commands.
pub mod git_ops;
/// Merge — rebase agent work onto master and validate.
pub mod merge;
/// Notifications — fan-out pipeline events to chat transports.
pub mod notifications;
/// OAuth 2.0 PKCE flow for Anthropic authentication.
pub mod oauth;
/// Pipeline status aggregation helpers.
pub mod pipeline;
/// Project open/close/list domain logic.
pub mod project;
/// QA — request, approve, and reject code reviews.
pub mod qa;
/// Project settings read/write and validation.
pub mod settings;
/// Shell command safety, sandboxing, and output helpers.
pub mod shell;
/// Status broadcaster — unified event fan-out to all consumers.
pub mod status;
/// Story CRUD — create, update, move, and manage work items.
pub mod story;
/// Timer — deferred agent start via one-shot timers.
pub mod timer;
/// Wizard — multi-step project setup domain logic.
pub mod wizard;
/// WebSocket — real-time pipeline updates and permission prompts.
pub mod ws;
+2
View File
@@ -7,8 +7,10 @@
//! - `pkce.rs` — pure PKCE helpers: generation, challenge, encoding
//! - `flow.rs` — pure flow types and token-expiry decision logic
/// Pure OAuth flow types and token-expiry decision logic.
pub mod flow;
pub(super) mod io;
/// Pure PKCE helpers — code verifier generation, challenge derivation, base64url encoding.
pub mod pkce;
pub use flow::AccountInfo;
+1
View File
@@ -7,6 +7,7 @@
//! Conventions: `docs/architecture/service-modules.md`
pub(super) mod io;
/// Pure project selection and path-matching logic.
pub mod selection;
use crate::state::SessionState;
+2
View File
@@ -6,7 +6,9 @@
//! - `io.rs` — the ONLY place that performs side effects (git, TCP, process)
//! - `lifecycle.rs` — pure QA routing decisions (spike vs. normal story)
/// Side-effectful QA I/O — git operations, port scanning, branch merging.
pub mod io;
/// Pure QA routing decisions (spike vs. normal story).
pub mod lifecycle;
pub use io::{find_free_port, merge_spike_branch_to_master};
+2
View File
@@ -9,7 +9,9 @@
//! - `validate.rs` — pure validation: [`validate_project_settings`]
pub(super) mod io;
/// Pure types: `ProjectSettings`, TOML serialization, config merging.
pub mod project;
/// Pure project-settings validation rules.
pub mod validate;
pub use project::{ProjectSettings, merge_settings_into_toml, settings_from_config};
+2
View File
@@ -6,7 +6,9 @@
//! - `io.rs` — the ONLY place that performs side effects (filesystem checks)
//! - `path_guard.rs` — pure command-safety checks and output utilities
/// Side-effectful shell I/O — filesystem permission checks.
pub mod io;
/// Pure command-safety checks, blocked-binary lists, and output truncation.
pub mod path_guard;
#[allow(unused_imports)]
+2
View File
@@ -23,7 +23,9 @@
//! - Story 643 (Web UI): calls `subscribe()` once at startup.
//! - Story 644 (chat transports): calls `subscribe()` once per transport.
/// Bounded ring buffer for recent status events.
pub mod buffer;
/// Pure status-event to human-readable string formatting.
pub mod format;
use chrono::{DateTime, Utc};
+5
View File
@@ -31,10 +31,15 @@
//! - `pipeline_items` row — updated on stage transitions and item creation/deletion
//! - `content_store` entry — updated on story content changes, deleted on purge/delete
/// Pure criterion parsing, checkbox toggling, and validation.
pub mod criteria;
/// Pure front-matter field validation (stage names, agent assignments).
pub mod front_matter;
/// Side-effectful story file I/O — read, write, move, and delete.
pub mod io;
/// Pure story-ID helpers and lifecycle state transitions.
pub mod lifecycle;
/// Pure story content validation rules.
pub mod validation;
pub use criteria::parse_test_cases;
+1
View File
@@ -7,6 +7,7 @@
//! step classification — all unit-testable without tempdirs or an async runtime
pub(crate) mod io;
/// Pure wizard state-machine helpers — bare-project detection, step classification.
pub mod state_machine;
use crate::io::wizard::{StepStatus, WizardState, WizardStep, format_wizard_state};
+3
View File
@@ -4,8 +4,11 @@
//! Conversions from domain events to WsResponse live here too.
//! All logic is pure data transformation; I/O lives in `io.rs`.
/// Conversions from domain events to `WsResponse` frames.
pub mod convert;
/// Client-to-server `WsRequest` message definitions.
pub mod request;
/// Server-to-client `WsResponse` message definitions.
pub mod response;
pub use convert::{needs_pipeline_refresh, wizard_steps_to_info};
+3
View File
@@ -9,9 +9,12 @@
//! - `dispatch.rs` — pure request routing and permission resolution
//! - `error.rs` — typed error enum
/// Pure request routing and permission resolution.
pub mod dispatch;
/// Typed WebSocket error enum.
pub mod error;
pub(super) mod io;
/// Pure WebSocket message types and domain-event conversions.
pub mod message;
pub use dispatch::{
+2
View File
@@ -3,6 +3,7 @@ use std::path::PathBuf;
use std::sync::Mutex;
use tokio::sync::watch;
/// Global server session state: the open project root and a cancellation signal.
pub struct SessionState {
pub project_root: Mutex<Option<PathBuf>>,
pub cancel_tx: watch::Sender<bool>,
@@ -21,6 +22,7 @@ impl Default for SessionState {
}
impl SessionState {
/// Return the currently open project root, or an error if no project is open.
pub fn get_project_root(&self) -> Result<PathBuf, String> {
let root_guard = self.project_root.lock().map_err(|e| e.to_string())?;
let root = root_guard.as_ref().ok_or_else(|| {
+5
View File
@@ -5,6 +5,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
/// Trait for a simple key-value store that can get, set, delete, and persist entries.
pub trait StoreOps: Send + Sync {
fn get(&self, key: &str) -> Option<Value>;
fn set(&self, key: &str, value: Value);
@@ -12,12 +13,14 @@ pub trait StoreOps: Send + Sync {
fn save(&self) -> Result<(), String>;
}
/// A JSON-backed file store that persists key-value data to a single file on disk.
pub struct JsonFileStore {
path: PathBuf,
data: Mutex<HashMap<String, Value>>,
}
impl JsonFileStore {
/// Create a new store backed by `path`, loading existing data if the file is present.
pub fn new(path: PathBuf) -> Result<Self, String> {
let data = if path.exists() {
let content =
@@ -38,10 +41,12 @@ impl JsonFileStore {
})
}
/// Convenience constructor accepting any path type; delegates to [`Self::new`].
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, String> {
Self::new(path.as_ref().to_path_buf())
}
/// Return the path to the backing JSON file.
#[allow(dead_code)]
pub fn path(&self) -> &Path {
&self.path
+6
View File
@@ -3,6 +3,7 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Whether an individual test case passed or failed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TestStatus {
@@ -10,6 +11,7 @@ pub enum TestStatus {
Fail,
}
/// The name, status, and optional details for a single test case execution.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestCaseResult {
pub name: String,
@@ -22,6 +24,7 @@ struct TestRunSummary {
failed: usize,
}
/// The outcome of evaluating whether a story is ready for acceptance.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcceptanceDecision {
pub can_accept: bool,
@@ -29,12 +32,14 @@ pub struct AcceptanceDecision {
pub warning: Option<String>,
}
/// Collected unit and integration test results recorded for a single story.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StoryTestResults {
pub unit: Vec<TestCaseResult>,
pub integration: Vec<TestCaseResult>,
}
/// Server-wide in-memory workflow state: test results and coverage reports keyed by story ID.
#[derive(Debug, Clone, Default)]
pub struct WorkflowState {
pub results: HashMap<String, StoryTestResults>,
@@ -42,6 +47,7 @@ pub struct WorkflowState {
}
impl WorkflowState {
/// Record unit and integration test results for a story, rejecting batches with more than one failure.
pub fn record_test_results_validated(
&mut self,
story_id: String,
+2
View File
@@ -12,6 +12,7 @@ pub use remove::remove_worktree_by_story_id;
pub use sweep::sweep_orphaned_worktrees;
#[derive(Debug, Clone)]
/// Details about a newly created worktree: path, branch, and base branch.
pub struct WorktreeInfo {
pub path: PathBuf,
pub branch: String,
@@ -19,6 +20,7 @@ pub struct WorktreeInfo {
}
#[derive(Debug, Clone)]
/// A discovered worktree on disk: its story ID and filesystem path.
pub struct WorktreeListEntry {
pub story_id: String,
pub path: PathBuf,