From 158337a6c55b0e8b7fdaf7ae4e6086a11b7b906f Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 1 Feb 2026 02:00:32 +0000 Subject: [WIP] Heartbeat checkpoint - 2026-02-01 02:00:32 UTC --- makima/src/bin/makima.rs | 187 +++++++++++++++++++++++++++++++++++++- makima/src/daemon/api/contract.rs | 89 ++++++++++++++++++ makima/src/daemon/cli/contract.rs | 96 +++++++++++++++++++ makima/src/daemon/cli/mod.rs | 42 +++++++++ 4 files changed, 413 insertions(+), 1 deletion(-) 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> { 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 Result<(), Box> { + 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, Box> { 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, +} + +/// 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, +} + +/// 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_reason: Option, +} + +impl ApiClient { + /// Pause a contract by setting its status to paused. + pub async fn pause_contract( + &self, + contract_id: Uuid, + reason: Option, + ) -> Result { + 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 { + 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 { + 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 { + 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 { + 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, +} + +/// 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 { -- cgit v1.2.3