//! Supervisor API methods.
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::client::{ApiClient, ApiError};
// Request/Response types
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpawnTaskRequest {
pub name: String,
pub plan: String,
pub contract_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_task_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checkpoint_sha: Option<String>,
/// If true, create a separate worktree for the task (requires merge after).
/// If false (default), the task shares the supervisor's worktree.
#[serde(default)]
pub use_own_worktree: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WaitRequest {
pub timeout_seconds: i32,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadFileRequest {
pub file_path: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateBranchRequest {
pub branch_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_ref: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MergeRequest {
pub squash: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_branch: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePrRequest {
pub branch: String,
pub title: String,
pub body: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckpointRequest {
pub message: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AskQuestionRequest {
pub question: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub choices: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
pub timeout_seconds: i32,
/// When true, the request will block indefinitely until user responds (no timeout)
pub phaseguard: bool,
/// When true, allow selecting multiple choices (response will be comma-separated)
#[serde(default)]
pub multi_select: bool,
/// When true, return immediately without waiting for response
#[serde(default)]
pub non_blocking: bool,
/// Question type: general, phase_confirmation, or contract_complete
#[serde(default)]
pub question_type: String,
}
// Generic response type for JSON output
#[derive(Deserialize, Serialize)]
pub struct JsonValue(pub serde_json::Value);
impl ApiClient {
/// Get all tasks in a contract.
pub async fn supervisor_tasks(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/mesh/supervisor/contracts/{}/tasks", contract_id))
.await
}
/// Get task tree structure.
pub async fn supervisor_tree(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/mesh/supervisor/contracts/{}/tree", contract_id))
.await
}
/// Spawn a new task.
pub async fn supervisor_spawn(&self, req: SpawnTaskRequest) -> Result<JsonValue, ApiError> {
self.post("/api/v1/mesh/supervisor/tasks", &req).await
}
/// Wait for a task to complete.
pub async fn supervisor_wait(
&self,
task_id: Uuid,
timeout_seconds: i32,
) -> Result<JsonValue, ApiError> {
let req = WaitRequest { timeout_seconds };
self.post(&format!("/api/v1/mesh/supervisor/tasks/{}/wait", task_id), &req)
.await
}
/// Read a file from a task's worktree.
pub async fn supervisor_read_file(
&self,
task_id: Uuid,
file_path: &str,
) -> Result<JsonValue, ApiError> {
let req = ReadFileRequest {
file_path: file_path.to_string(),
};
self.post(&format!("/api/v1/mesh/supervisor/tasks/{}/read-file", task_id), &req)
.await
}
/// Create a new branch.
pub async fn supervisor_branch(
&self,
branch_name: &str,
from_ref: Option<String>,
) -> Result<JsonValue, ApiError> {
let req = CreateBranchRequest {
branch_name: branch_name.to_string(),
from_ref,
};
self.post("/api/v1/mesh/supervisor/branches", &req).await
}
/// Merge a task's changes.
pub async fn supervisor_merge(
&self,
task_id: Uuid,
target_branch: Option<String>,
squash: bool,
) -> Result<JsonValue, ApiError> {
let req = MergeRequest {
squash,
target_branch,
};
self.post(&format!("/api/v1/mesh/supervisor/tasks/{}/merge", task_id), &req)
.await
}
/// Create a pull request.
pub async fn supervisor_pr(
&self,
branch: &str,
title: &str,
body: &str,
) -> Result<JsonValue, ApiError> {
let req = CreatePrRequest {
branch: branch.to_string(),
title: title.to_string(),
body: body.to_string(),
};
self.post("/api/v1/mesh/supervisor/pr", &req).await
}
/// Get task diff.
pub async fn supervisor_diff(&self, task_id: Uuid) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/mesh/supervisor/tasks/{}/diff", task_id))
.await
}
/// Create a checkpoint.
pub async fn supervisor_checkpoint(
&self,
task_id: Uuid,
message: &str,
) -> Result<JsonValue, ApiError> {
let req = CheckpointRequest {
message: message.to_string(),
};
self.post(&format!("/api/v1/mesh/tasks/{}/checkpoint", task_id), &req)
.await
}
/// List checkpoints.
pub async fn supervisor_checkpoints(&self, task_id: Uuid) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/mesh/tasks/{}/checkpoints", task_id))
.await
}
/// Get contract status.
pub async fn supervisor_status(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/contracts/{}/daemon/status", contract_id))
.await
}
/// Ask a question and wait for user feedback.
pub async fn supervisor_ask(
&self,
question: &str,
choices: Vec<String>,
context: Option<String>,
timeout_seconds: i32,
phaseguard: bool,
multi_select: bool,
non_blocking: bool,
question_type: String,
) -> Result<JsonValue, ApiError> {
let req = AskQuestionRequest {
question: question.to_string(),
choices,
context,
timeout_seconds,
phaseguard,
multi_select,
non_blocking,
question_type,
};
self.post("/api/v1/mesh/supervisor/questions", &req).await
}
/// Advance contract to a new phase.
pub async fn supervisor_advance_phase(
&self,
contract_id: Uuid,
phase: &str,
) -> Result<JsonValue, ApiError> {
#[derive(Serialize)]
struct AdvancePhaseRequest {
phase: String,
}
let req = AdvancePhaseRequest {
phase: phase.to_string(),
};
self.post(&format!("/api/v1/contracts/{}/phase", contract_id), &req)
.await
}
/// Get individual task details.
pub async fn supervisor_get_task(&self, task_id: Uuid) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/mesh/tasks/{}", task_id)).await
}
/// Get task output/claude log.
pub async fn supervisor_get_task_output(&self, task_id: Uuid) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/mesh/tasks/{}/output", task_id))
.await
}
/// Mark a contract as complete.
/// This will update contract status to 'completed', stop the supervisor, and clean up worktrees.
pub async fn supervisor_complete(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CompleteContractRequest {
status: String,
}
let req = CompleteContractRequest {
status: "completed".to_string(),
};
self.put(&format!("/api/v1/contracts/{}", contract_id), &req)
.await
}
/// Resume a completed contract (reactivate it).
///
/// This updates the contract status from 'completed' back to 'active'
/// and optionally respawns the supervisor task.
pub async fn supervisor_resume_contract(
&self,
contract_id: Uuid,
) -> Result<JsonValue, ApiError> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ResumeContractRequest {
status: String,
}
let req = ResumeContractRequest {
status: "active".to_string(),
};
self.put(&format!("/api/v1/contracts/{}", contract_id), &req)
.await
}
/// Delete a task.
pub async fn delete_task(&self, task_id: Uuid) -> Result<(), ApiError> {
self.delete(&format!("/api/v1/mesh/tasks/{}", task_id)).await
}
/// Mark a deliverable as complete.
pub async fn supervisor_mark_deliverable(
&self,
contract_id: Uuid,
deliverable_id: &str,
phase: Option<&str>,
) -> Result<JsonValue, ApiError> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct MarkDeliverableRequest {
deliverable_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
phase: Option<String>,
}
let req = MarkDeliverableRequest {
deliverable_id: deliverable_id.to_string(),
phase: phase.map(|s| s.to_string()),
};
self.post(
&format!("/api/v1/contracts/{}/deliverables/complete", contract_id),
&req,
)
.await
}
/// Update a task.
pub async fn update_task(
&self,
task_id: Uuid,
name: Option<String>,
plan: Option<String>,
) -> Result<JsonValue, ApiError> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct UpdateTaskRequest {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
plan: Option<String>,
}
let req = UpdateTaskRequest { name, plan };
self.put(&format!("/api/v1/mesh/tasks/{}", task_id), &req)
.await
}
}