summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/mesh_daemon.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-09 21:21:10 +0000
committersoryu <soryu@soryu.co>2026-02-09 21:21:21 +0000
commit526edf672aae73c3670ab6141253bf92f1fbfe8c (patch)
treeceb32352f5f38be4662564126e135724850dbc31 /makima/src/server/handlers/mesh_daemon.rs
parent76bb9da745f6c12c8e7e587a9096677bbf98f395 (diff)
downloadsoryu-526edf672aae73c3670ab6141253bf92f1fbfe8c.tar.gz
soryu-526edf672aae73c3670ab6141253bf92f1fbfe8c.zip
Add auto-PR creation for remote repos in contracts
Diffstat (limited to 'makima/src/server/handlers/mesh_daemon.rs')
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs108
1 files changed, 104 insertions, 4 deletions
diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs
index 60de2e6..743a1ca 100644
--- a/makima/src/server/handlers/mesh_daemon.rs
+++ b/makima/src/server/handlers/mesh_daemon.rs
@@ -626,10 +626,7 @@ async fn compute_action_directive(
if all_tasks_done && !pr_deliverable_complete {
let done_count = task_infos.len();
return Some(format!(
- "[ACTION REQUIRED] All {} task(s) completed successfully.\n\
- You MUST now create a PR and mark the 'pull-request' deliverable as complete:\n\
- 1. Ensure all changes are merged to your branch\n\
- 2. Create PR and then call mark_deliverable_complete with deliverable_id='pull-request'",
+ "[INFO] All {} task(s) completed. System is auto-creating PR.",
done_count
));
}
@@ -638,6 +635,98 @@ async fn compute_action_directive(
None
}
+/// Automatically create a PR when all non-supervisor tasks for a contract are done.
+/// Only applies to remote-repo contracts in the "execute" phase.
+/// Fires as a best-effort operation — errors are logged but not propagated.
+async fn auto_create_pr_if_ready(
+ pool: &sqlx::PgPool,
+ state: &SharedState,
+ contract_id: Uuid,
+ owner_id: Uuid,
+) {
+ // 1. Load contract — must be remote (not local_only) and in execute phase
+ let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
+ Ok(Some(c)) => c,
+ _ => return,
+ };
+ if contract.local_only || contract.phase != "execute" {
+ return;
+ }
+
+ // 2. Load non-supervisor tasks — all must be done
+ let tasks = match repository::list_tasks_by_contract(pool, contract_id, owner_id).await {
+ Ok(t) => t,
+ _ => return,
+ };
+ let non_supervisor_tasks: Vec<_> = tasks.iter().filter(|t| !t.is_supervisor).collect();
+ if non_supervisor_tasks.is_empty() || !non_supervisor_tasks.iter().all(|t| t.status == "done") {
+ return;
+ }
+
+ // 3. Check pull-request deliverable not already complete
+ let completed_deliverables = contract.get_completed_deliverables(&contract.phase);
+ if completed_deliverables.contains(&"pull-request".to_string()) {
+ return;
+ }
+
+ // 4. Check at least one repository has a remote URL
+ let repos = match repository::list_contract_repositories(pool, contract_id).await {
+ Ok(r) => r,
+ _ => return,
+ };
+ if !repos.iter().any(|r| r.repository_url.is_some()) {
+ return;
+ }
+
+ // 5. Load supervisor task
+ let supervisor = match repository::get_contract_supervisor_task(pool, contract_id).await {
+ Ok(Some(s)) => s,
+ _ => return,
+ };
+
+ // Need supervisor's daemon_id to send command
+ let daemon_id = match supervisor.daemon_id {
+ Some(id) => id,
+ None => return,
+ };
+
+ // 6. Construct branch name
+ let sanitized_name: String = supervisor
+ .name
+ .chars()
+ .map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '-' })
+ .collect::<String>()
+ .to_lowercase();
+ let short_id = &supervisor.id.to_string()[..8];
+ let branch = format!("makima/{}-{}", sanitized_name, short_id);
+
+ // 7. Send CreatePR command to supervisor's daemon
+ let command = DaemonCommand::CreatePR {
+ task_id: supervisor.id,
+ title: contract.name.clone(),
+ body: contract.description.clone(),
+ base_branch: supervisor.base_branch.clone(),
+ branch,
+ };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => {
+ tracing::info!(
+ contract_id = %contract_id,
+ supervisor_id = %supervisor.id,
+ "Auto-PR: sent CreatePR command to supervisor daemon"
+ );
+ }
+ Err(e) => {
+ tracing::warn!(
+ contract_id = %contract_id,
+ error = %e,
+ "Auto-PR: failed to send CreatePR command"
+ );
+ }
+ }
+}
+
/// 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);
@@ -1286,6 +1375,17 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
}
}
+ // Auto-create PR if all tasks are done and repo is remote
+ if updated_task.status == "done" {
+ if let Some(contract_id) = updated_task.contract_id {
+ let pool_c = pool.clone();
+ let state_c = state.clone();
+ tokio::spawn(async move {
+ auto_create_pr_if_ready(&pool_c, &state_c, contract_id, owner_id).await;
+ });
+ }
+ }
+
// Record history event for task completion
let subtype = if updated_task.status == "done" { "completed" } else { "failed" };
let _ = repository::record_history_event(