summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-01 02:00:32 +0000
committersoryu <soryu@soryu.co>2026-02-01 02:00:32 +0000
commit158337a6c55b0e8b7fdaf7ae4e6086a11b7b906f (patch)
tree3676d9629c9a46d19d4881b7f7a91de91b7d7a91
parent7567153e6281b94e39e52be5d060b381ed69597d (diff)
downloadsoryu-makima/task-task-5f420783-5f420783.tar.gz
soryu-makima/task-task-5f420783-5f420783.zip
[WIP] Heartbeat checkpoint - 2026-02-01 02:00:32 UTCmakima/task-task-5f420783-5f420783
-rw-r--r--makima/src/bin/makima.rs187
-rw-r--r--makima/src/daemon/api/contract.rs89
-rw-r--r--makima/src/daemon/cli/contract.rs96
-rw-r--r--makima/src/daemon/cli/mod.rs42
4 files changed, 413 insertions, 1 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index ac577b8..c5b7d37 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -6,7 +6,7 @@ use std::sync::Arc;
use makima::daemon::api::{ApiClient, CreateContractRequest};
use makima::daemon::cli::{
- Cli, CliConfig, Commands, ConfigCommand, ContractCommand, RedTeamCommand, SupervisorCommand, ViewArgs,
+ Cli, CliConfig, Commands, ConfigCommand, ContractCommand, ContractsCommand, RedTeamCommand, SupervisorCommand, ViewArgs,
};
use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion};
use makima::daemon::config::{DaemonConfig, RepoEntry};
@@ -28,6 +28,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Commands::Daemon(args) => run_daemon(args).await,
Commands::Supervisor(cmd) => run_supervisor(cmd).await,
Commands::Contract(cmd) => run_contract(cmd).await,
+ Commands::Contracts(cmd) => run_contracts(cmd).await,
Commands::View(args) => run_view(args).await,
Commands::Config(cmd) => run_config(cmd).await,
Commands::RedTeam(cmd) => run_red_team(cmd).await,
@@ -805,6 +806,190 @@ async fn run_red_team(cmd: RedTeamCommand) -> Result<(), Box<dyn std::error::Err
}
}
+/// Run contracts management helper commands.
+async fn run_contracts(
+ cmd: ContractsCommand,
+) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+ match cmd {
+ ContractsCommand::Pause(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ eprintln!("Pausing contract {}...", args.contract_id);
+ let result = client.pause_contract(args.contract_id, args.reason).await?;
+ println!("{}", serde_json::to_string_pretty(&result.0)?);
+ }
+ ContractsCommand::Resume(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ eprintln!("Resuming contract {}...", args.contract_id);
+ let result = client.resume_contract(args.contract_id).await?;
+ println!("{}", serde_json::to_string_pretty(&result.0)?);
+ }
+ ContractsCommand::Advance(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ if args.force {
+ eprintln!(
+ "Advancing contract {} to phase '{}' (forced)...",
+ args.contract_id, args.phase
+ );
+ } else {
+ eprintln!(
+ "Advancing contract {} to phase '{}'...",
+ args.contract_id, args.phase
+ );
+ }
+ let result = client
+ .advance_contract_phase(args.contract_id, &args.phase, args.force)
+ .await?;
+ println!("{}", serde_json::to_string_pretty(&result.0)?);
+ }
+ ContractsCommand::RestartSupervisor(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ eprintln!("Restarting supervisor for contract {}...", args.contract_id);
+ match client.restart_supervisor(args.contract_id).await {
+ Ok(result) => {
+ println!("{}", serde_json::to_string_pretty(&result.0)?);
+ }
+ Err(e) => {
+ // If the endpoint doesn't exist, provide a helpful message
+ eprintln!("Error: {}", e);
+ eprintln!();
+ eprintln!("Note: Supervisor restart may not be implemented yet.");
+ eprintln!("As a workaround, you can pause and resume the contract:");
+ eprintln!(" makima contracts pause {}", args.contract_id);
+ eprintln!(" makima contracts resume {}", args.contract_id);
+ std::process::exit(1);
+ }
+ }
+ }
+ ContractsCommand::Show(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client.get_contract(args.contract_id).await?;
+
+ if args.verbose {
+ // Full JSON output for verbose mode
+ println!("{}", serde_json::to_string_pretty(&result.0)?);
+ } else {
+ // Formatted summary for normal mode
+ let contract = &result.0.get("contract");
+ let tasks = result.0.get("tasks").and_then(|v| v.as_array());
+ let files = result.0.get("files").and_then(|v| v.as_array());
+ let repos = result.0.get("repositories").and_then(|v| v.as_array());
+
+ if let Some(c) = contract {
+ println!("Contract: {}", c.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown"));
+ println!(" ID: {}", c.get("id").and_then(|v| v.as_str()).unwrap_or("-"));
+ println!(" Status: {}", c.get("status").and_then(|v| v.as_str()).unwrap_or("-"));
+ println!(" Phase: {}", c.get("phase").and_then(|v| v.as_str()).unwrap_or("-"));
+ println!(" Type: {}", c.get("contractType").and_then(|v| v.as_str()).unwrap_or("-"));
+ if let Some(desc) = c.get("description").and_then(|v| v.as_str()) {
+ println!(" Description: {}", desc);
+ }
+ println!();
+ println!(" Repositories: {}", repos.map(|r| r.len()).unwrap_or(0));
+ println!(" Files: {}", files.map(|f| f.len()).unwrap_or(0));
+ println!(" Tasks: {}", tasks.map(|t| t.len()).unwrap_or(0));
+
+ if let Some(tasks) = tasks {
+ if !tasks.is_empty() {
+ println!();
+ println!(" Task Summary:");
+ for task in tasks.iter().take(10) {
+ let name = task.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
+ let status = task.get("status").and_then(|v| v.as_str()).unwrap_or("-");
+ let is_supervisor = task.get("isSupervisor").and_then(|v| v.as_bool()).unwrap_or(false);
+ let role = if is_supervisor { " [supervisor]" } else { "" };
+ println!(" - {} ({}){}", name, status, role);
+ }
+ if tasks.len() > 10 {
+ println!(" ... and {} more", tasks.len() - 10);
+ }
+ }
+ }
+ } else {
+ println!("{}", serde_json::to_string_pretty(&result.0)?);
+ }
+ }
+ }
+ ContractsCommand::Health(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+
+ // Get contract details
+ let contract = client.get_contract(args.contract_id).await?;
+
+ // Try to get health endpoint, fall back to constructing from contract data
+ match client.get_contract_health(args.contract_id).await {
+ Ok(result) => {
+ println!("{}", serde_json::to_string_pretty(&result.0)?);
+ }
+ Err(_) => {
+ // Health endpoint may not exist, construct from contract data
+ let c = contract.0.get("contract");
+ let tasks = contract.0.get("tasks").and_then(|v| v.as_array());
+
+ let status = c
+ .and_then(|c| c.get("status"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("unknown");
+
+ let phase = c
+ .and_then(|c| c.get("phase"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("unknown");
+
+ let supervisor_task_id = c
+ .and_then(|c| c.get("supervisorTaskId"))
+ .and_then(|v| v.as_str());
+
+ // Count tasks by status
+ let mut running = 0;
+ let mut pending = 0;
+ let mut done = 0;
+ let mut failed = 0;
+ let mut supervisor_status = "unknown";
+
+ if let Some(tasks) = tasks {
+ for task in tasks {
+ let task_status = task.get("status").and_then(|v| v.as_str()).unwrap_or("");
+ let task_id = task.get("id").and_then(|v| v.as_str());
+
+ // Check if this is the supervisor
+ if task_id == supervisor_task_id {
+ supervisor_status = task_status;
+ }
+
+ match task_status {
+ "running" | "working" => running += 1,
+ "pending" | "queued" => pending += 1,
+ "done" | "completed" | "merged" => done += 1,
+ "failed" | "error" => failed += 1,
+ _ => {}
+ }
+ }
+ }
+
+ let health = serde_json::json!({
+ "contractId": args.contract_id,
+ "status": status,
+ "phase": phase,
+ "supervisorStatus": supervisor_status,
+ "taskCounts": {
+ "running": running,
+ "pending": pending,
+ "done": done,
+ "failed": failed,
+ "total": running + pending + done + failed
+ },
+ "healthy": status == "active" && (supervisor_status == "running" || supervisor_status == "working")
+ });
+
+ println!("{}", serde_json::to_string_pretty(&health)?);
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
/// Load contracts from API
async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> {
let result = client.list_contracts().await?;
diff --git a/makima/src/daemon/api/contract.rs b/makima/src/daemon/api/contract.rs
index 7c76b40..b6da837 100644
--- a/makima/src/daemon/api/contract.rs
+++ b/makima/src/daemon/api/contract.rs
@@ -274,6 +274,7 @@ impl ApiClient {
}
}
+/// Request to add a remote repository.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AddRemoteRepositoryRequest {
@@ -281,3 +282,91 @@ struct AddRemoteRepositoryRequest {
repository_url: String,
is_primary: bool,
}
+
+// =============================================================================
+// Contract Management Helper API Methods
+// =============================================================================
+
+/// Request to pause a contract.
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PauseContractRequest {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reason: Option<String>,
+}
+
+/// Request to change contract phase.
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ChangePhaseRequest {
+ pub phase: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub confirmed: Option<bool>,
+}
+
+/// Request to update contract status.
+#[derive(Serialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateContractStatusRequest {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub pause_reason: Option<String>,
+}
+
+impl ApiClient {
+ /// Pause a contract by setting its status to paused.
+ pub async fn pause_contract(
+ &self,
+ contract_id: Uuid,
+ reason: Option<String>,
+ ) -> Result<JsonValue, ApiError> {
+ let req = UpdateContractStatusRequest {
+ status: Some("paused".to_string()),
+ pause_reason: reason,
+ };
+ self.put(&format!("/api/v1/contracts/{}", contract_id), &req)
+ .await
+ }
+
+ /// Resume a paused contract by setting its status back to active.
+ pub async fn resume_contract(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> {
+ let req = UpdateContractStatusRequest {
+ status: Some("active".to_string()),
+ pause_reason: None,
+ };
+ self.put(&format!("/api/v1/contracts/{}", contract_id), &req)
+ .await
+ }
+
+ /// Advance a contract to a specific phase.
+ pub async fn advance_contract_phase(
+ &self,
+ contract_id: Uuid,
+ phase: &str,
+ force: bool,
+ ) -> Result<JsonValue, ApiError> {
+ let req = ChangePhaseRequest {
+ phase: phase.to_string(),
+ confirmed: Some(force),
+ };
+ self.post(&format!("/api/v1/contracts/{}/phase", contract_id), &req)
+ .await
+ }
+
+ /// Request a supervisor restart for a contract.
+ /// This will stop the current supervisor (if running) and queue a new one.
+ pub async fn restart_supervisor(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!(
+ "/api/v1/contracts/{}/supervisor/restart",
+ contract_id
+ ))
+ .await
+ }
+
+ /// Get detailed contract information including health status.
+ pub async fn get_contract_health(&self, contract_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.get(&format!("/api/v1/contracts/{}/health", contract_id))
+ .await
+ }
+}
diff --git a/makima/src/daemon/cli/contract.rs b/makima/src/daemon/cli/contract.rs
index a443b85..b5bd7cc 100644
--- a/makima/src/daemon/cli/contract.rs
+++ b/makima/src/daemon/cli/contract.rs
@@ -1,4 +1,8 @@
//! Contract subcommand - task-contract interaction commands.
+//!
+//! This module contains two types of commands:
+//! 1. Task-contract interaction commands (used by agents via `makima contract`)
+//! 2. Contract management helper commands (used by users via `makima contracts`)
use clap::Args;
use uuid::Uuid;
@@ -85,3 +89,95 @@ pub struct CreateFileArgs {
/// Name of the new file
pub name: String,
}
+
+// =============================================================================
+// Contract Management Helper Commands (makima contracts)
+// =============================================================================
+
+/// Common arguments for contracts management commands.
+#[derive(Args, Debug, Clone)]
+pub struct ContractsCommonArgs {
+ /// API URL
+ #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp")]
+ pub api_url: String,
+
+ /// API key for authentication
+ #[arg(long, env = "MAKIMA_API_KEY")]
+ pub api_key: String,
+}
+
+/// Arguments for pause command.
+#[derive(Args, Debug)]
+pub struct PauseArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Contract ID to pause
+ pub contract_id: Uuid,
+
+ /// Reason for pausing the contract
+ #[arg(long)]
+ pub reason: Option<String>,
+}
+
+/// Arguments for resume command.
+#[derive(Args, Debug)]
+pub struct ResumeArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Contract ID to resume
+ pub contract_id: Uuid,
+}
+
+/// Arguments for advance command.
+#[derive(Args, Debug)]
+pub struct AdvanceArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Contract ID to advance
+ pub contract_id: Uuid,
+
+ /// Target phase to advance to
+ #[arg(long)]
+ pub phase: String,
+
+ /// Force the phase transition even if deliverables are incomplete
+ #[arg(long)]
+ pub force: bool,
+}
+
+/// Arguments for restart-supervisor command.
+#[derive(Args, Debug)]
+pub struct RestartSupervisorArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Contract ID to restart supervisor for
+ pub contract_id: Uuid,
+}
+
+/// Arguments for show command.
+#[derive(Args, Debug)]
+pub struct ShowArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Contract ID to show
+ pub contract_id: Uuid,
+
+ /// Show verbose output with all details
+ #[arg(long, short = 'v')]
+ pub verbose: bool,
+}
+
+/// Arguments for health command.
+#[derive(Args, Debug)]
+pub struct HealthArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Contract ID to check health for
+ pub contract_id: Uuid,
+}
diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs
index c848e8e..07f0b7c 100644
--- a/makima/src/daemon/cli/mod.rs
+++ b/makima/src/daemon/cli/mod.rs
@@ -13,6 +13,7 @@ use uuid::Uuid;
pub use config::CliConfig;
pub use contract::ContractArgs;
+pub use contract::{PauseArgs, ResumeArgs, AdvanceArgs, RestartSupervisorArgs, ShowArgs, HealthArgs};
pub use daemon::DaemonArgs;
pub use red_team::handle_notify;
pub use server::ServerArgs;
@@ -65,6 +66,12 @@ pub enum Commands {
/// Red team commands for adversarial monitoring
#[command(name = "red-team", subcommand)]
RedTeam(RedTeamCommand),
+
+ /// Contract management helper commands
+ ///
+ /// User-facing commands for managing contracts (pause, resume, show, etc.)
+ #[command(subcommand)]
+ Contracts(ContractsCommand),
}
/// Config subcommands for CLI configuration.
@@ -211,6 +218,41 @@ pub enum RedTeamCommand {
Notify(RedTeamNotifyArgs),
}
+/// Contract management helper subcommands.
+#[derive(Subcommand, Debug)]
+pub enum ContractsCommand {
+ /// Pause a contract
+ ///
+ /// Sets the contract status to paused, optionally with a reason.
+ /// The supervisor will be stopped and tasks will not be picked up.
+ Pause(contract::PauseArgs),
+
+ /// Resume a paused contract
+ ///
+ /// Sets the contract status back to active and restarts the supervisor.
+ Resume(contract::ResumeArgs),
+
+ /// Manually advance a contract to a specific phase
+ ///
+ /// Use --force to skip deliverable validation.
+ Advance(contract::AdvanceArgs),
+
+ /// Restart the supervisor for a contract
+ ///
+ /// Useful when the supervisor is stuck or needs to be refreshed.
+ RestartSupervisor(contract::RestartSupervisorArgs),
+
+ /// Show detailed contract information
+ ///
+ /// Use --verbose for all details including tasks and files.
+ Show(contract::ShowArgs),
+
+ /// Check contract and supervisor health
+ ///
+ /// Returns health status including supervisor state, task counts, and staleness.
+ Health(contract::HealthArgs),
+}
+
/// Arguments for red-team notify command.
#[derive(Args, Debug)]
pub struct RedTeamNotifyArgs {