use crate::*;
use carryctx::application::runtime::InvocationContext;
use carryctx::domain::collaboration::{Handoff, HandoffStatus};
use carryctx::error::{CarryCtxError, ExitCode};
use clap::Parser;
#[derive(Parser, Debug)]
pub enum HandoffCommand {
Create {
#[arg(long)]
target: String,
#[arg(long)]
summary: Option<String>,
#[arg(long)]
task: Option<String>,
},
List,
Show { handoff_ref: String },
Accept {
handoff_ref: String,
#[arg(long)]
claim_task: bool,
},
Reject {
handoff_ref: String,
#[arg(long)]
reason: Option<String>,
},
Close { handoff_ref: String },
}
#[derive(Parser, Debug)]
pub struct HandoffArgs {
#[command(subcommand)]
pub command: HandoffCommand,
}
pub fn handle_handoff(
args: &HandoffArgs,
ctx: &InvocationContext,
is_json: bool,
) -> Result<ExitCode, ExitCode> {
let mut runtime = try_open_runtime(ctx)?;
let project_id = &runtime.config.project.id;
let conn = runtime.database.connection_mut();
let handoff_repo = SqliteHandoffRepository::new(conn);
let event_repo = SqliteEventRepository::new(conn);
let now = chrono::Utc::now().to_rfc3339();
match &args.command {
HandoffCommand::Create {
target,
summary,
task,
} => {
let handoff_id = ulid::Ulid::generate().to_string();
let display_id = format!("HO-{}", &handoff_id[..8]);
let record = Handoff {
id: handoff_id,
display_id,
project_id: project_id.to_string(),
task_id: task.clone().unwrap_or_default(),
source_agent_id: ctx.agent.clone().unwrap_or_else(|| "unknown".to_string()),
source_session_id: ctx.session.clone(),
target_agent_id: Some(target.clone()),
summary: summary.clone(),
completed_work: vec![],
remaining_work: vec![],
blockers: vec![],
risks: vec![],
next_steps: vec![],
changed_files: vec![],
head: Some(runtime.git_project.head.clone()),
branch: runtime.git_project.branch.clone(),
status: HandoffStatus::Open,
created_at: now.clone(),
updated_at: now,
};
let result = handoff_repo.create(&record);
if let Ok(ref _h) = result {
let _ = event_repo.append(&NewEvent {
id: ulid::Ulid::generate().to_string(),
project_id: project_id.to_string(),
event_type: "handoff.created".into(),
actor_agent_id: ctx.agent.clone(),
session_id: ctx.session.clone(),
task_id: task.clone(),
payload: serde_json::json!({ "handoffId": record.id }),
occurred_at: chrono::Utc::now().to_rfc3339(),
});
}
render_and_print("handoff.create", result, is_json, ctx.quiet)
}
HandoffCommand::List => {
let result = handoff_repo.list(project_id);
render_and_print("handoff.list", result, is_json, ctx.quiet)
}
HandoffCommand::Show { handoff_ref } => {
let result = handoff_repo.find_by_id(project_id, handoff_ref);
let result = result.and_then(|opt| {
opt.ok_or_else(|| {
CarryCtxError::resource_not_found(format!("Handoff '{handoff_ref}' not found"))
})
});
render_and_print("handoff.show", result, is_json, ctx.quiet)
}
HandoffCommand::Accept {
handoff_ref,
claim_task: _,
} => {
let result = handoff_repo.find_by_id(project_id, handoff_ref);
let result = result.and_then(|opt| {
opt.ok_or_else(|| {
CarryCtxError::resource_not_found(format!("Handoff '{handoff_ref}' not found"))
})
});
match result {
Ok(handoff) => {
handoff_repo
.update_status(&handoff.id, project_id, HandoffStatus::Accepted, &now)
.map_err(|e| e.exit_code)?;
let _ = event_repo.append(&NewEvent {
id: ulid::Ulid::generate().to_string(),
project_id: project_id.to_string(),
event_type: "handoff.accepted".into(),
actor_agent_id: ctx.agent.clone(),
session_id: ctx.session.clone(),
task_id: Some(handoff.task_id.clone()),
payload: serde_json::json!({ "handoffId": handoff.id }),
occurred_at: chrono::Utc::now().to_rfc3339(),
});
render_and_print("handoff.accept", Ok(handoff), is_json, ctx.quiet)
}
Err(e) => render_and_print::<serde_json::Value>(
"handoff.accept",
Err(e),
is_json,
ctx.quiet,
),
}
}
HandoffCommand::Reject {
handoff_ref,
reason: _,
} => {
let result = handoff_repo.find_by_id(project_id, handoff_ref);
let result = result.and_then(|opt| {
opt.ok_or_else(|| {
CarryCtxError::resource_not_found(format!("Handoff '{handoff_ref}' not found"))
})
});
match result {
Ok(handoff) => {
handoff_repo
.update_status(&handoff.id, project_id, HandoffStatus::Rejected, &now)
.map_err(|e| e.exit_code)?;
let _ = event_repo.append(&NewEvent {
id: ulid::Ulid::generate().to_string(),
project_id: project_id.to_string(),
event_type: "handoff.rejected".into(),
actor_agent_id: ctx.agent.clone(),
session_id: ctx.session.clone(),
task_id: Some(handoff.task_id.clone()),
payload: serde_json::json!({ "handoffId": handoff.id }),
occurred_at: chrono::Utc::now().to_rfc3339(),
});
render_and_print("handoff.reject", Ok(handoff), is_json, ctx.quiet)
}
Err(e) => render_and_print::<serde_json::Value>(
"handoff.reject",
Err(e),
is_json,
ctx.quiet,
),
}
}
HandoffCommand::Close { handoff_ref } => {
let result = handoff_repo.find_by_id(project_id, handoff_ref);
let result = result.and_then(|opt| {
opt.ok_or_else(|| {
CarryCtxError::resource_not_found(format!("Handoff '{handoff_ref}' not found"))
})
});
match result {
Ok(handoff) => {
handoff_repo
.update_status(&handoff.id, project_id, HandoffStatus::Closed, &now)
.map_err(|e| e.exit_code)?;
render_and_print("handoff.close", Ok(handoff), is_json, ctx.quiet)
}
Err(e) => render_and_print::<serde_json::Value>(
"handoff.close",
Err(e),
is_json,
ctx.quiet,
),
}
}
}
}