diff options
| author | soryu <soryu@soryu.co> | 2026-02-04 01:07:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-04 01:07:14 +0000 |
| commit | a734bf1a472b19d63341769d26a66628575df7f4 (patch) | |
| tree | ec78f57e5721d157c620df0c99de5b5efe485231 /makima/src/db | |
| parent | c732dd048128808cd9f67f6e1176a5b565df5678 (diff) | |
| download | soryu-a734bf1a472b19d63341769d26a66628575df7f4.tar.gz soryu-a734bf1a472b19d63341769d26a66628575df7f4.zip | |
Add chain checkpoint contracts
Diffstat (limited to 'makima/src/db')
| -rw-r--r-- | makima/src/db/models.rs | 67 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 432 |
2 files changed, 494 insertions, 5 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index eeb30e4..0b8ef43 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2848,6 +2848,56 @@ pub struct CreateChainDeliverableRequest { pub priority: Option<String>, } +/// Validation configuration for checkpoint contracts. +/// Checkpoint contracts validate the outputs of their upstream dependencies +/// before allowing downstream contracts to proceed. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CheckpointValidation { + /// Check that all required deliverables from upstream contracts exist + #[serde(default)] + pub check_deliverables: bool, + + /// Run tests in the repository (requires repository to be configured) + #[serde(default)] + pub run_tests: bool, + + /// Custom validation instructions for Claude to execute. + /// Claude will review the outputs of upstream contracts and verify they meet these criteria. + pub check_content: Option<String>, + + /// Action to take on validation failure: "block" (default), "retry", "warn" + /// - block: Fail the checkpoint and block downstream contracts + /// - retry: Mark upstream contracts for retry (up to max_retries) + /// - warn: Log warning but allow downstream to proceed + #[serde(default = "default_checkpoint_on_failure")] + pub on_failure: String, + + /// Maximum retry attempts for upstream contracts (when on_failure = "retry") + #[serde(default = "default_checkpoint_max_retries")] + pub max_retries: i32, +} + +fn default_checkpoint_on_failure() -> String { + "block".to_string() +} + +fn default_checkpoint_max_retries() -> i32 { + 3 +} + +impl Default for CheckpointValidation { + fn default() -> Self { + Self { + check_deliverables: false, + run_tests: false, + check_content: None, + on_failure: default_checkpoint_on_failure(), + max_retries: default_checkpoint_max_retries(), + } + } +} + /// Request to update an existing chain #[derive(Debug, Clone, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] @@ -2962,6 +3012,8 @@ pub struct ChainContractDefinition { pub tasks: Option<serde_json::Value>, /// Deliverable definitions as JSON: [{id, name, priority}, ...] pub deliverables: Option<serde_json::Value>, + /// Validation configuration for checkpoint contracts (JSON) + pub validation: Option<serde_json::Value>, /// Position in GUI editor pub editor_x: Option<f64>, pub editor_y: Option<f64>, @@ -2984,6 +3036,8 @@ pub struct AddContractDefinitionRequest { pub tasks: Option<Vec<CreateChainTaskRequest>>, /// Deliverable definitions pub deliverables: Option<Vec<CreateChainDeliverableRequest>>, + /// Validation configuration (for checkpoint contracts) + pub validation: Option<CheckpointValidation>, /// Position in GUI editor pub editor_x: Option<f64>, pub editor_y: Option<f64>, @@ -3004,10 +3058,23 @@ pub struct UpdateContractDefinitionRequest { pub depends_on: Option<Vec<String>>, pub tasks: Option<Vec<CreateChainTaskRequest>>, pub deliverables: Option<Vec<CreateChainDeliverableRequest>>, + /// Validation configuration (for checkpoint contracts) + pub validation: Option<CheckpointValidation>, pub editor_x: Option<f64>, pub editor_y: Option<f64>, } +/// Request to start a chain +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct StartChainRequest { + /// Whether to create a supervisor task that monitors chain progress + #[serde(default)] + pub with_supervisor: bool, + /// Repository URL for the supervisor task to work with + pub repository_url: Option<String>, +} + /// Response when starting a chain #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 85af178..fb430ab 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -5448,19 +5448,23 @@ pub async fn create_chain_contract_definition( let order_index = max_order.unwrap_or(-1) + 1; - // Convert tasks and deliverables to JSON + // Convert tasks, deliverables, and validation to JSON let tasks_json = req.tasks.as_ref().map(|t| serde_json::to_value(t).unwrap()); let deliverables_json = req .deliverables .as_ref() .map(|d| serde_json::to_value(d).unwrap()); + let validation_json = req + .validation + .as_ref() + .map(|v| serde_json::to_value(v).unwrap()); let depends_on_names: Vec<String> = req.depends_on.unwrap_or_default(); sqlx::query_as::<_, ChainContractDefinition>( r#" INSERT INTO chain_contract_definitions - (chain_id, name, description, contract_type, initial_phase, depends_on_names, tasks, deliverables, editor_x, editor_y, order_index) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + (chain_id, name, description, contract_type, initial_phase, depends_on_names, tasks, deliverables, validation, editor_x, editor_y, order_index) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING * "#, ) @@ -5472,6 +5476,7 @@ pub async fn create_chain_contract_definition( .bind(&depends_on_names) .bind(&tasks_json) .bind(&deliverables_json) + .bind(&validation_json) .bind(req.editor_x) .bind(req.editor_y) .bind(order_index) @@ -5520,6 +5525,10 @@ pub async fn update_chain_contract_definition( .deliverables .as_ref() .map(|d| serde_json::to_value(d).unwrap()); + let validation_json = req + .validation + .as_ref() + .map(|v| serde_json::to_value(v).unwrap()); sqlx::query_as::<_, ChainContractDefinition>( r#" @@ -5531,8 +5540,9 @@ pub async fn update_chain_contract_definition( depends_on_names = COALESCE($6, depends_on_names), tasks = COALESCE($7, tasks), deliverables = COALESCE($8, deliverables), - editor_x = COALESCE($9, editor_x), - editor_y = COALESCE($10, editor_y) + validation = COALESCE($9, validation), + editor_x = COALESCE($10, editor_x), + editor_y = COALESCE($11, editor_y) WHERE id = $1 RETURNING * "#, @@ -5545,6 +5555,7 @@ pub async fn update_chain_contract_definition( .bind(&req.depends_on) .bind(&tasks_json) .bind(&deliverables_json) + .bind(&validation_json) .bind(req.editor_x) .bind(req.editor_y) .fetch_one(pool) @@ -5705,3 +5716,414 @@ pub async fn update_chain_status( .await?; Ok(()) } + +// ============================================================================= +// Chain Progression +// ============================================================================= + +/// Result of chain progression check +#[derive(Debug)] +pub struct ChainProgressionResult { + /// Contracts created from ready definitions + pub contracts_created: Vec<Uuid>, + /// Whether all definitions are instantiated and completed (chain is done) + pub chain_completed: bool, +} + +/// Progress a chain by creating contracts from ready definitions. +/// +/// This is called when a contract in the chain completes. It: +/// 1. Finds definitions whose dependencies are all satisfied (completed) +/// 2. Creates contracts from those definitions +/// 3. Links them to the chain +/// 4. Checks if chain is complete (all definitions instantiated and completed) +pub async fn progress_chain( + pool: &PgPool, + chain_id: Uuid, + owner_id: Uuid, +) -> Result<ChainProgressionResult, sqlx::Error> { + let mut contracts_created = Vec::new(); + + // Get all definitions for this chain + let definitions = list_chain_contract_definitions(pool, chain_id).await?; + if definitions.is_empty() { + return Ok(ChainProgressionResult { + contracts_created: vec![], + chain_completed: true, + }); + } + + // Get existing chain contracts to know what's already instantiated + let chain_contracts = list_chain_contracts(pool, chain_id).await?; + + // Build a map of definition name -> instantiated contract status + let instantiated: std::collections::HashMap<String, Option<String>> = chain_contracts + .iter() + .map(|cc| (cc.contract_name.clone(), Some(cc.contract_status.clone()))) + .collect(); + + // Find definitions that are ready to be instantiated: + // - Not yet instantiated + // - All dependencies are instantiated AND completed + for def in &definitions { + // Skip if already instantiated + if instantiated.contains_key(&def.name) { + continue; + } + + // Check if all dependencies are completed + let deps_satisfied = def.depends_on_names.iter().all(|dep_name| { + instantiated + .get(dep_name) + .map(|status| status.as_deref() == Some("completed")) + .unwrap_or(false) + }); + + // Root definitions (no dependencies) are always ready + let is_root = def.depends_on_names.is_empty(); + + if is_root || deps_satisfied { + // Create contract from definition + match create_contract_from_definition(pool, chain_id, owner_id, def).await { + Ok(contract_id) => { + contracts_created.push(contract_id); + tracing::info!( + chain_id = %chain_id, + definition_name = %def.name, + contract_id = %contract_id, + "Created contract from chain definition" + ); + } + Err(e) => { + tracing::error!( + chain_id = %chain_id, + definition_name = %def.name, + error = %e, + "Failed to create contract from chain definition" + ); + } + } + } + } + + // Check if chain is complete (all definitions instantiated and completed) + let updated_contracts = list_chain_contracts(pool, chain_id).await?; + let all_instantiated = definitions.len() == updated_contracts.len(); + let all_completed = updated_contracts + .iter() + .all(|cc| cc.contract_status == "completed"); + let chain_completed = all_instantiated && all_completed; + + if chain_completed { + update_chain_status(pool, chain_id, "completed").await?; + tracing::info!(chain_id = %chain_id, "Chain completed - all contracts done"); + } + + Ok(ChainProgressionResult { + contracts_created, + chain_completed, + }) +} + +/// Task definition parsed from JSON (matches chain YAML format) +#[derive(Debug, Clone, serde::Deserialize)] +struct ChainTaskDef { + name: String, + plan: String, +} + +/// Validation config parsed from definition JSON +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct ValidationConfig { + #[serde(default)] + check_deliverables: bool, + #[serde(default)] + run_tests: bool, + check_content: Option<String>, + #[serde(default = "default_on_failure_str")] + on_failure: String, + #[serde(default = "default_max_retries_val")] + max_retries: i32, +} + +fn default_on_failure_str() -> String { + "block".to_string() +} + +fn default_max_retries_val() -> i32 { + 3 +} + +/// Generate a validation plan for a checkpoint contract. +fn generate_checkpoint_plan( + def: &ChainContractDefinition, + upstream_contracts: &[&ChainContractDetail], + validation: &ValidationConfig, +) -> String { + let upstream_names: Vec<&str> = upstream_contracts.iter().map(|c| c.contract_name.as_str()).collect(); + + let mut plan = format!( + r#"# Checkpoint Validation: {} + +You are validating the outputs of upstream contracts before allowing downstream work to proceed. + +## Upstream Contracts to Validate +{} + +"#, + def.name, + upstream_names.iter().map(|n| format!("- {}", n)).collect::<Vec<_>>().join("\n") + ); + + // Add deliverables check section + if validation.check_deliverables { + plan.push_str(r#"## Deliverables Check +Verify that all required deliverables from upstream contracts exist and are properly completed. + +Use the makima CLI to check contract status: +```bash +makima contract status <contract_id> +``` + +For each upstream contract, verify: +1. Contract status is "completed" +2. All required deliverables are marked as complete +3. Deliverable content exists and is not empty + +"#); + } + + // Add tests check section + if validation.run_tests { + plan.push_str(r#"## Tests Check +Run the test suite to verify the codebase is in a good state. + +```bash +# Run tests appropriate for the project type +npm test # for Node.js projects +cargo test # for Rust projects +pytest # for Python projects +go test ./... # for Go projects +``` + +Verify: +1. All tests pass +2. No new test failures introduced +3. Test coverage is acceptable + +"#); + } + + // Add custom content check section + if let Some(content_check) = &validation.check_content { + plan.push_str(&format!(r#"## Custom Validation Criteria +{} + +"#, content_check)); + } + + // Add validation result section + plan.push_str(&format!(r#"## Reporting Results + +After completing all validation checks, you must report the result: + +**If ALL checks pass:** +Mark this checkpoint contract as completed using: +```bash +makima supervisor complete +``` + +**If ANY check fails (on_failure: "{}"):** +"#, validation.on_failure)); + + match validation.on_failure.as_str() { + "block" => plan.push_str(r#" +- Document the failure reason clearly +- Do NOT mark the contract as complete +- The chain will be blocked until issues are resolved manually +"#), + "retry" => plan.push_str(&format!(r#" +- Document the failure reason +- Request retry of the failed upstream contract (max {} retries) +- Use: `makima supervisor ask "Upstream validation failed. Retry?" --choices "Yes,No"` +"#, validation.max_retries)), + "warn" => plan.push_str(r#" +- Document the warning/issue found +- Mark the contract as complete anyway (downstream will proceed) +- Log the warning for visibility +"#), + _ => plan.push_str(r#" +- Document the failure reason +- Do NOT mark the contract as complete +"#), + } + + plan.push_str(r#" +## Begin Validation + +Start by checking the status of each upstream contract, then proceed with the validation criteria above. +"#); + + plan +} + +/// Create a contract from a chain definition. +async fn create_contract_from_definition( + pool: &PgPool, + chain_id: Uuid, + owner_id: Uuid, + def: &ChainContractDefinition, +) -> Result<Uuid, sqlx::Error> { + // Get the existing contracts to find dependency info + let existing_contracts = list_chain_contracts(pool, chain_id).await?; + let name_to_contract: std::collections::HashMap<&str, &ChainContractDetail> = existing_contracts + .iter() + .map(|cc| (cc.contract_name.as_str(), cc)) + .collect(); + + // Resolve dependency names to contract details + let upstream_contracts: Vec<&ChainContractDetail> = def + .depends_on_names + .iter() + .filter_map(|name| name_to_contract.get(name.as_str()).copied()) + .collect(); + + // Create the contract request with basic fields + let req = CreateContractRequest { + name: def.name.clone(), + description: def.description.clone(), + contract_type: Some(def.contract_type.clone()), + initial_phase: def.initial_phase.clone(), + template_id: None, + autonomous_loop: None, + phase_guard: None, + local_only: None, + auto_merge_local: None, + }; + + // Create the contract + let contract = create_contract_for_owner(pool, owner_id, req).await?; + + // For checkpoint contracts, generate a validation plan + if def.contract_type == "checkpoint" { + // Parse validation config + let validation: ValidationConfig = def + .validation + .as_ref() + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or(ValidationConfig { + check_deliverables: true, + run_tests: false, + check_content: None, + on_failure: default_on_failure_str(), + max_retries: default_max_retries_val(), + }); + + // Generate validation plan + let validation_plan = generate_checkpoint_plan(def, &upstream_contracts, &validation); + + // Create a supervisor task with the validation plan + let task_req = CreateTaskRequest { + contract_id: Some(contract.id), + name: format!("Validate: {}", def.name), + description: Some("Checkpoint validation task".to_string()), + plan: validation_plan, + parent_task_id: None, + is_supervisor: true, // Checkpoint uses supervisor task for validation + priority: 0, + repository_url: None, + base_branch: None, + target_branch: None, + merge_mode: None, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + checkpoint_sha: None, + branched_from_task_id: None, + conversation_history: None, + supervisor_worktree_task_id: None, + }; + + if let Err(e) = create_task_for_owner(pool, owner_id, task_req).await { + tracing::warn!( + contract_id = %contract.id, + error = %e, + "Failed to create validation task for checkpoint contract" + ); + } + + // Set initial validation status + sqlx::query( + "UPDATE chain_contracts SET validation_status = 'pending' WHERE chain_id = $1 AND contract_id = $2", + ) + .bind(chain_id) + .bind(contract.id) + .execute(pool) + .await?; + } else { + // Parse and create tasks from definition for regular contracts + if let Some(tasks_json) = &def.tasks { + if let Ok(tasks) = serde_json::from_value::<Vec<ChainTaskDef>>(tasks_json.clone()) { + for task_def in tasks { + let task_req = CreateTaskRequest { + contract_id: Some(contract.id), + name: task_def.name, + description: None, + plan: task_def.plan, + parent_task_id: None, + is_supervisor: false, + priority: 0, + repository_url: None, + base_branch: None, + target_branch: None, + merge_mode: None, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + checkpoint_sha: None, + branched_from_task_id: None, + conversation_history: None, + supervisor_worktree_task_id: None, + }; + if let Err(e) = create_task_for_owner(pool, owner_id, task_req).await { + tracing::warn!( + contract_id = %contract.id, + error = %e, + "Failed to create task from chain definition" + ); + } + } + } + } + } + + // Resolve dependency names to contract IDs + let depends_on: Vec<Uuid> = upstream_contracts.iter().map(|c| c.contract_id).collect(); + + // Link contract to chain + add_contract_to_chain( + pool, + chain_id, + contract.id, + depends_on, + def.order_index, + def.editor_x, + def.editor_y, + ) + .await?; + + // Update chain_contracts with definition_id link + sqlx::query( + "UPDATE chain_contracts SET definition_id = $1 WHERE chain_id = $2 AND contract_id = $3", + ) + .bind(def.id) + .bind(chain_id) + .bind(contract.id) + .execute(pool) + .await?; + + Ok(contract.id) +} |
