summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-01 01:35:18 +0000
committersoryu <soryu@soryu.co>2026-02-01 01:35:18 +0000
commitc2750f86ebd6ac5c04b70dd8249501262d6dd07c (patch)
treeecd4a32d74adc3479cdc30c94843a6447bf44649
parent7567153e6281b94e39e52be5d060b381ed69597d (diff)
downloadsoryu-c2750f86ebd6ac5c04b70dd8249501262d6dd07c.tar.gz
soryu-c2750f86ebd6ac5c04b70dd8249501262d6dd07c.zip
[WIP] Heartbeat checkpoint - 2026-02-01 01:35:18 UTC
-rw-r--r--makima/src/bin/makima.rs4
-rw-r--r--makima/src/daemon/api/contract.rs178
-rw-r--r--makima/src/daemon/cli/contract.rs208
-rw-r--r--makima/src/daemon/cli/mod.rs21
4 files changed, 408 insertions, 3 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index ac577b8..2795e5e 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -6,7 +6,8 @@ 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,
+ contract::{ListArgs, ListOutputFormat},
};
use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion};
use makima::daemon::config::{DaemonConfig, RepoEntry};
@@ -31,6 +32,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Commands::View(args) => run_view(args).await,
Commands::Config(cmd) => run_config(cmd).await,
Commands::RedTeam(cmd) => run_red_team(cmd).await,
+ Commands::Contracts(cmd) => run_contracts(cmd).await,
}
}
diff --git a/makima/src/daemon/api/contract.rs b/makima/src/daemon/api/contract.rs
index 7c76b40..119c0ba 100644
--- a/makima/src/daemon/api/contract.rs
+++ b/makima/src/daemon/api/contract.rs
@@ -1,6 +1,6 @@
//! Contract API methods.
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::client::{ApiClient, ApiError};
@@ -281,3 +281,179 @@ struct AddRemoteRepositoryRequest {
repository_url: String,
is_primary: bool,
}
+
+// ============================================================================
+// Contracts cleanup types
+// ============================================================================
+
+/// Request for batch contract operations (cleanup, archive, etc.).
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BatchOperationRequest {
+ /// The operation to perform: "archive", "delete_archived", "cleanup_worktrees"
+ pub operation: String,
+ /// Age threshold in seconds (for archive and delete operations)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub older_than_seconds: Option<u64>,
+ /// Status filter for the operation
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status_filter: Option<Vec<String>>,
+ /// If true, only show what would be affected without making changes
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub dry_run: Option<bool>,
+}
+
+/// Response from a batch operation.
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BatchOperationResponse {
+ /// The operation that was performed
+ pub operation: String,
+ /// Number of items affected
+ pub affected_count: usize,
+ /// IDs of affected items
+ #[serde(default)]
+ pub affected_ids: Vec<Uuid>,
+ /// Any errors that occurred
+ #[serde(default)]
+ pub errors: Vec<String>,
+ /// Whether this was a dry run
+ #[serde(default)]
+ pub dry_run: bool,
+}
+
+/// Summary of contracts that would be affected by cleanup.
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CleanupPreviewResponse {
+ /// Contracts that would be archived
+ #[serde(default)]
+ pub to_archive: Vec<ContractSummary>,
+ /// Archived contracts that would be deleted
+ #[serde(default)]
+ pub to_delete: Vec<ContractSummary>,
+ /// Orphaned worktrees that would be cleaned up
+ #[serde(default)]
+ pub orphaned_worktrees: Vec<String>,
+}
+
+/// Brief summary of a contract for cleanup operations.
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ContractSummary {
+ pub id: Uuid,
+ pub name: String,
+ pub status: String,
+ #[serde(default)]
+ pub updated_at: Option<String>,
+}
+
+impl ApiClient {
+ // ========================================================================
+ // Contracts cleanup operations
+ // ========================================================================
+
+ /// Preview cleanup operations (dry run).
+ /// Returns what would be affected by archive, delete, and worktree cleanup.
+ pub async fn cleanup_preview(
+ &self,
+ older_than_seconds: u64,
+ archive: bool,
+ delete_archived: bool,
+ worktrees: bool,
+ ) -> Result<CleanupPreviewResponse, ApiError> {
+ let mut params = vec![
+ format!("older_than_seconds={}", older_than_seconds),
+ "dry_run=true".to_string(),
+ ];
+ if archive {
+ params.push("archive=true".to_string());
+ }
+ if delete_archived {
+ params.push("delete_archived=true".to_string());
+ }
+ if worktrees {
+ params.push("worktrees=true".to_string());
+ }
+ let query = params.join("&");
+ let result = self.get(&format!("/api/v1/contracts/cleanup?{}", query)).await?;
+ serde_json::from_value(result.0)
+ .map_err(|e| ApiError::Other(format!("Failed to parse cleanup preview: {}", e)))
+ }
+
+ /// Archive completed/failed contracts older than the threshold.
+ pub async fn archive_contracts(
+ &self,
+ older_than_seconds: u64,
+ dry_run: bool,
+ ) -> Result<BatchOperationResponse, ApiError> {
+ let req = BatchOperationRequest {
+ operation: "archive".to_string(),
+ older_than_seconds: Some(older_than_seconds),
+ status_filter: Some(vec!["completed".to_string(), "failed".to_string()]),
+ dry_run: Some(dry_run),
+ };
+ let result = self.post("/api/v1/contracts/batch", &req).await?;
+ serde_json::from_value(result.0)
+ .map_err(|e| ApiError::Other(format!("Failed to parse archive response: {}", e)))
+ }
+
+ /// Delete archived contracts older than the threshold.
+ pub async fn delete_archived_contracts(
+ &self,
+ older_than_seconds: u64,
+ dry_run: bool,
+ ) -> Result<BatchOperationResponse, ApiError> {
+ let req = BatchOperationRequest {
+ operation: "delete_archived".to_string(),
+ older_than_seconds: Some(older_than_seconds),
+ status_filter: Some(vec!["archived".to_string()]),
+ dry_run: Some(dry_run),
+ };
+ let result = self.post("/api/v1/contracts/batch", &req).await?;
+ serde_json::from_value(result.0)
+ .map_err(|e| ApiError::Other(format!("Failed to parse delete response: {}", e)))
+ }
+
+ /// Clean up orphaned worktrees.
+ pub async fn cleanup_worktrees(&self, dry_run: bool) -> Result<BatchOperationResponse, ApiError> {
+ let req = BatchOperationRequest {
+ operation: "cleanup_worktrees".to_string(),
+ older_than_seconds: None,
+ status_filter: None,
+ dry_run: Some(dry_run),
+ };
+ let result = self.post("/api/v1/contracts/batch", &req).await?;
+ serde_json::from_value(result.0)
+ .map_err(|e| ApiError::Other(format!("Failed to parse worktree cleanup response: {}", e)))
+ }
+
+ /// List contracts with filtering options.
+ pub async fn list_contracts_filtered(
+ &self,
+ status: Option<&str>,
+ phase: Option<&str>,
+ stale: bool,
+ stale_threshold_seconds: Option<u64>,
+ ) -> Result<JsonValue, ApiError> {
+ let mut params = Vec::new();
+ if let Some(s) = status {
+ params.push(format!("status={}", s));
+ }
+ if let Some(p) = phase {
+ params.push(format!("phase={}", p));
+ }
+ if stale {
+ params.push("stale=true".to_string());
+ if let Some(threshold) = stale_threshold_seconds {
+ params.push(format!("stale_threshold={}", threshold));
+ }
+ }
+ let query_string = if params.is_empty() {
+ String::new()
+ } else {
+ format!("?{}", params.join("&"))
+ };
+ self.get(&format!("/api/v1/contracts{}", query_string)).await
+ }
+}
diff --git a/makima/src/daemon/cli/contract.rs b/makima/src/daemon/cli/contract.rs
index a443b85..9d6784d 100644
--- a/makima/src/daemon/cli/contract.rs
+++ b/makima/src/daemon/cli/contract.rs
@@ -1,6 +1,6 @@
//! Contract subcommand - task-contract interaction commands.
-use clap::Args;
+use clap::{Args, Subcommand};
use uuid::Uuid;
/// Common arguments for contract commands.
@@ -85,3 +85,209 @@ pub struct CreateFileArgs {
/// Name of the new file
pub name: String,
}
+
+// ============================================================================
+// Contracts management 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", global = true)]
+ pub api_url: String,
+
+ /// API key for authentication
+ #[arg(long, env = "MAKIMA_API_KEY", global = true)]
+ pub api_key: String,
+}
+
+/// Contracts management subcommands.
+#[derive(Subcommand, Debug)]
+pub enum ContractsSubcommand {
+ /// List all contracts with optional filters
+ List(ListArgs),
+
+ /// Clean up old contracts, archived contracts, and orphaned worktrees
+ Cleanup(CleanupArgs),
+}
+
+/// Output format for list command.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ListOutputFormat {
+ #[default]
+ Table,
+ Json,
+ Compact,
+}
+
+impl std::str::FromStr for ListOutputFormat {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.to_lowercase().as_str() {
+ "table" => Ok(ListOutputFormat::Table),
+ "json" => Ok(ListOutputFormat::Json),
+ "compact" => Ok(ListOutputFormat::Compact),
+ _ => Err(format!("Invalid format '{}'. Valid options: table, json, compact", s)),
+ }
+ }
+}
+
+impl std::fmt::Display for ListOutputFormat {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ListOutputFormat::Table => write!(f, "table"),
+ ListOutputFormat::Json => write!(f, "json"),
+ ListOutputFormat::Compact => write!(f, "compact"),
+ }
+ }
+}
+
+/// Arguments for the list subcommand.
+#[derive(Args, Debug)]
+pub struct ListArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Filter by status (active, completed, failed)
+ #[arg(long, value_delimiter = ',')]
+ pub status: Option<Vec<String>>,
+
+ /// Show only stale contracts (no activity within threshold)
+ #[arg(long)]
+ pub stale: bool,
+
+ /// Stale threshold duration (e.g., "30m", "1h", "2d"). Default: 30m
+ #[arg(long, default_value = "30m")]
+ pub threshold: String,
+
+ /// Show contracts waiting for user input
+ #[arg(long)]
+ pub waiting: bool,
+
+ /// Filter by phase (e.g., "plan", "execute", "review")
+ #[arg(long)]
+ pub phase: Option<String>,
+
+ /// Output format: table, json, or compact
+ #[arg(long, short = 'f', default_value = "table")]
+ pub format: String,
+
+ /// Limit number of results
+ #[arg(long, short = 'n')]
+ pub limit: Option<usize>,
+}
+
+impl ListArgs {
+ /// Parse the format string into a ListOutputFormat enum.
+ pub fn parse_format(&self) -> Result<ListOutputFormat, String> {
+ self.format.parse()
+ }
+
+ /// Parse the threshold duration into seconds.
+ pub fn parse_threshold(&self) -> Result<u64, String> {
+ parse_duration(&self.threshold)
+ }
+}
+
+/// Arguments for the cleanup subcommand.
+#[derive(Args, Debug)]
+pub struct CleanupArgs {
+ #[command(flatten)]
+ pub common: ContractsCommonArgs,
+
+ /// Archive completed/failed contracts older than the threshold
+ #[arg(long)]
+ pub archive: bool,
+
+ /// Delete archived contracts older than the threshold
+ #[arg(long)]
+ pub delete_archived: bool,
+
+ /// Clean up orphaned worktrees (worktrees without associated contracts)
+ #[arg(long)]
+ pub worktrees: bool,
+
+ /// Run all cleanup operations (archive, delete-archived, worktrees)
+ #[arg(long)]
+ pub all: bool,
+
+ /// Age threshold for cleanup operations (e.g., "7d", "24h", "30d")
+ /// Default: 7d (7 days)
+ #[arg(long, default_value = "7d")]
+ pub older_than: String,
+
+ /// Show what would be affected without making changes
+ #[arg(long)]
+ pub dry_run: bool,
+
+ /// Skip confirmation prompts for destructive operations
+ #[arg(long)]
+ pub force: bool,
+}
+
+impl CleanupArgs {
+ /// Parse the older_than duration string into seconds.
+ /// Supports formats like "7d", "24h", "30m", "60s".
+ pub fn parse_older_than(&self) -> Result<u64, String> {
+ parse_duration(&self.older_than)
+ }
+
+ /// Returns true if any cleanup operation is selected.
+ pub fn has_any_operation(&self) -> bool {
+ self.archive || self.delete_archived || self.worktrees || self.all
+ }
+}
+
+/// Parse a duration string like "7d", "24h", "30m", "60s" into seconds.
+pub fn parse_duration(s: &str) -> Result<u64, String> {
+ let s = s.trim();
+ if s.is_empty() {
+ return Err("Empty duration string".to_string());
+ }
+
+ let (num_str, unit) = if s.ends_with('d') || s.ends_with('D') {
+ (&s[..s.len() - 1], 'd')
+ } else if s.ends_with('h') || s.ends_with('H') {
+ (&s[..s.len() - 1], 'h')
+ } else if s.ends_with('m') || s.ends_with('M') {
+ (&s[..s.len() - 1], 'm')
+ } else if s.ends_with('s') || s.ends_with('S') {
+ (&s[..s.len() - 1], 's')
+ } else {
+ // Default to days if no unit
+ (s, 'd')
+ };
+
+ let num: u64 = num_str
+ .parse()
+ .map_err(|_| format!("Invalid number in duration: {}", num_str))?;
+
+ let seconds = match unit {
+ 'd' => num * 24 * 60 * 60,
+ 'h' => num * 60 * 60,
+ 'm' => num * 60,
+ 's' => num,
+ _ => unreachable!(),
+ };
+
+ Ok(seconds)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_duration() {
+ assert_eq!(parse_duration("7d").unwrap(), 7 * 24 * 60 * 60);
+ assert_eq!(parse_duration("24h").unwrap(), 24 * 60 * 60);
+ assert_eq!(parse_duration("30m").unwrap(), 30 * 60);
+ assert_eq!(parse_duration("60s").unwrap(), 60);
+ assert_eq!(parse_duration("7D").unwrap(), 7 * 24 * 60 * 60);
+ assert_eq!(parse_duration("7").unwrap(), 7 * 24 * 60 * 60); // defaults to days
+ assert!(parse_duration("").is_err());
+ assert!(parse_duration("abc").is_err());
+ }
+}
diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs
index c848e8e..8538afa 100644
--- a/makima/src/daemon/cli/mod.rs
+++ b/makima/src/daemon/cli/mod.rs
@@ -65,6 +65,27 @@ pub enum Commands {
/// Red team commands for adversarial monitoring
#[command(name = "red-team", subcommand)]
RedTeam(RedTeamCommand),
+
+ /// Contract management commands (list, cleanup)
+ #[command(subcommand)]
+ Contracts(ContractsCommand),
+}
+
+/// Contracts management subcommands for multi-contract operations.
+#[derive(Subcommand, Debug)]
+pub enum ContractsCommand {
+ /// List all contracts with optional filters
+ ///
+ /// Examples:
+ /// makima contracts list # List all contracts
+ /// makima contracts list --status active # List only active contracts
+ /// makima contracts list --stale # Show stale contracts
+ /// makima contracts list --waiting # Show contracts waiting for input
+ /// makima contracts list --format json # Output as JSON
+ List(contract::ListArgs),
+
+ /// Clean up old contracts, archived contracts, and orphaned worktrees
+ Cleanup(contract::CleanupArgs),
}
/// Config subcommands for CLI configuration.