use super::*;
#[derive(Debug)]
pub(crate) enum Cond {
Stop,
Hitl,
Auq,
Notification(Option<regex::Regex>),
Tool {
name: String,
input_re: Option<regex::Regex>,
},
Write {
path_re: regex::Regex,
line_re: Option<regex::Regex>,
},
VerdictIs(Verdict),
}
impl Cond {
#[must_use]
pub(crate) fn needs_verdict(&self) -> bool {
matches!(self, Cond::Stop | Cond::Hitl | Cond::VerdictIs(_))
}
}
pub(crate) fn parse_condition(s: &str) -> Result<Cond> {
let mk_re = |p: &str, what: &str| -> Result<regex::Regex> {
regex::Regex::new(p).map_err(|e| anyhow::anyhow!("--until {what}: bad regex `{p}`: {e}"))
};
if let Some(rest) = s.strip_prefix("notification") {
return Ok(match rest.strip_prefix(':') {
Some(re) => Cond::Notification(Some(mk_re(re, "notification")?)),
None if rest.is_empty() => Cond::Notification(None),
_ => bail!("--until: unknown condition `{s}` (did you mean `notification:{rest}`?)"),
});
}
if let Some(rest) = s.strip_prefix("tool:") {
let mut it = rest.splitn(2, ':');
let name = it.next().unwrap_or_default();
if name.is_empty() {
bail!("--until tool: needs a tool NAME (`tool:NAME[:REGEX]`)");
}
let input_re = it.next().map(|re| mk_re(re, "tool")).transpose()?;
return Ok(Cond::Tool {
name: name.to_string(),
input_re,
});
}
if let Some(rest) = s.strip_prefix("write:") {
let mut it = rest.splitn(2, ':');
let path = it.next().unwrap_or_default();
if path.is_empty() {
bail!("--until write: needs a path regex (`write:PATH_RE[:LINE_RE]`)");
}
let line_re = it.next().map(|re| mk_re(re, "write")).transpose()?;
return Ok(Cond::Write {
path_re: mk_re(path, "write")?,
line_re,
});
}
if let Some(v) = s.strip_prefix("verdict:") {
let verdict = match v {
"running" => Verdict::Running,
"waiting-children" => Verdict::WaitingChildren,
"waiting-hitl" => Verdict::WaitingHitl,
"idle-eot" => Verdict::IdleEot,
"stale-dead" => Verdict::StaleDead,
"unknown" => Verdict::Unknown,
other => bail!(
"--until verdict: unknown verdict `{other}` (running | waiting-children | \
waiting-hitl | idle-eot | stale-dead | unknown)"
),
};
return Ok(Cond::VerdictIs(verdict));
}
match s {
"stop" => Ok(Cond::Stop),
"hitl" => Ok(Cond::Hitl),
"auq" => Ok(Cond::Auq),
other => bail!(
"--until: unknown condition `{other}`. The set: stop | hitl | auq | \
notification[:REGEX] | tool:NAME[:REGEX] | write:PATH_RE[:LINE_RE] | \
verdict:V"
),
}
}
pub(crate) fn record_matches(cond: &Cond, rec: &crate::model::Record, is_main: bool) -> bool {
match cond {
Cond::Notification(re) => {
if !is_main {
return false;
}
let Some(label) = rec.automation_label() else {
return false;
};
re.as_ref().is_none_or(|r| {
r.is_match(&label)
|| rec
.reconstructed_user_text(None)
.as_deref()
.is_some_and(|t| r.is_match(t))
})
}
Cond::Auq => {
if rec.is_elicitation_marker() {
return rec.csift_phase.as_deref() == Some("pending");
}
rec.blocks().is_some_and(|bs| {
bs.iter().any(|b| {
matches!(b, crate::model::Block::ToolUse { name: Some(n), .. }
if n == "AskUserQuestion")
})
})
}
Cond::Tool { name, input_re } => rec.blocks().is_some_and(|bs| {
bs.iter().any(|b| match b {
crate::model::Block::ToolUse {
name: Some(n),
input,
..
} if n == name => input_re.as_ref().is_none_or(|re| {
input
.as_ref()
.map(|i| re.is_match(&i.to_string()))
.unwrap_or(false)
}),
_ => false,
})
}),
Cond::Write { path_re, line_re } => rec.blocks().is_some_and(|bs| {
bs.iter().any(|b| match b {
crate::model::Block::ToolUse {
name: Some(n),
input: Some(input),
..
} if matches!(n.as_str(), "Write" | "Edit" | "MultiEdit" | "NotebookEdit") => {
let path = input
.get("file_path")
.or_else(|| input.get("notebook_path"))
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
if !path_re.is_match(path) {
return false;
}
line_re.as_ref().is_none_or(|re| {
["content", "new_string", "new_source"].iter().any(|k| {
input
.get(*k)
.and_then(serde_json::Value::as_str)
.is_some_and(|c| c.lines().any(|l| re.is_match(l)))
})
})
}
_ => false,
})
}),
Cond::Stop | Cond::Hitl | Cond::VerdictIs(_) => false,
}
}
pub(crate) fn verdict_matches(cond: &Cond, verdict: Verdict) -> bool {
match cond {
Cond::Stop => matches!(verdict, Verdict::IdleEot | Verdict::StaleDead),
Cond::Hitl => verdict == Verdict::WaitingHitl,
Cond::VerdictIs(v) => verdict == *v,
_ => false,
}
}