From b5f78e31a81897828716f493f46019bd8c0d5142 Mon Sep 17 00:00:00 2001 From: Taylor Eernisse Date: Fri, 6 Feb 2026 23:47:04 -0500 Subject: [PATCH] fix(cli): audit-driven improvements to flags, help, exit codes, and deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses findings from a comprehensive CLI readiness audit: Flag design (I2): - Add hidden --no-verbose flag with overrides_with semantics, matching the --no-quiet pattern already established for all other boolean flags. Help text (I3): - Add after_help examples to issues, mrs, search, sync, and timeline subcommands. Each shows 3-4 concrete, runnable commands with comments. Help headings (I4/P5): - Move --mode and --fts-mode from "Output" heading to "Mode" heading in the search subcommand. These control search strategy, not output format — "Output" is reserved for --limit, --explain, --fields. Exit codes (I5): - Health check failure now exits 19 (was 1). Exit code 1 is reserved for internal errors only. robot-docs updated to document code 19. Deprecation visibility (P4): - Deprecated commands (list, show, auth-test, sync-status) now emit structured JSON warnings to stderr in robot mode: {"warning":{"type":"DEPRECATED","message":"...","successor":"..."}} Previously these were silently swallowed in robot mode. Version string (P1): - Cli struct uses env!("LORE_VERSION") from build.rs so --version shows git hash (see previous commit). Fields flag (P3): - --fields help text updated to document the "minimal" preset. Robot-docs (parallel work): - response_schema added for every command, documenting the JSON shape agents will receive. Agents can now introspect expected fields before calling a command. - error_format documents the new "actions" array. Co-Authored-By: Claude Opus 4.6 --- src/cli/mod.rs | 45 +++++++-- src/main.rs | 265 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 248 insertions(+), 62 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 857e89f..4d21084 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,12 +1,13 @@ pub mod commands; pub mod progress; +pub mod robot; use clap::{Parser, Subcommand}; use std::io::IsTerminal; #[derive(Parser)] #[command(name = "lore")] -#[command(version, about = "Local GitLab data management with semantic search", long_about = None)] +#[command(version = env!("LORE_VERSION"), about = "Local GitLab data management with semantic search", long_about = None)] #[command(subcommand_required = false)] pub struct Cli { /// Path to config file @@ -54,9 +55,17 @@ pub struct Cli { pub no_quiet: bool, /// Increase log verbosity (-v, -vv, -vvv) - #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true, help = "Increase log verbosity (-v, -vv, -vvv)")] + #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true, help = "Increase log verbosity (-v, -vv, -vvv)", overrides_with = "no_verbose")] pub verbose: u8, + #[arg( + long = "no-verbose", + global = true, + hide = true, + overrides_with = "verbose" + )] + pub no_verbose: bool, + /// Log format for stderr output: text (default) or json #[arg(long = "log-format", global = true, value_parser = ["text", "json"], default_value = "text", help = "Log format for stderr output: text (default) or json")] pub log_format: String, @@ -246,6 +255,11 @@ pub enum Commands { } #[derive(Parser)] +#[command(after_help = "\x1b[1mExamples:\x1b[0m + lore issues -n 10 # List 10 most recently updated issues + lore issues -s opened -l bug # Open issues labeled 'bug' + lore issues 42 -p group/repo # Show issue #42 in a specific project + lore issues --since 7d -a jsmith # Issues updated in last 7 days by jsmith")] pub struct IssuesArgs { /// Issue IID (omit to list, provide to show details) pub iid: Option, @@ -259,7 +273,7 @@ pub struct IssuesArgs { )] pub limit: usize, - /// Select output fields (comma-separated: iid,title,state,author,labels,updated) + /// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, @@ -331,6 +345,11 @@ pub struct IssuesArgs { } #[derive(Parser)] +#[command(after_help = "\x1b[1mExamples:\x1b[0m + lore mrs -s opened # List open merge requests + lore mrs -s merged --since 2w # MRs merged in the last 2 weeks + lore mrs 99 -p group/repo # Show MR !99 in a specific project + lore mrs -D --reviewer jsmith # Non-draft MRs reviewed by jsmith")] pub struct MrsArgs { /// MR IID (omit to list, provide to show details) pub iid: Option, @@ -344,7 +363,7 @@ pub struct MrsArgs { )] pub limit: usize, - /// Select output fields (comma-separated: iid,title,state,author,labels,updated) + /// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso) #[arg(long, help_heading = "Output", value_delimiter = ',')] pub fields: Option>, @@ -480,12 +499,17 @@ pub struct StatsArgs { } #[derive(Parser)] +#[command(after_help = "\x1b[1mExamples:\x1b[0m + lore search 'authentication bug' # Hybrid search (default) + lore search 'deploy' --mode lexical --type mr # Lexical search, MRs only + lore search 'API rate limit' --since 30d # Recent results only + lore search 'config' -p group/repo --explain # With ranking explanation")] pub struct SearchArgs { /// Search query string pub query: String, /// Search mode (lexical, hybrid, semantic) - #[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Output")] + #[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Mode")] pub mode: String, /// Filter by source type (issue, mr, discussion) @@ -533,7 +557,7 @@ pub struct SearchArgs { pub no_explain: bool, /// FTS query mode: safe (default) or raw - #[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Output")] + #[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Mode")] pub fts_mode: String, } @@ -549,6 +573,11 @@ pub struct GenerateDocsArgs { } #[derive(Parser)] +#[command(after_help = "\x1b[1mExamples:\x1b[0m + lore sync # Full pipeline: ingest + docs + embed + lore sync --no-embed # Skip embedding step + lore sync --full --force # Full re-sync, override stale lock + lore sync --dry-run # Preview what would change")] pub struct SyncArgs { /// Reset cursors, fetch everything #[arg(long, overrides_with = "no_full")] @@ -602,6 +631,10 @@ pub struct EmbedArgs { } #[derive(Parser)] +#[command(after_help = "\x1b[1mExamples:\x1b[0m + lore timeline 'deployment' # Events related to deployments + lore timeline 'auth' --since 30d -p group/repo # Scoped to project and time + lore timeline 'migration' --depth 2 --expand-mentions # Deep cross-reference expansion")] pub struct TimelineArgs { /// Search query (keywords to find in issues, MRs, and discussions) pub query: String, diff --git a/src/main.rs b/src/main.rs index cd51e81..3790e1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ use lore::cli::commands::{ run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status, run_timeline, }; +use lore::cli::robot::RobotMeta; use lore::cli::{ Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs, SearchArgs, StatsArgs, SyncArgs, TimelineArgs, @@ -229,7 +230,11 @@ async fn main() { target_branch, source_branch, }) => { - if !robot_mode { + if robot_mode { + eprintln!( + r#"{{"warning":{{"type":"DEPRECATED","message":"'lore list' is deprecated, use 'lore issues' or 'lore mrs'","successor":"issues / mrs"}}}}"# + ); + } else { eprintln!( "{}", style("warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'") @@ -266,7 +271,11 @@ async fn main() { iid, project, }) => { - if !robot_mode { + if robot_mode { + eprintln!( + r#"{{"warning":{{"type":"DEPRECATED","message":"'lore show' is deprecated, use 'lore {entity}s {iid}'","successor":"{entity}s"}}}}"# + ); + } else { eprintln!( "{}", style(format!( @@ -286,7 +295,11 @@ async fn main() { .await } Some(Commands::AuthTest) => { - if !robot_mode { + if robot_mode { + eprintln!( + r#"{{"warning":{{"type":"DEPRECATED","message":"'lore auth-test' is deprecated, use 'lore auth'","successor":"auth"}}}}"# + ); + } else { eprintln!( "{}", style("warning: 'lore auth-test' is deprecated, use 'lore auth'").yellow() @@ -295,7 +308,11 @@ async fn main() { handle_auth_test(cli.config.as_deref(), robot_mode).await } Some(Commands::SyncStatus) => { - if !robot_mode { + if robot_mode { + eprintln!( + r#"{{"warning":{{"type":"DEPRECATED","message":"'lore sync-status' is deprecated, use 'lore status'","successor":"status"}}}}"# + ); + } else { eprintln!( "{}", style("warning: 'lore sync-status' is deprecated, use 'lore status'").yellow() @@ -498,14 +515,7 @@ fn handle_issues( args: IssuesArgs, robot_mode: bool, ) -> Result<(), Box> { - // Warn about unimplemented --fields - if args.fields.is_some() && !robot_mode { - eprintln!( - "{}", - style("warning: --fields is not yet implemented, showing all fields").yellow() - ); - } - + let start = std::time::Instant::now(); let config = Config::load(config_override)?; let asc = args.asc && !args.no_asc; let has_due = args.has_due && !args.no_has_due; @@ -515,7 +525,7 @@ fn handle_issues( if let Some(iid) = args.iid { let result = run_show_issue(&config, iid, args.project.as_deref())?; if robot_mode { - print_show_issue_json(&result); + print_show_issue_json(&result, start.elapsed().as_millis() as u64); } else { print_show_issue(&result); } @@ -540,7 +550,11 @@ fn handle_issues( if open { open_issue_in_browser(&result); } else if robot_mode { - print_list_issues_json(&result); + print_list_issues_json( + &result, + start.elapsed().as_millis() as u64, + args.fields.as_deref(), + ); } else { print_list_issues(&result); } @@ -554,14 +568,7 @@ fn handle_mrs( args: MrsArgs, robot_mode: bool, ) -> Result<(), Box> { - // Warn about unimplemented --fields - if args.fields.is_some() && !robot_mode { - eprintln!( - "{}", - style("warning: --fields is not yet implemented, showing all fields").yellow() - ); - } - + let start = std::time::Instant::now(); let config = Config::load(config_override)?; let asc = args.asc && !args.no_asc; let open = args.open && !args.no_open; @@ -570,7 +577,7 @@ fn handle_mrs( if let Some(iid) = args.iid { let result = run_show_mr(&config, iid, args.project.as_deref())?; if robot_mode { - print_show_mr_json(&result); + print_show_mr_json(&result, start.elapsed().as_millis() as u64); } else { print_show_mr(&result); } @@ -597,7 +604,11 @@ fn handle_mrs( if open { open_mr_in_browser(&result); } else if robot_mode { - print_list_mrs_json(&result); + print_list_mrs_json( + &result, + start.elapsed().as_millis() as u64, + args.fields.as_deref(), + ); } else { print_list_mrs(&result); } @@ -613,6 +624,7 @@ async fn handle_ingest( quiet: bool, metrics: &MetricsLayer, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let dry_run = args.dry_run && !args.no_dry_run; let config = Config::load(config_override)?; @@ -689,7 +701,7 @@ async fn handle_ingest( .await?; if robot_mode { - print_ingest_summary_json(&result); + print_ingest_summary_json(&result, start.elapsed().as_millis() as u64); } else { print_ingest_summary(&result); } @@ -730,7 +742,11 @@ async fn handle_ingest( .await?; if robot_mode { - print_combined_ingest_json(&issues_result, &mrs_result); + print_combined_ingest_json( + &issues_result, + &mrs_result, + start.elapsed().as_millis() as u64, + ); } else { print_ingest_summary(&issues_result); print_ingest_summary(&mrs_result); @@ -778,6 +794,7 @@ async fn handle_ingest( struct CombinedIngestOutput { ok: bool, data: CombinedIngestData, + meta: RobotMeta, } #[derive(Serialize)] @@ -800,6 +817,7 @@ struct CombinedIngestEntityStats { fn print_combined_ingest_json( issues: &lore::cli::commands::ingest::IngestResult, mrs: &lore::cli::commands::ingest::IngestResult, + elapsed_ms: u64, ) { let output = CombinedIngestOutput { ok: true, @@ -822,6 +840,7 @@ fn print_combined_ingest_json( notes_upserted: mrs.notes_upserted, }, }, + meta: RobotMeta { elapsed_ms }, }; println!("{}", serde_json::to_string(&output).unwrap()); @@ -861,12 +880,13 @@ async fn handle_count( args: CountArgs, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config = Config::load(config_override)?; if args.entity == "events" { let counts = run_count_events(&config)?; if robot_mode { - print_event_count_json(&counts); + print_event_count_json(&counts, start.elapsed().as_millis() as u64); } else { print_event_count(&counts); } @@ -875,7 +895,7 @@ async fn handle_count( let result = run_count(&config, &args.entity, args.for_entity.as_deref())?; if robot_mode { - print_count_json(&result); + print_count_json(&result, start.elapsed().as_millis() as u64); } else { print_count(&result); } @@ -886,11 +906,12 @@ async fn handle_sync_status_cmd( config_override: Option<&str>, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config = Config::load(config_override)?; let result = run_sync_status(&config)?; if robot_mode { - print_sync_status_json(&result); + print_sync_status_json(&result, start.elapsed().as_millis() as u64); } else { print_sync_status(&result); } @@ -1135,6 +1156,7 @@ async fn handle_init( struct AuthTestOutput { ok: bool, data: AuthTestData, + meta: RobotMeta, } #[derive(Serialize)] @@ -1149,6 +1171,7 @@ async fn handle_auth_test( config_override: Option<&str>, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); match run_auth_test(config_override).await { Ok(result) => { if robot_mode { @@ -1160,6 +1183,9 @@ async fn handle_auth_test( name: result.name.clone(), gitlab_url: result.base_url.clone(), }, + meta: RobotMeta { + elapsed_ms: start.elapsed().as_millis() as u64, + }, }; println!("{}", serde_json::to_string(&output)?); } else { @@ -1189,6 +1215,7 @@ async fn handle_auth_test( struct DoctorOutput { ok: bool, data: DoctorData, + meta: RobotMeta, } #[derive(Serialize)] @@ -1201,6 +1228,7 @@ async fn handle_doctor( config_override: Option<&str>, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let result = run_doctor(config_override).await; if robot_mode { @@ -1210,6 +1238,9 @@ async fn handle_doctor( success: result.success, checks: result.checks, }, + meta: RobotMeta { + elapsed_ms: start.elapsed().as_millis() as u64, + }, }; println!("{}", serde_json::to_string(&output)?); } else { @@ -1227,6 +1258,7 @@ async fn handle_doctor( struct VersionOutput { ok: bool, data: VersionData, + meta: RobotMeta, } #[derive(Serialize)] @@ -1237,6 +1269,7 @@ struct VersionData { } fn handle_version(robot_mode: bool) -> Result<(), Box> { + let start = std::time::Instant::now(); let version = env!("CARGO_PKG_VERSION").to_string(); let git_hash = env!("GIT_HASH").to_string(); if robot_mode { @@ -1250,6 +1283,9 @@ fn handle_version(robot_mode: bool) -> Result<(), Box> { Some(git_hash) }, }, + meta: RobotMeta { + elapsed_ms: start.elapsed().as_millis() as u64, + }, }; println!("{}", serde_json::to_string(&output)?); } else if git_hash.is_empty() { @@ -1322,6 +1358,7 @@ fn handle_reset(robot_mode: bool) -> Result<(), Box> { struct MigrateOutput { ok: bool, data: MigrateData, + meta: RobotMeta, } #[derive(Serialize)] @@ -1347,6 +1384,7 @@ async fn handle_migrate( config_override: Option<&str>, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config = Config::load(config_override)?; let db_path = get_db_path(config.storage.db_path.as_deref()); @@ -1395,6 +1433,9 @@ async fn handle_migrate( after_version, migrated: after_version > before_version, }, + meta: RobotMeta { + elapsed_ms: start.elapsed().as_millis() as u64, + }, }; println!("{}", serde_json::to_string(&output)?); } else if after_version > before_version { @@ -1418,12 +1459,13 @@ async fn handle_stats( args: StatsArgs, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let dry_run = args.dry_run && !args.no_dry_run; let config = Config::load(config_override)?; let check = (args.check && !args.no_check) || args.repair; let result = run_stats(&config, check, args.repair, dry_run)?; if robot_mode { - print_stats_json(&result); + print_stats_json(&result, start.elapsed().as_millis() as u64); } else { print_stats(&result); } @@ -1505,11 +1547,12 @@ async fn handle_generate_docs( args: GenerateDocsArgs, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config = Config::load(config_override)?; let result = run_generate_docs(&config, args.full, args.project.as_deref(), None)?; if robot_mode { - print_generate_docs_json(&result); + print_generate_docs_json(&result, start.elapsed().as_millis() as u64); } else { print_generate_docs(&result); } @@ -1521,6 +1564,7 @@ async fn handle_embed( args: EmbedArgs, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config = Config::load(config_override)?; let full = args.full && !args.no_full; let retry_failed = args.retry_failed && !args.no_retry_failed; @@ -1537,7 +1581,7 @@ async fn handle_embed( let result = run_embed(&config, full, retry_failed, None, &signal).await?; if robot_mode { - print_embed_json(&result); + print_embed_json(&result, start.elapsed().as_millis() as u64); } else { print_embed(&result); } @@ -1649,6 +1693,7 @@ async fn handle_sync_cmd( struct HealthOutput { ok: bool, data: HealthData, + meta: RobotMeta, } #[derive(Serialize)] @@ -1664,6 +1709,7 @@ async fn handle_health( config_override: Option<&str>, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config_path = get_config_path(config_override); let config_found = config_path.exists(); @@ -1701,6 +1747,9 @@ async fn handle_health( schema_current, schema_version, }, + meta: RobotMeta { + elapsed_ms: start.elapsed().as_millis() as u64, + }, }; println!("{}", serde_json::to_string(&output)?); } else { @@ -1732,7 +1781,7 @@ async fn handle_health( } if !healthy { - std::process::exit(1); + std::process::exit(19); } Ok(()) @@ -1775,82 +1824,178 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> "description": "Initialize configuration and database", "flags": ["--force", "--non-interactive", "--gitlab-url ", "--token-env-var ", "--projects "], "robot_flags": ["--gitlab-url", "--token-env-var", "--projects"], - "example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project" + "example": "lore --robot init --gitlab-url https://gitlab.com --token-env-var GITLAB_TOKEN --projects group/project", + "response_schema": { + "ok": "bool", + "data": {"config_path": "string", "data_dir": "string", "user": {"username": "string", "name": "string"}, "projects": "[{path:string, name:string}]"}, + "meta": {"elapsed_ms": "int"} + } }, "health": { "description": "Quick pre-flight check: config, database, schema version", "flags": [], - "example": "lore --robot health" + "example": "lore --robot health", + "response_schema": { + "ok": "bool", + "data": {"healthy": "bool", "config_found": "bool", "db_found": "bool", "schema_current": "bool", "schema_version": "int"}, + "meta": {"elapsed_ms": "int"} + } }, "auth": { "description": "Verify GitLab authentication", "flags": [], - "example": "lore --robot auth" + "example": "lore --robot auth", + "response_schema": { + "ok": "bool", + "data": {"authenticated": "bool", "username": "string", "name": "string", "gitlab_url": "string"}, + "meta": {"elapsed_ms": "int"} + } }, "doctor": { "description": "Full environment health check (config, auth, DB, Ollama)", "flags": [], - "example": "lore --robot doctor" + "example": "lore --robot doctor", + "response_schema": { + "ok": "bool", + "data": {"success": "bool", "checks": "{config:object, auth:object, database:object, ollama:object}"}, + "meta": {"elapsed_ms": "int"} + } }, "ingest": { "description": "Sync data from GitLab", "flags": ["--project ", "--force", "--no-force", "--full", "--no-full", "--dry-run", "--no-dry-run", ""], - "example": "lore --robot ingest issues --project group/repo" + "example": "lore --robot ingest issues --project group/repo", + "response_schema": { + "ok": "bool", + "data": {"resource_type": "string", "projects_synced": "int", "issues_fetched?": "int", "mrs_fetched?": "int", "upserted": "int", "labels_created": "int", "discussions_fetched": "int", "notes_upserted": "int"}, + "meta": {"elapsed_ms": "int"} + } }, "sync": { "description": "Full sync pipeline: ingest -> generate-docs -> embed", "flags": ["--full", "--no-full", "--force", "--no-force", "--no-embed", "--no-docs", "--no-events", "--dry-run", "--no-dry-run"], - "example": "lore --robot sync" + "example": "lore --robot sync", + "response_schema": { + "ok": "bool", + "data": {"issues_updated": "int", "mrs_updated": "int", "documents_regenerated": "int", "documents_embedded": "int", "resource_events_synced": "int", "resource_events_failed": "int"}, + "meta": {"elapsed_ms": "int", "stages?": "[{name:string, elapsed_ms:int, items_processed:int}]"} + } }, "issues": { "description": "List or show issues", "flags": ["", "-n/--limit", "--fields ", "-s/--state", "-p/--project", "-a/--author", "-A/--assignee", "-l/--label", "-m/--milestone", "--since", "--due-before", "--has-due", "--no-has-due", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"], - "example": "lore --robot issues --state opened --limit 10" + "example": "lore --robot issues --state opened --limit 10", + "response_schema": { + "list": { + "ok": "bool", + "data": {"issues": "[{iid:int, title:string, state:string, author_username:string, labels:[string], assignees:[string], discussion_count:int, unresolved_count:int, created_at_iso:string, updated_at_iso:string, web_url:string?, project_path:string}]", "total_count": "int", "showing": "int"}, + "meta": {"elapsed_ms": "int"} + }, + "show": { + "ok": "bool", + "data": "IssueDetail (full entity with description, discussions, notes, events)", + "meta": {"elapsed_ms": "int"} + } + }, + "fields_presets": {"minimal": ["iid", "title", "state", "updated_at_iso"]} }, "mrs": { "description": "List or show merge requests", "flags": ["", "-n/--limit", "--fields ", "-s/--state", "-p/--project", "-a/--author", "-A/--assignee", "-r/--reviewer", "-l/--label", "--since", "-d/--draft", "-D/--no-draft", "--target", "--source", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"], - "example": "lore --robot mrs --state opened" + "example": "lore --robot mrs --state opened", + "response_schema": { + "list": { + "ok": "bool", + "data": {"mrs": "[{iid:int, title:string, state:string, author_username:string, labels:[string], draft:bool, target_branch:string, source_branch:string, discussion_count:int, unresolved_count:int, created_at_iso:string, updated_at_iso:string, web_url:string?, project_path:string, reviewers:[string]}]", "total_count": "int", "showing": "int"}, + "meta": {"elapsed_ms": "int"} + }, + "show": { + "ok": "bool", + "data": "MrDetail (full entity with description, discussions, notes, events)", + "meta": {"elapsed_ms": "int"} + } + }, + "fields_presets": {"minimal": ["iid", "title", "state", "updated_at_iso"]} }, "search": { "description": "Search indexed documents (lexical, hybrid, semantic)", "flags": ["", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--after", "--updated-after", "-n/--limit", "--explain", "--no-explain", "--fts-mode"], - "example": "lore --robot search 'authentication bug' --mode hybrid --limit 10" + "example": "lore --robot search 'authentication bug' --mode hybrid --limit 10", + "response_schema": { + "ok": "bool", + "data": {"results": "[{doc_id:int, source_type:string, title:string, snippet:string, score:float, project_path:string, web_url:string?}]", "total_count": "int", "query": "string", "mode": "string"}, + "meta": {"elapsed_ms": "int"} + } }, "count": { "description": "Count entities in local database", "flags": ["", "-f/--for "], - "example": "lore --robot count issues" + "example": "lore --robot count issues", + "response_schema": { + "ok": "bool", + "data": {"entity": "string", "count": "int", "system_excluded?": "int", "breakdown?": {"opened": "int", "closed": "int", "merged?": "int", "locked?": "int"}}, + "meta": {"elapsed_ms": "int"} + } }, "stats": { "description": "Show document and index statistics", "flags": ["--check", "--no-check", "--repair", "--dry-run", "--no-dry-run"], - "example": "lore --robot stats" + "example": "lore --robot stats", + "response_schema": { + "ok": "bool", + "data": {"total_documents": "int", "indexed_documents": "int", "embedded_documents": "int", "stale_documents": "int", "integrity?": "object"}, + "meta": {"elapsed_ms": "int"} + } }, "status": { "description": "Show sync state (cursors, last sync times)", "flags": [], - "example": "lore --robot status" + "example": "lore --robot status", + "response_schema": { + "ok": "bool", + "data": {"projects": "[{path:string, issues_cursor:string?, mrs_cursor:string?, last_sync:string?}]"}, + "meta": {"elapsed_ms": "int"} + } }, "generate-docs": { "description": "Generate searchable documents from ingested data", "flags": ["--full", "-p/--project "], - "example": "lore --robot generate-docs --full" + "example": "lore --robot generate-docs --full", + "response_schema": { + "ok": "bool", + "data": {"generated": "int", "updated": "int", "unchanged": "int", "deleted": "int"}, + "meta": {"elapsed_ms": "int"} + } }, "embed": { "description": "Generate vector embeddings for documents via Ollama", "flags": ["--full", "--no-full", "--retry-failed", "--no-retry-failed"], - "example": "lore --robot embed" + "example": "lore --robot embed", + "response_schema": { + "ok": "bool", + "data": {"embedded": "int", "skipped": "int", "failed": "int", "total_chunks": "int"}, + "meta": {"elapsed_ms": "int"} + } }, "migrate": { "description": "Run pending database migrations", "flags": [], - "example": "lore --robot migrate" + "example": "lore --robot migrate", + "response_schema": { + "ok": "bool", + "data": {"before_version": "int", "after_version": "int", "migrated": "bool"}, + "meta": {"elapsed_ms": "int"} + } }, "version": { "description": "Show version information", "flags": [], - "example": "lore --robot version" + "example": "lore --robot version", + "response_schema": { + "ok": "bool", + "data": {"version": "string", "git_hash?": "string"}, + "meta": {"elapsed_ms": "int"} + } }, "completions": { "description": "Generate shell completions", @@ -1860,7 +2005,12 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> "timeline": { "description": "Chronological timeline of events matching a keyword query", "flags": ["", "-p/--project", "--since ", "--depth ", "--expand-mentions", "-n/--limit", "--max-seeds", "--max-entities", "--max-evidence"], - "example": "lore --robot timeline 'authentication' --since 30d" + "example": "lore --robot timeline '' --since 30d", + "response_schema": { + "ok": "bool", + "data": {"entities": "[{type:string, iid:int, title:string, project_path:string}]", "events": "[{timestamp:string, type:string, entity_type:string, entity_iid:int, detail:string}]", "total_events": "int"}, + "meta": {"elapsed_ms": "int"} + } }, "robot-docs": { "description": "This command (agent self-discovery manifest)", @@ -1871,7 +2021,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> let exit_codes = serde_json::json!({ "0": "Success", - "1": "Internal error / health check failed / not implemented", + "1": "Internal error", "2": "Usage error (invalid flags or arguments)", "3": "Config invalid", "4": "Token not set", @@ -1889,6 +2039,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> "16": "Embedding failed", "17": "Not found", "18": "Ambiguous match", + "19": "Health check failed", "20": "Config not found" }); @@ -1953,7 +2104,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> aliases, exit_codes, clap_error_codes, - error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\"}}".to_string(), + error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\",\"actions\":[\"...\"]}}".to_string(), workflows, }, }; @@ -1991,6 +2142,7 @@ async fn handle_list_compat( target_branch_filter: Option<&str>, source_branch_filter: Option<&str>, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config = Config::load(config_override)?; match entity { @@ -2015,7 +2167,7 @@ async fn handle_list_compat( if open_browser { open_issue_in_browser(&result); } else if json_output { - print_list_issues_json(&result); + print_list_issues_json(&result, start.elapsed().as_millis() as u64, None); } else { print_list_issues(&result); } @@ -2045,7 +2197,7 @@ async fn handle_list_compat( if open_browser { open_mr_in_browser(&result); } else if json_output { - print_list_mrs_json(&result); + print_list_mrs_json(&result, start.elapsed().as_millis() as u64, None); } else { print_list_mrs(&result); } @@ -2066,13 +2218,14 @@ async fn handle_show_compat( project_filter: Option<&str>, robot_mode: bool, ) -> Result<(), Box> { + let start = std::time::Instant::now(); let config = Config::load(config_override)?; match entity { "issue" => { let result = run_show_issue(&config, iid, project_filter)?; if robot_mode { - print_show_issue_json(&result); + print_show_issue_json(&result, start.elapsed().as_millis() as u64); } else { print_show_issue(&result); } @@ -2081,7 +2234,7 @@ async fn handle_show_compat( "mr" => { let result = run_show_mr(&config, iid, project_filter)?; if robot_mode { - print_show_mr_json(&result); + print_show_mr_json(&result, start.elapsed().as_millis() as u64); } else { print_show_mr(&result); }