summaryrefslogtreecommitdiff
path: root/makima/src
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src')
-rw-r--r--makima/src/llm/mod.rs4
-rw-r--r--makima/src/llm/phase_guidance.rs296
-rw-r--r--makima/src/server/handlers/contract_chat.rs164
3 files changed, 461 insertions, 3 deletions
diff --git a/makima/src/llm/mod.rs b/makima/src/llm/mod.rs
index 6167a42..fc3802b 100644
--- a/makima/src/llm/mod.rs
+++ b/makima/src/llm/mod.rs
@@ -19,8 +19,10 @@ 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_phase_completion, check_phase_completion_for_type, format_checklist_markdown,
+ 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,
};
diff --git a/makima/src/llm/phase_guidance.rs b/makima/src/llm/phase_guidance.rs
index df7bd24..03f7c76 100644
--- a/makima/src/llm/phase_guidance.rs
+++ b/makima/src/llm/phase_guidance.rs
@@ -579,6 +579,302 @@ pub fn check_phase_completion_for_type(
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 {
+ /// Whether all required deliverables are met
+ pub deliverables_met: bool,
+ /// Whether the phase is ready to advance (includes all readiness checks)
+ pub ready_to_advance: bool,
+ /// Current phase
+ pub phase: String,
+ /// Next phase (if available)
+ pub next_phase: Option<String>,
+ /// List of required deliverables and their status
+ pub required_deliverables: Vec<DeliverableItem>,
+ /// List of what's missing (if any)
+ pub missing: Vec<String>,
+ /// Human-readable summary
+ pub summary: String,
+ /// Whether auto-progress is recommended
+ pub auto_progress_recommended: bool,
+}
+
+/// A single deliverable item status
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+pub struct DeliverableItem {
+ /// Name of the deliverable
+ pub name: String,
+ /// Type: "file", "repository", "pr", "tasks"
+ pub deliverable_type: String,
+ /// Whether it's met
+ pub met: bool,
+ /// Additional details
+ pub details: Option<String>,
+}
+
+/// 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],
+ tasks: &[TaskInfo],
+ has_repository: bool,
+ pr_url: Option<&str>,
+) -> DeliverableCheckResult {
+ let mut required_deliverables = 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())
+ } else {
+ None
+ },
+ });
+
+ if !matched {
+ missing.push(format!("Create {} (required)", rec.name_suggestion));
+ }
+ }
+ }
+
+ // Check repository for phases that require it
+ if deliverables.requires_repository {
+ required_deliverables.push(DeliverableItem {
+ name: "Repository".to_string(),
+ deliverable_type: "repository".to_string(),
+ met: has_repository,
+ details: if has_repository {
+ Some("Repository configured".to_string())
+ } else {
+ None
+ },
+ });
+
+ if !has_repository {
+ missing.push("Configure a repository".to_string());
+ }
+ }
+
+ // Check tasks for execute phase
+ if deliverables.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 {
+ name: "Tasks Completed".to_string(),
+ deliverable_type: "tasks".to_string(),
+ met: tasks_complete,
+ details: Some(format!("{}/{} tasks done", done_tasks, total_tasks)),
+ });
+
+ if !tasks_complete {
+ if total_tasks == 0 {
+ missing.push("Create and complete tasks".to_string());
+ } else {
+ 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 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)
+ } else {
+ format!("All deliverables met for {} phase. This is the final phase.", phase)
+ }
+ } else {
+ format!("{} deliverable(s) still needed for {} phase.", missing.len(), phase)
+ };
+
+ DeliverableCheckResult {
+ deliverables_met,
+ ready_to_advance,
+ phase: phase.to_string(),
+ next_phase,
+ required_deliverables,
+ missing,
+ summary,
+ auto_progress_recommended: deliverables_met && ready_to_advance,
+ }
+}
+
+/// Get the next phase based on contract type
+pub fn get_next_phase_for_contract(contract_type: &str, current_phase: &str) -> Option<String> {
+ match contract_type {
+ "simple" => match current_phase {
+ "plan" => Some("execute".to_string()),
+ "execute" => None, // Terminal phase for simple contracts
+ _ => None,
+ },
+ "execute" => None, // Execute-only contracts don't have phase transitions
+ "specification" | _ => match current_phase {
+ "research" => Some("specify".to_string()),
+ "specify" => Some("plan".to_string()),
+ "plan" => Some("execute".to_string()),
+ "execute" => Some("review".to_string()),
+ "review" => None, // Final phase
+ _ => None,
+ },
+ }
+}
+
+/// 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],
+ 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);
+
+ if !check.deliverables_met {
+ return AutoProgressDecision {
+ should_progress: false,
+ next_phase: None,
+ reason: format!("Deliverables not met: {}", check.missing.join(", ")),
+ action: AutoProgressAction::WaitForDeliverables,
+ };
+ }
+
+ if check.next_phase.is_none() {
+ return AutoProgressDecision {
+ should_progress: false,
+ next_phase: None,
+ reason: "This is the terminal phase. Contract can be completed.".to_string(),
+ action: AutoProgressAction::CompleteContract,
+ };
+ }
+
+ if autonomous_loop {
+ AutoProgressDecision {
+ should_progress: true,
+ next_phase: check.next_phase,
+ reason: "All deliverables met and autonomous_loop is enabled.".to_string(),
+ action: AutoProgressAction::AdvancePhase,
+ }
+ } else {
+ AutoProgressDecision {
+ should_progress: false,
+ next_phase: check.next_phase,
+ reason: "All deliverables met. Suggest advancing to next phase.".to_string(),
+ action: AutoProgressAction::SuggestAdvance,
+ }
+ }
+}
+
+/// Result of auto-progress decision
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AutoProgressDecision {
+ /// Whether to automatically progress
+ pub should_progress: bool,
+ /// The next phase to progress to
+ pub next_phase: Option<String>,
+ /// Reason for the decision
+ pub reason: String,
+ /// Recommended action
+ pub action: AutoProgressAction,
+}
+
+/// Actions that can be taken based on auto-progress decision
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum AutoProgressAction {
+ /// Wait for required deliverables
+ WaitForDeliverables,
+ /// Automatically advance to next phase
+ AdvancePhase,
+ /// Suggest user to advance (when not autonomous)
+ SuggestAdvance,
+ /// Contract is complete, mark as done
+ CompleteContract,
+}
+
+/// Generate enhanced prompt guidance for deliverable checking
+pub fn generate_deliverable_prompt_guidance(
+ phase: &str,
+ contract_type: &str,
+ check_result: &DeliverableCheckResult,
+) -> String {
+ 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));
+
+ // 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));
+ }
+
+ // Show status and next actions
+ guidance.push_str("\n### Status\n");
+ if check_result.deliverables_met {
+ guidance.push_str("**All deliverables are met.**\n\n");
+ if let Some(ref next) = check_result.next_phase {
+ guidance.push_str(&format!("Ready to advance to **{}** phase.\n", next));
+ if check_result.auto_progress_recommended {
+ 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");
+ }
+ } else {
+ guidance.push_str("**Deliverables not yet met.**\n\n");
+ guidance.push_str("Missing:\n");
+ 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
+}
+
/// 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));
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 0b6bad1..a0708da 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -433,8 +433,8 @@ When a new contract is created or the user seems unsure:
fn build_contract_context(contract: &crate::db::models::ContractWithRelations) -> String {
let c = &contract.contract;
let mut context = format!(
- "Name: {}\nID: {}\nPhase: {}\nStatus: {}\n",
- c.name, c.id, c.phase, c.status
+ "Name: {}\nID: {}\nPhase: {}\nStatus: {}\nContract Type: {}\nAutonomous Loop: {}\n",
+ c.name, c.id, c.phase, c.status, c.contract_type, c.autonomous_loop
);
if let Some(ref desc) = c.description {
@@ -461,6 +461,25 @@ fn build_contract_context(contract: &crate::db::models::ContractWithRelations) -
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,
+ &task_infos,
+ has_repository,
+ None, // pr_url not available in TaskSummary
+ );
+
+ // Add deliverable prompt guidance
+ context.push_str(&crate::llm::generate_deliverable_prompt_guidance(
+ &c.phase,
+ &c.contract_type,
+ &deliverable_check,
+ ));
+
// Files summary
context.push_str(&format!("\n### Files ({} total)\n", contract.files.len()));
if !contract.files.is_empty() {
@@ -1732,6 +1751,68 @@ async fn handle_contract_request(
};
}
+<<<<<<< HEAD
+=======
+ // Check if deliverables are met before allowing transition
+ let cwr = match get_contract_with_relations(pool, contract_id, owner_id).await {
+ Ok(Some(c)) => c,
+ Ok(None) | Err(_) => {
+ // Fall through - we'll just skip the deliverables check
+ return ContractRequestResult {
+ success: false,
+ message: "Failed to load contract for deliverables check".to_string(),
+ data: None,
+ };
+ }
+ };
+
+ 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 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,
+ &task_infos,
+ has_repository,
+ None, // pr_url not available in TaskSummary
+ );
+
+ // Block transition if deliverables are not met
+ if !check_result.deliverables_met {
+ return ContractRequestResult {
+ success: false,
+ message: format!(
+ "Cannot advance to '{}' phase: deliverables not met. {}",
+ new_phase, check_result.summary
+ ),
+ data: Some(json!({
+ "status": "deliverables_not_met",
+ "currentPhase": current_phase,
+ "requestedPhase": new_phase,
+ "deliverablesMet": false,
+ "requiredDeliverables": check_result.required_deliverables,
+ "missing": check_result.missing,
+ "action": "Complete the missing deliverables before advancing to the next phase"
+ })),
+ };
+ }
+
+>>>>>>> c6507b4 (feat: Add deliverables checking and auto-progress for contract phases)
// Check if phase_guard is enabled
if contract.phase_guard {
// If user provided feedback, return it for the task to address
@@ -1993,6 +2074,85 @@ async fn handle_contract_request(
}
}
+<<<<<<< HEAD
+=======
+ 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 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,
+ &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,
+ &task_infos,
+ has_repository,
+ None, // pr_url not available in TaskSummary
+ cwr.contract.autonomous_loop,
+ );
+
+ ContractRequestResult {
+ success: true,
+ message: check_result.summary.clone(),
+ data: Some(json!({
+ "deliverablesMet": check_result.deliverables_met,
+ "readyToAdvance": check_result.ready_to_advance,
+ "phase": check_result.phase,
+ "nextPhase": check_result.next_phase,
+ "requiredDeliverables": check_result.required_deliverables,
+ "missing": check_result.missing,
+ "summary": check_result.summary,
+ "autoProgressRecommended": check_result.auto_progress_recommended,
+ "autoProgress": {
+ "shouldProgress": auto_progress.should_progress,
+ "nextPhase": auto_progress.next_phase,
+ "reason": auto_progress.reason,
+ "action": format!("{:?}", auto_progress.action),
+ },
+ "autonomousLoop": cwr.contract.autonomous_loop,
+ })),
+ }
+ }
+ Ok(None) => ContractRequestResult {
+ success: false,
+ message: "Contract not found".to_string(),
+ data: None,
+ },
+ Err(e) => ContractRequestResult {
+ success: false,
+ message: format!("Database error: {}", e),
+ data: None,
+ },
+ }
+ }
+
+>>>>>>> c6507b4 (feat: Add deliverables checking and auto-progress for contract phases)
// =============================================================================
// Task Derivation Handlers
// =============================================================================