fn parse_task_id(s: &str) -> TaskId {
if s.is_empty() {
return TaskId::named(s);
}
let mut segments = Vec::new();
for part in s.split('.') {
if part.is_empty() {
return TaskId::named(s);
}
if let Ok(n) = part.parse::<u32>() {
segments.push(rhei_core::ast::TaskIdSegment::Number(n));
} else {
segments.push(rhei_core::ast::TaskIdSegment::Named(part.to_string()));
}
}
TaskId::from_segments(segments)
}
fn next_command(
input: &Path,
state_machine_path: Option<&Path>,
task_id_filter: Option<&str>,
as_json: bool,
no_callbacks: bool,
peek: bool,
rhei_scope: &[String],
) -> MietteResult<()> {
let input_buf = normalize_workspace_input(input);
let input = input_buf.as_path();
let loaded = load_plan(input)?;
let scope = resolve_rhei_scope(&loaded, rhei_scope)?;
let resolved = resolve_state_machines_for_loaded_plan(input, &loaded, state_machine_path)?;
let machines = ExecutionMachines::build(&resolved, input)?;
let workspace_root = execution_workspace_root(&machines.default_callbacks.plan_path);
let report = rhei_validator::validate_with_machine_set(&loaded.rhei, &machines.set);
if report.has_errors() {
return Err(validation_report(
input,
resolved.default.path.as_deref(),
&report.errors,
&report.help,
));
}
let resolved_filter = task_id_filter
.map(|tid| resolve_cli_task_id(&loaded, tid, &scope))
.transpose()?;
let (task_id_str, current_state_raw, current_state, task_workspace_root) = if let Some(tid) = resolved_filter.as_deref() {
let target_id = parse_task_id(tid);
let task = find_task_by_id(&loaded.rhei.tasks, &target_id)
.ok_or_else(|| {
miette!(
help = format!(
"list the task ids in this plan with: rhei list {}",
shell_quote(&input.display().to_string())
),
"task '{}' not found in the plan",
tid
)
})?;
let open_descendants = if task_is_supervising(task, machines.for_task_str(tid)) {
Vec::new()
} else {
open_descendant_tasks(task, &machines.set)
};
if !open_descendants.is_empty() {
let claimable = narrow_to_rhei_scope(
find_claimable_tasks(
&loaded.rhei,
&machines.set,
&workspace_root,
&loaded.task_roots,
),
&scope,
);
let next_step = match claimable.first() {
Some(candidate) => format!(
"claim what is ready instead: rhei next {} --task {}",
shell_quote(&input.display().to_string()),
candidate.id
),
None => format!(
"finish or cancel the open descendants first, then claim this ticket. \
See every task and its state with: rhei list {}",
shell_quote(&input.display().to_string())
),
};
return Err(miette!(
help = next_step,
"Task {} cannot be claimed while {} descendant task(s) are still open.\n\
Open descendants: {}",
tid,
open_descendants.len(),
format_open_descendants(&open_descendants, &machines.set)
));
}
if let Some(hold) = held_by_supervisor(task, &loaded.rhei, &machines.set) {
let (supervisor, supervisor_state) = (&hold.supervisor, &hold.state);
let holder = find_task_by_id(&loaded.rhei.tasks, supervisor)
.and_then(|task| task.assignee.clone());
let next_step = if let Some(holder) = holder {
format!(
"Task {supervisor} is the ticket to work and {holder} holds it; hand it \
back with: rhei release {} --task {supervisor}",
shell_quote(&input.display().to_string())
)
} else if hold.awaiting_human {
format!(
"Task {supervisor} is at a human gate and still holds this subtree. A human \
moves it back into its supervising state to resume supervision, or anywhere \
else to release the subtree: rhei{} transition {} --task {supervisor} \
--from {supervisor_state} --to <state>",
state_machine_flag(resolved.default.path.as_deref()),
shell_quote(&input.display().to_string())
)
} else {
format!(
"the supervisor releases the subtree on its next visit. Work the supervisor \
instead: rhei{} next {} --task {supervisor}",
state_machine_flag(resolved.default.path.as_deref()),
shell_quote(&input.display().to_string())
)
};
return Err(miette!(
help = next_step,
"Task {} is held by supervisor Task {} ({})",
tid,
supervisor,
supervisor_state
));
}
if let Some(assignee) = task.assignee.as_deref() {
let plan_arg = shell_quote(&input.display().to_string());
return Err(miette!(
help = format!(
"hand it back with: rhei release {plan_arg} --task {tid} — or claim \
whatever is ready instead: rhei next {plan_arg}"
),
"Task {} is already assigned to {}",
tid,
assignee
));
}
let machine = machines.for_task_str(tid);
let state_name = normalized_state_name(task.state.as_str(), machine);
let is_initial = task_is_in_initial_state(task, &state_name, machine);
if is_initial {
let mut all_tasks = Vec::new();
collect_plan_tasks(&loaded.rhei.tasks, &mut all_tasks);
let state_map = plan_state_map(&all_tasks, &machines.set);
let all_priors_done = task.prior.iter().all(|dep_id| {
state_map
.get(dep_id)
.map(|s| dependency_is_satisfied(s, machines.set.for_task(dep_id)))
.unwrap_or(false)
});
if !all_priors_done {
let detail = first_blocking_prior(task, &state_map, &machines.set, &scope)
.map(|prior| format!("; waiting on {}", prior))
.unwrap_or_default();
return Err(miette!(
help = format!(
"finish the prerequisite first, or see what is claimable now: rhei list {}",
shell_quote(&input.display().to_string())
),
"Task {} is blocked by incomplete prerequisites{}",
tid,
detail
));
}
}
let state_def = machine
.states
.get(&state_name)
.ok_or_else(|| {
miette!(help = internal_error_help(), "state '{}' missing from loaded machine", state_name)
})?;
let settings = load_merged_settings(&workspace_root)?;
let task_workspace_root = loaded.task_root(tid, &workspace_root);
ensure_state_inputs_exist_for_transition(
&task_workspace_root,
Some(task),
tid,
&state_name,
state_def,
Some(render_visit_count(
loaded.rhei.metadata.as_ref(),
&task.id,
&state_name,
task.state.as_str(),
machine,
)),
machine,
&settings,
&format!("Task {} cannot be claimed in state {}.", tid, state_name),
)?;
(tid.to_string(), task.state.as_str().to_string(), state_name, task_workspace_root)
} else {
let ready = narrow_to_rhei_scope(
find_claimable_tasks(&loaded.rhei, &machines.set, &workspace_root, &loaded.task_roots),
&scope,
);
if ready.is_empty() {
return Err(miette!(
help = "see every task and its state with: rhei list <plan>",
"{}",
diagnose_no_claimable(
&loaded.rhei,
&machines.set,
input,
resolved.default.path.as_deref(),
&scope
)
));
}
let task = ready.into_iter().next().unwrap();
let machine = machines.for_task(&task.id);
let state_name = normalized_state_name(task.state.as_str(), machine);
let state_def = machine
.states
.get(&state_name)
.ok_or_else(|| {
miette!(help = internal_error_help(), "state '{}' missing from loaded machine", state_name)
})?;
let settings = load_merged_settings(&workspace_root)?;
let task_workspace_root = loaded.task_root(&task.id.to_string(), &workspace_root);
ensure_state_inputs_exist_for_transition(
&task_workspace_root,
Some(task),
&task.id.to_string(),
&state_name,
state_def,
Some(render_visit_count(
loaded.rhei.metadata.as_ref(),
&task.id,
&state_name,
task.state.as_str(),
machine,
)),
machine,
&settings,
&format!("Task {} cannot be claimed in state {}.", task.id, state_name),
)?;
(task.id.to_string(), task.state.to_string(), state_name, task_workspace_root)
};
let target_id = parse_task_id(&task_id_str);
let machine = machines.for_task_str(&task_id_str);
let callback_paths = machines.callbacks_for_str(&task_id_str);
let selected_task = find_task_by_id(&loaded.rhei.tasks, &target_id)
.ok_or_else(|| {
miette!(
help = format!(
"list the task ids in this plan with: rhei list {}",
shell_quote(&input.display().to_string())
),
"task '{}' not found in the plan",
task_id_str
)
})?;
let is_initial = task_is_in_initial_state(selected_task, ¤t_state, machine);
let current_state_def = machine
.states
.get(¤t_state)
.ok_or_else(|| {
miette!(help = internal_error_help(), "state '{}' missing from loaded machine", current_state)
})?;
let auto_transition_initial = is_initial
&& !state_declares_autonomous_execution(current_state_def)
&& initial_state_has_non_terminal_forward_transition(selected_task, &loaded.rhei, machine)?;
let route = loaded.task_route(&task_id_str, input);
let final_state = if auto_transition_initial && !peek {
let target_id = parse_task_id(&task_id_str);
let task = find_task_by_id(&loaded.rhei.tasks, &target_id)
.ok_or_else(|| {
miette!(
help = format!(
"list the task ids in this plan with: rhei list {}",
shell_quote(&input.display().to_string())
),
"task '{}' not found in the plan",
task_id_str
)
})?;
let to_state = find_next_transition(task, &loaded.rhei, machine)?.ok_or_else(|| {
miette!(
help = format!(
"no transition leaves '{current_state_raw}'. See the machine's edges with: \
rhei states"
),
"no forward transition available from state '{}'",
current_state_raw
)
})?;
execute_transition(
TransitionFiles { task_file: &route.task_file, metadata_file: &route.metadata_file, metadata_id: &route.metadata_id, artifact_root: &route.execution_root, artifact_id: &task_id_str },
callback_paths,
machine,
&route.local_id,
¤t_state,
&to_state,
None,
no_callbacks,
)?
} else {
current_state.clone()
};
let loaded = load_plan(input)?;
let target_id = parse_task_id(&task_id_str);
let task = find_task_by_id(&loaded.rhei.tasks, &target_id)
.ok_or_else(|| {
miette!(help = internal_error_help(), "task '{}' not found after transition", task_id_str)
})?;
let settings = load_merged_settings(&workspace_root)?;
let no_agent_opts = default_run_options();
let resolved = match resolve_agent_for_task(machine, &final_state, &settings, &no_agent_opts, task) {
Ok(resolved) => resolved,
Err(err) => {
eprintln!(
"warning: could not resolve agent for state '{}': {}",
final_state, err
);
None
}
};
let agent_id_str = resolved.as_ref().map(|r| r.agent.id().to_string());
let model_id_str = resolved.as_ref().and_then(|r| r.model.clone());
let model_provider_str = resolved.as_ref().and_then(|r| r.model_provider.clone());
let model_name_str = resolved.as_ref().and_then(|r| r.model_name.clone());
let mut claimed_as: Option<String> = None;
if !peek && task.assignee.is_none() {
let assignee = agent_id_str.as_deref().unwrap_or("manual");
claimed_as = Some(assignee.to_string());
let final_state_def = machine
.states
.get(&final_state)
.ok_or_else(|| miette!(
help = internal_error_help(),
"state '{}' missing from loaded machine", final_state
))?;
write_task_assignee(
&route.task_file,
&route.local_id,
&task_id_str,
&final_state,
machine,
TaskAssigneeClaimContext {
workspace_root: &task_workspace_root,
metadata: loaded.rhei.metadata.as_ref(),
state_def: final_state_def,
settings: &settings,
},
assignee,
)?;
}
let tooling = resolve_tooling(machine, &final_state, &settings);
let mut memory =
prompt_memory(&loaded, input, &workspace_root.join("runtime"), BTreeSet::new());
memory.pastes_task_inputs = false;
memory.absolute_paths = true;
let render_context = RuntimeTemplateContext {
workspace_root: &task_workspace_root,
task_roots: Some(&loaded.task_roots),
plan_tasks: Some(&loaded.rhei.tasks),
checkout_root: &task_workspace_root,
plan_path: &callback_paths.plan_path,
state_machine_path: callback_paths.state_machine_path.as_deref(),
plan_title: &loaded.rhei.title,
task,
state_name: &final_state,
current_state_raw: task.state.as_str(),
machine,
metadata: loaded.rhei.metadata.as_ref(),
target: resolved.as_ref().and_then(|r| r.target.as_ref()),
model: model_id_str.as_deref(),
model_provider: model_provider_str.as_deref(),
model_name: model_name_str.as_deref(),
agent: agent_id_str.as_deref(),
agent_mode: resolved.as_ref().and_then(|r| r.mode.as_deref()),
tooling: Some(&tooling),
memory: Some(&memory),
};
let instructions = resolve_runtime_template_text(
state_instructions(machine, &final_state).as_str(),
&render_context,
);
let personality = state_personality(machine, final_state.as_str())
.map(|text| resolve_runtime_template_text(&text, &render_context));
let checkpoints = render_supervision_checkpoints(&render_context)?;
let supervisor_brief = render_supervisor_brief(&render_context)?;
let position = render_position(&render_context);
let plan_history = render_plan_history(&render_context)?;
let previous_visits = render_previous_visits(&render_context)?;
let navigation = render_rhei_navigation(&render_context);
let navigation = if navigation.is_empty() {
String::new()
} else {
format!("\n## Rhei Navigation\n{navigation}")
};
let release_command = format!(
"rhei{} transition {} --task {} --from {} --to {}",
state_machine_flag(state_machine_path),
shell_quote(&input.display().to_string()),
task.id,
shell_quote(&final_state),
shell_quote(&final_state)
);
let supervising = render_supervisor_visit_notes(&render_context, &release_command);
print_next_output(NextOutput {
as_json,
peek,
claimed_as: claimed_as.as_deref(),
task,
from_state: ¤t_state_raw,
to_state: task.state.as_str(),
from_state_name: ¤t_state,
to_state_name: &final_state,
machine,
personality: personality.as_deref(),
instructions: &instructions,
checkpoints: &checkpoints,
supervisor_brief: &supervisor_brief,
supervising: &supervising,
position: &position,
plan_history: &plan_history,
previous_visits: &previous_visits,
navigation: &navigation,
agent_id: agent_id_str.as_deref(),
model_id: model_id_str.as_deref(),
});
Ok(())
}