diff options
| author | soryu <soryu@soryu.co> | 2026-01-19 17:40:25 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-19 17:40:25 +0000 |
| commit | 164941cbd591b46f69a034bb9b86521fd7700ddb (patch) | |
| tree | a11f4dc7196f6e00c7d52da1cfc6aa982cce60aa | |
| parent | 0833fb1f30c0c3b920157deb882e0e902c3af02a (diff) | |
| download | soryu-164941cbd591b46f69a034bb9b86521fd7700ddb.tar.gz soryu-164941cbd591b46f69a034bb9b86521fd7700ddb.zip | |
Remove 'task' type contract
| -rw-r--r-- | makima/frontend/src/components/contracts/AutopilotPanel.tsx | 5 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 47 | ||||
| -rw-r--r-- | makima/frontend/src/routes/contracts.tsx | 22 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 37 | ||||
| -rw-r--r-- | makima/src/llm/phase_guidance.rs | 13 | ||||
| -rw-r--r-- | makima/src/llm/task_output.rs | 4 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 25 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 132 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 172 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/openapi.rs | 7 |
11 files changed, 105 insertions, 361 deletions
diff --git a/makima/frontend/src/components/contracts/AutopilotPanel.tsx b/makima/frontend/src/components/contracts/AutopilotPanel.tsx index cf42e44..1a13773 100644 --- a/makima/frontend/src/components/contracts/AutopilotPanel.tsx +++ b/makima/frontend/src/components/contracts/AutopilotPanel.tsx @@ -129,11 +129,6 @@ export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) { } }, [contract.id, contract.version, onUpdate]); - // Don't show panel for task-type contracts (they don't have supervisors) - if (contract.contractType === "task") { - return null; - } - return ( <div className="space-y-3"> <div className="flex items-center justify-between"> diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index daa2b5c..78e52cd 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -1417,7 +1417,7 @@ export async function deleteAccount( // ============================================================================= /** Contract type determines the workflow and required documents */ -export type ContractType = "simple" | "specification" | "task"; +export type ContractType = "simple" | "specification"; export type ContractPhase = "research" | "specify" | "plan" | "execute" | "review"; export type ContractStatus = "active" | "completed" | "archived"; export type RepositorySourceType = "remote" | "local" | "managed"; @@ -1428,16 +1428,12 @@ export function getValidPhases(contractType: ContractType): ContractPhase[] { if (contractType === "simple") { return ["plan", "execute"]; } - if (contractType === "task") { - return ["execute"]; - } return ["research", "specify", "plan", "execute", "review"]; } /** Get default initial phase for a contract type */ export function getDefaultPhase(contractType: ContractType): ContractPhase { if (contractType === "simple") return "plan"; - if (contractType === "task") return "execute"; return "research"; } @@ -2081,47 +2077,6 @@ export async function deleteRepositoryHistory(id: string): Promise<void> { } // ============================================================================= -// Adhoc Task Types (for one-off tasks without supervisor overhead) -// ============================================================================= - -/** Request payload for creating an adhoc (one-off) task */ -export interface AdhocTaskRequest { - /** Name/description of the task */ - name: string; - /** The plan/instructions for the task */ - plan: string; - /** Repository URL (optional) */ - repositoryUrl?: string; - /** Base branch to work from */ - baseBranch?: string; -} - -/** Response for adhoc task creation */ -export interface AdhocTaskResponse { - contract: ContractSummary; - task: Task; -} - -/** - * Create an adhoc (one-off) task without supervisor overhead. - * This creates a minimal "task" type contract with a single task. - * The contract auto-archives when the task completes. - */ -export async function createAdhocTask( - data: AdhocTaskRequest -): Promise<AdhocTaskResponse> { - const res = await authFetch(`${API_BASE}/api/v1/tasks/adhoc`, { - method: "POST", - body: JSON.stringify(data), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Failed to create adhoc task: ${errorText || res.statusText}`); - } - return res.json(); -} - -// ============================================================================= // History Types // ============================================================================= diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index cd385f9..d2b6b1b 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -474,20 +474,6 @@ function ContractsPageContent() { <button type="button" onClick={() => { - setContractType("task"); - setInitialPhase("execute"); - }} - className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ - contractType === "task" - ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" - : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" - }`} - > - Task - </button> - <button - type="button" - onClick={() => { setContractType("simple"); setInitialPhase("plan"); }} @@ -515,9 +501,7 @@ function ContractsPageContent() { </button> </div> <p className="mt-1 font-mono text-xs text-[#8b949e]"> - {contractType === "task" - ? "Execute: One-off adhoc task with no supervisor (auto-archives on completion)" - : contractType === "simple" + {contractType === "simple" ? "Plan → Execute: Simple workflow with a plan document" : "Research → Specify → Plan → Execute → Review: Full specification-driven development with TDD"} </p> @@ -540,9 +524,7 @@ function ContractsPageContent() { ))} </select> <p className="mt-1 font-mono text-xs text-[#8b949e]"> - {contractType === "task" - ? "Task contracts always start in Execute phase" - : contractType === "simple" + {contractType === "simple" ? "Start in Plan to define what to build, or Execute if already planned" : "Skip earlier phases if you already have requirements defined"} </p> diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 65f7168..291fad7 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1049,9 +1049,6 @@ pub struct MergeCompleteCheckResponse { // Contract Types // ============================================================================= -/// Contract type constant for task (adhoc) contracts -pub const CONTRACT_TYPE_TASK: &str = "task"; - /// Contract type determines the workflow and required documents #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] @@ -1067,11 +1064,6 @@ pub enum ContractType { /// - Execute: implement according to specs /// - Review: verify against specifications Specification, - /// Task type for adhoc one-off tasks - /// - Single "execute" phase - /// - No supervisor created - /// - Auto-archives on completion - Task, } impl Default for ContractType { @@ -1085,7 +1077,6 @@ impl std::fmt::Display for ContractType { match self { ContractType::Simple => write!(f, "simple"), ContractType::Specification => write!(f, "specification"), - ContractType::Task => write!(f, "task"), } } } @@ -1097,7 +1088,6 @@ impl std::str::FromStr for ContractType { match s.to_lowercase().as_str() { "simple" => Ok(ContractType::Simple), "specification" => Ok(ContractType::Specification), - "task" => Ok(ContractType::Task), _ => Err(format!("Unknown contract type: {}", s)), } } @@ -1923,30 +1913,3 @@ pub struct ForkPoint { pub checkpoint: Option<TaskCheckpoint>, pub timestamp: DateTime<Utc>, } - -// ============================================================================= -// Adhoc Task Types (for one-off tasks without supervisor overhead) -// ============================================================================= - -/// Request payload for creating an adhoc (one-off) task. -/// Creates a minimal "task" type contract with a single task, no supervisor. -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct AdhocTaskRequest { - /// Name/description of the task - pub name: String, - /// The plan/instructions for the task - pub plan: String, - /// Repository URL (optional) - pub repository_url: Option<String>, - /// Base branch to work from - pub base_branch: Option<String>, -} - -/// Response for adhoc task creation -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct AdhocTaskResponse { - pub contract: ContractSummary, - pub task: Task, -} diff --git a/makima/src/llm/phase_guidance.rs b/makima/src/llm/phase_guidance.rs index e2d6cd8..0d4bb3d 100644 --- a/makima/src/llm/phase_guidance.rs +++ b/makima/src/llm/phase_guidance.rs @@ -373,6 +373,19 @@ pub fn get_phase_checklist( 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()); } } diff --git a/makima/src/llm/task_output.rs b/makima/src/llm/task_output.rs index c71c05a..c5d709e 100644 --- a/makima/src/llm/task_output.rs +++ b/makima/src/llm/task_output.rs @@ -83,6 +83,10 @@ pub enum SuggestedAction { task_id: Uuid, task_name: String, }, + /// Mark the contract as completed + MarkContractComplete { + contract_id: Uuid, + }, } /// Analysis of a completed task's output diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index 7e7b476..0f794c1 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -2795,10 +2795,20 @@ fn analyze_phase_readiness(contract: &crate::db::models::ContractWithRelations) } let ready = total_tasks > 0 && done_tasks == total_tasks; + + // For simple contracts, execute is the terminal phase - suggest completion + if ready && contract.contract.contract_type == "simple" { + suggestions.push("Mark the contract as completed".to_string()); + } + PhaseReadinessAnalysis { ready, summary: if ready { - "All tasks completed. Ready for Review phase.".to_string() + if contract.contract.contract_type == "simple" { + "All tasks completed. Contract can be marked as completed.".to_string() + } else { + "All tasks completed. Ready for Review phase.".to_string() + } } else if total_tasks == 0 { "No tasks created yet. Create and complete tasks before reviewing.".to_string() } else { @@ -2815,12 +2825,19 @@ fn analyze_phase_readiness(contract: &crate::db::models::ContractWithRelations) if review_files == 0 { suggestions.push("Create review checklist or release notes".to_string()); + } else { + // Review documentation exists - suggest completion + suggestions.push("Mark the contract as completed".to_string()); } PhaseReadinessAnalysis { - ready: false, - summary: "Review is the final phase. Contract can be marked as complete when review is done.".to_string(), - reasons: vec!["Review phase is the final phase".to_string()], + ready: review_files > 0, + summary: if review_files > 0 { + "Review documentation complete. Contract can be marked as completed.".to_string() + } else { + "Review phase needs documentation before completion.".to_string() + }, + reasons: vec!["Review is the final phase".to_string()], suggestions, } } diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 4524860..11337f2 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -259,85 +259,75 @@ pub async fn create_contract( match repository::create_contract_for_owner(pool, auth.owner_id, req.clone()).await { Ok(contract) => { - // Only create supervisor task for non-task type contracts - // Task type contracts are lightweight adhoc tasks that don't need supervisors - if contract.contract_type != crate::db::models::CONTRACT_TYPE_TASK { - // Create supervisor task for this contract - let supervisor_name = format!("{} Supervisor", contract.name); - let supervisor_plan = format!( - "You are the supervisor for contract '{}'. Your goal is to drive this contract to completion.\n\n{}", - contract.name, - contract.description.as_deref().unwrap_or("No description provided.") - ); - - // Get repository info from contract if available - let repo_url = { - // Try to get the first repository associated with this contract - match repository::list_contract_repositories(pool, contract.id).await { - Ok(repos) if !repos.is_empty() => { - let repo = &repos[0]; - repo.repository_url.clone() - } - _ => None, - } - }; - - let supervisor_req = crate::db::models::CreateTaskRequest { - name: supervisor_name, - description: None, - plan: supervisor_plan, - repository_url: repo_url, - base_branch: None, - target_branch: None, - parent_task_id: None, - contract_id: contract.id, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - is_supervisor: true, - checkpoint_sha: None, - priority: 0, - merge_mode: None, - }; - - match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await { - Ok(supervisor_task) => { - tracing::info!( - contract_id = %contract.id, - supervisor_task_id = %supervisor_task.id, - is_supervisor = supervisor_task.is_supervisor, - "Created supervisor task for contract" - ); + // Create supervisor task for this contract + let supervisor_name = format!("{} Supervisor", contract.name); + let supervisor_plan = format!( + "You are the supervisor for contract '{}'. Your goal is to drive this contract to completion.\n\n{}", + contract.name, + contract.description.as_deref().unwrap_or("No description provided.") + ); - // Update contract with supervisor_task_id - let update_req = crate::db::models::UpdateContractRequest { - supervisor_task_id: Some(supervisor_task.id), - version: Some(contract.version), - ..Default::default() - }; - if let Err(e) = repository::update_contract_for_owner(pool, contract.id, auth.owner_id, update_req).await { - tracing::warn!( - contract_id = %contract.id, - error = %e, - "Failed to link supervisor task to contract" - ); - } + // Get repository info from contract if available + let repo_url = { + // Try to get the first repository associated with this contract + match repository::list_contract_repositories(pool, contract.id).await { + Ok(repos) if !repos.is_empty() => { + let repo = &repos[0]; + repo.repository_url.clone() } - Err(e) => { + _ => None, + } + }; + + let supervisor_req = crate::db::models::CreateTaskRequest { + name: supervisor_name, + description: None, + plan: supervisor_plan, + repository_url: repo_url, + base_branch: None, + target_branch: None, + parent_task_id: None, + contract_id: contract.id, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + is_supervisor: true, + checkpoint_sha: None, + priority: 0, + merge_mode: None, + }; + + match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await { + Ok(supervisor_task) => { + tracing::info!( + contract_id = %contract.id, + supervisor_task_id = %supervisor_task.id, + is_supervisor = supervisor_task.is_supervisor, + "Created supervisor task for contract" + ); + + // Update contract with supervisor_task_id + let update_req = crate::db::models::UpdateContractRequest { + supervisor_task_id: Some(supervisor_task.id), + version: Some(contract.version), + ..Default::default() + }; + if let Err(e) = repository::update_contract_for_owner(pool, contract.id, auth.owner_id, update_req).await { tracing::warn!( contract_id = %contract.id, error = %e, - "Failed to create supervisor task for contract" + "Failed to link supervisor task to contract" ); } } - } else { - tracing::info!( - contract_id = %contract.id, - contract_type = %contract.contract_type, - "Skipping supervisor creation for task-type contract" - ); + Err(e) => { + tracing::warn!( + contract_id = %contract.id, + error = %e, + "Failed to create supervisor task for contract" + ); + } } // Record history event for contract creation diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index ad3ec79..275dc3c 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -9,10 +9,9 @@ use axum::{ use uuid::Uuid; use crate::db::models::{ - AdhocTaskRequest, AdhocTaskResponse, ContractSummary, CreateContractRequest, CreateTaskRequest, DaemonDirectory, DaemonDirectoriesResponse, DaemonListResponse, SendMessageRequest, Task, TaskEventListResponse, TaskListResponse, TaskOutputEntry, - TaskOutputResponse, TaskWithSubtasks, UpdateTaskRequest, CONTRACT_TYPE_TASK, + TaskOutputResponse, TaskWithSubtasks, UpdateTaskRequest, }; use crate::db::repository::{self, RepositoryError}; use crate::server::auth::Authenticated; @@ -375,40 +374,6 @@ pub async fn update_task( } }); } - - // Auto-archive task-type contracts when task reaches terminal status - let terminal_statuses = ["done", "failed"]; - if terminal_statuses.contains(&task.status.as_str()) { - let pool = pool.clone(); - let owner_id = auth.owner_id; - let task_id = task.id; - tokio::spawn(async move { - if let Ok(Some(contract)) = repository::get_contract_for_owner(&pool, contract_id, owner_id).await { - if contract.contract_type == CONTRACT_TYPE_TASK { - // Archive the contract - let update_req = crate::db::models::UpdateContractRequest { - status: Some("archived".to_string()), - version: Some(contract.version), - ..Default::default() - }; - if let Err(e) = repository::update_contract_for_owner(&pool, contract_id, owner_id, update_req).await { - tracing::warn!( - contract_id = %contract_id, - task_id = %task_id, - error = %e, - "Failed to auto-archive task-type contract" - ); - } else { - tracing::info!( - contract_id = %contract_id, - task_id = %task_id, - "Auto-archived task-type contract on task completion" - ); - } - } - } - }); - } } Json(task).into_response() @@ -3339,138 +3304,3 @@ pub async fn branch_from_checkpoint( ) .into_response() } - -// ============================================================================= -// Adhoc Task Endpoint -// ============================================================================= - -/// Create an adhoc (one-off) task without supervisor overhead. -/// -/// This creates a minimal "task" type contract with a single task. -/// The contract auto-archives when the task completes. -#[utoipa::path( - post, - path = "/api/v1/tasks/adhoc", - request_body = AdhocTaskRequest, - responses( - (status = 201, description = "Adhoc task created", body = AdhocTaskResponse), - (status = 400, description = "Invalid request", body = ApiError), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Mesh" -)] -pub async fn create_adhoc_task( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Json(req): Json<AdhocTaskRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Generate a short unique name for the contract - let contract_name = format!("task-{}", &Uuid::new_v4().to_string()[..8]); - - // 1. Create a minimal "task" type contract - let contract_req = CreateContractRequest { - name: contract_name, - description: Some(req.name.clone()), - contract_type: Some(CONTRACT_TYPE_TASK.to_string()), - initial_phase: Some("execute".to_string()), // Skip planning - autonomous_loop: Some(false), - phase_guard: None, - }; - - let contract = match repository::create_contract_for_owner(pool, auth.owner_id, contract_req).await { - Ok(c) => c, - Err(e) => { - tracing::error!("Failed to create adhoc task contract: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response(); - } - }; - - tracing::info!( - contract_id = %contract.id, - contract_type = %contract.contract_type, - "Created task-type contract for adhoc task" - ); - - // 2. Create the actual task (no supervisor) - let task_req = CreateTaskRequest { - contract_id: contract.id, - name: req.name, - description: None, - plan: req.plan, - is_supervisor: false, - parent_task_id: None, - priority: 0, - repository_url: req.repository_url, - base_branch: req.base_branch, - target_branch: None, - merge_mode: None, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - }; - - let task = match repository::create_task_for_owner(pool, auth.owner_id, task_req).await { - Ok(t) => t, - Err(e) => { - tracing::error!("Failed to create adhoc task: {}", e); - // Clean up the contract we just created - let _ = repository::delete_contract_for_owner(pool, contract.id, auth.owner_id).await; - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response(); - } - }; - - tracing::info!( - task_id = %task.id, - contract_id = %contract.id, - "Created adhoc task" - ); - - // Build the contract summary for the response - let contract_summary = ContractSummary { - id: contract.id, - name: contract.name, - description: contract.description, - contract_type: contract.contract_type, - phase: contract.phase, - status: contract.status, - supervisor_task_id: contract.supervisor_task_id, - file_count: 0, - task_count: 1, - repository_count: 0, - version: contract.version, - created_at: contract.created_at, - }; - - ( - StatusCode::CREATED, - Json(AdhocTaskResponse { - contract: contract_summary, - task, - }), - ) - .into_response() -} diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index a4cb3d1..7e31285 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -63,8 +63,6 @@ pub fn make_router(state: SharedState) -> Router { .route("/files/{id}/versions/{version}", get(versions::get_version)) .route("/files/{id}/versions/restore", post(versions::restore_version)) // Mesh/task orchestration endpoints - // Adhoc task endpoint (creates task-type contract + task in one call) - .route("/tasks/adhoc", post(mesh::create_adhoc_task)) .route( "/mesh/tasks", get(mesh::list_tasks).post(mesh::create_task), diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs index 47e3456..4daae3b 100644 --- a/makima/src/server/openapi.rs +++ b/makima/src/server/openapi.rs @@ -3,8 +3,8 @@ use utoipa::OpenApi; use crate::db::models::{ - AddLocalRepositoryRequest, AddRemoteRepositoryRequest, AdhocTaskRequest, AdhocTaskResponse, - BranchInfo, BranchListResponse, ChangePhaseRequest, Contract, ContractChatHistoryResponse, + AddLocalRepositoryRequest, AddRemoteRepositoryRequest, BranchInfo, BranchListResponse, + ChangePhaseRequest, Contract, ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent, ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations, CreateContractRequest, CreateFileRequest, CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse, @@ -43,7 +43,6 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage mesh::list_tasks, mesh::get_task, mesh::create_task, - mesh::create_adhoc_task, mesh::update_task, mesh::delete_task, mesh::list_subtasks, @@ -124,8 +123,6 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage CreateTaskRequest, UpdateTaskRequest, SendMessageRequest, - AdhocTaskRequest, - AdhocTaskResponse, TaskEventListResponse, Daemon, DaemonListResponse, |
