fn shell_quote(value: &str) -> String {
if value.is_empty() {
return "''".to_string();
}
if value.starts_with('=') {
return format!("'{}'", value.replace('\'', "'\"'\"'"));
}
if value.bytes().all(|byte| {
matches!(
byte,
b'a'..=b'z'
| b'A'..=b'Z'
| b'0'..=b'9'
| b'_'
| b'-'
| b'.'
| b'/'
| b':'
| b'@'
| b'%'
| b'+'
| b'='
| b','
)
}) {
return value.to_string();
}
format!("'{}'", value.replace('\'', "'\"'\"'"))
}
fn shell_assignment(key: &str, value: &str) -> String {
format!("{key}={}", shell_quote(value))
}
fn shell_arg(value: &str) -> String {
if let Some((key, rhs)) = value.split_once('=') {
let is_identifier = !key.is_empty()
&& key.starts_with(|ch: char| ch.is_ascii_alphabetic())
&& key.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-');
if is_identifier {
return shell_assignment(key, rhs);
}
}
shell_quote(value)
}
fn shell_command<I, S>(parts: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
parts.into_iter().map(|part| shell_arg(part.as_ref())).collect::<Vec<_>>().join(" ")
}
fn nearest_match<I, S>(input: &str, candidates: I) -> Option<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let input = input.trim();
if input.is_empty() {
return None;
}
let lowered = input.to_ascii_lowercase();
let closest = candidates
.into_iter()
.map(|candidate| {
let candidate = candidate.as_ref().to_string();
let distance = levenshtein_distance(&lowered, &candidate.to_ascii_lowercase());
(candidate, distance)
})
.min_by(|(left_name, left), (right_name, right)| {
left.cmp(right).then_with(|| left_name.cmp(right_name))
})?;
let (name, distance) = closest;
let threshold = std::cmp::max(2, input.chars().count() / 3);
(distance <= threshold).then_some(name)
}
const MAX_LISTED_CANDIDATES: usize = 8;
fn did_you_mean(input: &str, known: &[String]) -> Option<String> {
let mut seen = HashSet::new();
let known = known.iter().filter(|name| seen.insert(name.as_str())).collect::<Vec<_>>();
if known.is_empty() {
return None;
}
if let Some(name) = nearest_match(input, known.iter().map(|name| name.as_str())) {
return Some(format!("Did you mean '{name}'?"));
}
if known.len() > MAX_LISTED_CANDIDATES {
let head = known[..MAX_LISTED_CANDIDATES]
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>()
.join(", ");
return Some(format!(
"Valid values include: {head} (and {} more).",
known.len() - MAX_LISTED_CANDIDATES
));
}
Some(format!(
"Valid values: {}.",
known.iter().map(|name| name.as_str()).collect::<Vec<_>>().join(", ")
))
}
fn levenshtein_distance(left: &str, right: &str) -> usize {
if left == right {
return 0;
}
if left.is_empty() {
return right.chars().count();
}
if right.is_empty() {
return left.chars().count();
}
let right_chars = right.chars().collect::<Vec<_>>();
let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
let mut current = vec![0; right_chars.len() + 1];
for (left_index, left_char) in left.chars().enumerate() {
current[0] = left_index + 1;
for (right_index, right_char) in right_chars.iter().enumerate() {
let insertion = current[right_index] + 1;
let deletion = previous[right_index + 1] + 1;
let substitution = previous[right_index] + usize::from(left_char != *right_char);
current[right_index + 1] = insertion.min(deletion).min(substitution);
}
std::mem::swap(&mut previous, &mut current);
}
previous[right_chars.len()]
}
fn io_error_help(path: &Path, kind: std::io::ErrorKind) -> String {
let parent = path.parent().filter(|parent| !parent.as_os_str().is_empty());
match kind {
std::io::ErrorKind::NotFound => match parent {
Some(parent) if parent.is_dir() => format!(
"nothing exists at that path. Check the spelling: ls {}",
shell_quote(&parent.display().to_string())
),
Some(parent) => format!(
"nothing exists at that path, and neither does its directory. Create it with: \
mkdir -p {}",
shell_quote(&parent.display().to_string())
),
None => "nothing exists at that path. Check the spelling.".to_string(),
},
std::io::ErrorKind::PermissionDenied => format!(
"the current user cannot access that path. Inspect it with: ls -ld {}",
shell_quote(&path.display().to_string())
),
std::io::ErrorKind::AlreadyExists => format!(
"that path already exists. Remove it or pick another: ls -ld {}",
shell_quote(&path.display().to_string())
),
_ => match parent {
Some(parent) => format!(
"check that '{}' exists, is writable, and has free space.",
parent.display()
),
None => "check that the path exists and is writable.".to_string(),
},
}
}
fn state_machine_load_report(path: &Path, err: rhei_validator::StateMachineLoadError) -> Report {
let quoted = shell_quote(&path.display().to_string());
match err {
rhei_validator::StateMachineLoadError::Io(err) => {
file_io_report(path, "failed to read state machine", err)
}
rhei_validator::StateMachineLoadError::Yaml(err) => miette!(
help = format!(
"'{}' is not valid YAML. Fix the syntax at the position above, then re-check it \
with: rhei states --state-machine {quoted}",
path.display()
),
"failed to parse state machine '{}': {err}",
path.display()
),
rhei_validator::StateMachineLoadError::Invalid(message) => miette!(
help = format!(
"edit '{}' so the state definition above is valid, then re-check it with: \
rhei states --state-machine {quoted}",
path.display()
),
"invalid state machine '{}': {message}",
path.display()
),
}
}
fn state_machine_help() -> &'static str {
"fix the state definition in the active states.yaml. Inspect the machine \
rhei resolved with: rhei states"
}
fn settings_help() -> &'static str {
"settings merge from ~/.config/rhei/settings.json then .agents/rhei/settings.json. \
Check both with: rhei diag"
}
fn plan_authoring_help() -> &'static str {
"check the plan's task metadata (**State:**, **Prior:**, **Assignee:**), then re-run: \
rhei validate <plan>"
}
fn snapshot_help() -> &'static str {
"inspect the snapshot store with: rhei snapshot list"
}
fn template_manifest_help() -> &'static str {
"fix template.yaml in the template directory, then re-check the bundle with: \
rhei instantiate <template> --dry-run"
}
fn worktree_ref_help() -> &'static str {
"a task worktree reference is written by the state that created the worktree. Delete the \
stale file under runtime/worktree-refs/ and re-run that state."
}
fn internal_error_help() -> &'static str {
"this is a bug in rhei, not a problem with your input. Please report it with \
the command you ran and this message."
}
fn temp_write_help() -> &'static str {
"rhei writes plan edits through a temp file in the same directory. Check that \
directory is writable and has free space."
}
fn cwd_help() -> &'static str {
"re-run from a directory that still exists."
}
fn task_id_help() -> &'static str {
"list the task ids in this plan with: rhei list <plan>"
}
fn task_moved_help() -> &'static str {
"someone moved the task since you looked. Re-read its current state with: \
rhei list <plan>"
}
fn unknown_state_help() -> &'static str {
"pick a state the machine declares. List them with: rhei states"
}
fn artifact_path_help() -> &'static str {
"artifact paths are workspace-relative. Remove the leading '/' or the '..' \
segments from this artifact's `path` in the state machine."
}
fn runtime_dir_help() -> &'static str {
"rhei records results and transitions under runtime/. Check that the workspace \
directory is writable."
}
fn runtime_results_help() -> &'static str {
"rhei records results under runtime/results/. Check that directory is writable."
}
fn transition_log_help() -> &'static str {
"rhei appends to runtime/state-transitions.log. Check that directory is writable."
}
fn program_log_help() -> &'static str {
"program output is logged under runtime/logs/. Check that directory is writable."
}
fn program_state_failed_help() -> &'static str {
"the program state failed. Its log is under runtime/logs/; fix the cause, then re-run."
}
fn agent_log_help() -> &'static str {
"agent output is logged under runtime/logs/. Check that directory is writable."
}
fn agent_command_help() -> &'static str {
"check the agent's command and flags in settings.json: rhei diag"
}
fn run_report_help() -> &'static str {
"inspect the run with the report it printed, fix the cause, and re-run: rhei run <plan>"
}
fn nothing_claimable_help() -> &'static str {
"every remaining task is blocked, gated, or assigned. See what is left with: \
rhei list <plan>"
}
fn callback_command_help() -> &'static str {
"the callback command is declared in the state machine. Fix the command or the \
state it redirects to, then retry the transition."
}
fn poll_operand_help() -> &'static str {
"pollAttempts and pollMaxAttempts exist only inside a state that declares \
`poll:`. Use a different operand, or make the state a poll state."
}
fn states_declaration_help() -> &'static str {
"the plan's `**States:**` declaration must match the name inside the states \
file. Rename one of them, or point --state-machine at the matching file."
}
fn duration_format_help() -> &'static str {
"durations are a number plus a unit: 7d, 4h, 30m, 10s."
}
fn git_worktree_help() -> &'static str {
"rhei needs a readable git worktree here. Check `git status` runs in this directory."
}
fn watch_help() -> &'static str {
"--watch needs an OS file-watch handle. Re-run without --watch, or raise the \
inotify limits."
}
fn viz_path_help() -> &'static str {
"check the path and re-run: rhei viz <plan-or-directory>"
}
fn dashboard_required_help() -> &'static str {
"the dashboard must be running to receive an intervention: rhei run <plan> --dashboard"
}
fn snapshot_reference_help() -> &'static str {
"a reference is <task>:<name>[:<state>][@<visit>][:<target>][/g<N>]. Copy one \
from: rhei snapshot list"
}
fn snapshot_corrupt_help() -> &'static str {
"this cached snapshot is corrupt. Delete its generation directory and re-record \
it: rhei snapshot gc --orphaned"
}
fn snapshot_redactor_help() -> &'static str {
"the redactor is the command in `snapshot.redact` in settings.json. Check it \
exists, reads stdin, and writes stdout."
}
fn session_capture_resume_help() -> &'static str {
"this agent profile cannot capture or resume a native session. Configure \
`agents.<id>.session` in settings.json, or continue with an agent that supports it."
}
fn session_capture_help() -> &'static str {
"this agent profile cannot capture a native session. Configure \
`agents.<id>.session` in settings.json, or drop snapshot emission for this state."
}
fn snapshot_inherit_help() -> &'static str {
"the override does not satisfy the state's snapshot.inherit contract. Pick a \
snapshot that does — list them with: rhei snapshot list — or relax \
snapshot.inherit in the state machine."
}
fn snapshot_resume_help() -> &'static str {
"that snapshot cannot be resumed by this agent. Pick another with: rhei snapshot \
list, or run the state without --from-snapshot."
}
fn snapshot_candidates_help() -> &'static str {
"the candidates above are the snapshot.inherit invocations this run offers. Pass \
one of them, or drop --from-snapshot."
}
fn snapshot_key_help() -> &'static str {
"snapshots are keyed by agent, provider, and model. Use a full \
<agent>:<provider>:<model> selector for this state."
}
fn snapshot_target_help() -> &'static str {
"a snapshot records the target it ran under. Re-create the snapshot, or pass an \
explicit target."
}
fn snapshot_ambiguous_help() -> &'static str {
"more than one cached generation matches. Narrow it with snapshot.inherit.select \
in the state machine, or prune with: rhei snapshot gc"
}
fn embedded_extraction_help() -> &'static str {
"built-in skills and templates are unpacked into a temp directory. Check that \
$TMPDIR exists, is writable, and has free space."
}
fn init_conflict_help() -> &'static str {
"inspect what is already here with: rhei list, then re-run init with the flag \
named above."
}
fn ticket_id_required_help() -> &'static str {
"ticket ids are the bold `Task <id>` values in the plan. List them with: rhei list <plan>"
}
fn rhei_scope_help() -> &'static str {
"drop --rhei to search the whole project, or name the rhei that owns the ticket. \
List the rheis with: rhei list"
}
fn local_install_help() -> &'static str {
"--local writes into the current project. Run it inside a git repository or a \
Panta project, or install for your user with --user."
}
fn require_project_root(project_root: Option<&Path>) -> MietteResult<&Path> {
project_root.ok_or_else(|| {
miette!(help = local_install_help(), "--local requires a project root")
})
}
fn unknown_agent_help(id: &str, known: &[String]) -> String {
let hint = did_you_mean(id, known).map(|hint| format!("{hint} ")).unwrap_or_default();
format!(
"{hint}Define it under `agents.<id>` in .agents/rhei/settings.json or \
~/.config/rhei/settings.json."
)
}
fn agent_flag_selector_help(value: &str, known: &[String]) -> Option<String> {
if !value.contains(':') && !value.contains('[') {
return None;
}
let target = parse_execution_target(value).ok()?;
let mut parts = vec!["--agent".to_string(), target.agent.clone()];
if let Some(mode) = target.mode {
parts.push("--agent-mode".to_string());
parts.push(mode);
}
parts.push("--model".to_string());
parts.push(target.model);
let mut help = format!(
"--agent takes a bare agent id; the mode and model have their own flags. \
Write it as: {}",
shell_command(&parts)
);
if !known.iter().any(|name| name == &target.agent) {
if let Some(name) = nearest_match(&target.agent, known) {
help = format!("{help}\nDid you mean '{name}'?");
}
}
Some(help)
}
fn handoff_missing_source_help() -> &'static str {
"a required handoff inherits from the transition that entered this state. Either \
reach this state through a transition that produces it, or set `required: false` \
on the `inherit` entry in the state machine: rhei states"
}
fn handoff_no_output_help() -> &'static str {
"the producing state must declare the handoff it hands over: add an `outputs` entry \
with `kind: handoff` to that state, or relax the `inherit` entry: rhei states"
}
fn handoff_ambiguous_help() -> &'static str {
"more than one handoff output matched. Name the one you want with `name:` on the \
`inherit` entry, or set `merge: all` to take every match: rhei states"
}
fn handoff_empty_artifact_help() -> &'static str {
"the producing state declared this handoff but wrote nothing to it. An empty file \
does not satisfy a handoff — check that state's agent log under runtime/logs/, \
then re-run the producing task."
}