diff options
| author | soryu <soryu@soryu.co> | 2026-01-11 05:52:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:21:16 +0000 |
| commit | 87044a747b47bd83249d61a45842c7f7b2eae56d (patch) | |
| tree | ef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/src/llm | |
| parent | 077820c4167c168072d217a1b01df840463a12a8 (diff) | |
| download | soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip | |
Contract system
Diffstat (limited to 'makima/src/llm')
| -rw-r--r-- | makima/src/llm/contract_tools.rs | 1091 | ||||
| -rw-r--r-- | makima/src/llm/markdown.rs | 334 | ||||
| -rw-r--r-- | makima/src/llm/mesh_tools.rs | 331 | ||||
| -rw-r--r-- | makima/src/llm/mod.rs | 20 | ||||
| -rw-r--r-- | makima/src/llm/phase_guidance.rs | 594 | ||||
| -rw-r--r-- | makima/src/llm/task_output.rs | 461 | ||||
| -rw-r--r-- | makima/src/llm/templates.rs | 1011 | ||||
| -rw-r--r-- | makima/src/llm/tools.rs | 170 |
8 files changed, 4012 insertions, 0 deletions
diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs new file mode 100644 index 0000000..0d6f9be --- /dev/null +++ b/makima/src/llm/contract_tools.rs @@ -0,0 +1,1091 @@ +//! Tool definitions for contract management via LLM. +//! +//! These tools allow the LLM to manage contracts: create tasks, add files, +//! manage repositories, and handle phase transitions. + +use serde_json::json; +use uuid::Uuid; + +use super::tools::Tool; + +/// Available tools for contract management +pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::Lazy::new(|| { + vec![ + // ============================================================================= + // Query Tools + // ============================================================================= + Tool { + name: "get_contract_status".to_string(), + description: "Get an overview of the contract including current phase, file count, task count, and repository count.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "list_contract_files".to_string(), + description: "List all files in the contract with their names, descriptions, and phases.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "list_contract_tasks".to_string(), + description: "List all tasks in the contract with their names, status, and progress.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "list_contract_repositories".to_string(), + description: "List all repositories attached to the contract with their types and URLs/paths.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "read_file".to_string(), + description: "Read the full contents of a file including its body, transcript, and summary.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "file_id": { + "type": "string", + "description": "ID of the file to read" + } + }, + "required": ["file_id"] + }), + }, + // ============================================================================= + // File Management Tools + // ============================================================================= + Tool { + name: "create_file_from_template".to_string(), + description: "Create a new file in the contract from a template. Templates are phase-appropriate document structures.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "ID of the template to use (e.g., 'research-notes', 'requirements', 'architecture')" + }, + "name": { + "type": "string", + "description": "Name for the new file" + }, + "description": { + "type": "string", + "description": "Optional description for the file" + } + }, + "required": ["template_id", "name"] + }), + }, + Tool { + name: "create_empty_file".to_string(), + description: "Create a new empty file in the contract without using a template.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name for the new file" + }, + "description": { + "type": "string", + "description": "Optional description for the file" + } + }, + "required": ["name"] + }), + }, + Tool { + name: "list_available_templates".to_string(), + description: "List all available templates, optionally filtered by phase. Use this to see what templates can be used with create_file_from_template.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "phase": { + "type": "string", + "enum": ["research", "specify", "plan", "execute", "review"], + "description": "Optional filter to show only templates for a specific phase" + } + } + }), + }, + // ============================================================================= + // Task Management Tools + // ============================================================================= + Tool { + name: "create_contract_task".to_string(), + description: "Create a new task within this contract. The task will be associated with the contract and can optionally use a contract repository.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the task" + }, + "plan": { + "type": "string", + "description": "Detailed instructions/plan for what the task should accomplish" + }, + "repository_url": { + "type": "string", + "description": "Git repository URL or local path. If not specified, uses the contract's primary repository." + }, + "base_branch": { + "type": "string", + "description": "Optional base branch to start from (default: main)" + } + }, + "required": ["name", "plan"] + }), + }, + Tool { + name: "delegate_content_generation".to_string(), + description: "Create a task to generate substantial content instead of writing it directly. Use this for filling templates, writing documentation, generating user stories, or any substantial writing task. The task will be created and can be started separately.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "file_id": { + "type": "string", + "description": "ID of the file to update with generated content (optional - if not specified, creates a new task without file context)" + }, + "instruction": { + "type": "string", + "description": "Clear instructions for what content should be generated" + }, + "context": { + "type": "string", + "description": "Additional context to help generate appropriate content" + } + }, + "required": ["instruction"] + }), + }, + Tool { + name: "start_task".to_string(), + description: "Start a task that is in 'pending' status. The task will be sent to a connected daemon for execution. A daemon must be connected for this to work.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ID of the task to start" + } + }, + "required": ["task_id"] + }), + }, + // ============================================================================= + // Phase Management Tools + // ============================================================================= + Tool { + name: "get_phase_info".to_string(), + description: "Get detailed information about the current phase and what it means for the contract workflow.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "suggest_phase_transition".to_string(), + description: "Analyze whether the contract is ready to advance to the NEXT phase. Returns: currentPhase, nextPhase (the phase to advance TO), readiness status, and what's missing. Use this BEFORE calling advance_phase to know exactly which phase to advance to.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "advance_phase".to_string(), + description: "Advance the contract to the NEXT phase in sequence. Phases progress: research -> specify -> plan -> execute -> review. You can ONLY advance forward one step. Always use suggest_phase_transition first to check readiness and find the correct next phase.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "new_phase": { + "type": "string", + "enum": ["specify", "plan", "execute", "review"], + "description": "The next phase to transition to. Must be exactly one step ahead of current phase (e.g., research->specify, specify->plan, plan->execute, execute->review)" + } + }, + "required": ["new_phase"] + }), + }, + // ============================================================================= + // Repository Management Tools + // ============================================================================= + Tool { + name: "list_daemon_directories".to_string(), + description: "List suggested directories from connected daemons. Use this to find valid local paths when the user wants to add a local repository or configure a target path. Returns working directories and home directories from connected agents.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "add_repository".to_string(), + description: "Add a repository to the contract. Can be a remote URL, local path, or create a managed repository.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["remote", "local", "managed"], + "description": "Type of repository to add" + }, + "name": { + "type": "string", + "description": "Display name for the repository" + }, + "url": { + "type": "string", + "description": "Repository URL (for remote type) or local path (for local type). Not needed for managed." + }, + "is_primary": { + "type": "boolean", + "description": "Whether this should be the primary repository for the contract" + } + }, + "required": ["type", "name"] + }), + }, + Tool { + name: "set_primary_repository".to_string(), + description: "Set a repository as the primary repository for this contract. The primary repo is used by default for new tasks.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "repository_id": { + "type": "string", + "description": "ID of the repository to set as primary" + } + }, + "required": ["repository_id"] + }), + }, + // ============================================================================= + // Phase Guidance Tools + // ============================================================================= + Tool { + name: "get_phase_checklist".to_string(), + description: "Get a detailed checklist of phase deliverables showing what's been created vs what's expected. Includes completion percentage and suggestions for next steps.".to_string(), + parameters: json!({ + "type": "object", + "properties": {} + }), + }, + // ============================================================================= + // Task Derivation Tools + // ============================================================================= + Tool { + name: "derive_tasks_from_file".to_string(), + description: "Parse a file (typically Task Breakdown) to extract a list of tasks. Returns structured task data that can be used with create_chained_tasks.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "file_id": { + "type": "string", + "description": "ID of the file to parse tasks from (usually a Task Breakdown document)" + } + }, + "required": ["file_id"] + }), + }, + Tool { + name: "create_chained_tasks".to_string(), + description: "Create multiple tasks in sequence with automatic chaining. Each task will continue from the previous task's work using continue_from_task_id.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "tasks": { + "type": "array", + "description": "List of tasks to create in order", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Task name" + }, + "plan": { + "type": "string", + "description": "Task plan/instructions" + } + }, + "required": ["name", "plan"] + } + } + }, + "required": ["tasks"] + }), + }, + // ============================================================================= + // Task Completion Processing Tools + // ============================================================================= + Tool { + name: "process_task_completion".to_string(), + description: "Analyze a completed task's output and suggest next actions. Returns summary, affected files, and suggestions for follow-up tasks or file updates.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ID of the completed task to analyze" + } + }, + "required": ["task_id"] + }), + }, + Tool { + name: "update_file_from_task".to_string(), + description: "Update a contract file with information from a completed task. Useful for updating Dev Notes or Implementation Log with task summaries.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "file_id": { + "type": "string", + "description": "ID of the file to update" + }, + "task_id": { + "type": "string", + "description": "ID of the task whose output should be added" + }, + "section_title": { + "type": "string", + "description": "Optional title for the section being added (e.g., 'Task: Implement Authentication')" + } + }, + "required": ["file_id", "task_id"] + }), + }, + // ============================================================================= + // Interactive Tools + // ============================================================================= + Tool { + name: "ask_user".to_string(), + description: "Ask the user one or more questions. Use this when you need clarification, want to offer choices, or need user input before proceeding.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "List of questions to ask the user", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this question" + }, + "question": { + "type": "string", + "description": "The question to ask the user" + }, + "options": { + "type": "array", + "items": { "type": "string" }, + "description": "Multiple choice options for the user to select from" + }, + "allowMultiple": { + "type": "boolean", + "description": "If true, user can select multiple options" + }, + "allowCustom": { + "type": "boolean", + "description": "If true, user can provide a custom answer" + } + }, + "required": ["id", "question", "options"] + } + } + }, + "required": ["questions"] + }), + }, + ] +}); + +/// Request for contract tool operations that require async database access +#[derive(Debug, Clone)] +pub enum ContractToolRequest { + // Query operations + GetContractStatus, + ListContractFiles, + ListContractTasks, + ListContractRepositories, + ReadFile { file_id: Uuid }, + + // File management + CreateFileFromTemplate { + template_id: String, + name: String, + description: Option<String>, + }, + CreateEmptyFile { + name: String, + description: Option<String>, + }, + ListAvailableTemplates { phase: Option<String> }, + + // Task management + CreateContractTask { + name: String, + plan: String, + repository_url: Option<String>, + base_branch: Option<String>, + }, + DelegateContentGeneration { + file_id: Option<Uuid>, + instruction: String, + context: Option<String>, + }, + StartTask { task_id: Uuid }, + + // Phase management + GetPhaseInfo, + SuggestPhaseTransition, + AdvancePhase { new_phase: String }, + + // Repository management + ListDaemonDirectories, + AddRepository { + repo_type: String, + name: String, + url: Option<String>, + is_primary: bool, + }, + SetPrimaryRepository { repository_id: Uuid }, + + // Phase guidance + GetPhaseChecklist, + + // Task derivation + DeriveTasksFromFile { file_id: Uuid }, + CreateChainedTasks { tasks: Vec<ChainedTaskDef> }, + + // Task completion processing + ProcessTaskCompletion { task_id: Uuid }, + UpdateFileFromTask { + file_id: Uuid, + task_id: Uuid, + section_title: Option<String>, + }, +} + +/// Task definition for chained task creation +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ChainedTaskDef { + pub name: String, + pub plan: String, +} + +/// Result from executing a contract tool +#[derive(Debug)] +pub struct ContractToolExecutionResult { + pub success: bool, + pub message: String, + pub data: Option<serde_json::Value>, + /// Request for async operations (handled by contract_chat handler) + pub request: Option<ContractToolRequest>, + /// Questions to ask the user (pauses conversation) + pub pending_questions: Option<Vec<super::tools::UserQuestion>>, +} + +/// Parse and validate a contract tool call, returning a ContractToolRequest for async handling +pub fn parse_contract_tool_call(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + match call.name.as_str() { + // Query operations + "get_contract_status" => parse_get_contract_status(), + "list_contract_files" => parse_list_contract_files(), + "list_contract_tasks" => parse_list_contract_tasks(), + "list_contract_repositories" => parse_list_contract_repositories(), + "read_file" => parse_read_file(call), + + // File management + "create_file_from_template" => parse_create_file_from_template(call), + "create_empty_file" => parse_create_empty_file(call), + "list_available_templates" => parse_list_available_templates(call), + + // Task management + "create_contract_task" => parse_create_contract_task(call), + "delegate_content_generation" => parse_delegate_content_generation(call), + "start_task" => parse_start_task(call), + + // Phase management + "get_phase_info" => parse_get_phase_info(), + "suggest_phase_transition" => parse_suggest_phase_transition(), + "advance_phase" => parse_advance_phase(call), + + // Repository management + "list_daemon_directories" => parse_list_daemon_directories(), + "add_repository" => parse_add_repository(call), + "set_primary_repository" => parse_set_primary_repository(call), + + // Phase guidance + "get_phase_checklist" => parse_get_phase_checklist(), + + // Task derivation + "derive_tasks_from_file" => parse_derive_tasks_from_file(call), + "create_chained_tasks" => parse_create_chained_tasks(call), + + // Task completion processing + "process_task_completion" => parse_process_task_completion(call), + "update_file_from_task" => parse_update_file_from_task(call), + + // Interactive tools + "ask_user" => parse_ask_user(call), + + _ => ContractToolExecutionResult { + success: false, + message: format!("Unknown contract tool: {}", call.name), + data: None, + request: None, + pending_questions: None, + }, + } +} + +// ============================================================================= +// Query Tool Parsing +// ============================================================================= + +fn parse_get_contract_status() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Getting contract status...".to_string(), + data: None, + request: Some(ContractToolRequest::GetContractStatus), + pending_questions: None, + } +} + +fn parse_list_contract_files() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Listing contract files...".to_string(), + data: None, + request: Some(ContractToolRequest::ListContractFiles), + pending_questions: None, + } +} + +fn parse_list_contract_tasks() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Listing contract tasks...".to_string(), + data: None, + request: Some(ContractToolRequest::ListContractTasks), + pending_questions: None, + } +} + +fn parse_list_contract_repositories() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Listing contract repositories...".to_string(), + data: None, + request: Some(ContractToolRequest::ListContractRepositories), + pending_questions: None, + } +} + +fn parse_read_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let file_id = parse_uuid_arg(call, "file_id"); + let Some(file_id) = file_id else { + return error_result("Missing or invalid required parameter: file_id"); + }; + + ContractToolExecutionResult { + success: true, + message: "Reading file...".to_string(), + data: None, + request: Some(ContractToolRequest::ReadFile { file_id }), + pending_questions: None, + } +} + +// ============================================================================= +// File Management Tool Parsing +// ============================================================================= + +fn parse_create_file_from_template(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let template_id = call.arguments.get("template_id").and_then(|v| v.as_str()); + let name = call.arguments.get("name").and_then(|v| v.as_str()); + + let Some(template_id) = template_id else { + return error_result("Missing required parameter: template_id"); + }; + let Some(name) = name else { + return error_result("Missing required parameter: name"); + }; + + let description = call + .arguments + .get("description") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ContractToolExecutionResult { + success: true, + message: format!("Creating file '{}' from template '{}'...", name, template_id), + data: None, + request: Some(ContractToolRequest::CreateFileFromTemplate { + template_id: template_id.to_string(), + name: name.to_string(), + description, + }), + pending_questions: None, + } +} + +fn parse_create_empty_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let name = call.arguments.get("name").and_then(|v| v.as_str()); + + let Some(name) = name else { + return error_result("Missing required parameter: name"); + }; + + let description = call + .arguments + .get("description") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ContractToolExecutionResult { + success: true, + message: format!("Creating empty file '{}'...", name), + data: None, + request: Some(ContractToolRequest::CreateEmptyFile { + name: name.to_string(), + description, + }), + pending_questions: None, + } +} + +fn parse_list_available_templates(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let phase = call + .arguments + .get("phase") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ContractToolExecutionResult { + success: true, + message: "Listing available templates...".to_string(), + data: None, + request: Some(ContractToolRequest::ListAvailableTemplates { phase }), + pending_questions: None, + } +} + +// ============================================================================= +// Task Management Tool Parsing +// ============================================================================= + +fn parse_create_contract_task(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let name = call.arguments.get("name").and_then(|v| v.as_str()); + let plan = call.arguments.get("plan").and_then(|v| v.as_str()); + + let Some(name) = name else { + return error_result("Missing required parameter: name"); + }; + let Some(plan) = plan else { + return error_result("Missing required parameter: plan"); + }; + + let repository_url = call + .arguments + .get("repository_url") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let base_branch = call + .arguments + .get("base_branch") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ContractToolExecutionResult { + success: true, + message: format!("Creating task '{}'...", name), + data: None, + request: Some(ContractToolRequest::CreateContractTask { + name: name.to_string(), + plan: plan.to_string(), + repository_url, + base_branch, + }), + pending_questions: None, + } +} + +fn parse_delegate_content_generation(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let instruction = call.arguments.get("instruction").and_then(|v| v.as_str()); + + let Some(instruction) = instruction else { + return error_result("Missing required parameter: instruction"); + }; + + let file_id = parse_uuid_arg(call, "file_id"); + let context = call + .arguments + .get("context") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ContractToolExecutionResult { + success: true, + message: "Creating content generation task...".to_string(), + data: None, + request: Some(ContractToolRequest::DelegateContentGeneration { + file_id, + instruction: instruction.to_string(), + context, + }), + pending_questions: None, + } +} + +fn parse_start_task(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let task_id = parse_uuid_arg(call, "task_id"); + + let Some(task_id) = task_id else { + return error_result("Missing or invalid required parameter: task_id"); + }; + + ContractToolExecutionResult { + success: true, + message: "Starting task...".to_string(), + data: None, + request: Some(ContractToolRequest::StartTask { task_id }), + pending_questions: None, + } +} + +// ============================================================================= +// Phase Management Tool Parsing +// ============================================================================= + +fn parse_get_phase_info() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Getting phase information...".to_string(), + data: None, + request: Some(ContractToolRequest::GetPhaseInfo), + pending_questions: None, + } +} + +fn parse_suggest_phase_transition() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Analyzing phase transition readiness...".to_string(), + data: None, + request: Some(ContractToolRequest::SuggestPhaseTransition), + pending_questions: None, + } +} + +fn parse_advance_phase(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let new_phase = call.arguments.get("new_phase").and_then(|v| v.as_str()); + + let Some(new_phase) = new_phase else { + return error_result("Missing required parameter: new_phase"); + }; + + let valid_phases = ["research", "specify", "plan", "execute", "review"]; + if !valid_phases.contains(&new_phase) { + return error_result("Invalid phase. Must be one of: research, specify, plan, execute, review"); + } + + ContractToolExecutionResult { + success: true, + message: format!("Advancing to '{}' phase...", new_phase), + data: None, + request: Some(ContractToolRequest::AdvancePhase { + new_phase: new_phase.to_string(), + }), + pending_questions: None, + } +} + +// ============================================================================= +// Repository Management Tool Parsing +// ============================================================================= + +fn parse_list_daemon_directories() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Listing daemon directories...".to_string(), + data: None, + request: Some(ContractToolRequest::ListDaemonDirectories), + pending_questions: None, + } +} + +fn parse_add_repository(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let repo_type = call.arguments.get("type").and_then(|v| v.as_str()); + let name = call.arguments.get("name").and_then(|v| v.as_str()); + + let Some(repo_type) = repo_type else { + return error_result("Missing required parameter: type"); + }; + let Some(name) = name else { + return error_result("Missing required parameter: name"); + }; + + let valid_types = ["remote", "local", "managed"]; + if !valid_types.contains(&repo_type) { + return error_result("Invalid type. Must be one of: remote, local, managed"); + } + + let url = call + .arguments + .get("url") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + // Validate URL is provided for remote and local types + if (repo_type == "remote" || repo_type == "local") && url.is_none() { + return error_result("URL/path is required for remote and local repository types"); + } + + let is_primary = call + .arguments + .get("is_primary") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + ContractToolExecutionResult { + success: true, + message: format!("Adding {} repository '{}'...", repo_type, name), + data: None, + request: Some(ContractToolRequest::AddRepository { + repo_type: repo_type.to_string(), + name: name.to_string(), + url, + is_primary, + }), + pending_questions: None, + } +} + +fn parse_set_primary_repository(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let repository_id = parse_uuid_arg(call, "repository_id"); + let Some(repository_id) = repository_id else { + return error_result("Missing or invalid required parameter: repository_id"); + }; + + ContractToolExecutionResult { + success: true, + message: "Setting primary repository...".to_string(), + data: None, + request: Some(ContractToolRequest::SetPrimaryRepository { repository_id }), + pending_questions: None, + } +} + +// ============================================================================= +// Interactive Tool Parsing +// ============================================================================= + +fn parse_ask_user(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let questions_value = call.arguments.get("questions"); + + let Some(questions_array) = questions_value.and_then(|v| v.as_array()) else { + return error_result("Missing or invalid 'questions' parameter"); + }; + + let mut questions: Vec<super::tools::UserQuestion> = Vec::new(); + + for q in questions_array { + let id = q.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let question = q.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let options: Vec<String> = q + .get("options") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|o| o.as_str()) + .map(|s| s.to_string()) + .collect() + }) + .unwrap_or_default(); + let allow_multiple = q.get("allowMultiple").and_then(|v| v.as_bool()).unwrap_or(false); + let allow_custom = q.get("allowCustom").and_then(|v| v.as_bool()).unwrap_or(true); + + if id.is_empty() || question.is_empty() || options.is_empty() { + continue; + } + + questions.push(super::tools::UserQuestion { + id, + question, + options, + allow_multiple, + allow_custom, + }); + } + + if questions.is_empty() { + return error_result("No valid questions provided"); + } + + let question_count = questions.len(); + ContractToolExecutionResult { + success: true, + message: format!("Asking user {} question(s). Waiting for response...", question_count), + data: None, + request: None, + pending_questions: Some(questions), + } +} + +// ============================================================================= +// Phase Guidance Tool Parsing +// ============================================================================= + +fn parse_get_phase_checklist() -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: true, + message: "Getting phase checklist...".to_string(), + data: None, + request: Some(ContractToolRequest::GetPhaseChecklist), + pending_questions: None, + } +} + +// ============================================================================= +// Task Derivation Tool Parsing +// ============================================================================= + +fn parse_derive_tasks_from_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let file_id = parse_uuid_arg(call, "file_id"); + let Some(file_id) = file_id else { + return error_result("Missing or invalid required parameter: file_id"); + }; + + ContractToolExecutionResult { + success: true, + message: "Deriving tasks from file...".to_string(), + data: None, + request: Some(ContractToolRequest::DeriveTasksFromFile { file_id }), + pending_questions: None, + } +} + +fn parse_create_chained_tasks(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let tasks_value = call.arguments.get("tasks"); + + let Some(tasks_array) = tasks_value.and_then(|v| v.as_array()) else { + return error_result("Missing or invalid 'tasks' parameter"); + }; + + let mut tasks: Vec<ChainedTaskDef> = Vec::new(); + + for task in tasks_array { + let name = task.get("name").and_then(|v| v.as_str()); + let plan = task.get("plan").and_then(|v| v.as_str()); + + match (name, plan) { + (Some(n), Some(p)) => { + tasks.push(ChainedTaskDef { + name: n.to_string(), + plan: p.to_string(), + }); + } + _ => { + return error_result("Each task must have 'name' and 'plan' fields"); + } + } + } + + if tasks.is_empty() { + return error_result("No valid tasks provided"); + } + + let task_count = tasks.len(); + ContractToolExecutionResult { + success: true, + message: format!("Creating {} chained task(s)...", task_count), + data: None, + request: Some(ContractToolRequest::CreateChainedTasks { tasks }), + pending_questions: None, + } +} + +// ============================================================================= +// Task Completion Processing Tool Parsing +// ============================================================================= + +fn parse_process_task_completion(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let task_id = parse_uuid_arg(call, "task_id"); + let Some(task_id) = task_id else { + return error_result("Missing or invalid required parameter: task_id"); + }; + + ContractToolExecutionResult { + success: true, + message: "Processing task completion...".to_string(), + data: None, + request: Some(ContractToolRequest::ProcessTaskCompletion { task_id }), + pending_questions: None, + } +} + +fn parse_update_file_from_task(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let file_id = parse_uuid_arg(call, "file_id"); + let task_id = parse_uuid_arg(call, "task_id"); + + let Some(file_id) = file_id else { + return error_result("Missing or invalid required parameter: file_id"); + }; + let Some(task_id) = task_id else { + return error_result("Missing or invalid required parameter: task_id"); + }; + + let section_title = call + .arguments + .get("section_title") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ContractToolExecutionResult { + success: true, + message: "Updating file from task...".to_string(), + data: None, + request: Some(ContractToolRequest::UpdateFileFromTask { + file_id, + task_id, + section_title, + }), + pending_questions: None, + } +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +fn parse_uuid_arg(call: &super::tools::ToolCall, key: &str) -> Option<Uuid> { + call.arguments + .get(key) + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()) +} + +fn error_result(message: &str) -> ContractToolExecutionResult { + ContractToolExecutionResult { + success: false, + message: message.to_string(), + data: None, + request: None, + pending_questions: None, + } +} diff --git a/makima/src/llm/markdown.rs b/makima/src/llm/markdown.rs new file mode 100644 index 0000000..482dc8c --- /dev/null +++ b/makima/src/llm/markdown.rs @@ -0,0 +1,334 @@ +//! Markdown conversion utilities for BodyElement arrays. +//! +//! Provides bidirectional conversion between structured BodyElement[] and markdown strings. + +use crate::db::models::BodyElement; + +/// Convert a slice of BodyElements to a markdown string. +/// +/// Handles: +/// - Headings: `# heading` through `###### heading` based on level +/// - Paragraphs: plain text with blank lines between +/// - Code blocks: ````language\ncontent\n```` +/// - Lists: ordered (1. 2. 3.) and unordered (- - -) +/// - Charts: rendered as fenced JSON with chart type +/// - Images: rendered as markdown image syntax +pub fn body_to_markdown(elements: &[BodyElement]) -> String { + elements + .iter() + .filter_map(|elem| match elem { + BodyElement::Heading { level, text } => { + let hashes = "#".repeat((*level).min(6) as usize); + Some(format!("{} {}", hashes, text)) + } + BodyElement::Paragraph { text } => Some(text.clone()), + BodyElement::Code { language, content } => { + let lang = language.as_deref().unwrap_or(""); + Some(format!("```{}\n{}\n```", lang, content)) + } + BodyElement::List { ordered, items } => { + let list: Vec<String> = items + .iter() + .enumerate() + .map(|(i, item)| { + if *ordered { + format!("{}. {}", i + 1, item) + } else { + format!("- {}", item) + } + }) + .collect(); + Some(list.join("\n")) + } + BodyElement::Chart { + chart_type, + title, + data, + config: _, + } => { + // Render chart as a fenced block with metadata + let title_str = title + .as_ref() + .map(|t| format!(" - {}", t)) + .unwrap_or_default(); + let data_str = serde_json::to_string_pretty(data).unwrap_or_default(); + Some(format!( + "```chart:{:?}{}\n{}\n```", + chart_type, title_str, data_str + )) + } + BodyElement::Image { src, alt, caption } => { + let alt_text = alt.as_deref().unwrap_or("image"); + let caption_str = caption + .as_ref() + .map(|c| format!("\n*{}*", c)) + .unwrap_or_default(); + Some(format!("{}", alt_text, src, caption_str)) + } + // Markdown elements output their content directly - it's already markdown + BodyElement::Markdown { content } => Some(content.clone()), + }) + .collect::<Vec<_>>() + .join("\n\n") +} + +/// Parse a markdown string into a vector of BodyElements. +/// +/// Handles: +/// - Headings: lines starting with # through ###### +/// - Code blocks: ````language ... ```` +/// - Ordered lists: lines starting with 1. 2. etc. +/// - Unordered lists: lines starting with - or * +/// - Paragraphs: all other non-empty lines +pub fn markdown_to_body(markdown: &str) -> Vec<BodyElement> { + let mut elements = Vec::new(); + let lines: Vec<&str> = markdown.lines().collect(); + let mut i = 0; + + while i < lines.len() { + let line = lines[i]; + let trimmed = line.trim(); + + // Skip empty lines + if trimmed.is_empty() { + i += 1; + continue; + } + + // Check for code blocks + if trimmed.starts_with("```") { + let language = trimmed.trim_start_matches('`').trim(); + let language = if language.is_empty() { + None + } else { + Some(language.to_string()) + }; + + let mut content_lines = Vec::new(); + i += 1; + + // Collect content until closing ``` + while i < lines.len() && !lines[i].trim().starts_with("```") { + content_lines.push(lines[i]); + i += 1; + } + + // Skip the closing ``` + if i < lines.len() { + i += 1; + } + + elements.push(BodyElement::Code { + language, + content: content_lines.join("\n"), + }); + continue; + } + + // Check for headings + if trimmed.starts_with('#') { + let level = trimmed.chars().take_while(|&c| c == '#').count() as u8; + let text = trimmed.trim_start_matches('#').trim().to_string(); + elements.push(BodyElement::Heading { level, text }); + i += 1; + continue; + } + + // Check for unordered lists (- or *) + if trimmed.starts_with("- ") || trimmed.starts_with("* ") { + let mut items = Vec::new(); + while i < lines.len() { + let current = lines[i].trim(); + if current.starts_with("- ") || current.starts_with("* ") { + items.push(current[2..].to_string()); + i += 1; + } else if current.is_empty() { + i += 1; + break; + } else { + break; + } + } + elements.push(BodyElement::List { + ordered: false, + items, + }); + continue; + } + + // Check for ordered lists (1. 2. etc.) + if let Some(rest) = try_parse_ordered_list_item(trimmed) { + let mut items = Vec::new(); + items.push(rest.to_string()); + i += 1; + + while i < lines.len() { + let current = lines[i].trim(); + if let Some(item_rest) = try_parse_ordered_list_item(current) { + items.push(item_rest.to_string()); + i += 1; + } else if current.is_empty() { + i += 1; + break; + } else { + break; + } + } + elements.push(BodyElement::List { + ordered: true, + items, + }); + continue; + } + + // Default: paragraph (collect consecutive non-empty lines) + let mut para_lines = Vec::new(); + while i < lines.len() { + let current = lines[i].trim(); + if current.is_empty() + || current.starts_with('#') + || current.starts_with("```") + || current.starts_with("- ") + || current.starts_with("* ") + || try_parse_ordered_list_item(current).is_some() + { + break; + } + para_lines.push(current); + i += 1; + } + + if !para_lines.is_empty() { + elements.push(BodyElement::Paragraph { + text: para_lines.join(" "), + }); + } + } + + elements +} + +/// Try to parse an ordered list item (e.g., "1. Item text") +/// Returns the text after the number and period, or None if not a list item. +fn try_parse_ordered_list_item(s: &str) -> Option<&str> { + let mut chars = s.char_indices(); + + // Must start with a digit + let (_, first) = chars.next()?; + if !first.is_ascii_digit() { + return None; + } + + // Consume remaining digits + let mut last_digit_end = 1; + for (idx, c) in chars.by_ref() { + if c.is_ascii_digit() { + last_digit_end = idx + 1; + } else if c == '.' { + // Found the period - check for space after + let rest = &s[last_digit_end + 1..]; + let rest = rest.trim_start(); + if !rest.is_empty() || s.ends_with(". ") { + return Some(rest); + } + return None; + } else { + return None; + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_body_to_markdown_heading() { + let elements = vec![BodyElement::Heading { + level: 2, + text: "Hello World".to_string(), + }]; + assert_eq!(body_to_markdown(&elements), "## Hello World"); + } + + #[test] + fn test_body_to_markdown_paragraph() { + let elements = vec![BodyElement::Paragraph { + text: "This is a paragraph.".to_string(), + }]; + assert_eq!(body_to_markdown(&elements), "This is a paragraph."); + } + + #[test] + fn test_body_to_markdown_code() { + let elements = vec![BodyElement::Code { + language: Some("rust".to_string()), + content: "fn main() {}".to_string(), + }]; + assert_eq!( + body_to_markdown(&elements), + "```rust\nfn main() {}\n```" + ); + } + + #[test] + fn test_body_to_markdown_list() { + let elements = vec![BodyElement::List { + ordered: false, + items: vec!["Item 1".to_string(), "Item 2".to_string()], + }]; + assert_eq!(body_to_markdown(&elements), "- Item 1\n- Item 2"); + } + + #[test] + fn test_markdown_to_body_heading() { + let md = "## Hello World"; + let elements = markdown_to_body(md); + assert_eq!(elements.len(), 1); + match &elements[0] { + BodyElement::Heading { level, text } => { + assert_eq!(*level, 2); + assert_eq!(text, "Hello World"); + } + _ => panic!("Expected Heading"), + } + } + + #[test] + fn test_markdown_to_body_code() { + let md = "```rust\nfn main() {}\n```"; + let elements = markdown_to_body(md); + assert_eq!(elements.len(), 1); + match &elements[0] { + BodyElement::Code { language, content } => { + assert_eq!(language.as_deref(), Some("rust")); + assert_eq!(content, "fn main() {}"); + } + _ => panic!("Expected Code"), + } + } + + #[test] + fn test_roundtrip() { + let original = vec![ + BodyElement::Heading { + level: 1, + text: "Title".to_string(), + }, + BodyElement::Paragraph { + text: "Some text here.".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["A".to_string(), "B".to_string()], + }, + ]; + + let markdown = body_to_markdown(&original); + let parsed = markdown_to_body(&markdown); + + assert_eq!(parsed.len(), 3); + } +} diff --git a/makima/src/llm/mesh_tools.rs b/makima/src/llm/mesh_tools.rs index 1d12c66..ec9dd01 100644 --- a/makima/src/llm/mesh_tools.rs +++ b/makima/src/llm/mesh_tools.rs @@ -418,6 +418,140 @@ pub static MESH_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::Lazy: "required": ["questions"] }), }, + // ============================================================================= + // Supervisor Tools (only available to supervisor tasks) + // ============================================================================= + Tool { + name: "get_all_contract_tasks".to_string(), + description: "Get status of all tasks in the contract tree. Only available to supervisor tasks.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "contract_id": { + "type": "string", + "description": "ID of the contract to query tasks for" + } + }, + "required": ["contract_id"] + }), + }, + Tool { + name: "wait_for_task_completion".to_string(), + description: "Block until a task completes or timeout. Only available to supervisor tasks.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ID of the task to wait for" + }, + "timeout_seconds": { + "type": "integer", + "description": "Maximum time to wait in seconds (default: 300)", + "default": 300 + } + }, + "required": ["task_id"] + }), + }, + Tool { + name: "read_task_worktree".to_string(), + description: "Read a file from any task's worktree. Only available to supervisor tasks.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ID of the task whose worktree to read from" + }, + "file_path": { + "type": "string", + "description": "Path to the file within the worktree" + } + }, + "required": ["task_id", "file_path"] + }), + }, + Tool { + name: "spawn_task".to_string(), + description: "Create and start a child task (fire and forget). Only available to supervisor tasks.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the task" + }, + "plan": { + "type": "string", + "description": "Detailed instructions/plan for what the task should accomplish" + }, + "parent_task_id": { + "type": "string", + "description": "Optional parent task to branch from" + }, + "checkpoint_sha": { + "type": "string", + "description": "Optional checkpoint SHA to branch from" + }, + "repository_url": { + "type": "string", + "description": "Git repository URL (optional - inherits from contract if not provided)" + }, + "base_branch": { + "type": "string", + "description": "Optional base branch to start from" + } + }, + "required": ["name", "plan"] + }), + }, + Tool { + name: "create_checkpoint".to_string(), + description: "Create a git checkpoint (commit) in the current task's worktree. Only available to supervisor tasks.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ID of the task to checkpoint" + }, + "message": { + "type": "string", + "description": "Commit message for the checkpoint" + } + }, + "required": ["task_id", "message"] + }), + }, + Tool { + name: "list_task_checkpoints".to_string(), + description: "List all checkpoints for a task.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ID of the task to list checkpoints for" + } + }, + "required": ["task_id"] + }), + }, + Tool { + name: "get_task_tree".to_string(), + description: "Get the full task tree starting from a specific task.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ID of the root task" + } + }, + "required": ["task_id"] + }), + }, ] }); @@ -506,6 +640,37 @@ pub enum MeshToolRequest { task_id: Uuid, mode: String, }, + + // Supervisor tools (only for supervisor tasks) + GetAllContractTasks { + contract_id: Uuid, + }, + WaitForTaskCompletion { + task_id: Uuid, + timeout_seconds: i32, + }, + ReadTaskWorktree { + task_id: Uuid, + file_path: String, + }, + SpawnTask { + name: String, + plan: String, + parent_task_id: Option<Uuid>, + checkpoint_sha: Option<String>, + repository_url: Option<String>, + base_branch: Option<String>, + }, + CreateCheckpoint { + task_id: Uuid, + message: String, + }, + ListTaskCheckpoints { + task_id: Uuid, + }, + GetTaskTree { + task_id: Uuid, + }, } /// Result from executing a mesh tool @@ -560,6 +725,15 @@ pub fn parse_mesh_tool_call( // Interactive tools "ask_user" => parse_ask_user(call), + // Supervisor tools + "get_all_contract_tasks" => parse_get_all_contract_tasks(call), + "wait_for_task_completion" => parse_wait_for_task_completion(call), + "read_task_worktree" => parse_read_task_worktree(call), + "spawn_task" => parse_spawn_task(call), + "create_checkpoint" => parse_create_checkpoint(call), + "list_task_checkpoints" => parse_list_task_checkpoints(call), + "get_task_tree" => parse_get_task_tree(call), + _ => MeshToolExecutionResult { success: false, message: format!("Unknown mesh tool: {}", call.name), @@ -1059,6 +1233,163 @@ fn parse_ask_user(call: &super::tools::ToolCall) -> MeshToolExecutionResult { } // ============================================================================= +// Supervisor Tool Parsing Functions +// ============================================================================= + +fn parse_get_all_contract_tasks(call: &super::tools::ToolCall) -> MeshToolExecutionResult { + let Some(contract_id) = parse_uuid_arg(call, "contract_id") else { + return error_result("Missing or invalid contract_id"); + }; + + MeshToolExecutionResult { + success: true, + message: "Querying all contract tasks...".to_string(), + data: None, + request: Some(MeshToolRequest::GetAllContractTasks { contract_id }), + pending_questions: None, + } +} + +fn parse_wait_for_task_completion(call: &super::tools::ToolCall) -> MeshToolExecutionResult { + let Some(task_id) = parse_uuid_arg(call, "task_id") else { + return error_result("Missing or invalid task_id"); + }; + + let timeout_seconds = call + .arguments + .get("timeout_seconds") + .and_then(|v| v.as_i64()) + .map(|v| v as i32) + .unwrap_or(300); + + MeshToolExecutionResult { + success: true, + message: format!("Waiting for task completion (timeout: {}s)...", timeout_seconds), + data: None, + request: Some(MeshToolRequest::WaitForTaskCompletion { + task_id, + timeout_seconds, + }), + pending_questions: None, + } +} + +fn parse_read_task_worktree(call: &super::tools::ToolCall) -> MeshToolExecutionResult { + let Some(task_id) = parse_uuid_arg(call, "task_id") else { + return error_result("Missing or invalid task_id"); + }; + + let Some(file_path) = call.arguments.get("file_path").and_then(|v| v.as_str()) else { + return error_result("Missing required parameter: file_path"); + }; + + MeshToolExecutionResult { + success: true, + message: format!("Reading file from task worktree: {}", file_path), + data: None, + request: Some(MeshToolRequest::ReadTaskWorktree { + task_id, + file_path: file_path.to_string(), + }), + pending_questions: None, + } +} + +fn parse_spawn_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult { + let Some(name) = call.arguments.get("name").and_then(|v| v.as_str()) else { + return error_result("Missing required parameter: name"); + }; + + let Some(plan) = call.arguments.get("plan").and_then(|v| v.as_str()) else { + return error_result("Missing required parameter: plan"); + }; + + let parent_task_id = parse_uuid_arg(call, "parent_task_id"); + + let checkpoint_sha = call + .arguments + .get("checkpoint_sha") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let repository_url = call + .arguments + .get("repository_url") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let base_branch = call + .arguments + .get("base_branch") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + MeshToolExecutionResult { + success: true, + message: format!("Spawning task: {}", name), + data: None, + request: Some(MeshToolRequest::SpawnTask { + name: name.to_string(), + plan: plan.to_string(), + parent_task_id, + checkpoint_sha, + repository_url, + base_branch, + }), + pending_questions: None, + } +} + +fn parse_create_checkpoint(call: &super::tools::ToolCall) -> MeshToolExecutionResult { + let Some(task_id) = parse_uuid_arg(call, "task_id") else { + return error_result("Missing or invalid task_id"); + }; + + let Some(message) = call.arguments.get("message").and_then(|v| v.as_str()) else { + return error_result("Missing required parameter: message"); + }; + + MeshToolExecutionResult { + success: true, + message: format!("Creating checkpoint: {}", message), + data: None, + request: Some(MeshToolRequest::CreateCheckpoint { + task_id, + message: message.to_string(), + }), + pending_questions: None, + } +} + +fn parse_list_task_checkpoints(call: &super::tools::ToolCall) -> MeshToolExecutionResult { + let Some(task_id) = parse_uuid_arg(call, "task_id") else { + return error_result("Missing or invalid task_id"); + }; + + MeshToolExecutionResult { + success: true, + message: "Listing task checkpoints...".to_string(), + data: None, + request: Some(MeshToolRequest::ListTaskCheckpoints { task_id }), + pending_questions: None, + } +} + +fn parse_get_task_tree(call: &super::tools::ToolCall) -> MeshToolExecutionResult { + let Some(task_id) = parse_uuid_arg(call, "task_id") else { + return error_result("Missing or invalid task_id"); + }; + + MeshToolExecutionResult { + success: true, + message: "Getting task tree...".to_string(), + data: None, + request: Some(MeshToolRequest::GetTaskTree { task_id }), + pending_questions: None, + } +} + +// ============================================================================= // Helper Functions // ============================================================================= diff --git a/makima/src/llm/mod.rs b/makima/src/llm/mod.rs index 39cdbdd..da8c0a4 100644 --- a/makima/src/llm/mod.rs +++ b/makima/src/llm/mod.rs @@ -1,13 +1,33 @@ //! LLM integration module for file editing via tool calling. pub mod claude; +pub mod contract_tools; pub mod groq; +pub mod markdown; pub mod mesh_tools; +pub mod phase_guidance; +pub mod task_output; +pub mod templates; pub mod tools; pub use claude::{ClaudeClient, ClaudeModel}; +pub use contract_tools::{ + parse_contract_tool_call, ChainedTaskDef, ContractToolExecutionResult, ContractToolRequest, + CONTRACT_TOOLS, +}; pub use groq::GroqClient; pub use mesh_tools::{parse_mesh_tool_call, MeshToolExecutionResult, MeshToolRequest, MESH_TOOLS}; +pub use phase_guidance::{ + check_phase_completion, format_checklist_markdown, get_phase_checklist, get_phase_deliverables, + DeliverableStatus, FileInfo, FilePriority, PhaseChecklist, PhaseDeliverables, RecommendedFile, + TaskInfo, TaskStats, +}; +pub use task_output::{ + analyze_task_output, format_parsed_tasks, parse_tasks_from_breakdown, ParsedTask, + PhaseImpact, SuggestedAction, TaskOutputAnalysis, TaskParseResult, +}; +pub use markdown::{body_to_markdown, markdown_to_body}; +pub use templates::{all_templates, templates_for_phase, FileTemplate}; pub use tools::{ execute_tool_call, Tool, ToolCall, ToolResult, UserAnswer, UserQuestion, VersionToolRequest, AVAILABLE_TOOLS, diff --git a/makima/src/llm/phase_guidance.rs b/makima/src/llm/phase_guidance.rs new file mode 100644 index 0000000..e2d6cd8 --- /dev/null +++ b/makima/src/llm/phase_guidance.rs @@ -0,0 +1,594 @@ +//! Phase guidance and deliverables tracking for contract management. +//! +//! This module provides structured guidance for each contract phase, tracking +//! expected deliverables and completion criteria. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Priority level for recommended deliverables +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum FilePriority { + /// Must exist before advancing phase + Required, + /// Strongly suggested for phase completion + Recommended, + /// Nice to have, not blocking + Optional, +} + +/// A recommended file for a phase +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecommendedFile { + /// Template ID to create from + pub template_id: String, + /// Suggested file name + pub name_suggestion: String, + /// Priority level + pub priority: FilePriority, + /// Brief description of purpose + pub description: String, +} + +/// Expected deliverables for a phase +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhaseDeliverables { + /// Phase name + pub phase: String, + /// Recommended files to create + pub recommended_files: Vec<RecommendedFile>, + /// Whether a repository is required for this phase + pub requires_repository: bool, + /// Whether tasks should exist in this phase + pub requires_tasks: bool, + /// Guidance text for this phase + pub guidance: String, +} + +/// Status of a deliverable item +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct DeliverableStatus { + /// Template ID + pub template_id: String, + /// Expected name + pub name: String, + /// Priority + pub priority: FilePriority, + /// Whether it has been created + pub completed: bool, + /// File ID if created + pub file_id: Option<Uuid>, + /// Actual file name if created + pub actual_name: Option<String>, +} + +/// Checklist for phase completion +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct PhaseChecklist { + /// Current phase + pub phase: String, + /// File deliverables status + pub file_deliverables: Vec<DeliverableStatus>, + /// Whether repository is configured + pub has_repository: bool, + /// Whether repository was required + pub repository_required: bool, + /// Task statistics (for execute phase) + pub task_stats: Option<TaskStats>, + /// Overall completion percentage (0-100) + pub completion_percentage: u8, + /// Summary message + pub summary: String, + /// Suggestions for next actions + pub suggestions: Vec<String>, +} + +/// Task statistics for execute phase +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TaskStats { + pub total: usize, + pub pending: usize, + pub running: usize, + pub done: usize, + pub failed: usize, +} + +/// Minimal file info for checklist building +pub struct FileInfo { + pub id: Uuid, + pub name: String, + pub contract_phase: Option<String>, +} + +/// Minimal task info for checklist building +pub struct TaskInfo { + pub id: Uuid, + pub name: String, + pub status: String, +} + +/// Get phase deliverables configuration +pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables { + match phase { + "research" => PhaseDeliverables { + phase: "research".to_string(), + recommended_files: vec![ + RecommendedFile { + template_id: "research-notes".to_string(), + name_suggestion: "Research Notes".to_string(), + priority: FilePriority::Recommended, + description: "Document findings and insights during research".to_string(), + }, + RecommendedFile { + template_id: "competitor-analysis".to_string(), + name_suggestion: "Competitor Analysis".to_string(), + priority: FilePriority::Recommended, + description: "Analyze competitors and market positioning".to_string(), + }, + RecommendedFile { + template_id: "user-research".to_string(), + name_suggestion: "User Research".to_string(), + priority: FilePriority::Optional, + description: "Document user interviews and persona insights".to_string(), + }, + ], + requires_repository: false, + requires_tasks: false, + guidance: "Focus on understanding the problem space, gathering information, and documenting findings. Create at least one research document before moving to Specify phase.".to_string(), + }, + "specify" => PhaseDeliverables { + phase: "specify".to_string(), + recommended_files: vec![ + RecommendedFile { + template_id: "requirements".to_string(), + name_suggestion: "Requirements Document".to_string(), + priority: FilePriority::Required, + description: "Define functional and non-functional requirements".to_string(), + }, + RecommendedFile { + template_id: "user-stories".to_string(), + name_suggestion: "User Stories".to_string(), + priority: FilePriority::Recommended, + description: "Define features from the user's perspective".to_string(), + }, + RecommendedFile { + template_id: "acceptance-criteria".to_string(), + name_suggestion: "Acceptance Criteria".to_string(), + priority: FilePriority::Recommended, + description: "Define testable conditions for completion".to_string(), + }, + ], + requires_repository: false, + requires_tasks: false, + guidance: "Define what needs to be built with clear requirements and acceptance criteria. Ensure specifications are detailed enough for planning.".to_string(), + }, + "plan" => PhaseDeliverables { + phase: "plan".to_string(), + recommended_files: vec![ + RecommendedFile { + template_id: "architecture".to_string(), + name_suggestion: "Architecture Document".to_string(), + priority: FilePriority::Recommended, + description: "Document system architecture and design decisions".to_string(), + }, + RecommendedFile { + template_id: "task-breakdown".to_string(), + name_suggestion: "Task Breakdown".to_string(), + priority: FilePriority::Required, + description: "Break down work into implementable tasks".to_string(), + }, + RecommendedFile { + template_id: "technical-design".to_string(), + name_suggestion: "Technical Design".to_string(), + priority: FilePriority::Optional, + description: "Detailed technical specification".to_string(), + }, + ], + requires_repository: true, + requires_tasks: false, + guidance: "Design the solution and break down work into tasks. A repository must be configured before moving to Execute phase.".to_string(), + }, + "execute" => PhaseDeliverables { + phase: "execute".to_string(), + recommended_files: vec![ + RecommendedFile { + template_id: "dev-notes".to_string(), + name_suggestion: "Development Notes".to_string(), + priority: FilePriority::Recommended, + description: "Track implementation details and decisions".to_string(), + }, + RecommendedFile { + template_id: "test-plan".to_string(), + name_suggestion: "Test Plan".to_string(), + priority: FilePriority::Optional, + description: "Document testing strategy and test cases".to_string(), + }, + RecommendedFile { + template_id: "implementation-log".to_string(), + name_suggestion: "Implementation Log".to_string(), + priority: FilePriority::Optional, + description: "Chronological log of implementation progress".to_string(), + }, + ], + requires_repository: true, + requires_tasks: true, + guidance: "Execute the planned tasks, implement features, and track progress. Complete all tasks before moving to Review phase.".to_string(), + }, + "review" => PhaseDeliverables { + phase: "review".to_string(), + recommended_files: vec![ + RecommendedFile { + template_id: "release-notes".to_string(), + name_suggestion: "Release Notes".to_string(), + priority: FilePriority::Required, + description: "Document changes for release communication".to_string(), + }, + RecommendedFile { + template_id: "review-checklist".to_string(), + name_suggestion: "Review Checklist".to_string(), + priority: FilePriority::Recommended, + description: "Comprehensive checklist for code and feature review".to_string(), + }, + RecommendedFile { + template_id: "retrospective".to_string(), + name_suggestion: "Retrospective".to_string(), + priority: FilePriority::Optional, + description: "Reflect on the project and capture learnings".to_string(), + }, + ], + requires_repository: false, + requires_tasks: false, + guidance: "Review completed work, document the release, and conduct a retrospective. The contract can be completed after review.".to_string(), + }, + _ => PhaseDeliverables { + phase: phase.to_string(), + recommended_files: vec![], + requires_repository: false, + requires_tasks: false, + guidance: "Unknown phase".to_string(), + }, + } +} + +/// Build a phase checklist comparing expected vs actual deliverables +pub fn get_phase_checklist( + phase: &str, + files: &[FileInfo], + tasks: &[TaskInfo], + has_repository: bool, +) -> PhaseChecklist { + let deliverables = get_phase_deliverables(phase); + + // Match files to expected deliverables + let file_deliverables: Vec<DeliverableStatus> = deliverables + .recommended_files + .iter() + .map(|rec| { + // Check if a file with matching template ID or similar name exists + let matched_file = files.iter().find(|f| { + // Match by phase first + f.contract_phase.as_deref() == Some(phase) && + // Then by name similarity (case-insensitive contains) + (f.name.to_lowercase().contains(&rec.name_suggestion.to_lowercase()) || + rec.name_suggestion.to_lowercase().contains(&f.name.to_lowercase()) || + f.name.to_lowercase().contains(&rec.template_id.replace("-", " "))) + }); + + DeliverableStatus { + template_id: rec.template_id.clone(), + name: rec.name_suggestion.clone(), + priority: rec.priority, + completed: matched_file.is_some(), + file_id: matched_file.map(|f| f.id), + actual_name: matched_file.map(|f| f.name.clone()), + } + }) + .collect(); + + // Calculate task stats for execute phase + let task_stats = if phase == "execute" { + let total = tasks.len(); + let pending = tasks.iter().filter(|t| t.status == "pending").count(); + let running = tasks.iter().filter(|t| t.status == "running").count(); + let done = tasks.iter().filter(|t| t.status == "done").count(); + let failed = tasks.iter().filter(|t| t.status == "failed" || t.status == "error").count(); + + Some(TaskStats { total, pending, running, done, failed }) + } else { + None + }; + + // Calculate completion percentage + let mut completed_items = 0; + let mut total_items = 0; + + // Count required and recommended files (not optional) + for status in &file_deliverables { + if status.priority != FilePriority::Optional { + total_items += 1; + if status.completed { + completed_items += 1; + } + } + } + + // Count repository if required + if deliverables.requires_repository { + total_items += 1; + if has_repository { + completed_items += 1; + } + } + + // Count tasks if in execute phase + if let Some(ref stats) = task_stats { + if stats.total > 0 { + total_items += 1; + if stats.done == stats.total && stats.total > 0 { + completed_items += 1; + } + } + } + + let completion_percentage = if total_items > 0 { + ((completed_items as f64 / total_items as f64) * 100.0) as u8 + } else { + 100 // No requirements means complete + }; + + // Generate suggestions + let mut suggestions = Vec::new(); + + // Suggest missing required files + for status in &file_deliverables { + if !status.completed { + match status.priority { + FilePriority::Required => { + suggestions.push(format!("Create {} (required)", status.name)); + } + FilePriority::Recommended => { + suggestions.push(format!("Consider creating {} (recommended)", status.name)); + } + FilePriority::Optional => { + // Don't suggest optional items + } + } + } + } + + // Suggest repository if needed + if deliverables.requires_repository && !has_repository { + suggestions.push("Configure a repository for task execution".to_string()); + } + + // Suggest task actions for execute phase + if let Some(ref stats) = task_stats { + if stats.total == 0 { + suggestions.push("Create tasks from the Task Breakdown document".to_string()); + } else if stats.pending > 0 { + suggestions.push(format!("Run {} pending task(s)", stats.pending)); + } else if stats.running > 0 { + suggestions.push(format!("Wait for {} running task(s) to complete", stats.running)); + } else if stats.failed > 0 { + suggestions.push(format!("Address {} failed task(s)", stats.failed)); + } + } + + // Generate summary + let summary = generate_phase_summary(phase, &file_deliverables, has_repository, &task_stats, completion_percentage); + + PhaseChecklist { + phase: phase.to_string(), + file_deliverables, + has_repository, + repository_required: deliverables.requires_repository, + task_stats, + completion_percentage, + summary, + suggestions, + } +} + +fn generate_phase_summary( + phase: &str, + deliverables: &[DeliverableStatus], + has_repository: bool, + task_stats: &Option<TaskStats>, + completion_percentage: u8, +) -> String { + let completed_count = deliverables.iter().filter(|d| d.completed).count(); + let total_count = deliverables.len(); + + match phase { + "research" => { + if completed_count == 0 { + "Research phase needs documentation. Create research notes or competitor analysis.".to_string() + } else { + format!("{}/{} research documents created. Consider transitioning to Specify phase.", completed_count, total_count) + } + } + "specify" => { + let has_required = deliverables.iter() + .filter(|d| d.priority == FilePriority::Required) + .all(|d| d.completed); + + if !has_required { + "Specify phase requires a Requirements Document before transitioning.".to_string() + } else if completion_percentage >= 66 { + "Specifications are ready. Consider transitioning to Plan phase.".to_string() + } else { + format!("{}/{} specification documents created.", completed_count, total_count) + } + } + "plan" => { + let has_task_breakdown = deliverables.iter() + .any(|d| d.template_id == "task-breakdown" && d.completed); + + if !has_task_breakdown { + "Plan phase requires a Task Breakdown document.".to_string() + } else if !has_repository { + "Repository not configured. Configure a repository before Execute phase.".to_string() + } else { + "Planning complete. Ready to transition to Execute phase.".to_string() + } + } + "execute" => { + if let Some(stats) = task_stats { + if stats.total == 0 { + "No tasks created. Create tasks from the Task Breakdown document.".to_string() + } else if stats.done == stats.total { + "All tasks complete! Ready for Review phase.".to_string() + } else { + format!("{}/{} tasks completed ({}% done)", stats.done, stats.total, + if stats.total > 0 { (stats.done * 100) / stats.total } else { 0 }) + } + } else { + "Execute phase in progress.".to_string() + } + } + "review" => { + let has_release_notes = deliverables.iter() + .any(|d| d.template_id == "release-notes" && d.completed); + + if !has_release_notes { + "Review phase requires Release Notes before completion.".to_string() + } else { + "Review documentation complete. Contract can be marked as done.".to_string() + } + } + _ => format!("Phase {} - {}% complete", phase, completion_percentage), + } +} + +/// Check if phase targets are met for transition +pub fn check_phase_completion( + phase: &str, + files: &[FileInfo], + tasks: &[TaskInfo], + has_repository: bool, +) -> bool { + let checklist = get_phase_checklist(phase, files, tasks, has_repository); + + // Check required files are complete + let required_files_complete = checklist.file_deliverables.iter() + .filter(|d| d.priority == FilePriority::Required) + .all(|d| d.completed); + + // Check repository if required + let repository_ok = !checklist.repository_required || checklist.has_repository; + + // Check tasks if in execute phase + let tasks_ok = if let Some(stats) = &checklist.task_stats { + stats.total > 0 && stats.done == stats.total + } else { + true + }; + + required_files_complete && repository_ok && tasks_ok +} + +/// Format checklist as markdown for LLM context +pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String { + let mut md = format!("## Phase Progress ({} Phase)\n\n", capitalize(&checklist.phase)); + + // File deliverables + md.push_str("### Deliverables\n"); + for status in &checklist.file_deliverables { + let check = if status.completed { "+" } else { "-" }; + let priority_label = match status.priority { + FilePriority::Required => " (required)", + FilePriority::Recommended => " (recommended)", + FilePriority::Optional => " (optional)", + }; + + if status.completed { + md.push_str(&format!("[{}] {} - \"{}\"\n", check, status.name, status.actual_name.as_deref().unwrap_or("created"))); + } else { + md.push_str(&format!("[{}] {}{}\n", check, status.name, priority_label)); + } + } + + // Repository status + if checklist.repository_required { + let check = if checklist.has_repository { "+" } else { "-" }; + md.push_str(&format!("[{}] Repository configured (required)\n", check)); + } + + // Task stats for execute phase + if let Some(ref stats) = checklist.task_stats { + md.push_str(&format!("\n### Task Progress\n")); + md.push_str(&format!("- Total: {}\n", stats.total)); + md.push_str(&format!("- Done: {}\n", stats.done)); + if stats.pending > 0 { + md.push_str(&format!("- Pending: {}\n", stats.pending)); + } + if stats.running > 0 { + md.push_str(&format!("- Running: {}\n", stats.running)); + } + if stats.failed > 0 { + md.push_str(&format!("- Failed: {}\n", stats.failed)); + } + } + + // Summary + md.push_str(&format!("\n**Status**: {} ({}% complete)\n", checklist.summary, checklist.completion_percentage)); + + // Suggestions + if !checklist.suggestions.is_empty() { + md.push_str("\n**Next Steps**:\n"); + for suggestion in &checklist.suggestions { + md.push_str(&format!("- {}\n", suggestion)); + } + } + + md +} + +fn capitalize(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_phase_deliverables() { + let research = get_phase_deliverables("research"); + assert_eq!(research.phase, "research"); + assert!(!research.requires_repository); + assert_eq!(research.recommended_files.len(), 3); + + let plan = get_phase_deliverables("plan"); + assert!(plan.requires_repository); + assert!(plan.recommended_files.iter().any(|f| f.template_id == "task-breakdown")); + } + + #[test] + fn test_phase_checklist_empty() { + let checklist = get_phase_checklist("research", &[], &[], false); + assert_eq!(checklist.completion_percentage, 0); + assert!(!checklist.suggestions.is_empty()); + } + + #[test] + fn test_check_phase_completion() { + let files = vec![ + FileInfo { + id: Uuid::new_v4(), + name: "Requirements Document".to_string(), + contract_phase: Some("specify".to_string()), + }, + ]; + + // Specify phase has required file + let complete = check_phase_completion("specify", &files, &[], false); + assert!(complete); + } +} diff --git a/makima/src/llm/task_output.rs b/makima/src/llm/task_output.rs new file mode 100644 index 0000000..c71c05a --- /dev/null +++ b/makima/src/llm/task_output.rs @@ -0,0 +1,461 @@ +//! Task output processing and task derivation utilities. +//! +//! This module provides utilities for: +//! - Parsing task lists from markdown documents +//! - Analyzing completed task outputs +//! - Suggesting follow-up actions based on task results + +use regex::Regex; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// A parsed task from a markdown document +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ParsedTask { + /// Task name/title + pub name: String, + /// Task description or plan + pub description: Option<String>, + /// Group/phase this task belongs to + pub group: Option<String>, + /// Order within the group (0-indexed) + pub order: usize, + /// Whether this task was marked as completed in source + pub completed: bool, + /// Dependencies (names of other tasks) + pub dependencies: Vec<String>, +} + +/// Result of parsing tasks from a document +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskParseResult { + /// Successfully parsed tasks + pub tasks: Vec<ParsedTask>, + /// Groups/phases found + pub groups: Vec<String>, + /// Total tasks found + pub total: usize, + /// Any parsing warnings + pub warnings: Vec<String>, +} + +/// Impact on contract phase +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhaseImpact { + /// Current phase + pub phase: String, + /// Whether phase targets are now met + pub targets_met: bool, + /// Tasks remaining in phase + pub tasks_remaining: usize, + /// Suggestion for phase transition + pub transition_suggestion: Option<String>, +} + +/// Suggested action based on task output +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum SuggestedAction { + /// Create a follow-up task + CreateTask { + name: String, + plan: String, + chain_from: Option<Uuid>, + }, + /// Create a new file from template + CreateFile { + template_id: String, + name: String, + seed_content: Option<String>, + }, + /// Update an existing file + UpdateFile { + file_id: Uuid, + file_name: String, + additions: String, + }, + /// Advance to next phase + AdvancePhase { + to_phase: String, + }, + /// Run the next chained task + RunNextTask { + task_id: Uuid, + task_name: String, + }, +} + +/// Analysis of a completed task's output +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskOutputAnalysis { + /// Summary of what was accomplished + pub summary: String, + /// Files that were created/modified (from diff) + pub files_affected: Vec<String>, + /// Suggested next actions + pub next_steps: Vec<SuggestedAction>, + /// Impact on contract phase + pub phase_impact: Option<PhaseImpact>, +} + +/// Parse tasks from a markdown task breakdown document +/// +/// Supports formats like: +/// - `[ ] Task name` +/// - `[x] Completed task` +/// - `1. Task name` +/// - `- Task name` +/// +/// Groups are detected from `## Phase/Section` headings. +pub fn parse_tasks_from_breakdown(content: &str) -> TaskParseResult { + let mut tasks = Vec::new(); + let mut groups = Vec::new(); + let mut warnings = Vec::new(); + let mut current_group: Option<String> = None; + let mut task_order = 0; + + // Patterns for task items + let checkbox_pattern = Regex::new(r"^\s*[-*]\s*\[([ xX])\]\s*(.+)$").unwrap(); + let numbered_checkbox = Regex::new(r"^\s*\d+\.\s*\[([ xX])\]\s*(.+)$").unwrap(); + let numbered_pattern = Regex::new(r"^\s*\d+\.\s+(.+)$").unwrap(); + let bullet_pattern = Regex::new(r"^\s*[-*]\s+(.+)$").unwrap(); + let heading_pattern = Regex::new(r"^##\s+(?:Phase\s*\d*:?\s*)?(.+)$").unwrap(); + + // Patterns for dependencies (inline) + let depends_pattern = Regex::new(r"(?i)(?:depends on|after|requires):?\s*(.+)").unwrap(); + + for line in content.lines() { + let trimmed = line.trim(); + + // Skip empty lines + if trimmed.is_empty() { + continue; + } + + // Check for section headings + if let Some(caps) = heading_pattern.captures(trimmed) { + let group_name = caps[1].trim().to_string(); + if !groups.contains(&group_name) { + groups.push(group_name.clone()); + } + current_group = Some(group_name); + task_order = 0; + continue; + } + + // Try to parse as a task + let mut task_name: Option<String> = None; + let mut completed = false; + + // Try checkbox patterns first (more specific) + if let Some(caps) = checkbox_pattern.captures(trimmed) { + completed = &caps[1] != " "; + task_name = Some(caps[2].trim().to_string()); + } else if let Some(caps) = numbered_checkbox.captures(trimmed) { + completed = &caps[1] != " "; + task_name = Some(caps[2].trim().to_string()); + } else if let Some(caps) = numbered_pattern.captures(trimmed) { + task_name = Some(caps[1].trim().to_string()); + } else if let Some(caps) = bullet_pattern.captures(trimmed) { + // Only treat as task if it looks like a task (has actionable verbs) + let text = caps[1].trim(); + if looks_like_task(text) { + task_name = Some(text.to_string()); + } + } + + if let Some(name) = task_name { + // Skip items that are clearly not tasks + if name.to_lowercase().starts_with("note:") || + name.to_lowercase().starts_with("todo:") && name.len() < 10 || + name.starts_with('#') { + continue; + } + + // Extract dependencies if present + let dependencies = if let Some(dep_caps) = depends_pattern.captures(&name) { + dep_caps[1] + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + } else { + Vec::new() + }; + + // Clean task name (remove dependency info) + let clean_name = depends_pattern.replace(&name, "").trim().to_string(); + + // Extract description if there's a colon + let (final_name, description) = if let Some(idx) = clean_name.find(':') { + let (n, d) = clean_name.split_at(idx); + (n.trim().to_string(), Some(d[1..].trim().to_string())) + } else { + (clean_name, None) + }; + + tasks.push(ParsedTask { + name: final_name, + description, + group: current_group.clone(), + order: task_order, + completed, + dependencies, + }); + + task_order += 1; + } + } + + let total = tasks.len(); + + // Add warnings + if tasks.is_empty() { + warnings.push("No tasks found in document. Ensure tasks are formatted as checkbox items (- [ ] Task) or numbered lists (1. Task).".to_string()); + } + + TaskParseResult { + tasks, + groups, + total, + warnings, + } +} + +/// Check if text looks like a task (has action verbs) +fn looks_like_task(text: &str) -> bool { + let lower = text.to_lowercase(); + let action_verbs = [ + "add", "create", "implement", "build", "write", "fix", "update", + "refactor", "test", "configure", "set up", "setup", "deploy", + "integrate", "migrate", "design", "review", "document", "remove", + "delete", "modify", "change", "improve", "optimize", "enable", + "disable", "install", "initialize", "define", "extend", "extract", + ]; + + action_verbs.iter().any(|verb| lower.starts_with(verb) || lower.contains(&format!(" {}", verb))) +} + +/// Analyze a completed task's output to suggest next actions +pub fn analyze_task_output( + _task_id: Uuid, + task_name: &str, + task_result: Option<&str>, + task_diff: Option<&str>, + contract_phase: &str, + total_tasks: usize, + completed_tasks: usize, + next_task: Option<(Uuid, String)>, + dev_notes_file: Option<(Uuid, String)>, +) -> TaskOutputAnalysis { + let mut next_steps = Vec::new(); + let mut files_affected = Vec::new(); + + // Parse files from diff if available + if let Some(diff) = task_diff { + files_affected = extract_files_from_diff(diff); + } + + // Generate summary + let summary = if let Some(result) = task_result { + if result.len() > 200 { + format!("{}...", &result[..200]) + } else { + result.to_string() + } + } else { + format!("Task '{}' completed", task_name) + }; + + // If there's a next chained task, suggest running it + if let Some((next_id, next_name)) = next_task { + next_steps.push(SuggestedAction::RunNextTask { + task_id: next_id, + task_name: next_name, + }); + } + + // Suggest updating Dev Notes if in execute phase and file exists + if contract_phase == "execute" { + if let Some((file_id, file_name)) = dev_notes_file { + let additions = format!( + "\n## Task: {}\n\n{}\n\n### Files Modified\n{}\n", + task_name, + summary, + files_affected.iter() + .map(|f| format!("- {}", f)) + .collect::<Vec<_>>() + .join("\n") + ); + + next_steps.push(SuggestedAction::UpdateFile { + file_id, + file_name, + additions, + }); + } else { + // Suggest creating Dev Notes + next_steps.push(SuggestedAction::CreateFile { + template_id: "dev-notes".to_string(), + name: "Development Notes".to_string(), + seed_content: Some(format!( + "# Development Notes\n\n## Task: {}\n\n{}\n", + task_name, summary + )), + }); + } + } + + // Calculate phase impact + let new_completed = completed_tasks + 1; + let targets_met = new_completed >= total_tasks && total_tasks > 0; + let tasks_remaining = total_tasks.saturating_sub(new_completed); + + let transition_suggestion = if targets_met && contract_phase == "execute" { + Some("All tasks complete. Ready to advance to Review phase.".to_string()) + } else { + None + }; + + // If targets are met, suggest phase transition + if targets_met && contract_phase == "execute" { + next_steps.push(SuggestedAction::AdvancePhase { + to_phase: "review".to_string(), + }); + } + + let phase_impact = Some(PhaseImpact { + phase: contract_phase.to_string(), + targets_met, + tasks_remaining, + transition_suggestion, + }); + + TaskOutputAnalysis { + summary, + files_affected, + next_steps, + phase_impact, + } +} + +/// Extract file paths from a git diff +fn extract_files_from_diff(diff: &str) -> Vec<String> { + let mut files = Vec::new(); + let file_pattern = Regex::new(r"^(?:diff --git a/|[+]{3} b/|[-]{3} a/)(.+)$").unwrap(); + + for line in diff.lines() { + if let Some(caps) = file_pattern.captures(line) { + let path = caps[1].trim().to_string(); + // Skip /dev/null and duplicates + if path != "/dev/null" && !files.contains(&path) { + // Clean up path (remove a/ or b/ prefix from git diff) + let clean_path = path.trim_start_matches("a/").trim_start_matches("b/").to_string(); + if !files.contains(&clean_path) { + files.push(clean_path); + } + } + } + } + + files +} + +/// Format parsed tasks for display +pub fn format_parsed_tasks(result: &TaskParseResult) -> String { + let mut output = String::new(); + + if result.tasks.is_empty() { + output.push_str("No tasks found in the document.\n"); + for warning in &result.warnings { + output.push_str(&format!("Warning: {}\n", warning)); + } + return output; + } + + output.push_str(&format!("Found {} task(s)", result.total)); + if !result.groups.is_empty() { + output.push_str(&format!(" in {} group(s)", result.groups.len())); + } + output.push_str(":\n\n"); + + let mut current_group: Option<&str> = None; + for (i, task) in result.tasks.iter().enumerate() { + // Print group header if changed + if task.group.as_deref() != current_group { + current_group = task.group.as_deref(); + if let Some(group) = current_group { + output.push_str(&format!("**{}**\n", group)); + } + } + + let status = if task.completed { "[x]" } else { "[ ]" }; + output.push_str(&format!("{}. {} {}", i + 1, status, task.name)); + + if !task.dependencies.is_empty() { + output.push_str(&format!(" (depends on: {})", task.dependencies.join(", "))); + } + + output.push('\n'); + } + + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_checkbox_tasks() { + let content = r#" +## Phase 1: Setup +- [ ] Set up project structure +- [x] Configure dev environment + +## Phase 2: Features +1. [ ] Implement authentication +2. [ ] Add user dashboard +"#; + + let result = parse_tasks_from_breakdown(content); + assert_eq!(result.total, 4); + assert_eq!(result.groups.len(), 2); + assert!(!result.tasks[0].completed); + assert!(result.tasks[1].completed); + } + + #[test] + fn test_parse_with_dependencies() { + let content = r#" +- [ ] Task A +- [ ] Task B (depends on: Task A) +"#; + + let result = parse_tasks_from_breakdown(content); + assert_eq!(result.tasks[1].dependencies, vec!["Task A"]); + } + + #[test] + fn test_extract_files_from_diff() { + let diff = r#" +diff --git a/src/main.rs b/src/main.rs +--- a/src/main.rs ++++ b/src/main.rs +@@ -1,3 +1,4 @@ ++fn new_function() {} +"#; + + let files = extract_files_from_diff(diff); + assert!(files.contains(&"src/main.rs".to_string())); + } + + #[test] + fn test_looks_like_task() { + assert!(looks_like_task("Add authentication")); + assert!(looks_like_task("Create user model")); + assert!(looks_like_task("implement feature X")); + assert!(!looks_like_task("This is a note")); + assert!(!looks_like_task("Summary of changes")); + } +} diff --git a/makima/src/llm/templates.rs b/makima/src/llm/templates.rs new file mode 100644 index 0000000..18ef46d --- /dev/null +++ b/makima/src/llm/templates.rs @@ -0,0 +1,1011 @@ +//! Template definitions for phase-appropriate file structures. +//! +//! Templates provide starting structures for files based on the contract phase. +//! Each phase has templates suited for that stage of work. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::db::models::BodyElement; + +/// A file template with suggested structure +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct FileTemplate { + /// Template identifier + pub id: String, + /// Display name + pub name: String, + /// Contract phase this template is designed for + pub phase: String, + /// Brief description of what this template is for + pub description: String, + /// Suggested body elements (structure only - content to be filled by LLM) + pub suggested_body: Vec<BodyElement>, +} + +/// Get templates appropriate for a given contract phase +pub fn templates_for_phase(phase: &str) -> Vec<FileTemplate> { + match phase { + "research" => vec![ + research_notes_template(), + competitor_analysis_template(), + user_research_template(), + ], + "specify" => vec![ + requirements_template(), + user_stories_template(), + acceptance_criteria_template(), + ], + "plan" => vec![ + architecture_template(), + technical_design_template(), + task_breakdown_template(), + ], + "execute" => vec![ + dev_notes_template(), + test_plan_template(), + implementation_log_template(), + ], + "review" => vec![ + review_checklist_template(), + release_notes_template(), + retrospective_template(), + ], + _ => vec![], + } +} + +/// Get all available templates across all phases +pub fn all_templates() -> Vec<FileTemplate> { + vec![ + // Research phase + research_notes_template(), + competitor_analysis_template(), + user_research_template(), + // Specify phase + requirements_template(), + user_stories_template(), + acceptance_criteria_template(), + // Plan phase + architecture_template(), + technical_design_template(), + task_breakdown_template(), + // Execute phase + dev_notes_template(), + test_plan_template(), + implementation_log_template(), + // Review phase + review_checklist_template(), + release_notes_template(), + retrospective_template(), + ] +} + +// ============================================================================= +// Research Phase Templates +// ============================================================================= + +fn research_notes_template() -> FileTemplate { + FileTemplate { + id: "research-notes".to_string(), + name: "Research Notes".to_string(), + phase: "research".to_string(), + description: "Document findings, insights, and questions during research".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Research Notes".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Context".to_string(), + }, + BodyElement::Paragraph { + text: "Describe the research objective and scope...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Key Findings".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Finding 1...".to_string(), + "Finding 2...".to_string(), + "Finding 3...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Open Questions".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec![ + "Question to investigate...".to_string(), + "Area needing more research...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Next Steps".to_string(), + }, + BodyElement::Paragraph { + text: "Outline follow-up actions...".to_string(), + }, + ], + } +} + +fn competitor_analysis_template() -> FileTemplate { + FileTemplate { + id: "competitor-analysis".to_string(), + name: "Competitor Analysis".to_string(), + phase: "research".to_string(), + description: "Analyze competitors and market positioning".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Competitor Analysis".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Market Overview".to_string(), + }, + BodyElement::Paragraph { + text: "Describe the market landscape...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Competitor 1: [Name]".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Strengths: ...".to_string(), + "Weaknesses: ...".to_string(), + "Key Features: ...".to_string(), + "Pricing: ...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Competitive Advantages".to_string(), + }, + BodyElement::Paragraph { + text: "Our differentiation strategy...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Gaps & Opportunities".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["Opportunity 1...".to_string(), "Opportunity 2...".to_string()], + }, + ], + } +} + +fn user_research_template() -> FileTemplate { + FileTemplate { + id: "user-research".to_string(), + name: "User Research".to_string(), + phase: "research".to_string(), + description: "Document user interviews, surveys, and persona insights".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "User Research".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Research Method".to_string(), + }, + BodyElement::Paragraph { + text: "Describe the research methodology used...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "User Personas".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "Persona 1: [Name]".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Role: ...".to_string(), + "Goals: ...".to_string(), + "Pain Points: ...".to_string(), + "Behaviors: ...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Key Insights".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec!["Insight from research...".to_string()], + }, + BodyElement::Heading { + level: 2, + text: "Recommendations".to_string(), + }, + BodyElement::Paragraph { + text: "Based on research findings...".to_string(), + }, + ], + } +} + +// ============================================================================= +// Specify Phase Templates +// ============================================================================= + +fn requirements_template() -> FileTemplate { + FileTemplate { + id: "requirements".to_string(), + name: "Requirements Document".to_string(), + phase: "specify".to_string(), + description: "Define functional and non-functional requirements".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Requirements Document".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Overview".to_string(), + }, + BodyElement::Paragraph { + text: "Brief description of the feature/project...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Functional Requirements".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec![ + "FR-001: The system shall...".to_string(), + "FR-002: Users must be able to...".to_string(), + "FR-003: ...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Non-Functional Requirements".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec![ + "NFR-001: Performance - ...".to_string(), + "NFR-002: Security - ...".to_string(), + "NFR-003: Scalability - ...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Constraints".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Technical constraints...".to_string(), + "Business constraints...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Dependencies".to_string(), + }, + BodyElement::Paragraph { + text: "External dependencies and integrations...".to_string(), + }, + ], + } +} + +fn user_stories_template() -> FileTemplate { + FileTemplate { + id: "user-stories".to_string(), + name: "User Stories".to_string(), + phase: "specify".to_string(), + description: "Define features from the user's perspective".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "User Stories".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Epic: [Feature Name]".to_string(), + }, + BodyElement::Paragraph { + text: "High-level description of the epic...".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "US-001: [Story Title]".to_string(), + }, + BodyElement::Paragraph { + text: "As a [user type], I want to [action], so that [benefit].".to_string(), + }, + BodyElement::Heading { + level: 4, + text: "Acceptance Criteria".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Given... When... Then...".to_string(), + "Given... When... Then...".to_string(), + ], + }, + BodyElement::Heading { + level: 3, + text: "US-002: [Story Title]".to_string(), + }, + BodyElement::Paragraph { + text: "As a [user type], I want to [action], so that [benefit].".to_string(), + }, + ], + } +} + +fn acceptance_criteria_template() -> FileTemplate { + FileTemplate { + id: "acceptance-criteria".to_string(), + name: "Acceptance Criteria".to_string(), + phase: "specify".to_string(), + description: "Define testable conditions for feature completion".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Acceptance Criteria".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Feature: [Name]".to_string(), + }, + BodyElement::Paragraph { + text: "Description of the feature being specified...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Scenarios".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "Scenario 1: [Happy Path]".to_string(), + }, + BodyElement::Code { + language: Some("gherkin".to_string()), + content: "Given [precondition]\nWhen [action]\nThen [expected result]".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "Scenario 2: [Edge Case]".to_string(), + }, + BodyElement::Code { + language: Some("gherkin".to_string()), + content: "Given [precondition]\nWhen [action]\nThen [expected result]".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Out of Scope".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["Items explicitly not included...".to_string()], + }, + ], + } +} + +// ============================================================================= +// Plan Phase Templates +// ============================================================================= + +fn architecture_template() -> FileTemplate { + FileTemplate { + id: "architecture".to_string(), + name: "Architecture Document".to_string(), + phase: "plan".to_string(), + description: "Document system architecture and design decisions".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Architecture Document".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Overview".to_string(), + }, + BodyElement::Paragraph { + text: "High-level architecture description...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "System Components".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Component A: Description and responsibility".to_string(), + "Component B: Description and responsibility".to_string(), + "Component C: Description and responsibility".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Data Flow".to_string(), + }, + BodyElement::Paragraph { + text: "Describe how data flows through the system...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Technology Stack".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Frontend: ...".to_string(), + "Backend: ...".to_string(), + "Database: ...".to_string(), + "Infrastructure: ...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Design Decisions".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "ADR-001: [Decision Title]".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Context: ...".to_string(), + "Decision: ...".to_string(), + "Consequences: ...".to_string(), + ], + }, + ], + } +} + +fn technical_design_template() -> FileTemplate { + FileTemplate { + id: "technical-design".to_string(), + name: "Technical Design".to_string(), + phase: "plan".to_string(), + description: "Detailed technical specification for implementation".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Technical Design".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Purpose".to_string(), + }, + BodyElement::Paragraph { + text: "What this design document covers...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "API Design".to_string(), + }, + BodyElement::Code { + language: Some("typescript".to_string()), + content: "// Interface definitions\ninterface Example {\n // ...\n}".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Database Schema".to_string(), + }, + BodyElement::Code { + language: Some("sql".to_string()), + content: "-- Table definitions\nCREATE TABLE example (\n -- ...\n);".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Implementation Notes".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Key implementation consideration...".to_string(), + "Performance consideration...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Migration Strategy".to_string(), + }, + BodyElement::Paragraph { + text: "How to migrate from current state...".to_string(), + }, + ], + } +} + +fn task_breakdown_template() -> FileTemplate { + FileTemplate { + id: "task-breakdown".to_string(), + name: "Task Breakdown".to_string(), + phase: "plan".to_string(), + description: "Break down work into implementable tasks".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Task Breakdown".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Overview".to_string(), + }, + BodyElement::Paragraph { + text: "Summary of the work to be done...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Phase 1: Foundation".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec![ + "[ ] Task 1: Set up project structure".to_string(), + "[ ] Task 2: Configure development environment".to_string(), + "[ ] Task 3: Create base components".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Phase 2: Core Features".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec![ + "[ ] Task 4: Implement feature A".to_string(), + "[ ] Task 5: Implement feature B".to_string(), + "[ ] Task 6: Add tests".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Phase 3: Polish & Deploy".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec![ + "[ ] Task 7: Error handling".to_string(), + "[ ] Task 8: Documentation".to_string(), + "[ ] Task 9: Deployment".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Dependencies".to_string(), + }, + BodyElement::Paragraph { + text: "Task dependencies and blockers...".to_string(), + }, + ], + } +} + +// ============================================================================= +// Execute Phase Templates +// ============================================================================= + +fn dev_notes_template() -> FileTemplate { + FileTemplate { + id: "dev-notes".to_string(), + name: "Development Notes".to_string(), + phase: "execute".to_string(), + description: "Track implementation details, decisions, and learnings".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Development Notes".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Current Status".to_string(), + }, + BodyElement::Paragraph { + text: "Brief summary of implementation progress...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Implementation Details".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "[Component/Feature Name]".to_string(), + }, + BodyElement::Paragraph { + text: "How this was implemented and why...".to_string(), + }, + BodyElement::Code { + language: Some("typescript".to_string()), + content: "// Key code snippet or example".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Challenges & Solutions".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Challenge: ... | Solution: ...".to_string(), + "Challenge: ... | Solution: ...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "TODOs".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "[ ] Remaining item...".to_string(), + "[ ] Follow-up task...".to_string(), + ], + }, + ], + } +} + +fn test_plan_template() -> FileTemplate { + FileTemplate { + id: "test-plan".to_string(), + name: "Test Plan".to_string(), + phase: "execute".to_string(), + description: "Document testing strategy and test cases".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Test Plan".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Test Scope".to_string(), + }, + BodyElement::Paragraph { + text: "What is being tested and the testing approach...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Test Types".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Unit Tests: Component-level testing".to_string(), + "Integration Tests: API and service integration".to_string(), + "E2E Tests: User flow testing".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Test Cases".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "TC-001: [Test Name]".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Preconditions: ...".to_string(), + "Steps: ...".to_string(), + "Expected Result: ...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Test Data".to_string(), + }, + BodyElement::Paragraph { + text: "Required test data and fixtures...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Test Results".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "[ ] TC-001: Pending".to_string(), + "[ ] TC-002: Pending".to_string(), + ], + }, + ], + } +} + +fn implementation_log_template() -> FileTemplate { + FileTemplate { + id: "implementation-log".to_string(), + name: "Implementation Log".to_string(), + phase: "execute".to_string(), + description: "Chronological log of implementation progress".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Implementation Log".to_string(), + }, + BodyElement::Paragraph { + text: "Tracking daily progress and decisions...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "[Date]".to_string(), + }, + BodyElement::Heading { + level: 3, + text: "Completed".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["What was accomplished...".to_string()], + }, + BodyElement::Heading { + level: 3, + text: "In Progress".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["Current work...".to_string()], + }, + BodyElement::Heading { + level: 3, + text: "Blockers".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["Any blockers or issues...".to_string()], + }, + BodyElement::Heading { + level: 3, + text: "Notes".to_string(), + }, + BodyElement::Paragraph { + text: "Additional context or decisions made...".to_string(), + }, + ], + } +} + +// ============================================================================= +// Review Phase Templates +// ============================================================================= + +fn review_checklist_template() -> FileTemplate { + FileTemplate { + id: "review-checklist".to_string(), + name: "Review Checklist".to_string(), + phase: "review".to_string(), + description: "Comprehensive checklist for code and feature review".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Review Checklist".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Code Quality".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "[ ] Code follows style guidelines".to_string(), + "[ ] No unnecessary complexity".to_string(), + "[ ] Functions are well-named and focused".to_string(), + "[ ] No dead code or commented-out code".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Testing".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "[ ] Unit tests pass".to_string(), + "[ ] Integration tests pass".to_string(), + "[ ] Edge cases covered".to_string(), + "[ ] Test coverage acceptable".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Security".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "[ ] No hardcoded credentials".to_string(), + "[ ] Input validation in place".to_string(), + "[ ] Authentication/authorization correct".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Documentation".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "[ ] README updated".to_string(), + "[ ] API documentation complete".to_string(), + "[ ] Inline comments where needed".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Review Notes".to_string(), + }, + BodyElement::Paragraph { + text: "Additional review comments and feedback...".to_string(), + }, + ], + } +} + +fn release_notes_template() -> FileTemplate { + FileTemplate { + id: "release-notes".to_string(), + name: "Release Notes".to_string(), + phase: "review".to_string(), + description: "Document changes for release communication".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Release Notes - v[X.Y.Z]".to_string(), + }, + BodyElement::Paragraph { + text: "Release date: [DATE]".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "Highlights".to_string(), + }, + BodyElement::Paragraph { + text: "Key features and improvements in this release...".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "New Features".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Feature 1: Description".to_string(), + "Feature 2: Description".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Improvements".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Improvement 1: Description".to_string(), + "Improvement 2: Description".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Bug Fixes".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Fixed: Issue description".to_string(), + "Fixed: Issue description".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Breaking Changes".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["Breaking change description (if any)...".to_string()], + }, + BodyElement::Heading { + level: 2, + text: "Known Issues".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec!["Known issue (if any)...".to_string()], + }, + ], + } +} + +fn retrospective_template() -> FileTemplate { + FileTemplate { + id: "retrospective".to_string(), + name: "Retrospective".to_string(), + phase: "review".to_string(), + description: "Reflect on the project and capture learnings".to_string(), + suggested_body: vec![ + BodyElement::Heading { + level: 1, + text: "Retrospective".to_string(), + }, + BodyElement::Paragraph { + text: "Project: [Name] | Date: [DATE]".to_string(), + }, + BodyElement::Heading { + level: 2, + text: "What Went Well".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Success 1...".to_string(), + "Success 2...".to_string(), + "Success 3...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "What Could Be Improved".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Area for improvement 1...".to_string(), + "Area for improvement 2...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Lessons Learned".to_string(), + }, + BodyElement::List { + ordered: true, + items: vec![ + "Key lesson from this project...".to_string(), + "Technical insight gained...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Action Items".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "[ ] Action to improve future projects...".to_string(), + "[ ] Process change to implement...".to_string(), + ], + }, + BodyElement::Heading { + level: 2, + text: "Metrics".to_string(), + }, + BodyElement::List { + ordered: false, + items: vec![ + "Timeline: Planned vs Actual".to_string(), + "Scope: Delivered vs Planned".to_string(), + "Quality: Bug count, test coverage".to_string(), + ], + }, + ], + } +} diff --git a/makima/src/llm/tools.rs b/makima/src/llm/tools.rs index 649633e..ae1dc5a 100644 --- a/makima/src/llm/tools.rs +++ b/makima/src/llm/tools.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use crate::db::models::{BodyElement, ChartType, TranscriptEntry}; +use crate::llm::templates; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tool { @@ -411,6 +412,36 @@ pub static AVAILABLE_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = "required": ["target_version"] }), }, + // Template tools + Tool { + name: "suggest_templates".to_string(), + description: "Get suggested file templates based on a contract phase. Returns templates with predefined structures appropriate for research, specify, plan, execute, or review phases. Use this to help users start documents with proper structure.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "phase": { + "type": "string", + "enum": ["research", "specify", "plan", "execute", "review"], + "description": "The contract phase to get templates for. If not provided, returns all templates." + } + }, + "required": [] + }), + }, + Tool { + name: "apply_template".to_string(), + description: "Apply a template to the current file, replacing the body with the template structure. The template provides a starting structure that should be customized for the user's needs.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "The template ID to apply (e.g., 'research-notes', 'requirements', 'architecture')" + } + }, + "required": ["template_id"] + }), + }, ] }); @@ -500,6 +531,9 @@ pub fn execute_tool_call( "list_versions" => execute_list_versions(), "read_version" => execute_read_version(call), "restore_version" => execute_restore_version(call), + // Template tools + "suggest_templates" => execute_suggest_templates(call), + "apply_template" => execute_apply_template(call), _ => ToolExecutionResult { result: ToolResult { success: false, @@ -1350,6 +1384,11 @@ fn execute_view_body(current_body: &[BodyElement]) -> ToolExecutionResult { "alt": alt, "caption": caption }), + BodyElement::Markdown { content } => json!({ + "index": i, + "type": "markdown", + "content": content + }), } }) .collect(); @@ -1439,6 +1478,11 @@ fn execute_read_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolEx "alt": alt, "caption": caption }), + BodyElement::Markdown { content } => json!({ + "index": index, + "type": "markdown", + "content": content + }), }; let type_str = match element { @@ -1448,6 +1492,7 @@ fn execute_read_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolEx BodyElement::List { .. } => "list", BodyElement::Chart { .. } => "chart", BodyElement::Image { .. } => "image", + BodyElement::Markdown { .. } => "markdown", }; ToolExecutionResult { @@ -1603,6 +1648,131 @@ fn execute_restore_version(call: &ToolCall) -> ToolExecutionResult { } } +// ============================================================================= +// Template Tool Execution Functions +// ============================================================================= + +fn execute_suggest_templates(call: &ToolCall) -> ToolExecutionResult { + let phase = call.arguments.get("phase").and_then(|v| v.as_str()); + + let template_list = match phase { + Some(p) => templates::templates_for_phase(p), + None => templates::all_templates(), + }; + + if template_list.is_empty() { + return ToolExecutionResult { + result: ToolResult { + success: true, + message: format!( + "No templates available for phase: {}", + phase.unwrap_or("(none)") + ), + }, + new_body: None, + new_summary: None, + parsed_data: Some(json!([])), + version_request: None, + pending_questions: None, + }; + } + + // Convert templates to JSON (without the full body for display) + let templates_json: Vec<serde_json::Value> = template_list + .iter() + .map(|t| { + json!({ + "id": t.id, + "name": t.name, + "phase": t.phase, + "description": t.description, + "elementCount": t.suggested_body.len() + }) + }) + .collect(); + + let phase_msg = phase + .map(|p| format!(" for '{}' phase", p)) + .unwrap_or_default(); + + ToolExecutionResult { + result: ToolResult { + success: true, + message: format!( + "Found {} template(s){}. Use apply_template with a template_id to apply one.", + templates_json.len(), + phase_msg + ), + }, + new_body: None, + new_summary: None, + parsed_data: Some(json!(templates_json)), + version_request: None, + pending_questions: None, + } +} + +fn execute_apply_template(call: &ToolCall) -> ToolExecutionResult { + let template_id = call + .arguments + .get("template_id") + .and_then(|v| v.as_str()); + + let Some(template_id) = template_id else { + return ToolExecutionResult { + result: ToolResult { + success: false, + message: "Missing template_id parameter".to_string(), + }, + new_body: None, + new_summary: None, + parsed_data: None, + version_request: None, + pending_questions: None, + }; + }; + + // Find the template + let all = templates::all_templates(); + let template = all.iter().find(|t| t.id == template_id); + + let Some(template) = template else { + let available: Vec<String> = all.iter().map(|t| t.id.clone()).collect(); + return ToolExecutionResult { + result: ToolResult { + success: false, + message: format!( + "Template '{}' not found. Available: {}", + template_id, + available.join(", ") + ), + }, + new_body: None, + new_summary: None, + parsed_data: None, + version_request: None, + pending_questions: None, + }; + }; + + ToolExecutionResult { + result: ToolResult { + success: true, + message: format!( + "Applied template '{}' ({}) with {} elements. You can now customize the content.", + template.name, + template.phase, + template.suggested_body.len() + ), + }, + new_body: Some(template.suggested_body.clone()), + new_summary: None, + parsed_data: None, + version_request: None, + pending_questions: None, + } +} + /// Convert serde_json::Value to jaq_interpret::Val fn json_to_jaq(value: &serde_json::Value) -> jaq_interpret::Val { match value { |
