//! 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>,
}
#[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,
}
/// Request to create an order for future work.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrderRequest {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub priority: String,
pub order_type: String,
pub labels: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository_url: Option<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
}
/// Poll for a question response by question_id.
///
/// Used after a still_pending response from supervisor_ask. Blocks for up to
/// 5 minutes on the server side. Returns still_pending if no response yet.
pub async fn supervisor_poll_question(
&self,
question_id: Uuid,
) -> Result<JsonValue, ApiError> {
self.get(&format!("/api/v1/mesh/supervisor/questions/{}/poll", question_id))
.await
}
/// Advance contract to a new phase.
///
/// When `confirmed` is false and phase_guard is enabled, returns a response with
/// `status: "pending_confirmation"` containing deliverables for user review.
/// When `confirmed` is true, proceeds with the phase transition.
pub async fn supervisor_advance_phase(
&self,
contract_id: Uuid,
phase: &str,
confirmed: bool,
) -> Result<JsonValue, ApiError> {
#[derive(Serialize)]
struct AdvancePhaseRequest {
phase: String,
confirmed: bool,
}
let req = AdvancePhaseRequest {
phase: phase.to_string(),
confirmed,
};
self.post(&format!("/api/v1/contracts/{}/phase", contract_id), &req)
.await
}
/// Request phase advancement without confirmation (for phase_guard check).
///
/// This method calls `supervisor_advance_phase` with `confirmed=false`.
/// If phase_guard is enabled, the response will have `status: "pending_confirmation"`
/// and the caller should prompt the user for confirmation before calling again with
/// `confirmed=true`.
pub async fn supervisor_request_phase_advance(
&self,
contract_id: Uuid,
phase: &str,
) -> Result<JsonValue, ApiError> {
self.supervisor_advance_phase(contract_id, phase, false).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
}
/// Create an order for future work from a directive task.
pub async fn create_order(&self, req: &CreateOrderRequest) -> Result<JsonValue, ApiError> {
self.post("/api/v1/mesh/supervisor/orders", 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
}
}