Initial scaffold: GyanDoot v2.0 in Rust

Full Cargo workspace with 11 crates:
- gyandoot-core: Shared types, config, error types
- gyandoot-agent: Agent loop, tool execution, context compression
- gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama)
- gyandoot-memory: MemPalace persistent memory system
- gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI)
- gyandoot-security: Credential vault, sanitizer, threat detector
- gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics
- gyandoot-subagents: Sub-agent spawning and orchestration
- gyandoot-skills: Skills discovery, loading, preprocessing, bundles
- gyandoot-profile: Multi-agent profile manager
- gyandoot-gateway: Gateway sessions, routing, access control

Plus: CLI entry point with clap, cross-platform installer script,
KANBAN.md, .gnpl/AGENTS.md

Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
2026-06-11 20:04:12 +05:30
commit 851c838ca5
53 changed files with 8037 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
[package]
name = "gyandoot-kanban"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "GyanDoot v2 — Kanban task system (decomposition, swarm, diagnostics)"
[dependencies]
gyandoot-core = { path = "../gyandoot-core" }
gyandoot-providers = { path = "../gyandoot-providers" }
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
anyhow.workspace = true
tracing.workspace = true
uuid.workspace = true
chrono.workspace = true
async-trait.workspace = true

View File

@@ -0,0 +1,118 @@
//! Task decomposition — breaks complex tasks into smaller subtasks.
use gyandoot_core::{KanbanResult, Priority, Task, TaskStatus};
use uuid::Uuid;
/// Decomposes a complex task into smaller, actionable subtasks.
pub struct Decomposer {
/// Maximum depth of decomposition.
max_depth: usize,
}
impl Decomposer {
/// Create a new decomposer.
pub fn new() -> Self {
Self { max_depth: 3 }
}
/// Decompose a task into subtasks using heuristics.
///
/// In production, this would use the LLM to intelligently break down
/// tasks. For now, it uses a simple heuristic based on the description.
pub fn decompose(&self, task: &Task) -> KanbanResult<Vec<Task>> {
let mut subtasks = Vec::new();
// Heuristic: if the task mentions "and" or lists, create subtasks
let description_lower = task.description.to_lowercase();
if description_lower.contains(" and ") {
subtasks.push(self.make_subtask(task, &format!("Part 1 of: {}", task.title), 1));
subtasks.push(self.make_subtask(task, &format!("Part 2 of: {}", task.title), 2));
}
if description_lower.contains("first") || description_lower.contains("step 1") {
subtasks.push(self.make_subtask(task, &format!("Step 1: {}", task.title), 1));
}
if description_lower.contains("second") || description_lower.contains("step 2") {
subtasks.push(self.make_subtask(task, &format!("Step 2: {}", task.title), 2));
}
// If no specific decomposition found, create a default 3-part split
if subtasks.is_empty() && task.description.len() > 100 {
subtasks.push(self.make_subtask(task, &format!("Research: {}", task.title), 1));
subtasks.push(self.make_subtask(task, &format!("Implement: {}", task.title), 2));
subtasks.push(self.make_subtask(task, &format!("Verify: {}", task.title), 3));
}
Ok(subtasks)
}
/// Create a single subtask.
fn make_subtask(&self, parent: &Task, title: &str, _index: usize) -> Task {
Task {
id: Uuid::new_v4(),
title: title.to_string(),
description: format!("Subtask of '{}': {}", parent.title, parent.description),
priority: parent.priority,
status: TaskStatus::Backlog,
assigned_agent: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
subtasks: Vec::new(),
dependencies: vec![parent.id],
tags: parent.tags.clone(),
}
}
}
impl Default for Decomposer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decompose_simple() {
let decomposer = Decomposer::new();
let task = Task {
id: Uuid::new_v4(),
title: "Build feature".to_string(),
description: "Implement the login system and add tests".to_string(),
priority: Priority::High,
status: TaskStatus::Backlog,
assigned_agent: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
subtasks: Vec::new(),
dependencies: Vec::new(),
tags: Vec::new(),
};
let subtasks = decomposer.decompose(&task).unwrap();
assert!(subtasks.len() >= 2);
}
#[test]
fn test_decompose_short_description() {
let decomposer = Decomposer::new();
let task = Task {
id: Uuid::new_v4(),
title: "Simple task".to_string(),
description: "Do the thing".to_string(),
priority: Priority::Low,
status: TaskStatus::Backlog,
assigned_agent: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
subtasks: Vec::new(),
dependencies: Vec::new(),
tags: Vec::new(),
};
let subtasks = decomposer.decompose(&task).unwrap();
assert!(subtasks.is_empty()); // too short to decompose
}
}

View File

@@ -0,0 +1,152 @@
//! Diagnostics — task tracking, bottlenecks, and performance metrics.
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use gyandoot_core::{KanbanResult, Task, TaskStatus};
use uuid::Uuid;
/// A diagnostic snapshot of the Kanban system.
#[derive(Debug, Clone)]
pub struct KanbanDiagnostics {
pub total_tasks: usize,
pub backlog_count: usize,
pub in_progress_count: usize,
pub done_count: usize,
pub blocked_count: usize,
pub average_completion_time_ms: Option<f64>,
pub bottlenecks: Vec<String>,
pub timestamp: DateTime<Utc>,
}
/// Tracks task lifecycle and produces diagnostic information.
pub struct Diagnostics {
task_timings: HashMap<Uuid, TaskTiming>,
bottlenecks: Vec<String>,
}
#[derive(Debug, Clone)]
struct TaskTiming {
created_at: DateTime<Utc>,
started_at: Option<DateTime<Utc>>,
completed_at: Option<DateTime<Utc>>,
status_changes: Vec<(DateTime<Utc>, TaskStatus)>,
}
impl Diagnostics {
/// Create a new diagnostics tracker.
pub fn new() -> Self {
Self {
task_timings: HashMap::new(),
bottlenecks: Vec::new(),
}
}
/// Record a task creation.
pub fn record_creation(&mut self, task_id: Uuid) {
self.task_timings.insert(
task_id,
TaskTiming {
created_at: Utc::now(),
started_at: None,
completed_at: None,
status_changes: vec![(Utc::now(), TaskStatus::Backlog)],
},
);
}
/// Record a task status change.
pub fn record_status_change(&mut self, task_id: &Uuid, new_status: TaskStatus) {
if let Some(timing) = self.task_timings.get_mut(task_id) {
timing.status_changes.push((Utc::now(), new_status.clone()));
if new_status == TaskStatus::InProgress && timing.started_at.is_none() {
timing.started_at = Some(Utc::now());
}
if new_status == TaskStatus::Done {
timing.completed_at = Some(Utc::now());
}
}
}
/// Generate a diagnostics snapshot for a set of tasks.
pub fn analyze(&self, tasks: &[Task]) -> KanbanResult<KanbanDiagnostics> {
let total = tasks.len();
let backlog = tasks.iter().filter(|t| t.status == TaskStatus::Backlog).count();
let in_progress = tasks.iter().filter(|t| t.status == TaskStatus::InProgress).count();
let done = tasks.iter().filter(|t| t.status == TaskStatus::Done).count();
let blocked = tasks.iter().filter(|t| t.status == TaskStatus::Blocked).count();
// Calculate average completion time
let completion_times: Vec<f64> = self
.task_timings
.values()
.filter_map(|t| {
let start = t.started_at?;
let end = t.completed_at?;
Some((end - start).num_milliseconds() as f64)
})
.collect();
let avg_completion = if completion_times.is_empty() {
None
} else {
Some(completion_times.iter().sum::<f64>() / completion_times.len() as f64)
};
Ok(KanbanDiagnostics {
total_tasks: total,
backlog_count: backlog,
in_progress_count: in_progress,
done_count: done,
blocked_count: blocked,
average_completion_time_ms: avg_completion,
bottlenecks: self.bottlenecks.clone(),
timestamp: Utc::now(),
})
}
/// Add a bottleneck warning.
pub fn report_bottleneck(&mut self, description: String) {
self.bottlenecks.push(description);
}
}
impl Default for Diagnostics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostics_tracking() {
let mut diagnostics = Diagnostics::new();
let task_id = Uuid::new_v4();
diagnostics.record_creation(task_id);
diagnostics.record_status_change(&task_id, TaskStatus::InProgress);
diagnostics.record_status_change(&task_id, TaskStatus::Done);
let tasks = vec![Task {
id: task_id,
title: "Test".to_string(),
description: String::new(),
priority: gyandoot_core::Priority::Low,
status: TaskStatus::Done,
assigned_agent: None,
created_at: Utc::now(),
updated_at: Utc::now(),
subtasks: Vec::new(),
dependencies: Vec::new(),
tags: Vec::new(),
}];
let report = diagnostics.analyze(&tasks).unwrap();
assert_eq!(report.total_tasks, 1);
assert_eq!(report.done_count, 1);
}
}

View File

@@ -0,0 +1,127 @@
//! GyanDoot Kanban — Task decomposition, swarm execution, and diagnostics.
//!
//! The Kanban system manages tasks from decomposition into subtasks,
//! parallel execution via a swarm of sub-agents, and diagnostics
//! for tracking progress and bottlenecks.
use gyandoot_core::{KanbanResult, Priority, Task, TaskStatus};
use uuid::Uuid;
pub mod decompose;
pub mod swarm;
pub mod diagnostics;
pub use decompose::Decomposer;
pub use swarm::Swarm;
pub use diagnostics::Diagnostics;
/// The central Kanban board managing all tasks.
pub struct KanbanBoard {
tasks: Vec<Task>,
decomposer: decompose::Decomposer,
swarm: swarm::Swarm,
diagnostics: diagnostics::Diagnostics,
}
impl KanbanBoard {
/// Create a new empty Kanban board.
pub fn new() -> Self {
Self {
tasks: Vec::new(),
decomposer: decompose::Decomposer::new(),
swarm: swarm::Swarm::new(),
diagnostics: diagnostics::Diagnostics::new(),
}
}
/// Add a task to the board.
pub fn add_task(&mut self, title: String, description: String, priority: Priority) -> Task {
let task = Task {
id: Uuid::new_v4(),
title,
description,
priority,
status: TaskStatus::Backlog,
assigned_agent: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
subtasks: Vec::new(),
dependencies: Vec::new(),
tags: Vec::new(),
};
self.tasks.push(task.clone());
task
}
/// Get a task by ID.
pub fn get_task(&self, id: &Uuid) -> Option<&Task> {
self.tasks.iter().find(|t| t.id == *id)
}
/// Update the status of a task.
pub fn update_status(&mut self, id: &Uuid, status: TaskStatus) -> KanbanResult<()> {
let task = self
.tasks
.iter_mut()
.find(|t| t.id == *id)
.ok_or_else(|| gyandoot_core::KanbanError::TaskNotFound(id.to_string()))?;
task.status = status;
task.updated_at = chrono::Utc::now();
Ok(())
}
/// List all tasks with a given status.
pub fn tasks_by_status(&self, status: TaskStatus) -> Vec<&Task> {
self.tasks.iter().filter(|t| t.status == status).collect()
}
/// Get a reference to the decomposer.
pub fn decomposer(&self) -> &decompose::Decomposer {
&self.decomposer
}
/// Get a mutable reference to the decomposer.
pub fn decomposer_mut(&mut self) -> &mut decompose::Decomposer {
&mut self.decomposer
}
/// Get a reference to the swarm.
pub fn swarm(&self) -> &swarm::Swarm {
&self.swarm
}
/// Get a mutable reference to the swarm.
pub fn swarm_mut(&mut self) -> &mut swarm::Swarm {
&mut self.swarm
}
/// Get a reference to the diagnostics.
pub fn diagnostics(&self) -> &diagnostics::Diagnostics {
&self.diagnostics
}
}
impl Default for KanbanBoard {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_board_lifecycle() {
let mut board = KanbanBoard::new();
let task = board.add_task(
"Write tests".to_string(),
"Add unit tests for the agent loop".to_string(),
Priority::High,
);
assert_eq!(task.status, TaskStatus::Backlog);
board.update_status(&task.id, TaskStatus::InProgress).unwrap();
assert_eq!(board.get_task(&task.id).unwrap().status, TaskStatus::InProgress);
}
}

View File

@@ -0,0 +1,108 @@
//! Swarm — parallel task execution via sub-agents.
use std::collections::HashMap;
use gyandoot_core::{KanbanResult, Task, TaskStatus};
use tracing::{debug, info};
/// Manages parallel execution of tasks via a swarm of sub-agents.
pub struct Swarm {
max_concurrent: usize,
running: HashMap<String, Task>,
}
impl Swarm {
/// Create a new swarm.
pub fn new() -> Self {
Self {
max_concurrent: 5,
running: HashMap::new(),
}
}
/// Set the maximum number of concurrent tasks.
pub fn set_concurrency(&mut self, max: usize) {
self.max_concurrent = max;
}
/// Submit a task to the swarm for execution.
pub async fn submit(&mut self, task: Task) -> KanbanResult<String> {
if self.running.len() >= self.max_concurrent {
return Err(gyandoot_core::KanbanError::Swarm(
"Maximum concurrent tasks reached".into(),
));
}
let task_id = task.id.to_string();
info!("Swarm submitting task: {}", task.title);
// In production, this would spawn a sub-agent to handle the task.
// For the scaffold, we simulate execution.
self.running.insert(task_id.clone(), task);
Ok(task_id)
}
/// Get the status of a running task.
pub fn task_status(&self, task_id: &str) -> Option<&Task> {
self.running.get(task_id)
}
/// Mark a task as completed and remove it from the running set.
pub fn complete_task(&mut self, task_id: &str) -> Option<Task> {
let task = self.running.remove(task_id);
if let Some(ref t) = task {
debug!("Swarm completed task: {}", t.title);
}
task
}
/// Get the number of currently running tasks.
pub fn running_count(&self) -> usize {
self.running.len()
}
/// Check if the swarm can accept more tasks.
pub fn can_accept(&self) -> bool {
self.running.len() < self.max_concurrent
}
}
impl Default for Swarm {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
#[tokio::test]
async fn test_swarm_submit() {
let mut swarm = Swarm::new();
let task = Task {
id: Uuid::new_v4(),
title: "Test task".to_string(),
description: "A test".to_string(),
priority: gyandoot_core::Priority::Medium,
status: TaskStatus::Ready,
assigned_agent: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
subtasks: Vec::new(),
dependencies: Vec::new(),
tags: Vec::new(),
};
let id = swarm.submit(task).await.unwrap();
assert_eq!(swarm.running_count(), 1);
assert!(swarm.task_status(&id).is_some());
}
#[test]
fn test_concurrency_limit() {
let mut swarm = Swarm::new();
swarm.set_concurrency(0);
assert!(!swarm.can_accept());
}
}