feat(cli): improve error recovery with alias-aware suggestions and error tolerance manifest
Two related improvements to agent ergonomics in main.rs: 1. suggest_similar_command now matches against aliases (issue->issues, mr->mrs, find->search, stat->stats, note->notes, etc.) and provides contextual usage examples via a new command_example() helper, so agents get actionable recovery hints like "Did you mean 'lore mrs'? Example: lore --robot mrs -n 10" instead of just the command name. 2. robot-docs now includes an error_tolerance section documenting every auto-correction the CLI performs: types (single_dash_long_flag, case_normalization, flag_prefix, fuzzy_flag, subcommand_alias, value_normalization, value_fuzzy, prefix_matching), examples, and mode behavior (threshold differences). Also expands the aliases section with command_aliases and pre_clap_aliases maps for complete agent self-discovery. Together these ensure agents can programmatically discover and recover from any CLI input error without human intervention.
This commit is contained in:
129
src/main.rs
129
src/main.rs
@@ -651,27 +651,37 @@ fn extract_invalid_value_context(e: &clap::Error) -> (Option<String>, Option<Vec
|
||||
|
||||
/// Phase 4: Suggest similar command using fuzzy matching
|
||||
fn suggest_similar_command(invalid: &str) -> String {
|
||||
const VALID_COMMANDS: &[&str] = &[
|
||||
"issues",
|
||||
"mrs",
|
||||
"search",
|
||||
"sync",
|
||||
"ingest",
|
||||
"count",
|
||||
"status",
|
||||
"auth",
|
||||
"doctor",
|
||||
"version",
|
||||
"init",
|
||||
"stats",
|
||||
"generate-docs",
|
||||
"embed",
|
||||
"migrate",
|
||||
"health",
|
||||
"robot-docs",
|
||||
"completions",
|
||||
"timeline",
|
||||
"who",
|
||||
// Primary commands + common aliases for fuzzy matching
|
||||
const VALID_COMMANDS: &[(&str, &str)] = &[
|
||||
("issues", "issues"),
|
||||
("issue", "issues"),
|
||||
("mrs", "mrs"),
|
||||
("mr", "mrs"),
|
||||
("merge-requests", "mrs"),
|
||||
("search", "search"),
|
||||
("find", "search"),
|
||||
("query", "search"),
|
||||
("sync", "sync"),
|
||||
("ingest", "ingest"),
|
||||
("count", "count"),
|
||||
("status", "status"),
|
||||
("auth", "auth"),
|
||||
("doctor", "doctor"),
|
||||
("version", "version"),
|
||||
("init", "init"),
|
||||
("stats", "stats"),
|
||||
("stat", "stats"),
|
||||
("generate-docs", "generate-docs"),
|
||||
("embed", "embed"),
|
||||
("migrate", "migrate"),
|
||||
("health", "health"),
|
||||
("robot-docs", "robot-docs"),
|
||||
("completions", "completions"),
|
||||
("timeline", "timeline"),
|
||||
("who", "who"),
|
||||
("notes", "notes"),
|
||||
("note", "notes"),
|
||||
("drift", "drift"),
|
||||
];
|
||||
|
||||
let invalid_lower = invalid.to_lowercase();
|
||||
@@ -679,19 +689,43 @@ fn suggest_similar_command(invalid: &str) -> String {
|
||||
// Find the best match using Jaro-Winkler similarity
|
||||
let best_match = VALID_COMMANDS
|
||||
.iter()
|
||||
.map(|cmd| (*cmd, jaro_winkler(&invalid_lower, cmd)))
|
||||
.map(|(alias, canonical)| (*canonical, jaro_winkler(&invalid_lower, alias)))
|
||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
if let Some((cmd, score)) = best_match
|
||||
&& score > 0.7
|
||||
{
|
||||
let example = command_example(cmd);
|
||||
return format!(
|
||||
"Did you mean 'lore {}'? Run 'lore robot-docs' for all commands",
|
||||
cmd
|
||||
"Did you mean 'lore {cmd}'? Example: {example}. Run 'lore robot-docs' for all commands"
|
||||
);
|
||||
}
|
||||
|
||||
"Run 'lore robot-docs' for valid commands".to_string()
|
||||
"Run 'lore robot-docs' for valid commands. Common: issues, mrs, search, sync, timeline, who"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Return a contextual usage example for a command.
|
||||
fn command_example(cmd: &str) -> &'static str {
|
||||
match cmd {
|
||||
"issues" => "lore --robot issues -n 10",
|
||||
"mrs" => "lore --robot mrs -n 10",
|
||||
"search" => "lore --robot search \"auth bug\"",
|
||||
"sync" => "lore --robot sync",
|
||||
"ingest" => "lore --robot ingest issues",
|
||||
"notes" => "lore --robot notes --for-issue 123",
|
||||
"count" => "lore --robot count issues",
|
||||
"status" => "lore --robot status",
|
||||
"stats" => "lore --robot stats",
|
||||
"timeline" => "lore --robot timeline \"auth flow\"",
|
||||
"who" => "lore --robot who --path src/",
|
||||
"health" => "lore --robot health",
|
||||
"generate-docs" => "lore --robot generate-docs",
|
||||
"embed" => "lore --robot embed",
|
||||
"robot-docs" => "lore robot-docs",
|
||||
"init" => "lore init",
|
||||
_ => "lore --robot <command>",
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_issues(
|
||||
@@ -2135,6 +2169,8 @@ struct RobotDocsData {
|
||||
commands: serde_json::Value,
|
||||
/// Deprecated command aliases (old -> new)
|
||||
aliases: serde_json::Value,
|
||||
/// Pre-clap error tolerance: what the CLI auto-corrects
|
||||
error_tolerance: serde_json::Value,
|
||||
exit_codes: serde_json::Value,
|
||||
/// Error codes emitted by clap parse failures
|
||||
clap_error_codes: serde_json::Value,
|
||||
@@ -2489,12 +2525,54 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
||||
|
||||
// Phase 3: Deprecated command aliases
|
||||
let aliases = serde_json::json!({
|
||||
"deprecated_commands": {
|
||||
"list issues": "issues",
|
||||
"list mrs": "mrs",
|
||||
"show issue <IID>": "issues <IID>",
|
||||
"show mr <IID>": "mrs <IID>",
|
||||
"auth-test": "auth",
|
||||
"sync-status": "status"
|
||||
},
|
||||
"command_aliases": {
|
||||
"issue": "issues",
|
||||
"mr": "mrs",
|
||||
"merge-requests": "mrs",
|
||||
"merge-request": "mrs",
|
||||
"note": "notes",
|
||||
"find": "search",
|
||||
"query": "search",
|
||||
"stat": "stats",
|
||||
"st": "status"
|
||||
},
|
||||
"pre_clap_aliases": {
|
||||
"note": "Underscore/no-separator forms auto-corrected before parsing",
|
||||
"merge_requests": "mrs",
|
||||
"merge_request": "mrs",
|
||||
"mergerequests": "mrs",
|
||||
"mergerequest": "mrs",
|
||||
"generate_docs": "generate-docs",
|
||||
"generatedocs": "generate-docs",
|
||||
"gendocs": "generate-docs",
|
||||
"gen-docs": "generate-docs",
|
||||
"robot_docs": "robot-docs",
|
||||
"robotdocs": "robot-docs"
|
||||
},
|
||||
"prefix_matching": "Enabled via infer_subcommands. Unambiguous prefixes work: 'iss' -> issues, 'time' -> timeline, 'sea' -> search"
|
||||
});
|
||||
|
||||
let error_tolerance = serde_json::json!({
|
||||
"note": "The CLI auto-corrects common mistakes before parsing. Corrections are applied silently with a teaching note on stderr.",
|
||||
"auto_corrections": [
|
||||
{"type": "single_dash_long_flag", "example": "-robot -> --robot", "mode": "all"},
|
||||
{"type": "case_normalization", "example": "--Robot -> --robot, --State -> --state", "mode": "all"},
|
||||
{"type": "flag_prefix", "example": "--proj -> --project (when unambiguous)", "mode": "all"},
|
||||
{"type": "fuzzy_flag", "example": "--projct -> --project", "mode": "all (threshold 0.9 in robot, 0.8 in human)"},
|
||||
{"type": "subcommand_alias", "example": "merge_requests -> mrs, robotdocs -> robot-docs", "mode": "all"},
|
||||
{"type": "value_normalization", "example": "--state Opened -> --state opened", "mode": "all"},
|
||||
{"type": "value_fuzzy", "example": "--state opend -> --state opened", "mode": "all"},
|
||||
{"type": "prefix_matching", "example": "lore iss -> lore issues, lore time -> lore timeline", "mode": "all (via clap infer_subcommands)"}
|
||||
],
|
||||
"teaching_notes": "Auto-corrections emit a JSON warning on stderr: {\"warning\":{\"type\":\"ARG_CORRECTED\",\"corrections\":[...],\"teaching\":[...]}}"
|
||||
});
|
||||
|
||||
// Phase 3: Clap error codes (emitted by handle_clap_error)
|
||||
@@ -2533,6 +2611,7 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
||||
quick_start,
|
||||
commands,
|
||||
aliases,
|
||||
error_tolerance,
|
||||
exit_codes,
|
||||
clap_error_codes,
|
||||
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\",\"actions\":[\"...\"]}}".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user