diff options
| author | soryu <soryu@soryu.co> | 2026-01-26 20:19:30 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-26 20:19:30 +0000 |
| commit | 04e1e8f0dd85d19917ac5ba0b73cba65ebac8976 (patch) | |
| tree | e52537dd2a33c10156f1378ffdc6803bc983482d | |
| parent | 6328477bc459eca0243b685553dbd75b925fdc8a (diff) | |
| download | soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.tar.gz soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.zip | |
Add completion phases
| -rw-r--r-- | makima/migrations/20250125000000_add_completed_deliverables.sql | 8 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 15 | ||||
| -rw-r--r-- | makima/src/daemon/api/supervisor.rs | 25 | ||||
| -rw-r--r-- | makima/src/daemon/cli/mod.rs | 3 | ||||
| -rw-r--r-- | makima/src/daemon/cli/supervisor.rs | 15 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 22 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 24 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 55 | ||||
| -rw-r--r-- | makima/src/llm/contract_tools.rs | 107 | ||||
| -rw-r--r-- | makima/src/llm/mod.rs | 13 | ||||
| -rw-r--r-- | makima/src/llm/phase_guidance.rs | 688 | ||||
| -rw-r--r-- | makima/src/llm/templates.rs | 1013 | ||||
| -rw-r--r-- | makima/src/llm/tools.rs | 159 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 225 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_daemon.rs | 38 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 130 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_daemon.rs | 50 | ||||
| -rw-r--r-- | makima/src/server/handlers/templates.rs | 106 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 4 |
19 files changed, 743 insertions, 1957 deletions
diff --git a/makima/migrations/20250125000000_add_completed_deliverables.sql b/makima/migrations/20250125000000_add_completed_deliverables.sql new file mode 100644 index 0000000..662a2bd --- /dev/null +++ b/makima/migrations/20250125000000_add_completed_deliverables.sql @@ -0,0 +1,8 @@ +-- Add completed_deliverables column to track which phase deliverables have been marked complete +-- Structure: { "plan": ["plan-document"], "execute": ["pull-request"] } + +ALTER TABLE contracts +ADD COLUMN completed_deliverables JSONB NOT NULL DEFAULT '{}'; + +-- Add index for efficient querying +CREATE INDEX idx_contracts_completed_deliverables ON contracts USING gin (completed_deliverables); diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 96dc252..6ddecab 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -590,6 +590,21 @@ async fn run_supervisor( "contract": result.0 }))?); } + SupervisorCommand::MarkDeliverable(args) => { + let client = ApiClient::new(args.common.api_url, args.common.api_key)?; + eprintln!( + "Marking deliverable '{}' as complete for contract {}...", + args.deliverable_id, args.common.contract_id + ); + let result = client + .supervisor_mark_deliverable( + args.common.contract_id, + &args.deliverable_id, + args.phase.as_deref(), + ) + .await?; + println!("{}", serde_json::to_string(&result.0)?); + } } Ok(()) diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs index 74c27e0..e79a9bb 100644 --- a/makima/src/daemon/api/supervisor.rs +++ b/makima/src/daemon/api/supervisor.rs @@ -299,6 +299,31 @@ impl ApiClient { self.delete(&format!("/api/v1/mesh/tasks/{}", task_id)).await } + /// Mark a deliverable as complete. + pub async fn supervisor_mark_deliverable( + &self, + contract_id: Uuid, + deliverable_id: &str, + phase: Option<&str>, + ) -> Result<JsonValue, ApiError> { + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct MarkDeliverableRequest { + deliverable_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + phase: Option<String>, + } + let req = MarkDeliverableRequest { + deliverable_id: deliverable_id.to_string(), + phase: phase.map(|s| s.to_string()), + }; + self.post( + &format!("/api/v1/contracts/{}/deliverables/complete", contract_id), + &req, + ) + .await + } + /// Update a task. pub async fn update_task( &self, diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index 216f281..0805edd 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -157,6 +157,9 @@ pub enum SupervisorCommand { /// Resume a completed contract (reactivate it) ResumeContract(supervisor::ResumeContractArgs), + + /// Mark a deliverable as complete + MarkDeliverable(supervisor::MarkDeliverableArgs), } /// Contract subcommands for task-contract interaction. diff --git a/makima/src/daemon/cli/supervisor.rs b/makima/src/daemon/cli/supervisor.rs index 3bc8525..4f36fd8 100644 --- a/makima/src/daemon/cli/supervisor.rs +++ b/makima/src/daemon/cli/supervisor.rs @@ -220,6 +220,21 @@ pub struct AdvancePhaseArgs { pub phase: String, } +/// Arguments for mark-deliverable command. +#[derive(Args, Debug)] +pub struct MarkDeliverableArgs { + #[command(flatten)] + pub common: SupervisorArgs, + + /// The deliverable ID to mark as complete (e.g., 'plan-document', 'pull-request', 'research-notes') + #[arg(index = 1)] + pub deliverable_id: String, + + /// Phase the deliverable belongs to. Defaults to current contract phase if not specified. + #[arg(long)] + pub phase: Option<String>, +} + /// Arguments for task command (get individual task details). #[derive(Args, Debug)] pub struct GetTaskArgs { diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 86d7e05..8abff3f 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -720,6 +720,9 @@ makima supervisor status # Advance to the next phase (specify, plan, execute, review) makima supervisor advance-phase <phase> + +# Mark a phase deliverable as complete (e.g., 'plan-document', 'pull-request') +makima supervisor mark-deliverable <deliverable_id> [--phase <phase>] ``` ### User Feedback @@ -781,6 +784,25 @@ makima supervisor advance-phase <phase> Valid phases: `specify`, `plan`, `execute`, `review` +### Marking Deliverables Complete + +Each phase has deliverables that must be completed before advancing. Use `mark-deliverable` to explicitly mark them as complete when you've verified the requirement is satisfied: + +```bash +# Mark a deliverable complete (defaults to current phase) +makima supervisor mark-deliverable plan-document + +# Mark a deliverable for a specific phase +makima supervisor mark-deliverable pull-request --phase execute +``` + +Common deliverable IDs by phase: +- **plan**: `plan-document`, `requirements-document` +- **execute**: `pull-request` +- **review**: `release-notes`, `retrospective` + +**Use `status` to see which deliverables are pending for the current phase.** + ## When to Advance Phases **IMPORTANT**: You MUST advance the contract phase as you complete work in each phase! diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 0c1d9f2..95517a1 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1321,6 +1321,11 @@ pub struct Contract { /// phase outputs (like plans, requirements, etc.) before continuing. #[serde(default)] pub phase_guard: bool, + /// Completed deliverables per phase. + /// Structure: { "plan": ["plan-document"], "execute": ["pull-request"] } + #[sqlx(json)] + #[serde(default)] + pub completed_deliverables: serde_json::Value, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -1374,6 +1379,25 @@ impl Contract { _ => ContractPhase::Execute, // simple and execute both end at execute } } + + /// Get completed deliverable IDs for a specific phase + pub fn get_completed_deliverables(&self, phase: &str) -> Vec<String> { + self.completed_deliverables + .get(phase) + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect() + }) + .unwrap_or_default() + } + + /// Check if a specific deliverable is marked as complete for a phase + pub fn is_deliverable_complete(&self, phase: &str, deliverable_id: &str) -> bool { + self.get_completed_deliverables(phase) + .contains(&deliverable_id.to_string()) + } } /// Contract repository record from the database diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index d3e4c56..b55b05e 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -3018,6 +3018,61 @@ pub async fn update_contract_supervisor( .await } +/// Mark a deliverable as complete for a specific phase. +/// Uses JSONB operations to append the deliverable_id to the phase's array. +pub async fn mark_deliverable_complete( + pool: &PgPool, + contract_id: Uuid, + phase: &str, + deliverable_id: &str, +) -> Result<Contract, sqlx::Error> { + // Use jsonb_set to add the deliverable to the phase's array + // If the phase key doesn't exist, create an empty array first + // COALESCE handles the case where the phase array doesn't exist yet + sqlx::query_as::<_, Contract>( + r#" + UPDATE contracts + SET completed_deliverables = jsonb_set( + completed_deliverables, + ARRAY[$2::text], + COALESCE(completed_deliverables->$2, '[]'::jsonb) || to_jsonb($3::text), + true + ), + updated_at = NOW() + WHERE id = $1 + AND NOT (COALESCE(completed_deliverables->$2, '[]'::jsonb) ? $3) + RETURNING * + "#, + ) + .bind(contract_id) + .bind(phase) + .bind(deliverable_id) + .fetch_one(pool) + .await +} + +/// Clear all completed deliverables for a specific phase. +/// Used when phase changes or deliverables need to be reset. +pub async fn clear_phase_deliverables( + pool: &PgPool, + contract_id: Uuid, + phase: &str, +) -> Result<Contract, sqlx::Error> { + sqlx::query_as::<_, Contract>( + r#" + UPDATE contracts + SET completed_deliverables = completed_deliverables - $2, + updated_at = NOW() + WHERE id = $1 + RETURNING * + "#, + ) + .bind(contract_id) + .bind(phase) + .fetch_one(pool) + .await +} + /// Get the supervisor task for a contract. pub async fn get_contract_supervisor_task( pool: &PgPool, diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs index 44c1e20..0f50132 100644 --- a/makima/src/llm/contract_tools.rs +++ b/makima/src/llm/contract_tools.rs @@ -64,30 +64,8 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L // 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(), + description: "Create a new empty file in the contract.".to_string(), parameters: json!({ "type": "object", "properties": { @@ -103,18 +81,26 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L "required": ["name"] }), }, + // ============================================================================= + // Deliverable Management Tools + // ============================================================================= 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(), + name: "mark_deliverable_complete".to_string(), + description: "Mark a phase deliverable as complete. Use this when you have verified that a deliverable requirement has been satisfied. Use get_phase_info or check_deliverables_met first to see available deliverable IDs.".to_string(), parameters: json!({ "type": "object", "properties": { + "deliverable_id": { + "type": "string", + "description": "The ID of the deliverable to mark as complete (e.g., 'plan-document', 'pull-request', 'research-notes')" + }, "phase": { "type": "string", "enum": ["research", "specify", "plan", "execute", "review"], - "description": "Optional filter to show only templates for a specific phase" + "description": "Phase the deliverable belongs to. Defaults to the current contract phase if not specified." } - } + }, + "required": ["deliverable_id"] }), }, // ============================================================================= @@ -488,16 +474,16 @@ pub enum ContractToolRequest { 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> }, + + // Deliverable management + MarkDeliverableComplete { + deliverable_id: String, + phase: Option<String>, + }, // Task management CreateContractTask { @@ -592,9 +578,10 @@ pub fn parse_contract_tool_call(call: &super::tools::ToolCall) -> ContractToolEx "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), + + // Deliverable management + "mark_deliverable_complete" => parse_mark_deliverable_complete(call), // Task management "create_contract_task" => parse_create_contract_task(call), @@ -703,13 +690,9 @@ fn parse_read_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult // 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()); +fn parse_create_empty_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult { 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"); }; @@ -722,10 +705,9 @@ fn parse_create_file_from_template(call: &super::tools::ToolCall) -> ContractToo ContractToolExecutionResult { success: true, - message: format!("Creating file '{}' from template '{}'...", name, template_id), + message: format!("Creating empty file '{}'...", name), data: None, - request: Some(ContractToolRequest::CreateFileFromTemplate { - template_id: template_id.to_string(), + request: Some(ContractToolRequest::CreateEmptyFile { name: name.to_string(), description, }), @@ -733,32 +715,20 @@ fn parse_create_file_from_template(call: &super::tools::ToolCall) -> ContractToo } } -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"); - }; +// ============================================================================= +// Deliverable Management Tool Parsing +// ============================================================================= - let description = call +fn parse_mark_deliverable_complete(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let deliverable_id = call .arguments - .get("description") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); + .get("deliverable_id") + .and_then(|v| v.as_str()); - ContractToolExecutionResult { - success: true, - message: format!("Creating empty file '{}'...", name), - data: None, - request: Some(ContractToolRequest::CreateEmptyFile { - name: name.to_string(), - description, - }), - pending_questions: None, - } -} + let Some(deliverable_id) = deliverable_id else { + return error_result("Missing required parameter: deliverable_id"); + }; -fn parse_list_available_templates(call: &super::tools::ToolCall) -> ContractToolExecutionResult { let phase = call .arguments .get("phase") @@ -767,9 +737,12 @@ fn parse_list_available_templates(call: &super::tools::ToolCall) -> ContractTool ContractToolExecutionResult { success: true, - message: "Listing available templates...".to_string(), + message: format!("Marking deliverable '{}' as complete...", deliverable_id), data: None, - request: Some(ContractToolRequest::ListAvailableTemplates { phase }), + request: Some(ContractToolRequest::MarkDeliverableComplete { + deliverable_id: deliverable_id.to_string(), + phase, + }), pending_questions: None, } } diff --git a/makima/src/llm/mod.rs b/makima/src/llm/mod.rs index fc3802b..212876a 100644 --- a/makima/src/llm/mod.rs +++ b/makima/src/llm/mod.rs @@ -19,19 +19,18 @@ pub use contract_tools::{ pub use groq::GroqClient; pub use mesh_tools::{parse_mesh_tool_call, MeshToolExecutionResult, MeshToolRequest, MESH_TOOLS}; pub use phase_guidance::{ - check_deliverables_met, check_phase_completion, check_phase_completion_for_type, - format_checklist_markdown, generate_deliverable_prompt_guidance, get_next_phase_for_contract, - get_phase_checklist, get_phase_checklist_for_type, get_phase_deliverables, get_phase_deliverables_for_type, - should_auto_progress, AutoProgressAction, AutoProgressDecision, DeliverableCheckResult, DeliverableItem, - DeliverableStatus, FileInfo, FilePriority, PhaseChecklist, PhaseDeliverables, RecommendedFile, - TaskInfo, TaskStats, + check_deliverables_met, format_checklist_markdown, generate_deliverable_prompt_guidance, + get_next_phase_for_contract, get_phase_checklist_for_type, get_phase_deliverables, + get_phase_deliverables_for_type, should_auto_progress, AutoProgressAction, AutoProgressDecision, + Deliverable, DeliverableCheckResult, DeliverableItem, DeliverablePriority, DeliverableStatus, + PhaseChecklist, PhaseDeliverables, 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 templates::{all_contract_types, ContractTypeTemplate}; 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 index 03f7c76..379bdca 100644 --- a/makima/src/llm/phase_guidance.rs +++ b/makima/src/llm/phase_guidance.rs @@ -21,13 +21,12 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use uuid::Uuid; -/// Priority level for recommended deliverables +/// Priority level for deliverables #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] -pub enum FilePriority { - /// Must exist before advancing phase +pub enum DeliverablePriority { + /// Must be completed before advancing phase Required, /// Strongly suggested for phase completion Recommended, @@ -35,49 +34,45 @@ pub enum FilePriority { 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, +/// A deliverable for a phase +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Deliverable { + /// Unique identifier for the deliverable + pub id: String, + /// Display name + pub name: String, /// Priority level - pub priority: FilePriority, + pub priority: DeliverablePriority, /// Brief description of purpose pub description: String, } /// Expected deliverables for a phase -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct PhaseDeliverables { /// Phase name pub phase: String, - /// Recommended files to create - pub recommended_files: Vec<RecommendedFile>, + /// Deliverables for this phase + pub deliverables: Vec<Deliverable>, /// Whether a repository is required for this phase pub requires_repository: bool, - /// Whether tasks should exist in this phase + /// Whether tasks should be completed in this phase pub requires_tasks: bool, /// Guidance text for this phase pub guidance: String, } -/// Status of a deliverable item +/// Status of a deliverable #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct DeliverableStatus { - /// Template ID - pub template_id: String, - /// Expected name + /// Deliverable ID + pub id: String, + /// Display name pub name: String, /// Priority - pub priority: FilePriority, - /// Whether it has been created + pub priority: DeliverablePriority, + /// Whether it has been completed 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 @@ -85,8 +80,8 @@ pub struct DeliverableStatus { pub struct PhaseChecklist { /// Current phase pub phase: String, - /// File deliverables status - pub file_deliverables: Vec<DeliverableStatus>, + /// Deliverable status list + pub deliverables: Vec<DeliverableStatus>, /// Whether repository is configured pub has_repository: bool, /// Whether repository was required @@ -111,16 +106,8 @@ pub struct TaskStats { 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, } @@ -131,22 +118,6 @@ pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables { } /// Get phase deliverables configuration for a specific contract type -/// -/// ## Contract Types -/// -/// ### Simple -/// - Plan: Only "Plan" deliverable (required) -/// - Execute: Only "PR" deliverable (required) -/// -/// ### Specification -/// - Research: Only "Research Notes" deliverable (required) -/// - Specify: Only "Requirements Document" deliverable (required) -/// - Plan: Only "Plan" deliverable (required) -/// - Execute: Only "PR" deliverable (required) -/// - Review: Only "Release Notes" deliverable (required) -/// -/// ### Execute -/// - Execute: No deliverables at all pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> PhaseDeliverables { match contract_type { "execute" => get_execute_type_deliverables(phase), @@ -156,41 +127,35 @@ pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> Phas } /// Get deliverables for 'simple' contract type -/// - Plan phase: Only "Plan" deliverable (required) -/// - Execute phase: Only "PR" deliverable (required) fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables { match phase { "plan" => PhaseDeliverables { phase: "plan".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "plan".to_string(), - name_suggestion: "Plan".to_string(), - priority: FilePriority::Required, - description: "Implementation plan detailing the approach and tasks".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "plan-document".to_string(), + name: "Plan".to_string(), + priority: DeliverablePriority::Required, + description: "Implementation plan detailing the approach and tasks".to_string(), + }], requires_repository: true, requires_tasks: false, guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(), }, "execute" => PhaseDeliverables { phase: "execute".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "pr".to_string(), - name_suggestion: "PR".to_string(), - priority: FilePriority::Required, - description: "Pull request with the implemented changes".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "pull-request".to_string(), + name: "Pull Request".to_string(), + priority: DeliverablePriority::Required, + description: "Pull request with the implemented changes".to_string(), + }], requires_repository: true, requires_tasks: true, guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks to finish the contract.".to_string(), }, _ => PhaseDeliverables { phase: phase.to_string(), - recommended_files: vec![], + deliverables: vec![], requires_repository: false, requires_tasks: false, guidance: "Unknown phase for simple contract type".to_string(), @@ -199,86 +164,71 @@ fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables { } /// Get deliverables for 'specification' contract type -/// - Research: Only "Research Notes" deliverable (required) -/// - Specify: Only "Requirements Document" deliverable (required) -/// - Plan: Only "Plan" deliverable (required) -/// - Execute: Only "PR" deliverable (required) -/// - Review: Only "Release Notes" deliverable (required) fn get_specification_type_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::Required, - description: "Document findings and insights during research".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "research-notes".to_string(), + name: "Research Notes".to_string(), + priority: DeliverablePriority::Required, + description: "Document findings and insights during research".to_string(), + }], requires_repository: false, requires_tasks: false, guidance: "Focus on understanding the problem space and document your findings in the Research Notes 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(), - }, - ], + deliverables: vec![Deliverable { + id: "requirements-document".to_string(), + name: "Requirements Document".to_string(), + priority: DeliverablePriority::Required, + description: "Define functional and non-functional requirements".to_string(), + }], requires_repository: false, requires_tasks: false, guidance: "Define what needs to be built with clear requirements in the Requirements Document. Ensure specifications are detailed enough for planning.".to_string(), }, "plan" => PhaseDeliverables { phase: "plan".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "plan".to_string(), - name_suggestion: "Plan".to_string(), - priority: FilePriority::Required, - description: "Implementation plan detailing the approach and tasks".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "plan-document".to_string(), + name: "Plan".to_string(), + priority: DeliverablePriority::Required, + description: "Implementation plan detailing the approach and tasks".to_string(), + }], requires_repository: true, requires_tasks: false, guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(), }, "execute" => PhaseDeliverables { phase: "execute".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "pr".to_string(), - name_suggestion: "PR".to_string(), - priority: FilePriority::Required, - description: "Pull request with the implemented changes".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "pull-request".to_string(), + name: "Pull Request".to_string(), + priority: DeliverablePriority::Required, + description: "Pull request with the implemented changes".to_string(), + }], requires_repository: true, requires_tasks: true, guidance: "Execute the plan and create a PR with the implemented changes. 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(), - }, - ], + deliverables: vec![Deliverable { + id: "release-notes".to_string(), + name: "Release Notes".to_string(), + priority: DeliverablePriority::Required, + description: "Document changes for release communication".to_string(), + }], requires_repository: false, requires_tasks: false, guidance: "Review completed work and document the release in the Release Notes. The contract can be completed after review.".to_string(), }, _ => PhaseDeliverables { phase: phase.to_string(), - recommended_files: vec![], + deliverables: vec![], requires_repository: false, requires_tasks: false, guidance: "Unknown phase for specification contract type".to_string(), @@ -287,19 +237,18 @@ fn get_specification_type_deliverables(phase: &str) -> PhaseDeliverables { } /// Get deliverables for 'execute' contract type -/// - Execute phase only: No deliverables at all fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables { match phase { "execute" => PhaseDeliverables { phase: "execute".to_string(), - recommended_files: vec![], // No deliverables for execute-only contract type + deliverables: vec![], // No deliverables for execute-only contract type requires_repository: true, requires_tasks: true, guidance: "Execute the tasks directly. No deliverable documents are required for this contract type.".to_string(), }, _ => PhaseDeliverables { phase: phase.to_string(), - recommended_files: vec![], + deliverables: vec![], requires_repository: false, requires_tasks: false, guidance: "The 'execute' contract type only supports the 'execute' phase.".to_string(), @@ -307,49 +256,25 @@ fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables { } } -/// Build a phase checklist comparing expected vs actual deliverables (legacy, defaults to "simple") -pub fn get_phase_checklist( - phase: &str, - files: &[FileInfo], - tasks: &[TaskInfo], - has_repository: bool, -) -> PhaseChecklist { - get_phase_checklist_for_type(phase, files, tasks, has_repository, "simple") -} - -/// Build a phase checklist comparing expected vs actual deliverables for a specific contract type +/// Build a phase checklist comparing expected vs actual deliverables pub fn get_phase_checklist_for_type( phase: &str, - files: &[FileInfo], + completed_deliverables: &[String], tasks: &[TaskInfo], has_repository: bool, contract_type: &str, ) -> PhaseChecklist { - let deliverables = get_phase_deliverables_for_type(phase, contract_type); + let phase_config = get_phase_deliverables_for_type(phase, contract_type); - // Match files to expected deliverables - let file_deliverables: Vec<DeliverableStatus> = deliverables - .recommended_files + // Build deliverable status list + let deliverables: Vec<DeliverableStatus> = phase_config + .deliverables .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()), - } + .map(|d| DeliverableStatus { + id: d.id.clone(), + name: d.name.clone(), + priority: d.priority, + completed: completed_deliverables.contains(&d.id), }) .collect(); @@ -359,9 +284,18 @@ pub fn get_phase_checklist_for_type( 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 }) + let failed = tasks + .iter() + .filter(|t| t.status == "failed" || t.status == "error") + .count(); + + Some(TaskStats { + total, + pending, + running, + done, + failed, + }) } else { None }; @@ -370,9 +304,9 @@ pub fn get_phase_checklist_for_type( 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 { + // Count required and recommended deliverables (not optional) + for status in &deliverables { + if status.priority != DeliverablePriority::Optional { total_items += 1; if status.completed { completed_items += 1; @@ -381,7 +315,7 @@ pub fn get_phase_checklist_for_type( } // Count repository if required - if deliverables.requires_repository { + if phase_config.requires_repository { total_items += 1; if has_repository { completed_items += 1; @@ -407,62 +341,64 @@ pub fn get_phase_checklist_for_type( // Generate suggestions let mut suggestions = Vec::new(); - // Suggest missing required files - for status in &file_deliverables { + // Suggest missing deliverables + for status in &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)); + DeliverablePriority::Required => { + suggestions.push(format!( + "Mark '{}' as complete using mark_deliverable_complete (required)", + status.name + )); } - FilePriority::Optional => { - // Don't suggest optional items + DeliverablePriority::Recommended => { + suggestions.push(format!( + "Consider completing '{}' (recommended)", + status.name + )); } + DeliverablePriority::Optional => {} } } } // Suggest repository if needed - if deliverables.requires_repository && !has_repository { + if phase_config.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()); + suggestions.push("Create tasks to implement the plan".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)); + 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)); } else if stats.done == stats.total && stats.total > 0 { - // All tasks complete - for simple contracts, this is the terminal phase - // For specification contracts, they should advance to review phase - suggestions.push("Mark the contract as completed (for simple contracts) or advance to Review phase".to_string()); - } - } - - // Suggest completion for review phase (terminal for specification contracts) - if phase == "review" { - let has_release_notes = file_deliverables.iter() - .any(|d| d.template_id == "release-notes" && d.completed); - if has_release_notes { - suggestions.push("Mark the contract as completed".to_string()); + suggestions.push("All tasks complete. Mark deliverables and advance phase.".to_string()); } } // Generate summary - let summary = generate_phase_summary(phase, &file_deliverables, has_repository, &task_stats, completion_percentage); + let summary = generate_phase_summary( + phase, + &deliverables, + has_repository, + &task_stats, + completion_percentage, + ); PhaseChecklist { phase: phase.to_string(), - file_deliverables, + deliverables, has_repository, - repository_required: deliverables.requires_repository, + repository_required: phase_config.requires_repository, task_stats, completion_percentage, summary, @@ -483,32 +419,39 @@ fn generate_phase_summary( match phase { "research" => { if completed_count == 0 { - "Research phase needs documentation. Create research notes or competitor analysis.".to_string() + "Research phase needs documentation. Mark deliverables complete when ready." + .to_string() } else { - format!("{}/{} research documents created. Consider transitioning to Specify phase.", completed_count, total_count) + format!( + "{}/{} deliverables complete. Ready to transition to Specify phase.", + completed_count, total_count + ) } } "specify" => { - let has_required = deliverables.iter() - .filter(|d| d.priority == FilePriority::Required) + let has_required = deliverables + .iter() + .filter(|d| d.priority == DeliverablePriority::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() + "Specify phase requires completing the Requirements Document deliverable." + .to_string() } else { - format!("{}/{} specification documents created.", completed_count, total_count) + "Specifications ready. Consider transitioning to Plan phase.".to_string() } } "plan" => { - let has_task_breakdown = deliverables.iter() - .any(|d| d.template_id == "task-breakdown" && d.completed); + let has_required = deliverables + .iter() + .filter(|d| d.priority == DeliverablePriority::Required) + .all(|d| d.completed); - if !has_task_breakdown { - "Plan phase requires a Task Breakdown document.".to_string() + if !has_required { + "Plan phase requires completing the Plan deliverable.".to_string() } else if !has_repository { - "Repository not configured. Configure a repository before Execute phase.".to_string() + "Repository not configured. Configure a repository before Execute phase." + .to_string() } else { "Planning complete. Ready to transition to Execute phase.".to_string() } @@ -516,23 +459,33 @@ fn generate_phase_summary( "execute" => { if let Some(stats) = task_stats { if stats.total == 0 { - "No tasks created. Create tasks from the Task Breakdown document.".to_string() + "No tasks created. Create tasks to implement the plan.".to_string() } else if stats.done == stats.total { - "All tasks complete! Ready for Review phase.".to_string() + "All tasks complete! Mark deliverables and advance to Review phase (or complete contract).".to_string() } else { - format!("{}/{} tasks completed ({}% done)", stats.done, stats.total, - if stats.total > 0 { (stats.done * 100) / stats.total } else { 0 }) + 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); + let has_required = deliverables + .iter() + .filter(|d| d.priority == DeliverablePriority::Required) + .all(|d| d.completed); - if !has_release_notes { - "Review phase requires Release Notes before completion.".to_string() + if !has_required { + "Review phase requires completing the Release Notes deliverable.".to_string() } else { "Review documentation complete. Contract can be marked as done.".to_string() } @@ -541,44 +494,6 @@ fn generate_phase_summary( } } -/// Check if phase targets are met for transition (legacy, defaults to "simple") -pub fn check_phase_completion( - phase: &str, - files: &[FileInfo], - tasks: &[TaskInfo], - has_repository: bool, -) -> bool { - check_phase_completion_for_type(phase, files, tasks, has_repository, "simple") -} - -/// Check if phase targets are met for transition for a specific contract type -pub fn check_phase_completion_for_type( - phase: &str, - files: &[FileInfo], - tasks: &[TaskInfo], - has_repository: bool, - contract_type: &str, -) -> bool { - let checklist = get_phase_checklist_for_type(phase, files, tasks, has_repository, contract_type); - - // 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 -} - /// Result of checking if deliverables are met for the current phase #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct DeliverableCheckResult { @@ -603,9 +518,11 @@ pub struct DeliverableCheckResult { /// A single deliverable item status #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct DeliverableItem { + /// ID of the deliverable + pub id: String, /// Name of the deliverable pub name: String, - /// Type: "file", "repository", "pr", "tasks" + /// Type: "deliverable", "repository", "tasks" pub deliverable_type: String, /// Whether it's met pub met: bool, @@ -614,51 +531,49 @@ pub struct DeliverableItem { } /// Check if all required deliverables for the current phase are met -/// This is used for both prompts and the check_deliverables_met tool pub fn check_deliverables_met( phase: &str, contract_type: &str, - files: &[FileInfo], + completed_deliverables: &[String], tasks: &[TaskInfo], has_repository: bool, - pr_url: Option<&str>, ) -> DeliverableCheckResult { - let mut required_deliverables = Vec::new(); + let mut required_items = Vec::new(); let mut missing = Vec::new(); // Get the deliverables for this contract type and phase - let deliverables = get_phase_deliverables_for_type(phase, contract_type); - - // Check required files for this phase - for rec in &deliverables.recommended_files { - if rec.priority == FilePriority::Required { - let matched = files.iter().any(|f| { - f.contract_phase.as_deref() == Some(phase) && - (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("-", " "))) - }); - - required_deliverables.push(DeliverableItem { - name: rec.name_suggestion.clone(), - deliverable_type: "file".to_string(), - met: matched, - details: if matched { - Some("Document exists".to_string()) + let phase_config = get_phase_deliverables_for_type(phase, contract_type); + + // Check required deliverables for this phase + for deliverable in &phase_config.deliverables { + if deliverable.priority == DeliverablePriority::Required { + let is_complete = completed_deliverables.contains(&deliverable.id); + + required_items.push(DeliverableItem { + id: deliverable.id.clone(), + name: deliverable.name.clone(), + deliverable_type: "deliverable".to_string(), + met: is_complete, + details: if is_complete { + Some("Marked complete".to_string()) } else { None }, }); - if !matched { - missing.push(format!("Create {} (required)", rec.name_suggestion)); + if !is_complete { + missing.push(format!( + "Mark '{}' as complete (required)", + deliverable.name + )); } } } // Check repository for phases that require it - if deliverables.requires_repository { - required_deliverables.push(DeliverableItem { + if phase_config.requires_repository { + required_items.push(DeliverableItem { + id: "repository".to_string(), name: "Repository".to_string(), deliverable_type: "repository".to_string(), met: has_repository, @@ -675,12 +590,13 @@ pub fn check_deliverables_met( } // Check tasks for execute phase - if deliverables.requires_tasks { + if phase_config.requires_tasks { let total_tasks = tasks.len(); let done_tasks = tasks.iter().filter(|t| t.status == "done").count(); let tasks_complete = total_tasks > 0 && done_tasks == total_tasks; - required_deliverables.push(DeliverableItem { + required_items.push(DeliverableItem { + id: "tasks".to_string(), name: "Tasks Completed".to_string(), deliverable_type: "tasks".to_string(), met: tasks_complete, @@ -691,38 +607,36 @@ pub fn check_deliverables_met( if total_tasks == 0 { missing.push("Create and complete tasks".to_string()); } else { - missing.push(format!("Complete remaining {} task(s)", total_tasks - done_tasks)); + missing.push(format!( + "Complete remaining {} task(s)", + total_tasks - done_tasks + )); } } } - // For simple/specification contracts in execute phase, PR is a key deliverable - if (contract_type == "simple" || contract_type == "specification") && phase == "execute" { - let has_pr = pr_url.is_some() && !pr_url.unwrap_or("").is_empty(); - required_deliverables.push(DeliverableItem { - name: "Pull Request".to_string(), - deliverable_type: "pr".to_string(), - met: has_pr, - details: pr_url.map(|u| format!("PR: {}", u)), - }); - - if !has_pr { - missing.push("Create a Pull Request for the completed work".to_string()); - } - } - - let deliverables_met = required_deliverables.iter().all(|d| d.met); + let deliverables_met = required_items.iter().all(|d| d.met); let next_phase = get_next_phase_for_contract(contract_type, phase); let ready_to_advance = deliverables_met && next_phase.is_some(); let summary = if deliverables_met { if let Some(ref next) = next_phase { - format!("All deliverables met for {} phase. Ready to advance to {} phase.", phase, next) + format!( + "All deliverables met for {} phase. Ready to advance to {} phase.", + phase, next + ) } else { - format!("All deliverables met for {} phase. This is the final phase.", phase) + format!( + "All deliverables met for {} phase. This is the final phase.", + phase + ) } } else { - format!("{} deliverable(s) still needed for {} phase.", missing.len(), phase) + format!( + "{} deliverable(s) still needed for {} phase.", + missing.len(), + phase + ) }; DeliverableCheckResult { @@ -730,7 +644,7 @@ pub fn check_deliverables_met( ready_to_advance, phase: phase.to_string(), next_phase, - required_deliverables, + required_deliverables: required_items, missing, summary, auto_progress_recommended: deliverables_met && ready_to_advance, @@ -758,17 +672,21 @@ pub fn get_next_phase_for_contract(contract_type: &str, current_phase: &str) -> } /// Determine if the contract should auto-progress to the next phase -/// This is called when deliverables are met and autonomous_loop is enabled pub fn should_auto_progress( phase: &str, contract_type: &str, - files: &[FileInfo], + completed_deliverables: &[String], tasks: &[TaskInfo], has_repository: bool, - pr_url: Option<&str>, autonomous_loop: bool, ) -> AutoProgressDecision { - let check = check_deliverables_met(phase, contract_type, files, tasks, has_repository, pr_url); + let check = check_deliverables_met( + phase, + contract_type, + completed_deliverables, + tasks, + has_repository, + ); if !check.deliverables_met { return AutoProgressDecision { @@ -840,15 +758,25 @@ pub fn generate_deliverable_prompt_guidance( let mut guidance = String::new(); guidance.push_str("\n## Phase Deliverables Status\n\n"); - guidance.push_str(&format!("**Current Phase**: {} | **Contract Type**: {}\n\n", - capitalize(phase), contract_type)); + guidance.push_str(&format!( + "**Current Phase**: {} | **Contract Type**: {}\n\n", + capitalize(phase), + contract_type + )); // Show required deliverables checklist guidance.push_str("### Required Deliverables Checklist\n"); for item in &check_result.required_deliverables { let status = if item.met { "[x]" } else { "[ ]" }; - let details = item.details.as_ref().map(|d| format!(" - {}", d)).unwrap_or_default(); - guidance.push_str(&format!("{} **{}** ({}){}\n", status, item.name, item.deliverable_type, details)); + let details = item + .details + .as_ref() + .map(|d| format!(" - {}", d)) + .unwrap_or_default(); + guidance.push_str(&format!( + "{} **{}** ({}){}\n", + status, item.name, item.deliverable_type, details + )); } // Show status and next actions @@ -861,7 +789,9 @@ pub fn generate_deliverable_prompt_guidance( guidance.push_str(&format!("\n**ACTION REQUIRED**: Since all deliverables are met, you should call `advance_phase` with `new_phase=\"{}\"` to progress the contract.\n", next)); } } else { - guidance.push_str("This is the terminal phase. The contract can be marked as completed.\n"); + guidance.push_str( + "This is the terminal phase. The contract can be marked as completed.\n", + ); } } else { guidance.push_str("**Deliverables not yet met.**\n\n"); @@ -869,7 +799,9 @@ pub fn generate_deliverable_prompt_guidance( for item in &check_result.missing { guidance.push_str(&format!("- {}\n", item)); } - guidance.push_str("\nComplete the missing deliverables before advancing to the next phase.\n"); + guidance.push_str( + "\nUse `mark_deliverable_complete` to mark deliverables as complete when ready.\n", + ); } guidance @@ -877,20 +809,23 @@ pub fn generate_deliverable_prompt_guidance( /// 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)); + let mut md = format!( + "## Phase Progress ({} Phase)\n\n", + capitalize(&checklist.phase) + ); - // File deliverables + // Deliverables md.push_str("### Deliverables\n"); - for status in &checklist.file_deliverables { + for status in &checklist.deliverables { let check = if status.completed { "+" } else { "-" }; let priority_label = match status.priority { - FilePriority::Required => " (required)", - FilePriority::Recommended => " (recommended)", - FilePriority::Optional => " (optional)", + DeliverablePriority::Required => " (required)", + DeliverablePriority::Recommended => " (recommended)", + DeliverablePriority::Optional => " (optional)", }; if status.completed { - md.push_str(&format!("[{}] {} - \"{}\"\n", check, status.name, status.actual_name.as_deref().unwrap_or("created"))); + md.push_str(&format!("[{}] {} - completed\n", check, status.name)); } else { md.push_str(&format!("[{}] {}{}\n", check, status.name, priority_label)); } @@ -904,7 +839,7 @@ pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String { // Task stats for execute phase if let Some(ref stats) = checklist.task_stats { - md.push_str(&format!("\n### Task Progress\n")); + md.push_str("\n### Task Progress\n"); md.push_str(&format!("- Total: {}\n", stats.total)); md.push_str(&format!("- Done: {}\n", stats.done)); if stats.pending > 0 { @@ -919,7 +854,10 @@ pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String { } // Summary - md.push_str(&format!("\n**Status**: {} ({}% complete)\n", checklist.summary, checklist.completion_percentage)); + md.push_str(&format!( + "\n**Status**: {} ({}% complete)\n", + checklist.summary, checklist.completion_percentage + )); // Suggestions if !checklist.suggestions.is_empty() { @@ -946,125 +884,63 @@ mod tests { #[test] fn test_get_phase_deliverables_simple() { - // Simple contract type: Plan phase has only "Plan" deliverable let plan = get_phase_deliverables_for_type("plan", "simple"); assert_eq!(plan.phase, "plan"); assert!(plan.requires_repository); - assert_eq!(plan.recommended_files.len(), 1); - assert_eq!(plan.recommended_files[0].template_id, "plan"); - assert_eq!(plan.recommended_files[0].priority, FilePriority::Required); + assert_eq!(plan.deliverables.len(), 1); + assert_eq!(plan.deliverables[0].id, "plan-document"); + assert_eq!(plan.deliverables[0].priority, DeliverablePriority::Required); - // Simple contract type: Execute phase has only "PR" deliverable let execute = get_phase_deliverables_for_type("execute", "simple"); assert_eq!(execute.phase, "execute"); assert!(execute.requires_repository); assert!(execute.requires_tasks); - assert_eq!(execute.recommended_files.len(), 1); - assert_eq!(execute.recommended_files[0].template_id, "pr"); - assert_eq!(execute.recommended_files[0].priority, FilePriority::Required); + assert_eq!(execute.deliverables.len(), 1); + assert_eq!(execute.deliverables[0].id, "pull-request"); } #[test] fn test_get_phase_deliverables_specification() { - // Specification: Research phase has only "Research Notes" deliverable let research = get_phase_deliverables_for_type("research", "specification"); - assert_eq!(research.phase, "research"); - assert!(!research.requires_repository); - assert_eq!(research.recommended_files.len(), 1); - assert_eq!(research.recommended_files[0].template_id, "research-notes"); - assert_eq!(research.recommended_files[0].priority, FilePriority::Required); + assert_eq!(research.deliverables.len(), 1); + assert_eq!(research.deliverables[0].id, "research-notes"); - // Specification: Specify phase has only "Requirements Document" deliverable let specify = get_phase_deliverables_for_type("specify", "specification"); - assert_eq!(specify.phase, "specify"); - assert_eq!(specify.recommended_files.len(), 1); - assert_eq!(specify.recommended_files[0].template_id, "requirements"); - assert_eq!(specify.recommended_files[0].priority, FilePriority::Required); - - // Specification: Plan phase has only "Plan" deliverable - let plan = get_phase_deliverables_for_type("plan", "specification"); - assert_eq!(plan.phase, "plan"); - assert_eq!(plan.recommended_files.len(), 1); - assert_eq!(plan.recommended_files[0].template_id, "plan"); + assert_eq!(specify.deliverables.len(), 1); + assert_eq!(specify.deliverables[0].id, "requirements-document"); - // Specification: Execute phase has only "PR" deliverable - let execute = get_phase_deliverables_for_type("execute", "specification"); - assert_eq!(execute.phase, "execute"); - assert_eq!(execute.recommended_files.len(), 1); - assert_eq!(execute.recommended_files[0].template_id, "pr"); - - // Specification: Review phase has only "Release Notes" deliverable let review = get_phase_deliverables_for_type("review", "specification"); - assert_eq!(review.phase, "review"); - assert_eq!(review.recommended_files.len(), 1); - assert_eq!(review.recommended_files[0].template_id, "release-notes"); - assert_eq!(review.recommended_files[0].priority, FilePriority::Required); + assert_eq!(review.deliverables.len(), 1); + assert_eq!(review.deliverables[0].id, "release-notes"); } #[test] fn test_get_phase_deliverables_execute_type() { - // Execute contract type: Only execute phase, NO deliverables let execute = get_phase_deliverables_for_type("execute", "execute"); - assert_eq!(execute.phase, "execute"); + assert!(execute.deliverables.is_empty()); assert!(execute.requires_repository); assert!(execute.requires_tasks); - assert!(execute.recommended_files.is_empty()); // NO deliverables - - // Execute contract type: Other phases should return empty deliverables - let plan = get_phase_deliverables_for_type("plan", "execute"); - assert!(plan.recommended_files.is_empty()); } #[test] - fn test_phase_checklist_empty_simple() { - let checklist = get_phase_checklist_for_type("plan", &[], &[], false, "simple"); - assert_eq!(checklist.completion_percentage, 0); - assert!(!checklist.suggestions.is_empty()); - } - - #[test] - fn test_phase_checklist_execute_type_no_deliverables() { - // Execute contract type with no file deliverables - let checklist = get_phase_checklist_for_type("execute", &[], &[], true, "execute"); - // Should have no file deliverables - assert!(checklist.file_deliverables.is_empty()); - } - - #[test] - fn test_check_phase_completion_specification() { - let files = vec![ - FileInfo { - id: Uuid::new_v4(), - name: "Requirements Document".to_string(), - contract_phase: Some("specify".to_string()), - }, - ]; - - // Specify phase has required file for specification contract type - let complete = check_phase_completion_for_type("specify", &files, &[], false, "specification"); - assert!(complete); - } - - #[test] - fn test_check_phase_completion_simple() { - let files = vec![ - FileInfo { - id: Uuid::new_v4(), - name: "Plan".to_string(), - contract_phase: Some("plan".to_string()), - }, - ]; + fn test_check_deliverables_met() { + // No deliverables marked complete + let result = check_deliverables_met("plan", "simple", &[], &[], true); + assert!(!result.deliverables_met); + assert!(!result.missing.is_empty()); - // Plan phase has required "Plan" file for simple contract type - let complete = check_phase_completion_for_type("plan", &files, &[], true, "simple"); - assert!(complete); + // Deliverable marked complete + let completed = vec!["plan-document".to_string()]; + let result = check_deliverables_met("plan", "simple", &completed, &[], true); + assert!(result.deliverables_met); + assert!(result.ready_to_advance); } #[test] - fn test_legacy_functions_default_to_simple() { - // Legacy get_phase_deliverables defaults to simple - let plan = get_phase_deliverables("plan"); - assert_eq!(plan.recommended_files.len(), 1); - assert_eq!(plan.recommended_files[0].template_id, "plan"); + fn test_phase_checklist() { + let completed = vec!["plan-document".to_string()]; + let checklist = get_phase_checklist_for_type("plan", &completed, &[], true, "simple"); + assert_eq!(checklist.completion_percentage, 100); + assert!(checklist.deliverables[0].completed); } } diff --git a/makima/src/llm/templates.rs b/makima/src/llm/templates.rs index 8d3c04d..48b7515 100644 --- a/makima/src/llm/templates.rs +++ b/makima/src/llm/templates.rs @@ -1,13 +1,10 @@ -//! Template definitions for phase-appropriate file structures. +//! Contract type template definitions. //! -//! Templates provide starting structures for files based on the contract phase. -//! Each phase has templates suited for that stage of work. +//! Defines the available contract types and their workflow phases. use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::db::models::BodyElement; - // ============================================================================= // Contract Type Templates (Workflow Definitions) // ============================================================================= @@ -81,1009 +78,3 @@ fn execute_contract_type() -> ContractTypeTemplate { is_builtin: true, } } - -// ============================================================================= -// File Templates -// ============================================================================= - -/// 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 1e43c40..c192398 100644 --- a/makima/src/llm/tools.rs +++ b/makima/src/llm/tools.rs @@ -5,7 +5,6 @@ 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 { @@ -412,36 +411,6 @@ 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"] - }), - }, ] }); @@ -531,9 +500,6 @@ 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, @@ -1648,131 +1614,6 @@ 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 { diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index 28c3436..e035368 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -19,11 +19,11 @@ use crate::db::{ repository, }; use crate::llm::{ - all_templates, analyze_task_output, body_to_markdown, format_checklist_markdown, + analyze_task_output, body_to_markdown, format_checklist_markdown, format_parsed_tasks, parse_tasks_from_breakdown, claude::{self, ClaudeClient, ClaudeError, ClaudeModel}, groq::{GroqClient, GroqError, Message, ToolCallResponse}, - parse_contract_tool_call, templates_for_phase, ContractToolRequest, FileInfo, + parse_contract_tool_call, ContractToolRequest, LlmModel, TaskInfo, ToolCall, ToolResult, UserQuestion, CONTRACT_TOOLS, format_transcript_for_analysis, calculate_speaker_stats, build_analysis_prompt, parse_analysis_response, @@ -441,36 +441,29 @@ fn build_contract_context(contract: &crate::db::models::ContractWithRelations) - context.push_str(&format!("Description: {}\n", desc)); } - // Build phase checklist - let file_infos: Vec<FileInfo> = contract.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + // Get completed deliverables for the current phase + let completed_deliverables = c.get_completed_deliverables(&c.phase); + // Build task infos for checklist let task_infos: Vec<TaskInfo> = contract.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !contract.repositories.is_empty(); - let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &file_infos, &task_infos, has_repository, &c.contract_type); + let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &completed_deliverables, &task_infos, has_repository, &c.contract_type); // Add phase checklist to context context.push_str("\n"); context.push_str(&format_checklist_markdown(&phase_checklist)); // Add deliverable check result for phase transition readiness - // Note: pr_url is not available in TaskSummary, so we pass None here - // Full PR checking should be done via the check_deliverables_met tool let deliverable_check = crate::llm::check_deliverables_met( &c.phase, &c.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Add deliverable prompt guidance @@ -1204,23 +1197,7 @@ async fn handle_contract_request( } } - ContractToolRequest::CreateFileFromTemplate { - template_id, - name, - description, - } => { - // Find the template - let templates = all_templates(); - let template = templates.iter().find(|t| t.id == template_id); - - let Some(template) = template else { - return ContractRequestResult { - success: false, - message: format!("Template '{}' not found", template_id), - data: None, - }; - }; - + ContractToolRequest::CreateEmptyFile { name, description } => { // Verify contract exists and get current phase let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, @@ -1240,32 +1217,25 @@ async fn handle_contract_request( } }; - // Use template's phase if available, otherwise use contract's current phase - let contract_phase = Some(template.phase.clone()).or(Some(contract.phase.clone())); - - // Create the file (contract_id is now required) + // Create the file with current contract phase let create_req = crate::db::models::CreateFileRequest { contract_id, name: Some(name.clone()), description, - body: template.suggested_body.clone(), + body: Vec::new(), transcript: Vec::new(), location: None, repo_file_path: None, - contract_phase, + contract_phase: Some(contract.phase.clone()), }; match repository::create_file_for_owner(pool, owner_id, create_req).await { Ok(file) => ContractRequestResult { success: true, - message: format!( - "Created file '{}' from template '{}'", - name, template.name - ), + message: format!("Created empty file '{}'", name), data: Some(json!({ "fileId": file.id, "name": file.name, - "templateId": template_id, })), }, Err(e) => ContractRequestResult { @@ -1276,8 +1246,11 @@ async fn handle_contract_request( } } - ContractToolRequest::CreateEmptyFile { name, description } => { - // Verify contract exists and get current phase + ContractToolRequest::MarkDeliverableComplete { + deliverable_id, + phase, + } => { + // Get the contract to determine current phase and contract type let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, Ok(None) => { @@ -1296,61 +1269,60 @@ async fn handle_contract_request( } }; - // Create the file with current contract phase - let create_req = crate::db::models::CreateFileRequest { - contract_id, - name: Some(name.clone()), - description, - body: Vec::new(), - transcript: Vec::new(), - location: None, - repo_file_path: None, - contract_phase: Some(contract.phase.clone()), - }; + // Use specified phase or default to current contract phase + let target_phase = phase.unwrap_or_else(|| contract.phase.clone()); - match repository::create_file_for_owner(pool, owner_id, create_req).await { - Ok(file) => ContractRequestResult { + // Validate the deliverable ID exists for this phase/contract type + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type); + let deliverable_exists = phase_deliverables.deliverables.iter().any(|d| d.id == deliverable_id); + + if !deliverable_exists { + let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect(); + return ContractRequestResult { + success: false, + message: format!( + "Invalid deliverable_id '{}' for {} phase. Valid IDs: {:?}", + deliverable_id, target_phase, valid_ids + ), + data: None, + }; + } + + // Check if already completed + if contract.is_deliverable_complete(&target_phase, &deliverable_id) { + return ContractRequestResult { success: true, - message: format!("Created empty file '{}'", name), + message: format!("Deliverable '{}' is already marked complete for {} phase", deliverable_id, target_phase), data: Some(json!({ - "fileId": file.id, - "name": file.name, + "deliverableId": deliverable_id, + "phase": target_phase, + "alreadyComplete": true, })), - }, + }; + } + + // Mark the deliverable as complete + match repository::mark_deliverable_complete(pool, contract_id, &target_phase, &deliverable_id).await { + Ok(updated_contract) => { + let completed = updated_contract.get_completed_deliverables(&target_phase); + ContractRequestResult { + success: true, + message: format!("Marked deliverable '{}' as complete for {} phase", deliverable_id, target_phase), + data: Some(json!({ + "deliverableId": deliverable_id, + "phase": target_phase, + "completedDeliverables": completed, + })), + } + } Err(e) => ContractRequestResult { success: false, - message: format!("Failed to create file: {}", e), + message: format!("Failed to mark deliverable complete: {}", e), data: None, }, } } - ContractToolRequest::ListAvailableTemplates { phase } => { - let templates = if let Some(p) = phase { - templates_for_phase(&p) - } else { - all_templates() - }; - - let template_data: Vec<serde_json::Value> = templates - .iter() - .map(|t| { - json!({ - "id": t.id, - "name": t.name, - "phase": t.phase, - "description": t.description, - }) - }) - .collect(); - - ContractRequestResult { - success: true, - message: format!("Found {} templates", templates.len()), - data: Some(json!({ "templates": template_data })), - } - } - ContractToolRequest::CreateContractTask { name, plan, @@ -1666,8 +1638,8 @@ async fn handle_contract_request( }; let phase_info = get_phase_description(&contract.phase); - let templates = templates_for_phase(&contract.phase); - let template_names: Vec<String> = templates.iter().map(|t| t.name.clone()).collect(); + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&contract.phase, &contract.contract_type); + let deliverable_names: Vec<String> = phase_deliverables.deliverables.iter().map(|d| d.name.clone()).collect(); ContractRequestResult { success: true, @@ -1676,7 +1648,8 @@ async fn handle_contract_request( "phase": contract.phase, "description": phase_info.0, "activities": phase_info.1, - "suggestedTemplates": template_names, + "deliverables": deliverable_names, + "guidance": phase_deliverables.guidance, "nextPhase": get_next_phase(&contract.phase), })), } @@ -1764,30 +1737,22 @@ async fn handle_contract_request( } }; - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + // Get completed deliverables for the current phase + let completed_deliverables = cwr.contract.get_completed_deliverables(current_phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - // Note: pr_url is not available in TaskSummary, so we skip PR checking here - // For simple contracts, the PR deliverable check will need to be done - // by fetching full task details if needed let check_result = crate::llm::check_deliverables_met( current_phase, &contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Block transition if deliverables are not met @@ -1895,17 +1860,17 @@ async fn handle_contract_request( match repository::change_contract_phase_for_owner(pool, contract_id, owner_id, &new_phase).await { Ok(Some(updated)) => { // Get deliverables for the new phase (using contract type) - let deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type); + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type); - // Build suggested files list - let suggested_files: Vec<serde_json::Value> = deliverables - .recommended_files + // Build deliverables list + let deliverables_list: Vec<serde_json::Value> = phase_deliverables + .deliverables .iter() - .map(|f| json!({ - "templateId": f.template_id, - "name": f.name_suggestion, - "priority": format!("{:?}", f.priority).to_lowercase(), - "description": f.description, + .map(|d| json!({ + "id": d.id, + "name": d.name, + "priority": format!("{:?}", d.priority).to_lowercase(), + "description": d.description, })) .collect(); @@ -1913,16 +1878,16 @@ async fn handle_contract_request( success: true, message: format!( "Advanced contract from '{}' to '{}' phase. {}", - current_phase, new_phase, deliverables.guidance + current_phase, new_phase, phase_deliverables.guidance ), data: Some(json!({ "status": "advanced", "previousPhase": current_phase, "newPhase": updated.phase, - "phaseGuidance": deliverables.guidance, - "suggestedFiles": suggested_files, - "requiresRepository": deliverables.requires_repository, - "requiresTasks": deliverables.requires_tasks, + "phaseGuidance": phase_deliverables.guidance, + "deliverables": deliverables_list, + "requiresRepository": phase_deliverables.requires_repository, + "requiresTasks": phase_deliverables.requires_tasks, })), } }, @@ -2028,20 +1993,15 @@ async fn handle_contract_request( ContractToolRequest::GetPhaseChecklist => { match get_contract_with_relations(pool, contract_id, owner_id).await { Ok(Some(cwr)) => { - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &file_infos, &task_infos, has_repository, &cwr.contract.contract_type); + let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &completed_deliverables, &task_infos, has_repository, &cwr.contract.contract_type); ContractRequestResult { success: true, @@ -2049,7 +2009,7 @@ async fn handle_contract_request( data: Some(json!({ "phase": checklist.phase, "completionPercentage": checklist.completion_percentage, - "deliverables": checklist.file_deliverables, + "deliverables": checklist.deliverables, "hasRepository": checklist.has_repository, "repositoryRequired": checklist.repository_required, "taskStats": checklist.task_stats, @@ -2074,41 +2034,30 @@ async fn handle_contract_request( ContractToolRequest::CheckDeliverablesMet => { match get_contract_with_relations(pool, contract_id, owner_id).await { Ok(Some(cwr)) => { - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - // Note: pr_url is not available in TaskSummary - // For simple contracts needing PR checking, full task details would need to be fetched - // For now, we pass None and the LLM can guide the user to ensure a PR exists - let check_result = crate::llm::check_deliverables_met( &cwr.contract.phase, &cwr.contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Check if we should auto-progress let auto_progress = crate::llm::should_auto_progress( &cwr.contract.phase, &cwr.contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary cwr.contract.autonomous_loop, ); diff --git a/makima/src/server/handlers/contract_daemon.rs b/makima/src/server/handlers/contract_daemon.rs index 5b23831..5f56f06 100644 --- a/makima/src/server/handlers/contract_daemon.rs +++ b/makima/src/server/handlers/contract_daemon.rs @@ -15,7 +15,7 @@ use utoipa::ToSchema; use uuid::Uuid; use crate::db::{models::FileSummary, repository}; -use crate::llm::phase_guidance::{self, FileInfo, PhaseChecklist, TaskInfo}; +use crate::llm::phase_guidance::{self, PhaseChecklist, TaskInfo}; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; use crate::server::state::SharedState; @@ -242,28 +242,14 @@ pub async fn get_contract_checklist( } }; - // Get files for this contract - let files = match repository::list_files_in_contract(pool, id, auth.owner_id).await { - Ok(f) => f - .into_iter() - .map(|f| FileInfo { - id: f.id, - name: f.name, - contract_phase: f.contract_phase, - }) - .collect::<Vec<_>>(), - Err(e) => { - tracing::warn!("Failed to get files for contract {}: {}", id, e); - Vec::new() - } - }; + // Get completed deliverables for the current phase + let completed_deliverables = contract.get_completed_deliverables(&contract.phase); // Get tasks for this contract let tasks = match repository::list_tasks_in_contract(pool, id, auth.owner_id).await { Ok(t) => t .into_iter() .map(|t| TaskInfo { - id: t.id, name: t.name, status: t.status, }) @@ -280,7 +266,7 @@ pub async fn get_contract_checklist( Err(_) => false, }; - let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type); + let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &completed_deliverables, &tasks, has_repository, &contract.contract_type); Json(checklist).into_response() } @@ -463,24 +449,14 @@ pub async fn get_suggest_action( } }; - // Get files and tasks for checklist - let files = repository::list_files_in_contract(pool, id, auth.owner_id) - .await - .unwrap_or_default() - .into_iter() - .map(|f| FileInfo { - id: f.id, - name: f.name, - contract_phase: f.contract_phase, - }) - .collect::<Vec<_>>(); + // Get completed deliverables and tasks for checklist + let completed_deliverables = contract.get_completed_deliverables(&contract.phase); let tasks = repository::list_tasks_in_contract(pool, id, auth.owner_id) .await .unwrap_or_default() .into_iter() .map(|t| TaskInfo { - id: t.id, name: t.name, status: t.status, }) @@ -491,7 +467,7 @@ pub async fn get_suggest_action( .map(|r| !r.is_empty()) .unwrap_or(false); - let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type); + let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &completed_deliverables, &tasks, has_repository, &contract.contract_type); // Determine suggested action based on checklist let (action, description) = if !checklist.suggestions.is_empty() { diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index f16f33d..de3164c 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -6,6 +6,8 @@ use axum::{ response::IntoResponse, Json, }; +use serde::Deserialize; +use utoipa::ToSchema; use uuid::Uuid; use crate::db::models::{ @@ -1423,6 +1425,134 @@ pub async fn change_phase( } // ============================================================================= +// Deliverables +// ============================================================================= + +/// Request body for marking a deliverable complete +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MarkDeliverableRequest { + /// The deliverable ID to mark as complete (e.g., 'plan-document', 'pull-request') + pub deliverable_id: String, + /// Phase the deliverable belongs to. Defaults to current contract phase if not specified. + pub phase: Option<String>, +} + +/// Mark a deliverable as complete for a contract phase. +#[utoipa::path( + post, + path = "/api/v1/contracts/{id}/deliverables/complete", + params( + ("id" = Uuid, Path, description = "Contract ID") + ), + request_body = MarkDeliverableRequest, + responses( + (status = 200, description = "Deliverable marked complete", body = serde_json::Value), + (status = 400, description = "Invalid deliverable ID", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Contract not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Contracts" +)] +pub async fn mark_deliverable_complete( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(req): Json<MarkDeliverableRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Get contract + let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await { + Ok(Some(c)) => c, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Contract not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get contract {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + // Use specified phase or default to current contract phase + let target_phase = req.phase.unwrap_or_else(|| contract.phase.clone()); + + // Validate the deliverable ID exists for this phase/contract type + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type); + let deliverable = phase_deliverables.deliverables.iter().find(|d| d.id == req.deliverable_id); + + if deliverable.is_none() { + let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect(); + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "INVALID_DELIVERABLE", + format!( + "Invalid deliverable_id '{}' for {} phase (contract type: {}). Valid IDs: {:?}", + req.deliverable_id, target_phase, contract.contract_type, valid_ids + ), + )), + ) + .into_response(); + } + + // Check if already completed + if contract.is_deliverable_complete(&target_phase, &req.deliverable_id) { + return Json(serde_json::json!({ + "success": true, + "message": format!("Deliverable '{}' is already marked complete for {} phase", req.deliverable_id, target_phase), + "deliverableId": req.deliverable_id, + "phase": target_phase, + "alreadyComplete": true, + })) + .into_response(); + } + + // Mark the deliverable as complete + match repository::mark_deliverable_complete(pool, id, &target_phase, &req.deliverable_id).await { + Ok(updated_contract) => { + let completed = updated_contract.get_completed_deliverables(&target_phase); + Json(serde_json::json!({ + "success": true, + "message": format!("Marked deliverable '{}' as complete for {} phase", req.deliverable_id, target_phase), + "deliverableId": req.deliverable_id, + "phase": target_phase, + "completedDeliverables": completed, + })) + .into_response() + } + Err(e) => { + tracing::error!("Failed to mark deliverable complete for contract {}: {}", id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +// ============================================================================= // Events // ============================================================================= diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index f5a3c10..270118f 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -23,7 +23,7 @@ use uuid::Uuid; use crate::db::models::Task; use crate::db::repository; -use crate::llm::{check_deliverables_met, FileInfo, TaskInfo}; +use crate::llm::{check_deliverables_met, TaskInfo}; use crate::server::auth::{hash_api_key, API_KEY_HEADER}; use crate::server::messages::ApiError; use crate::server::state::{ @@ -494,12 +494,6 @@ async fn compute_action_directive( _ => return None, }; - // Get files - let files = match repository::list_files_in_contract(pool, contract_id, owner_id).await { - Ok(f) => f, - _ => return None, - }; - // Get tasks (non-supervisor only) let tasks = match repository::list_tasks_by_contract(pool, contract_id, owner_id).await { Ok(t) => t.into_iter().filter(|t| !t.is_supervisor).collect::<Vec<_>>(), @@ -512,20 +506,12 @@ async fn compute_action_directive( _ => return None, }; - // Convert to FileInfo and TaskInfo for check_deliverables_met - let file_infos: Vec<FileInfo> = files - .iter() - .map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }) - .collect(); + // Get completed deliverables for the current phase + let completed_deliverables = contract.get_completed_deliverables(&contract.phase); let task_infos: Vec<TaskInfo> = tasks .iter() .map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }) @@ -533,29 +519,29 @@ async fn compute_action_directive( let has_repository = !repos.is_empty(); - // Check if any task has a PR URL set - let pr_url = tasks.iter().find_map(|t| t.pr_url.as_deref()); - - // Check deliverables - let check = check_deliverables_met( + // Check deliverables (unused, but kept for future reference) + let _check = check_deliverables_met( &contract.phase, &contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - pr_url, ); - // Only generate directive if deliverables are met and we're in execute phase - if check.deliverables_met && contract.phase == "execute" { - // All tasks done, need to create PR - if pr_url.is_none() || pr_url.unwrap_or("").is_empty() { - let done_count = task_infos.iter().filter(|t| t.status == "done").count(); + // Generate directive based on deliverable status + if contract.phase == "execute" { + // Check if all tasks are done but PR deliverable is not marked complete + let all_tasks_done = !task_infos.is_empty() + && task_infos.iter().all(|t| t.status == "done"); + let pr_deliverable_complete = completed_deliverables.contains(&"pull-request".to_string()); + + if all_tasks_done && !pr_deliverable_complete { + let done_count = task_infos.len(); return Some(format!( "[ACTION REQUIRED] All {} task(s) completed successfully.\n\ - You MUST now create a PR:\n\ - 1. Ensure all changes are merged to your makima branch\n\ - 2. Create PR: `makima supervisor pr \"makima/...\" --title \"...\" --base main`", + You MUST now create a PR and mark the 'pull-request' deliverable as complete:\n\ + 1. Ensure all changes are merged to your branch\n\ + 2. Create PR and then call mark_deliverable_complete with deliverable_id='pull-request'", done_count )); } diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs index 6d95e86..c73007e 100644 --- a/makima/src/server/handlers/templates.rs +++ b/makima/src/server/handlers/templates.rs @@ -1,112 +1,12 @@ -//! Templates API handler. +//! Contract types API handler. -use axum::{extract::Query, http::StatusCode, response::IntoResponse, Json}; -use serde::{Deserialize, Serialize}; +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde::Serialize; use utoipa::ToSchema; use crate::llm::templates; use crate::llm::templates::ContractTypeTemplate; -/// Query parameters for listing templates -#[derive(Debug, Deserialize, ToSchema)] -pub struct ListTemplatesQuery { - /// Filter by contract phase (research, specify, plan, execute, review) - pub phase: Option<String>, -} - -/// Template summary for API response -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct TemplateSummary { - /// Template identifier - pub id: String, - /// Display name - pub name: String, - /// Contract phase this template is designed for - pub phase: String, - /// Brief description - pub description: String, - /// Number of body elements in the template - pub element_count: usize, -} - -/// Response for listing templates -#[derive(Debug, Serialize, ToSchema)] -pub struct ListTemplatesResponse { - pub templates: Vec<TemplateSummary>, -} - -/// List available file templates -#[utoipa::path( - get, - path = "/api/v1/templates", - params( - ("phase" = Option<String>, Query, description = "Filter by contract phase") - ), - responses( - (status = 200, description = "Templates retrieved successfully", body = ListTemplatesResponse) - ), - tag = "templates" -)] -pub async fn list_templates( - Query(query): Query<ListTemplatesQuery>, -) -> impl IntoResponse { - let template_list = match query.phase.as_deref() { - Some(phase) => templates::templates_for_phase(phase), - None => templates::all_templates(), - }; - - let summaries: Vec<TemplateSummary> = template_list - .iter() - .map(|t| TemplateSummary { - id: t.id.clone(), - name: t.name.clone(), - phase: t.phase.clone(), - description: t.description.clone(), - element_count: t.suggested_body.len(), - }) - .collect(); - - ( - StatusCode::OK, - Json(ListTemplatesResponse { - templates: summaries, - }), - ) - .into_response() -} - -/// Get a specific template by ID -#[utoipa::path( - get, - path = "/api/v1/templates/{id}", - params( - ("id" = String, Path, description = "Template ID") - ), - responses( - (status = 200, description = "Template retrieved successfully", body = templates::FileTemplate), - (status = 404, description = "Template not found") - ), - tag = "templates" -)] -pub async fn get_template( - axum::extract::Path(id): axum::extract::Path<String>, -) -> impl IntoResponse { - let all = templates::all_templates(); - let template = all.into_iter().find(|t| t.id == id); - - match template { - Some(t) => (StatusCode::OK, Json(serde_json::json!(t))).into_response(), - None => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "error": format!("Template '{}' not found", id) - })), - ) - .into_response(), - } -} - // ============================================================================= // Contract Type Templates (Workflow Definitions) // ============================================================================= diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 75f64c6..bf302a5 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -159,6 +159,7 @@ pub fn make_router(state: SharedState) -> Router { .delete(contracts::delete_contract), ) .route("/contracts/{id}/phase", post(contracts::change_phase)) + .route("/contracts/{id}/deliverables/complete", post(contracts::mark_deliverable_complete)) .route("/contracts/{id}/events", get(contracts::get_events)) .route("/contracts/{id}/chat", post(contract_chat::contract_chat_handler)) .route( @@ -205,9 +206,6 @@ pub fn make_router(state: SharedState) -> Router { ) // Timeline endpoint (unified history for user) .route("/timeline", get(history::get_timeline)) - // Template endpoints - .route("/templates", get(templates::list_templates)) - .route("/templates/{id}", get(templates::get_template)) // Contract type templates (workflow definitions) .route("/contract-types", get(templates::list_contract_types)) // Settings endpoints |
