summaryrefslogtreecommitdiff
path: root/makima/src/db
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-04 01:07:14 +0000
committersoryu <soryu@soryu.co>2026-02-04 01:07:14 +0000
commita734bf1a472b19d63341769d26a66628575df7f4 (patch)
treeec78f57e5721d157c620df0c99de5b5efe485231 /makima/src/db
parentc732dd048128808cd9f67f6e1176a5b565df5678 (diff)
downloadsoryu-a734bf1a472b19d63341769d26a66628575df7f4.tar.gz
soryu-a734bf1a472b19d63341769d26a66628575df7f4.zip
Add chain checkpoint contracts
Diffstat (limited to 'makima/src/db')
-rw-r--r--makima/src/db/models.rs67
-rw-r--r--makima/src/db/repository.rs432
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)
+}