feat: implement lore who — people intelligence commands (5 modes)

Add `lore who` command with 5 query modes answering collaboration questions
using existing DB data (280K notes, 210K discussions, 33K DiffNotes):

- Expert: who knows about a file/directory (DiffNote path analysis + MR breadth scoring)
- Workload: what is a person working on (assigned issues, authored/reviewing MRs, discussions)
- Active: what discussions need attention (unresolved resolvable, global/project-scoped)
- Overlap: who else is touching these files (dual author+reviewer role tracking)
- Reviews: what review patterns does a person have (prefix-based category extraction)

Includes migration 017 (5 composite indexes), CLI skeleton with clap conflicts_with
validation, robot JSON output with input+resolved_input reproducibility, human terminal
output, and 20 unit tests. All quality gates pass.

Closes: bd-1q8z, bd-34rr, bd-2rk9, bd-2ldg, bd-zqpf, bd-s3rc, bd-m7k1, bd-b51e,
bd-2711, bd-1rdi, bd-3mj2, bd-tfh3, bd-zibc, bd-g0d5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-07 23:11:14 -05:00
parent 859923f86b
commit f267578aab
9 changed files with 2831 additions and 7 deletions

View File

@@ -18,15 +18,15 @@ use lore::cli::commands::{
print_list_mrs_json, print_search_results, print_search_results_json, print_show_issue,
print_show_issue_json, print_show_mr, print_show_mr_json, print_stats, print_stats_json,
print_sync, print_sync_json, print_sync_status, print_sync_status_json, print_timeline,
print_timeline_json_with_meta, run_auth_test, run_count, run_count_events, run_doctor,
run_embed, run_generate_docs, run_ingest, run_ingest_dry_run, run_init, run_list_issues,
run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status,
run_timeline,
print_timeline_json_with_meta, print_who_human, print_who_json, run_auth_test, run_count,
run_count_events, run_doctor, run_embed, run_generate_docs, run_ingest, run_ingest_dry_run,
run_init, run_list_issues, run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats,
run_sync, run_sync_status, run_timeline, run_who,
};
use lore::cli::robot::RobotMeta;
use lore::cli::{
Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs,
SearchArgs, StatsArgs, SyncArgs, TimelineArgs,
SearchArgs, StatsArgs, SyncArgs, TimelineArgs, WhoArgs,
};
use lore::core::db::{
LATEST_SCHEMA_VERSION, create_connection, get_schema_version, run_migrations,
@@ -161,6 +161,7 @@ async fn main() {
handle_search(cli.config.as_deref(), args, robot_mode).await
}
Some(Commands::Timeline(args)) => handle_timeline(cli.config.as_deref(), args, robot_mode),
Some(Commands::Who(args)) => handle_who(cli.config.as_deref(), args, robot_mode),
Some(Commands::Stats(args)) => handle_stats(cli.config.as_deref(), args, robot_mode).await,
Some(Commands::Embed(args)) => handle_embed(cli.config.as_deref(), args, robot_mode).await,
Some(Commands::Sync(args)) => {
@@ -488,6 +489,7 @@ fn suggest_similar_command(invalid: &str) -> String {
"robot-docs",
"completions",
"timeline",
"who",
];
let invalid_lower = invalid.to_lowercase();
@@ -2012,6 +2014,28 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
"meta": {"elapsed_ms": "int"}
}
},
"who": {
"description": "People intelligence: experts, workload, active discussions, overlap, review patterns",
"flags": ["<target>", "--path <path>", "--active", "--overlap <path>", "--reviews", "--since <duration>", "-p/--project", "-n/--limit"],
"modes": {
"expert": "lore who <file-path> -- Who knows about this area? (also: --path for root files)",
"workload": "lore who <username> -- What is someone working on?",
"reviews": "lore who <username> --reviews -- Review pattern analysis",
"active": "lore who --active -- Active unresolved discussions",
"overlap": "lore who --overlap <path> -- Who else is touching these files?"
},
"example": "lore --robot who src/features/auth/",
"response_schema": {
"ok": "bool",
"data": {
"mode": "string",
"input": {"target": "string|null", "path": "string|null", "project": "string|null", "since": "string|null", "limit": "int"},
"resolved_input": {"mode": "string", "project_id": "int|null", "project_path": "string|null", "since_ms": "int", "since_iso": "string", "since_mode": "string (default|explicit|none)", "limit": "int"},
"...": "mode-specific fields"
},
"meta": {"elapsed_ms": "int"}
}
},
"robot-docs": {
"description": "This command (agent self-discovery manifest)",
"flags": [],
@@ -2062,6 +2086,14 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
"lore --robot sync",
"lore --robot timeline '<keyword>' --since 30d",
"lore --robot timeline '<keyword>' --depth 2 --expand-mentions"
],
"people_intelligence": [
"lore --robot who src/path/to/feature/",
"lore --robot who @username",
"lore --robot who @username --reviews",
"lore --robot who --active --since 7d",
"lore --robot who --overlap src/path/",
"lore --robot who --path README.md"
]
});
@@ -2118,6 +2150,24 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
Ok(())
}
fn handle_who(
config_override: Option<&str>,
args: WhoArgs,
robot_mode: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let start = std::time::Instant::now();
let config = Config::load(config_override)?;
let run = run_who(&config, &args)?;
let elapsed_ms = start.elapsed().as_millis() as u64;
if robot_mode {
print_who_json(&run, &args, elapsed_ms);
} else {
print_who_human(&run.result, run.resolved_input.project_path.as_deref());
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn handle_list_compat(
config_override: Option<&str>,