summaryrefslogtreecommitdiff
path: root/makima/src/server/mod.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/src/server/mod.rs
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/src/server/mod.rs')
-rw-r--r--makima/src/server/mod.rs118
1 files changed, 117 insertions, 1 deletions
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index a096a5c..568b287 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
-use crate::server::handlers::{api_keys, chat, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_ws, users, versions};
+use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, templates, users, versions};
use crate::server::openapi::ApiDoc;
use crate::server::state::SharedState;
@@ -53,6 +53,7 @@ pub fn make_router(state: SharedState) -> Router {
.delete(files::delete_file),
)
.route("/files/{id}/chat", post(chat::chat_handler))
+ .route("/files/{id}/sync-from-repo", post(files::sync_file_from_repo))
// Version history endpoints
.route("/files/{id}/versions", get(versions::list_versions))
.route("/files/{id}/versions/{version}", get(versions::get_version))
@@ -95,6 +96,20 @@ pub fn make_router(state: SharedState) -> Router {
.route("/mesh/tasks/{id}/merge/abort", post(mesh_merge::merge_abort))
.route("/mesh/tasks/{id}/merge/skip", post(mesh_merge::merge_skip))
.route("/mesh/tasks/{id}/merge/check", get(mesh_merge::merge_check))
+ // Checkpoint endpoints
+ .route("/mesh/tasks/{id}/checkpoint", post(mesh_supervisor::create_checkpoint))
+ .route("/mesh/tasks/{id}/checkpoints", get(mesh_supervisor::list_checkpoints))
+ // Supervisor endpoints (for supervisor.sh)
+ .route("/mesh/supervisor/contracts/{contract_id}/tasks", get(mesh_supervisor::list_contract_tasks))
+ .route("/mesh/supervisor/contracts/{contract_id}/tree", get(mesh_supervisor::get_contract_tree))
+ .route("/mesh/supervisor/tasks", post(mesh_supervisor::spawn_task))
+ .route("/mesh/supervisor/tasks/{task_id}/wait", post(mesh_supervisor::wait_for_task))
+ .route("/mesh/supervisor/tasks/{task_id}/read-file", post(mesh_supervisor::read_worktree_file))
+ // Supervisor git operations
+ .route("/mesh/supervisor/branches", post(mesh_supervisor::create_branch))
+ .route("/mesh/supervisor/tasks/{task_id}/merge", post(mesh_supervisor::merge_task))
+ .route("/mesh/supervisor/tasks/{task_id}/diff", get(mesh_supervisor::get_task_diff))
+ .route("/mesh/supervisor/pr", post(mesh_supervisor::create_pr))
// Mesh WebSocket endpoints
.route("/mesh/tasks/subscribe", get(mesh_ws::task_subscription_handler))
.route("/mesh/daemons/connect", get(mesh_daemon::daemon_handler))
@@ -113,6 +128,59 @@ pub fn make_router(state: SharedState) -> Router {
)
.route("/users/me/password", axum::routing::put(users::change_password_handler))
.route("/users/me/email", axum::routing::put(users::change_email_handler))
+ // Contract endpoints
+ .route(
+ "/contracts",
+ get(contracts::list_contracts).post(contracts::create_contract),
+ )
+ .route(
+ "/contracts/{id}",
+ get(contracts::get_contract)
+ .put(contracts::update_contract)
+ .delete(contracts::delete_contract),
+ )
+ .route("/contracts/{id}/phase", post(contracts::change_phase))
+ .route("/contracts/{id}/events", get(contracts::get_events))
+ .route("/contracts/{id}/chat", post(contract_chat::contract_chat_handler))
+ .route(
+ "/contracts/{id}/chat/history",
+ get(contract_chat::get_contract_chat_history).delete(contract_chat::clear_contract_chat_history),
+ )
+ // Contract daemon endpoints (for tasks to interact with contracts)
+ .route("/contracts/{id}/daemon/status", get(contract_daemon::get_contract_status))
+ .route("/contracts/{id}/daemon/checklist", get(contract_daemon::get_contract_checklist))
+ .route("/contracts/{id}/daemon/goals", get(contract_daemon::get_contract_goals))
+ .route("/contracts/{id}/daemon/report", post(contract_daemon::post_progress_report))
+ .route("/contracts/{id}/daemon/suggest-action", post(contract_daemon::get_suggest_action))
+ .route("/contracts/{id}/daemon/completion-action", post(contract_daemon::get_completion_action))
+ .route(
+ "/contracts/{id}/daemon/files",
+ get(contract_daemon::list_contract_files).post(contract_daemon::create_contract_file),
+ )
+ .route(
+ "/contracts/{id}/daemon/files/{file_id}",
+ get(contract_daemon::get_contract_file).put(contract_daemon::update_contract_file),
+ )
+ // Contract repository endpoints
+ .route("/contracts/{id}/repositories/remote", post(contracts::add_remote_repository))
+ .route("/contracts/{id}/repositories/local", post(contracts::add_local_repository))
+ .route("/contracts/{id}/repositories/managed", post(contracts::create_managed_repository))
+ .route(
+ "/contracts/{id}/repositories/{repo_id}",
+ axum::routing::delete(contracts::delete_repository),
+ )
+ .route(
+ "/contracts/{id}/repositories/{repo_id}/primary",
+ axum::routing::put(contracts::set_repository_primary),
+ )
+ // Contract task association endpoints
+ .route(
+ "/contracts/{id}/tasks/{task_id}",
+ post(contracts::add_task_to_contract).delete(contracts::remove_task_from_contract),
+ )
+ // Template endpoints
+ .route("/templates", get(templates::list_templates))
+ .route("/templates/{id}", get(templates::get_template))
.with_state(state);
let swagger = SwaggerUi::new("/swagger-ui")
@@ -131,12 +199,60 @@ pub fn make_router(state: SharedState) -> Router {
.layer(TraceLayer::new_for_http())
}
+/// Stale daemon cleanup interval in seconds
+const DAEMON_CLEANUP_INTERVAL_SECS: u64 = 60;
+/// Daemon heartbeat timeout in seconds (delete daemons older than this)
+const DAEMON_HEARTBEAT_TIMEOUT_SECS: i64 = 120;
+
/// Run the HTTP server with graceful shutdown support.
///
/// # Arguments
/// * `state` - Shared application state containing ML models
/// * `addr` - Address to bind to (e.g., "0.0.0.0:8080")
pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> {
+ // Start background daemon cleanup task if database is available
+ if let Some(pool) = state.db_pool.clone() {
+ // Initial cleanup of any stale daemons from previous server run
+ match crate::db::repository::delete_stale_daemons(&pool, 0).await {
+ Ok(deleted) if deleted > 0 => {
+ tracing::info!(
+ deleted = deleted,
+ "Cleaned up stale daemons from previous server run"
+ );
+ }
+ Err(e) => {
+ tracing::warn!(error = %e, "Failed to clean up stale daemons on startup");
+ }
+ _ => {}
+ }
+
+ // Spawn periodic cleanup task
+ tokio::spawn(async move {
+ let mut interval = tokio::time::interval(
+ std::time::Duration::from_secs(DAEMON_CLEANUP_INTERVAL_SECS)
+ );
+ loop {
+ interval.tick().await;
+ match crate::db::repository::delete_stale_daemons(
+ &pool,
+ DAEMON_HEARTBEAT_TIMEOUT_SECS,
+ ).await {
+ Ok(deleted) if deleted > 0 => {
+ tracing::info!(
+ deleted = deleted,
+ timeout_secs = DAEMON_HEARTBEAT_TIMEOUT_SECS,
+ "Deleted stale daemons"
+ );
+ }
+ Err(e) => {
+ tracing::warn!(error = %e, "Failed to delete stale daemons");
+ }
+ _ => {}
+ }
+ }
+ });
+ }
+
let app = make_router(state);
let listener = tokio::net::TcpListener::bind(addr).await?;