summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-24 20:06:28 +0000
committersoryu <soryu@soryu.co>2026-01-24 20:06:28 +0000
commit6364363d1418728351f252b799d397b756e1f985 (patch)
tree9b5227f141bfc587b487265b3687a11f6f504be3 /makima/src/server/handlers
parent792d12df6b1b1bc4f327cbe8e71e7986c67e98f6 (diff)
downloadsoryu-6364363d1418728351f252b799d397b756e1f985.tar.gz
soryu-6364363d1418728351f252b799d397b756e1f985.zip
feat: Simplify contract deliverables and add Templates UI
## Backend Changes ### Phase Deliverables Simplified - **Simple contract type**: - Plan phase: Only 'Plan' deliverable (required) - Execute phase: Only 'PR' deliverable (required) - **Specification contract type**: - Research phase: Only 'Research Notes' deliverable (required) - Specify phase: Only 'Requirements Document' deliverable (required) - Plan phase: Only 'Plan' deliverable (required) - Execute phase: Only 'PR' deliverable (required) - Review phase: Only 'Release Notes' deliverable (required) ### New 'execute' Contract Type - Only has 'execute' phase (no plan or review phases) - NO deliverables at all - executes tasks directly - Added to ContractType enum with proper Display/FromStr implementations - Added helper methods: `initial_phase()`, `terminal_phase()` ### API Updates - Added `get_phase_deliverables_for_type()` for contract-type-aware deliverables - Added `get_phase_checklist_for_type()` for contract-type-aware checklists - Added `check_phase_completion_for_type()` for contract-type-aware completion checks - Added `check_deliverables_met()` function for deliverable validation - Added `should_auto_progress()` for autonomous contract progression - Added new ContractToolRequest::CheckDeliverablesMet tool ## Frontend Changes (makima/frontend) ### Templates Page - Add TemplateEditor component for editing phase deliverables - Create Templates page with template card grid layout - Add navigation link in NavStrip - Implement three built-in templates: Simple, Specification, Execute - Support for creating custom templates with configurable phases/deliverables - Templates are persisted to localStorage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/server/handlers')
-rw-r--r--makima/src/server/handlers/contract_chat.rs168
-rw-r--r--makima/src/server/handlers/contract_daemon.rs6
2 files changed, 164 insertions, 10 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index e2adb72..28c3436 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -20,7 +20,7 @@ use crate::db::{
};
use crate::llm::{
all_templates, analyze_task_output, body_to_markdown, format_checklist_markdown,
- format_parsed_tasks, get_phase_checklist, parse_tasks_from_breakdown,
+ 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,
@@ -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 {
@@ -455,12 +455,31 @@ fn build_contract_context(contract: &crate::db::models::ContractWithRelations) -
}).collect();
let has_repository = !contract.repositories.is_empty();
- let phase_checklist = get_phase_checklist(&c.phase, &file_infos, &task_infos, has_repository);
+ let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &file_infos, &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,
+ &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,65 @@ async fn handle_contract_request(
};
}
+ // 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"
+ })),
+ };
+ }
+
// Check if phase_guard is enabled
if contract.phase_guard {
// If user provided feedback, return it for the task to address
@@ -1816,8 +1894,8 @@ async fn handle_contract_request(
// Update phase (either phase_guard is disabled, or user confirmed)
match repository::change_contract_phase_for_owner(pool, contract_id, owner_id, &new_phase).await {
Ok(Some(updated)) => {
- // Get deliverables for the new phase
- let deliverables = crate::llm::get_phase_deliverables(&new_phase);
+ // Get deliverables for the new phase (using contract type)
+ let 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
@@ -1963,7 +2041,7 @@ async fn handle_contract_request(
}).collect();
let has_repository = !cwr.repositories.is_empty();
- let checklist = get_phase_checklist(&cwr.contract.phase, &file_infos, &task_infos, has_repository);
+ let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &file_infos, &task_infos, has_repository, &cwr.contract.contract_type);
ContractRequestResult {
success: true,
@@ -1993,6 +2071,82 @@ 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 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,
+ },
+ }
+ }
+
// =============================================================================
// Task Derivation Handlers
// =============================================================================
diff --git a/makima/src/server/handlers/contract_daemon.rs b/makima/src/server/handlers/contract_daemon.rs
index 13c5640..5b23831 100644
--- a/makima/src/server/handlers/contract_daemon.rs
+++ b/makima/src/server/handlers/contract_daemon.rs
@@ -280,7 +280,7 @@ pub async fn get_contract_checklist(
Err(_) => false,
};
- let checklist = phase_guidance::get_phase_checklist(&contract.phase, &files, &tasks, has_repository);
+ let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type);
Json(checklist).into_response()
}
@@ -319,7 +319,7 @@ pub async fn get_contract_goals(
match repository::get_contract_for_owner(pool, id, auth.owner_id).await {
Ok(Some(contract)) => {
- let deliverables = phase_guidance::get_phase_deliverables(&contract.phase);
+ let deliverables = phase_guidance::get_phase_deliverables_for_type(&contract.phase, &contract.contract_type);
Json(ContractGoalsResponse {
description: contract.description,
phase: contract.phase,
@@ -491,7 +491,7 @@ pub async fn get_suggest_action(
.map(|r| !r.is_empty())
.unwrap_or(false);
- let checklist = phase_guidance::get_phase_checklist(&contract.phase, &files, &tasks, has_repository);
+ let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type);
// Determine suggested action based on checklist
let (action, description) = if !checklist.suggestions.is_empty() {