summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 04:39:37 +0000
committersoryu <soryu@soryu.co>2026-01-25 04:39:37 +0000
commitcb4f2fc40dbabb40de948512eee74c7e46264665 (patch)
tree0a4f142609dceaf6cf6eafa4fa6a59beff4b1f80
parentc908854e7e3571c99cce9f46497ce5337ea0aed1 (diff)
downloadsoryu-cb4f2fc40dbabb40de948512eee74c7e46264665.tar.gz
soryu-cb4f2fc40dbabb40de948512eee74c7e46264665.zip
Add automatic phase transitions and fix PR creation
-rw-r--r--makima/src/daemon/task/manager.rs124
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs201
-rw-r--r--makima/src/server/handlers/templates.rs70
-rw-r--r--makima/src/server/state.rs10
4 files changed, 304 insertions, 101 deletions
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index 6ba0f52..74a37bf 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -840,6 +840,26 @@ Valid phases: `specify`, `plan`, `execute`, `review`
- Create checkpoints before major transitions
- **Mark contract complete** when PR is created by updating status
+## MANDATORY ACTIONS
+
+### [ACTION REQUIRED] Messages
+When you receive an `[ACTION REQUIRED]` message from the system:
+1. Read the directive carefully
+2. Execute the specified command IMMEDIATELY
+3. Do NOT ignore or defer these actions
+4. These messages indicate conditions have been met and action is needed NOW
+
+### Phase Completion Checklist
+After all tasks are "done" and merged, you MUST take the following actions:
+
+**If in execute phase:**
+1. Create PR immediately: `makima supervisor pr "makima/{name}" --title "..." --base main`
+2. After PR created:
+ - Simple contract: Mark complete with `makima supervisor complete`
+ - Specification contract: Advance to review with `makima supervisor advance-phase review`
+
+**Never leave a contract hanging** - when work is done, create the PR and complete/advance.
+
---
"#;
@@ -2924,58 +2944,100 @@ impl TaskManager {
body: Option<String>,
base_branch: String,
) -> Result<(), DaemonError> {
- // Get task's worktree path
- let worktree_path = {
+ // Get task's worktree path and base branch
+ let (worktree_path, task_base_branch) = {
let tasks = self.tasks.read().await;
- tasks.get(&task_id)
+ let worktree = tasks.get(&task_id)
.and_then(|t| t.worktree.as_ref())
- .map(|w| w.path.clone())
+ .map(|w| w.path.clone());
+ let base = tasks.get(&task_id)
+ .and_then(|t| t.base_branch.clone());
+ (worktree, base)
};
+ // Use task's base_branch if the provided one is the default "main" and task has a detected one
+ let effective_base_branch = if base_branch == "main" {
+ task_base_branch.unwrap_or(base_branch)
+ } else {
+ base_branch
+ };
+
+ tracing::info!(
+ task_id = %task_id,
+ effective_base_branch = %effective_base_branch,
+ worktree_exists = worktree_path.is_some(),
+ "Creating PR with effective base branch"
+ );
+
let (success, message, pr_url, pr_number) = if let Some(path) = worktree_path {
// Push the current branch first
+ tracing::info!(path = %path.display(), "Pushing branch to origin");
let push_result = tokio::process::Command::new("git")
.current_dir(&path)
.args(["push", "-u", "origin", "HEAD"])
.output()
.await;
- if let Err(e) = push_result {
- (false, format!("Failed to push branch: {}", e), None, None)
- } else {
- // Create PR using gh CLI
- let mut pr_cmd = tokio::process::Command::new("gh");
- pr_cmd.current_dir(&path);
- pr_cmd.args(["pr", "create", "--title", &title, "--base", &base_branch]);
-
- if let Some(ref body_text) = body {
- pr_cmd.args(["--body", body_text]);
- } else {
- pr_cmd.args(["--body", ""]);
+ match push_result {
+ Err(e) => {
+ tracing::error!(error = %e, "Failed to execute git push");
+ (false, format!("Failed to push branch: {}", e), None, None)
}
-
- match pr_cmd.output().await {
- Ok(output) if output.status.success() => {
- let stdout = String::from_utf8_lossy(&output.stdout);
- // gh pr create outputs the PR URL
- let url = stdout.lines().last().map(|s| s.trim().to_string());
- // Extract PR number from URL
- let number = url.as_ref().and_then(|u| {
- u.split('/').last().and_then(|n| n.parse::<i32>().ok())
- });
- (true, "Pull request created".to_string(), url, number)
+ Ok(output) if !output.status.success() => {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ tracing::error!(stderr = %stderr, "git push failed");
+ (false, format!("Failed to push branch: {}", stderr), None, None)
+ }
+ Ok(_) => {
+ tracing::info!("Branch pushed successfully, creating PR");
+ // Create PR using gh CLI
+ let mut pr_cmd = tokio::process::Command::new("gh");
+ pr_cmd.current_dir(&path);
+ pr_cmd.args(["pr", "create", "--title", &title, "--base", &effective_base_branch]);
+
+ if let Some(ref body_text) = body {
+ pr_cmd.args(["--body", body_text]);
+ } else {
+ pr_cmd.args(["--body", ""]);
}
- Ok(output) => {
- let stderr = String::from_utf8_lossy(&output.stderr);
- (false, format!("Failed to create PR: {}", stderr), None, None)
+
+ match pr_cmd.output().await {
+ Ok(output) if output.status.success() => {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ // gh pr create outputs the PR URL
+ let url = stdout.lines().last().map(|s| s.trim().to_string());
+ // Extract PR number from URL
+ let number = url.as_ref().and_then(|u| {
+ u.split('/').last().and_then(|n| n.parse::<i32>().ok())
+ });
+ tracing::info!(pr_url = ?url, pr_number = ?number, "PR created successfully");
+ (true, "Pull request created".to_string(), url, number)
+ }
+ Ok(output) => {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ tracing::error!(stderr = %stderr, "gh pr create failed");
+ (false, format!("Failed to create PR: {}", stderr), None, None)
+ }
+ Err(e) => {
+ tracing::error!(error = %e, "Failed to execute gh command");
+ (false, format!("Failed to run gh: {}", e), None, None)
+ }
}
- Err(e) => (false, format!("Failed to run gh: {}", e), None, None),
}
}
} else {
+ tracing::error!(task_id = %task_id, "Task not found or has no worktree");
(false, format!("Task {} not found or has no worktree", task_id), None, None)
};
+ tracing::info!(
+ task_id = %task_id,
+ success = success,
+ message = %message,
+ pr_url = ?pr_url,
+ "PR creation completed"
+ );
+
let msg = DaemonMessage::PRCreated {
task_id,
success,
diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs
index 53ee806..f5a3c10 100644
--- a/makima/src/server/handlers/mesh_daemon.rs
+++ b/makima/src/server/handlers/mesh_daemon.rs
@@ -23,6 +23,7 @@ use uuid::Uuid;
use crate::db::models::Task;
use crate::db::repository;
+use crate::llm::{check_deliverables_met, FileInfo, TaskInfo};
use crate::server::auth::{hash_api_key, API_KEY_HEADER};
use crate::server::messages::ApiError;
use crate::server::state::{
@@ -447,6 +448,28 @@ pub enum DaemonMessage {
/// Error message if operation failed
error: Option<String>,
},
+ /// Response to MergeTaskToTarget command
+ MergeToTargetResult {
+ #[serde(rename = "taskId")]
+ task_id: Uuid,
+ success: bool,
+ message: String,
+ #[serde(rename = "commitSha")]
+ commit_sha: Option<String>,
+ conflicts: Option<Vec<String>>,
+ },
+ /// Response to CreatePR command
+ #[serde(rename = "prCreated")]
+ PRCreated {
+ #[serde(rename = "taskId")]
+ task_id: Uuid,
+ success: bool,
+ message: String,
+ #[serde(rename = "prUrl")]
+ pr_url: Option<String>,
+ #[serde(rename = "prNumber")]
+ pr_number: Option<i32>,
+ },
}
/// Validated daemon authentication result.
@@ -458,6 +481,89 @@ struct DaemonAuthResult {
owner_id: Uuid,
}
+/// Compute an action directive for the supervisor based on deliverable status.
+/// Returns an [ACTION REQUIRED] message if all deliverables are met.
+async fn compute_action_directive(
+ pool: &sqlx::PgPool,
+ contract_id: Uuid,
+ owner_id: Uuid,
+) -> Option<String> {
+ // Get contract
+ let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
+ Ok(Some(c)) => c,
+ _ => return None,
+ };
+
+ // Get files
+ let files = match repository::list_files_in_contract(pool, contract_id, owner_id).await {
+ Ok(f) => f,
+ _ => return None,
+ };
+
+ // Get tasks (non-supervisor only)
+ let tasks = match repository::list_tasks_by_contract(pool, contract_id, owner_id).await {
+ Ok(t) => t.into_iter().filter(|t| !t.is_supervisor).collect::<Vec<_>>(),
+ _ => return None,
+ };
+
+ // Get repositories
+ let repos = match repository::list_contract_repositories(pool, contract_id).await {
+ Ok(r) => r,
+ _ => return None,
+ };
+
+ // Convert to FileInfo and TaskInfo for check_deliverables_met
+ let file_infos: Vec<FileInfo> = files
+ .iter()
+ .map(|f| FileInfo {
+ id: f.id,
+ name: f.name.clone(),
+ contract_phase: f.contract_phase.clone(),
+ })
+ .collect();
+
+ let task_infos: Vec<TaskInfo> = tasks
+ .iter()
+ .map(|t| TaskInfo {
+ id: t.id,
+ name: t.name.clone(),
+ status: t.status.clone(),
+ })
+ .collect();
+
+ let has_repository = !repos.is_empty();
+
+ // Check if any task has a PR URL set
+ let pr_url = tasks.iter().find_map(|t| t.pr_url.as_deref());
+
+ // Check deliverables
+ let check = check_deliverables_met(
+ &contract.phase,
+ &contract.contract_type,
+ &file_infos,
+ &task_infos,
+ has_repository,
+ pr_url,
+ );
+
+ // Only generate directive if deliverables are met and we're in execute phase
+ if check.deliverables_met && contract.phase == "execute" {
+ // All tasks done, need to create PR
+ if pr_url.is_none() || pr_url.unwrap_or("").is_empty() {
+ let done_count = task_infos.iter().filter(|t| t.status == "done").count();
+ return Some(format!(
+ "[ACTION REQUIRED] All {} task(s) completed successfully.\n\
+ You MUST now create a PR:\n\
+ 1. Ensure all changes are merged to your makima branch\n\
+ 2. Create PR: `makima supervisor pr \"makima/...\" --title \"...\" --base main`",
+ done_count
+ ));
+ }
+ }
+
+ None
+}
+
/// Validate an API key and return (user_id, owner_id).
async fn validate_daemon_api_key(pool: &sqlx::PgPool, key: &str) -> Result<DaemonAuthResult, String> {
let key_hash = hash_api_key(key);
@@ -946,6 +1052,13 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
// Don't notify for supervisor tasks (they don't report to themselves)
if !updated_task.is_supervisor {
if let Ok(Some(supervisor)) = repository::get_contract_supervisor_task(&pool, contract_id).await {
+ // Compute action directive if task completed successfully
+ let action_directive = if updated_task.status == "done" {
+ compute_action_directive(&pool, contract_id, owner_id).await
+ } else {
+ None
+ };
+
state.notify_supervisor_of_task_completion(
supervisor.id,
supervisor.daemon_id,
@@ -954,6 +1067,7 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
&updated_task.status,
updated_task.progress_summary.as_deref(),
updated_task.error_message.as_deref(),
+ action_directive.as_deref(),
).await;
}
}
@@ -1561,6 +1675,93 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
});
}
}
+ Ok(DaemonMessage::MergeToTargetResult {
+ task_id,
+ success,
+ message,
+ commit_sha: _,
+ conflicts: _,
+ }) => {
+ tracing::info!(
+ task_id = %task_id,
+ success = success,
+ "Merge to target result received"
+ );
+
+ // On successful merge, notify supervisor to check if all merges complete
+ if success {
+ if let Some(pool) = state.db_pool.as_ref() {
+ if let Ok(Some(task)) = repository::get_task(pool, task_id).await {
+ if let Some(contract_id) = task.contract_id {
+ if let Ok(Some(supervisor)) = repository::get_contract_supervisor_task(pool, contract_id).await {
+ let prompt = format!(
+ "[INFO] Merge completed: {}\n\
+ Check if all tasks are merged with `makima supervisor tasks`.\n\
+ If ready, create PR with `makima supervisor pr`.",
+ message
+ );
+ let _ = state.notify_supervisor(
+ supervisor.id,
+ supervisor.daemon_id,
+ &prompt,
+ ).await;
+ }
+ }
+ }
+ }
+ }
+ }
+ Ok(DaemonMessage::PRCreated {
+ task_id,
+ success,
+ message,
+ pr_url,
+ pr_number: _,
+ }) => {
+ tracing::info!(
+ task_id = %task_id,
+ success = success,
+ pr_url = ?pr_url,
+ "PR created result received"
+ );
+
+ // On successful PR creation, notify supervisor of next steps
+ if success {
+ if let Some(pool) = state.db_pool.as_ref() {
+ if let Ok(Some(task)) = repository::get_task(pool, task_id).await {
+ if let Some(contract_id) = task.contract_id {
+ // Get contract to determine next action
+ if let Ok(Some(contract)) = repository::get_contract_for_owner(pool, contract_id, task.owner_id).await {
+ if let Ok(Some(supervisor)) = repository::get_contract_supervisor_task(pool, contract_id).await {
+ let next_action = match (contract.contract_type.as_str(), contract.phase.as_str()) {
+ ("simple", "execute") => {
+ "Mark contract complete with `makima supervisor complete`".to_string()
+ }
+ ("specification", "execute") => {
+ "Advance to review phase with `makima supervisor advance-phase review`".to_string()
+ }
+ _ => "Check contract status with `makima supervisor status`".to_string()
+ };
+
+ let prompt = format!(
+ "[ACTION REQUIRED] PR created successfully!\n\
+ PR: {}\n\n\
+ Next step: {}",
+ pr_url.as_deref().unwrap_or(&message),
+ next_action
+ );
+ let _ = state.notify_supervisor(
+ supervisor.id,
+ supervisor.daemon_id,
+ &prompt,
+ ).await;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
Ok(DaemonMessage::GitConfigInherited {
success,
user_email,
diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs
index 5cad44f..6d95e86 100644
--- a/makima/src/server/handlers/templates.rs
+++ b/makima/src/server/handlers/templates.rs
@@ -7,76 +7,6 @@ use utoipa::ToSchema;
use crate::llm::templates;
use crate::llm::templates::ContractTypeTemplate;
-// =============================================================================
-// Contract Type Templates
-// =============================================================================
-
-/// Contract type template for API response
-#[derive(Debug, Serialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ContractTypeTemplate {
- /// Template identifier (e.g., "simple", "specification")
- pub id: String,
- /// Display name
- pub name: String,
- /// Description of the contract type workflow
- pub description: String,
- /// Ordered list of phases for this contract type
- pub phases: Vec<String>,
- /// Default starting phase
- pub default_phase: String,
- /// Whether this is a built-in type (always available)
- pub is_builtin: bool,
-}
-
-/// Response for listing contract types
-#[derive(Debug, Serialize, ToSchema)]
-pub struct ListContractTypesResponse {
- pub types: Vec<ContractTypeTemplate>,
-}
-
-/// List available contract type templates
-#[utoipa::path(
- get,
- path = "/api/v1/contract-types",
- responses(
- (status = 200, description = "Contract types retrieved successfully", body = ListContractTypesResponse)
- ),
- tag = "contract-types"
-)]
-pub async fn list_contract_types() -> impl IntoResponse {
- let types = vec![
- ContractTypeTemplate {
- id: "simple".to_string(),
- name: "Simple".to_string(),
- description: "Plan \u{2192} Execute: Simple workflow with a plan document".to_string(),
- phases: vec!["plan".to_string(), "execute".to_string()],
- default_phase: "plan".to_string(),
- is_builtin: true,
- },
- ContractTypeTemplate {
- id: "specification".to_string(),
- name: "Specification".to_string(),
- description: "Research \u{2192} Specify \u{2192} Plan \u{2192} Execute \u{2192} Review: Full specification-driven development with TDD".to_string(),
- phases: vec![
- "research".to_string(),
- "specify".to_string(),
- "plan".to_string(),
- "execute".to_string(),
- "review".to_string(),
- ],
- default_phase: "research".to_string(),
- is_builtin: true,
- },
- ];
-
- (
- StatusCode::OK,
- Json(ListContractTypesResponse { types }),
- )
- .into_response()
-}
-
/// Query parameters for listing templates
#[derive(Debug, Deserialize, ToSchema)]
pub struct ListTemplatesQuery {
diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs
index 32c0af3..b954efe 100644
--- a/makima/src/server/state.rs
+++ b/makima/src/server/state.rs
@@ -1065,6 +1065,9 @@ impl AppState {
}
/// Format and send a task completion notification to a supervisor.
+ ///
+ /// If `action_directive` is provided, it will be appended to the message
+ /// as an [ACTION REQUIRED] block to prompt the supervisor to take action.
pub async fn notify_supervisor_of_task_completion(
&self,
supervisor_task_id: Uuid,
@@ -1074,6 +1077,7 @@ impl AppState {
status: &str,
progress_summary: Option<&str>,
error_message: Option<&str>,
+ action_directive: Option<&str>,
) {
let mut message = format!(
"TASK_COMPLETED task_id={} name=\"{}\" status={}",
@@ -1091,6 +1095,12 @@ impl AppState {
message.push_str(&format!(" error=\"{}\"", escaped));
}
+ // Append action directive if provided
+ if let Some(directive) = action_directive {
+ message.push_str("\n\n");
+ message.push_str(directive);
+ }
+
if let Err(e) = self.notify_supervisor(
supervisor_task_id,
supervisor_daemon_id,