use super::{Context, args};
use crate::boundary::{Action, Gesture};
use crate::fork::Attempt;
const GOAL_FLAG: &str = "--goal";
const FROM: &str = "from";
const ROLE: &str = "role";
const SKILLS: &str = "skills";
const SKILL_SEP: char = ',';
pub(super) fn fork(tail: &str, ctx: &Context, verb: &str) -> Result<Gesture, String> {
let (head, goal) = split_goal(tail, verb)?;
let (extra, flags) = args::split_flags(&head);
if !extra.is_empty() {
return Err(format!("/{verb}: unexpected words {extra:?} before --goal"));
}
args::only(&flags, &[FROM, ROLE, SKILLS], verb)?;
let from = args::flag(&flags, FROM, verb)?
.ok_or_else(|| format!("/{verb}: --from is required — name the ref to fork off"))?;
let role = args::flag(&flags, ROLE, verb)?
.ok_or_else(|| format!("/{verb}: --role is required — it is what names the model"))?;
Ok(Gesture::Act(Action::Fork {
workspace: args::workspace(ctx, verb)?,
parent: args::agent(ctx, verb)?,
attempt: Attempt {
from,
role,
skills: skills(args::flag(&flags, SKILLS, verb)?),
},
goal,
}))
}
fn split_goal(tail: &str, verb: &str) -> Result<(String, String), String> {
let (head, payload) = tail.split_once(GOAL_FLAG).ok_or_else(|| {
format!("/{verb}: --goal is required, and everything after it is the goal")
})?;
let goal = payload.trim();
if goal.is_empty() {
return Err(format!("/{verb}: --goal needs a value"));
}
Ok((head.to_owned(), goal.to_owned()))
}
fn skills(value: Option<String>) -> Vec<String> {
value
.iter()
.flat_map(|text| text.split(SKILL_SEP))
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
pub(super) fn spell(attempt: &Attempt, goal: &str) -> String {
let list = if attempt.skills.is_empty() {
String::new()
} else {
format!(" --skills {}", attempt.skills.join(","))
};
format!(
"/fork --from {} --role {}{list} {GOAL_FLAG} {goal}",
attempt.from, attempt.role
)
}