feat(robot): add elapsed_ms timing, --fields support, and actionable error actions
Robot mode consistency improvements across all command output:
Timing:
- Every robot JSON response now includes meta.elapsed_ms measuring
wall-clock time from command start to serialization. Agents can use
this to detect slow queries and tune --limit or --project filters.
Field selection (--fields):
- print_list_issues_json and print_list_mrs_json accept an optional
fields slice that prunes each item in the response array to only
the requested keys. A "minimal" preset expands to
[iid, title, state, updated_at_iso] for token-efficient agent scans.
- filter_fields and expand_fields_preset live in the new
src/cli/robot.rs module alongside RobotMeta.
Actionable error recovery:
- LoreError gains an actions() method returning concrete shell commands
an agent can execute to recover (e.g. "ollama serve" for
OllamaUnavailable, "lore init" for ConfigNotFound).
- RobotError now serializes an "actions" array (empty array omitted)
so agents can parse and offer one-click fixes.
Envelope consistency:
- show issue/MR JSON responses now use the standard
{"ok":true,"data":...,"meta":...} envelope instead of bare data,
matching all other commands.
Files: src/cli/robot.rs (new), src/core/error.rs,
src/cli/commands/{count,embed,generate_docs,ingest,list,show,stats,sync_status}.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::RobotMeta;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::events_db::{self, EventCounts};
|
||||
@@ -196,6 +197,7 @@ fn format_number(n: i64) -> String {
|
||||
struct CountJsonOutput {
|
||||
ok: bool,
|
||||
data: CountJsonData,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -228,6 +230,7 @@ pub fn run_count_events(config: &Config) -> Result<EventCounts> {
|
||||
struct EventCountJsonOutput {
|
||||
ok: bool,
|
||||
data: EventCountJsonData,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -245,7 +248,7 @@ struct EventTypeCounts {
|
||||
total: usize,
|
||||
}
|
||||
|
||||
pub fn print_event_count_json(counts: &EventCounts) {
|
||||
pub fn print_event_count_json(counts: &EventCounts, elapsed_ms: u64) {
|
||||
let output = EventCountJsonOutput {
|
||||
ok: true,
|
||||
data: EventCountJsonData {
|
||||
@@ -266,6 +269,7 @@ pub fn print_event_count_json(counts: &EventCounts) {
|
||||
},
|
||||
total: counts.total(),
|
||||
},
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
@@ -317,7 +321,7 @@ pub fn print_event_count(counts: &EventCounts) {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn print_count_json(result: &CountResult) {
|
||||
pub fn print_count_json(result: &CountResult, elapsed_ms: u64) {
|
||||
let breakdown = result.state_breakdown.as_ref().map(|b| CountJsonBreakdown {
|
||||
opened: b.opened,
|
||||
closed: b.closed,
|
||||
@@ -333,6 +337,7 @@ pub fn print_count_json(result: &CountResult) {
|
||||
system_excluded: result.system_count,
|
||||
breakdown,
|
||||
},
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
|
||||
@@ -2,6 +2,7 @@ use console::style;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::RobotMeta;
|
||||
use crate::core::db::{LATEST_SCHEMA_VERSION, create_connection, get_schema_version};
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::paths::get_db_path;
|
||||
@@ -112,12 +113,14 @@ pub fn print_embed(result: &EmbedCommandResult) {
|
||||
struct EmbedJsonOutput<'a> {
|
||||
ok: bool,
|
||||
data: &'a EmbedCommandResult,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
pub fn print_embed_json(result: &EmbedCommandResult) {
|
||||
pub fn print_embed_json(result: &EmbedCommandResult, elapsed_ms: u64) {
|
||||
let output = EmbedJsonOutput {
|
||||
ok: true,
|
||||
data: result,
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use serde::Serialize;
|
||||
use tracing::info;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::RobotMeta;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::paths::get_db_path;
|
||||
@@ -150,6 +151,7 @@ pub fn print_generate_docs(result: &GenerateDocsResult) {
|
||||
struct GenerateDocsJsonOutput {
|
||||
ok: bool,
|
||||
data: GenerateDocsJsonData,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -162,7 +164,7 @@ struct GenerateDocsJsonData {
|
||||
errored: usize,
|
||||
}
|
||||
|
||||
pub fn print_generate_docs_json(result: &GenerateDocsResult) {
|
||||
pub fn print_generate_docs_json(result: &GenerateDocsResult, elapsed_ms: u64) {
|
||||
let output = GenerateDocsJsonOutput {
|
||||
ok: true,
|
||||
data: GenerateDocsJsonData {
|
||||
@@ -180,6 +182,7 @@ pub fn print_generate_docs_json(result: &GenerateDocsResult) {
|
||||
unchanged: result.unchanged,
|
||||
errored: result.errored,
|
||||
},
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use serde::Serialize;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::RobotMeta;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::lock::{AppLock, LockOptions};
|
||||
@@ -732,6 +733,7 @@ fn print_mr_project_summary(path: &str, result: &IngestMrProjectResult) {
|
||||
struct IngestJsonOutput {
|
||||
ok: bool,
|
||||
data: IngestJsonData,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -768,7 +770,7 @@ struct IngestMrStats {
|
||||
diffnotes_count: usize,
|
||||
}
|
||||
|
||||
pub fn print_ingest_summary_json(result: &IngestResult) {
|
||||
pub fn print_ingest_summary_json(result: &IngestResult, elapsed_ms: u64) {
|
||||
let (issues, merge_requests) = if result.resource_type == "issues" {
|
||||
(
|
||||
Some(IngestIssueStats {
|
||||
@@ -807,6 +809,7 @@ pub fn print_ingest_summary_json(result: &IngestResult) {
|
||||
resource_events_fetched: result.resource_events_fetched,
|
||||
resource_events_failed: result.resource_events_failed,
|
||||
},
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
|
||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::{RobotMeta, expand_fields_preset, filter_fields};
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::paths::get_db_path;
|
||||
@@ -734,9 +735,20 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
println!("{table}");
|
||||
}
|
||||
|
||||
pub fn print_list_issues_json(result: &ListResult) {
|
||||
pub fn print_list_issues_json(result: &ListResult, elapsed_ms: u64, fields: Option<&[String]>) {
|
||||
let json_result = ListResultJson::from(result);
|
||||
match serde_json::to_string_pretty(&json_result) {
|
||||
let meta = RobotMeta { elapsed_ms };
|
||||
let output = serde_json::json!({
|
||||
"ok": true,
|
||||
"data": json_result,
|
||||
"meta": meta,
|
||||
});
|
||||
let mut output = output;
|
||||
if let Some(f) = fields {
|
||||
let expanded = expand_fields_preset(f, "issues");
|
||||
filter_fields(&mut output, "issues", &expanded);
|
||||
}
|
||||
match serde_json::to_string(&output) {
|
||||
Ok(json) => println!("{json}"),
|
||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||
}
|
||||
@@ -819,9 +831,20 @@ pub fn print_list_mrs(result: &MrListResult) {
|
||||
println!("{table}");
|
||||
}
|
||||
|
||||
pub fn print_list_mrs_json(result: &MrListResult) {
|
||||
pub fn print_list_mrs_json(result: &MrListResult, elapsed_ms: u64, fields: Option<&[String]>) {
|
||||
let json_result = MrListResultJson::from(result);
|
||||
match serde_json::to_string_pretty(&json_result) {
|
||||
let meta = RobotMeta { elapsed_ms };
|
||||
let output = serde_json::json!({
|
||||
"ok": true,
|
||||
"data": json_result,
|
||||
"meta": meta,
|
||||
});
|
||||
let mut output = output;
|
||||
if let Some(f) = fields {
|
||||
let expanded = expand_fields_preset(f, "mrs");
|
||||
filter_fields(&mut output, "mrs", &expanded);
|
||||
}
|
||||
match serde_json::to_string(&output) {
|
||||
Ok(json) => println!("{json}"),
|
||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::RobotMeta;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::paths::get_db_path;
|
||||
@@ -1042,17 +1043,29 @@ impl From<&MrNoteDetail> for MrNoteDetailJson {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_show_issue_json(issue: &IssueDetail) {
|
||||
pub fn print_show_issue_json(issue: &IssueDetail, elapsed_ms: u64) {
|
||||
let json_result = IssueDetailJson::from(issue);
|
||||
match serde_json::to_string_pretty(&json_result) {
|
||||
let meta = RobotMeta { elapsed_ms };
|
||||
let output = serde_json::json!({
|
||||
"ok": true,
|
||||
"data": json_result,
|
||||
"meta": meta,
|
||||
});
|
||||
match serde_json::to_string(&output) {
|
||||
Ok(json) => println!("{json}"),
|
||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_show_mr_json(mr: &MrDetail) {
|
||||
pub fn print_show_mr_json(mr: &MrDetail, elapsed_ms: u64) {
|
||||
let json_result = MrDetailJson::from(mr);
|
||||
match serde_json::to_string_pretty(&json_result) {
|
||||
let meta = RobotMeta { elapsed_ms };
|
||||
let output = serde_json::json!({
|
||||
"ok": true,
|
||||
"data": json_result,
|
||||
"meta": meta,
|
||||
});
|
||||
match serde_json::to_string(&output) {
|
||||
Ok(json) => println!("{json}"),
|
||||
Err(e) => eprintln!("Error serializing to JSON: {e}"),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::RobotMeta;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::paths::get_db_path;
|
||||
@@ -441,9 +442,10 @@ pub fn print_stats(result: &StatsResult) {
|
||||
struct StatsJsonOutput {
|
||||
ok: bool,
|
||||
data: StatsResult,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
pub fn print_stats_json(result: &StatsResult) {
|
||||
pub fn print_stats_json(result: &StatsResult, elapsed_ms: u64) {
|
||||
let output = StatsJsonOutput {
|
||||
ok: true,
|
||||
data: StatsResult {
|
||||
@@ -471,6 +473,7 @@ pub fn print_stats_json(result: &StatsResult) {
|
||||
}),
|
||||
}),
|
||||
},
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::robot::RobotMeta;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::metrics::StageTiming;
|
||||
@@ -190,6 +191,7 @@ fn format_number(n: i64) -> String {
|
||||
struct SyncStatusJsonOutput {
|
||||
ok: bool,
|
||||
data: SyncStatusJsonData,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -238,7 +240,7 @@ struct SummaryJsonInfo {
|
||||
system_notes: i64,
|
||||
}
|
||||
|
||||
pub fn print_sync_status_json(result: &SyncStatusResult) {
|
||||
pub fn print_sync_status_json(result: &SyncStatusResult, elapsed_ms: u64) {
|
||||
let runs = result
|
||||
.runs
|
||||
.iter()
|
||||
@@ -284,6 +286,7 @@ pub fn print_sync_status_json(result: &SyncStatusResult) {
|
||||
system_notes: result.summary.system_note_count,
|
||||
},
|
||||
},
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
|
||||
Reference in New Issue
Block a user