fn ancestor_chain<'a>(
tasks: &'a [rhei_core::ast::Task],
target: &TaskId,
) -> Vec<&'a rhei_core::ast::Task> {
fn walk<'a>(
tasks: &'a [rhei_core::ast::Task],
target: &TaskId,
stack: &mut Vec<&'a rhei_core::ast::Task>,
) -> bool {
for task in tasks {
if &task.id == target {
return true;
}
stack.push(task);
if walk(&task.children, target, stack) {
return true;
}
stack.pop();
}
false
}
let mut stack = Vec::new();
if walk(tasks, target, &mut stack) {
stack.reverse();
return stack;
}
Vec::new()
}
fn supervisor_is_in_flight(supervisor: &rhei_core::ast::Task, local_id: &str) -> bool {
if supervisor.assignee.is_some() {
return true;
}
let matches = |var: &str, want: &str| {
std::env::var(var).is_ok_and(|value| value == want)
};
matches("RHEI_TASK_ID", &supervisor.id.to_string()) || matches("RHEI_TASK_ID_LOCAL", local_id)
}
struct SupervisionTransition<'a> {
machine: &'a rhei_validator::StateMachine,
task: &'a rhei_core::ast::Task,
ancestors: &'a [rhei_core::ast::Task],
metadata_key: &'a TaskId,
metadata_prefix: &'a str,
local_id: &'a str,
from: &'a str,
to: &'a str,
to_visit: u64,
}
impl SupervisionTransition<'_> {
fn to_is_terminal(&self) -> bool {
self.machine.states.get(self.to).map(|def| def.terminal).unwrap_or(false)
}
fn checkpoint(&self) -> Option<(&rhei_core::ast::Task, SupervisionCheckpoint)> {
if self.from == self.to {
let from_def = self.machine.states.get(self.from);
if from_def.map(|def| def.poll.is_some()).unwrap_or(false)
|| from_def.and_then(|def| def.execute_on()).is_some()
{
return None;
}
}
let (supervisor, execute_on) =
self.ancestors.iter().enumerate().find_map(|(distance, ancestor)| {
let execute_on = execute_on_of(
self.machine,
&normalized_state_name(ancestor.state.as_str(), self.machine),
)?;
let in_scope = match execute_on.scope() {
rhei_validator::SupervisionScope::Child => distance == 0,
rhei_validator::SupervisionScope::Descendant => true,
};
in_scope.then_some((ancestor, execute_on))
})?;
if execute_on.event() == rhei_validator::SupervisionEvent::Terminal && !self.to_is_terminal()
{
return None;
}
if supervisor_is_in_flight(supervisor, &supervisor_local_id(supervisor, self.local_id, self.task)) {
return None;
}
Some((
supervisor,
SupervisionCheckpoint {
task: self.local_id.to_string(),
from: self.from.to_string(),
to: self.to.to_string(),
visit: self.to_visit.max(1),
},
))
}
}
fn supervisor_local_id(
supervisor: &rhei_core::ast::Task,
local_id: &str,
task: &rhei_core::ast::Task,
) -> String {
let qualified = task.id.to_string();
let prefix = qualified.strip_suffix(local_id).unwrap_or("");
supervisor.id.to_string().strip_prefix(prefix).unwrap_or(&supervisor.id.to_string()).to_string()
}
fn apply_supervision_transition(
existing: Option<&Metadata>,
move_: SupervisionTransition<'_>,
) -> Option<Metadata> {
let from_supervises = execute_on_of(move_.machine, move_.from).is_some();
let to_supervises = execute_on_of(move_.machine, move_.to).is_some();
let to_is_gating =
move_.machine.states.get(move_.to).map(|def| def.gating).unwrap_or(false);
let mut updated: Option<Metadata> = None;
if from_supervises && move_.from == move_.to {
updated =
Some(record_supervision_release(updated.as_ref().or(existing), move_.metadata_key));
} else if from_supervises && to_is_gating {
updated = Some(record_supervision_hold(
updated.as_ref().or(existing),
move_.metadata_key,
None,
));
} else if from_supervises {
updated = clear_supervision_for_task(updated.as_ref().or(existing), move_.metadata_key);
} else if move_.from != move_.to
&& recorded_supervision_phase(updated.as_ref().or(existing), move_.metadata_key)
.is_some()
{
updated = clear_supervision_for_task(updated.as_ref().or(existing), move_.metadata_key);
}
if to_supervises && move_.from != move_.to {
updated = Some(record_supervision_hold(
updated.as_ref().or(existing),
move_.metadata_key,
None,
));
}
if let Some((supervisor, checkpoint)) = move_.checkpoint() {
let supervisor_key = parse_task_id(&format!(
"{}{}",
move_.metadata_prefix,
supervisor_local_id(supervisor, move_.local_id, move_.task)
));
updated = Some(record_supervision_hold(
updated.as_ref().or(existing),
&supervisor_key,
Some(&checkpoint),
));
}
updated
}
fn transition_ends_supervisor_visit(
machine: &rhei_validator::StateMachine,
from: &str,
to: &str,
) -> bool {
from == to && execute_on_of(machine, from).is_some()
}
fn announce_supervision_gate_handoff(
machine: &rhei_validator::StateMachine,
task_id: &str,
from: &str,
to: &str,
) {
if from == to || execute_on_of(machine, from).is_none() {
return;
}
if !machine.states.get(to).map(|def| def.gating).unwrap_or(false) {
return;
}
emit_run_diag(
rhei_tui::MessageLevel::Warn,
format!(
"Task {task_id} left supervision for human gate '{to}'; its subtree stays held \
until a human moves it"
),
);
}
fn supervision_after_transition(
existing: Option<&Metadata>,
machine: &rhei_validator::StateMachine,
task_info: &TransitionTaskInfo,
files: TransitionFiles<'_>,
metadata_key: &TaskId,
move_: (&str, &str, &str),
to_visit: u64,
) -> Option<Metadata> {
let (local_id, from, to) = move_;
announce_supervision_gate_handoff(machine, files.artifact_id, from, to);
apply_supervision_transition(
existing,
SupervisionTransition {
machine,
task: &task_info.task,
ancestors: &task_info.ancestors,
metadata_key,
metadata_prefix: files.metadata_id.strip_suffix(local_id).unwrap_or(""),
local_id,
from,
to,
to_visit,
},
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum SupervisionVerdict {
Unsupervised,
SupervisorReady,
SupervisorWaiting,
Held { supervisor: TaskId, state: String },
}
fn task_index<'a>(
tasks: &[&'a rhei_core::ast::Task],
) -> std::collections::HashMap<TaskId, &'a rhei_core::ast::Task> {
tasks.iter().map(|task| (task.id.clone(), *task)).collect()
}
fn any_descendant_in_flight(
task: &rhei_core::ast::Task,
in_flight: &dyn Fn(&rhei_core::ast::Task) -> bool,
) -> bool {
task.children
.iter()
.any(|child| in_flight(child) || any_descendant_in_flight(child, in_flight))
}
fn supervision_holds_subtree(
task: &rhei_core::ast::Task,
machine: &rhei_validator::StateMachine,
metadata: Option<&Metadata>,
) -> bool {
match recorded_supervision_phase(metadata, &task.id) {
Some(phase) => phase == SupervisionPhase::Held,
None => task_is_supervising(task, machine),
}
}
fn supervision_verdict(
task: &rhei_core::ast::Task,
index: &std::collections::HashMap<TaskId, &rhei_core::ast::Task>,
machines: &rhei_validator::MachineSet,
metadata: Option<&Metadata>,
in_flight: &dyn Fn(&rhei_core::ast::Task) -> bool,
) -> SupervisionVerdict {
let mut cursor = task.id.parent();
while let Some(id) = cursor {
let Some(ancestor) = index.get(&id) else { break };
let machine = machines.for_task(&ancestor.id);
if supervision_holds_subtree(ancestor, machine, metadata)
|| (task_is_supervising(ancestor, machine) && in_flight(ancestor))
{
return SupervisionVerdict::Held {
supervisor: ancestor.id.clone(),
state: normalized_state_name(ancestor.state.as_str(), machine),
};
}
cursor = id.parent();
}
let machine = machines.for_task(&task.id);
if !task_is_supervising(task, machine) {
return SupervisionVerdict::Unsupervised;
}
if any_descendant_in_flight(task, in_flight) {
return SupervisionVerdict::SupervisorWaiting;
}
match supervision_phase(metadata, &task.id) {
SupervisionPhase::Held => SupervisionVerdict::SupervisorReady,
SupervisionPhase::Released => SupervisionVerdict::SupervisorWaiting,
}
}
fn supervision_verdict_for(
task: &rhei_core::ast::Task,
index: &std::collections::HashMap<TaskId, &rhei_core::ast::Task>,
machines: &rhei_validator::MachineSet,
metadata: Option<&Metadata>,
spawned: &HashSet<String>,
) -> SupervisionVerdict {
let in_flight = |candidate: &rhei_core::ast::Task| {
candidate.assignee.is_some() || spawned.contains(&candidate.id.to_string())
};
supervision_verdict(task, index, machines, metadata, &in_flight)
}
fn subtree_admits_to_ready_set(
task: &rhei_core::ast::Task,
index: &std::collections::HashMap<TaskId, &rhei_core::ast::Task>,
machines: &rhei_validator::MachineSet,
metadata: Option<&Metadata>,
spawned: &HashSet<String>,
) -> bool {
match supervision_verdict_for(task, index, machines, metadata, spawned) {
SupervisionVerdict::SupervisorReady => true,
SupervisionVerdict::SupervisorWaiting | SupervisionVerdict::Held { .. } => false,
SupervisionVerdict::Unsupervised => descendants_are_terminal(task, machines),
}
}
struct SupervisorHold {
supervisor: TaskId,
state: String,
awaiting_human: bool,
}
fn held_by_supervisor(
task: &rhei_core::ast::Task,
rhei: &rhei_core::ast::Rhei,
machines: &rhei_validator::MachineSet,
) -> Option<SupervisorHold> {
let mut all = Vec::new();
collect_plan_tasks(&rhei.tasks, &mut all);
let index = task_index(&all);
match supervision_verdict_for(
task,
&index,
machines,
rhei.metadata.as_ref(),
&HashSet::new(),
) {
SupervisionVerdict::Held { supervisor, state } => {
let awaiting_human = machines
.for_task(&supervisor)
.states
.get(&state)
.map(|def| def.gating)
.unwrap_or(false);
Some(SupervisorHold { supervisor, state, awaiting_human })
}
_ => None,
}
}