use std::io::Read;
use borg::{collective, conscience, expert, harness, install, learn, outcome, prefilter};
use serde_json::{Value, json};
fn facets_for_written(store: &std::path::Path, from: u64) {
let ids = borg::facets::appended_ids(store, from);
if ids.is_empty() || !borg::facets::enabled() {
return;
}
let r = borg::facets::fill(store, Some(&ids), true, &|prompt: &str, schema: &Value| learn::ask_model(prompt, schema), borg::runner::JOBS, learn::now_ms());
if r.written > 0 || !r.failed.is_empty() {
println!(
"facets written for {} lessons (${:.4}){}",
r.written,
r.cost_usd,
if r.failed.is_empty() { String::new() } else { format!("; {} failed, `borg facets --apply` retries", r.failed.len()) }
);
}
}
fn atty(fd: i32) -> bool {
std::env::var_os("NO_COLOR").is_none() && unsafe { libc::isatty(fd) } == 1
}
fn help(color: bool, debug: bool) -> String {
use borg::completions::{Command, DEBUG, GROUPS};
let bold = |t: &str| if color { format!("\x1b[1m{t}\x1b[0m") } else { t.to_string() };
let names = harness::names("|");
let line = |c: &Command, args: &str| format!("{} {args}", c.name).trim_end().replace("HARNESS", &names);
let groups: Vec<(&str, &[Command])> = if debug { vec![("Debugging and evaluation", DEBUG)] } else { GROUPS.to_vec() };
let width = groups.iter().flat_map(|(_, cmds)| cmds.iter().flat_map(|c| c.help.iter().map(|(args, _)| line(c, args).chars().count()))).max().unwrap_or(0);
let (title, usage) = match debug {
true => ("borg debug: evaluation and debugging tools, not for everyday use", "borg debug <command> [options]"),
false => ("borg: a shared memory for coding agents", "borg <command> [options]"),
};
let mut out = format!("{}\n\n{} {usage}\n", bold(title), bold("Usage:"));
for (group, cmds) in &groups {
out.push_str(&format!("\n{}\n", bold(group)));
for c in *cmds {
for (args, what) in c.help {
out.push_str(&format!(" {:<width$} {what}\n", line(c, args)));
}
}
}
out.push('\n');
if !debug {
out.push_str("Evaluation and debugging tools: borg debug help\n");
}
out.push_str("More: docs/commands.md, or https://github.com/debe/borg_ai\n");
out
}
fn command_help<'a>(mut cmds: impl Iterator<Item = &'a borg::completions::Command>, args: &[&str], prefix: &str) -> Option<String> {
let [name, rest @ ..] = args else { return None };
if !rest.iter().any(|a| matches!(*a, "--help" | "-h")) || matches!(*name, "mesh" | "web") {
return None;
}
let c = cmds.find(|c| c.name == *name)?;
let names = harness::names("|");
Some(
c.help
.iter()
.map(|(args, what)| format!("usage: {prefix}{}\n {what}\n", format!("{} {args}", c.name).trim_end().replace("HARNESS", &names)))
.collect(),
)
}
fn exec(part: &str, args: &[&str]) -> ! {
use std::os::unix::process::CommandExt;
let bin = format!("borg-{part}");
let err = std::process::Command::new(borg::install::sibling(part).unwrap_or_else(|| bin.clone().into())).args(args).exec();
eprintln!("borg {part}: cannot run {bin} ({err}); cargo install {bin} --locked");
std::process::exit(1);
}
fn collective_cmd(rest: &[&str]) {
let dir = rest.first().map_or_else(|| std::env::current_dir().unwrap_or_default(), std::path::PathBuf::from);
let cwd = std::path::absolute(&dir).unwrap_or(dir).display().to_string();
match collective::load() {
Err(e) => println!("off (invalid config: {e})"),
Ok(cfg) => match &collective::route(cfg.as_ref(), &cwd, &learn::home()) {
collective::Route::Off => println!("off: {cwd} belongs to no collective; hooks do nothing here"),
r @ collective::Route::Collective { name, home, share } => {
println!("{name} ({share:?}, may sync: {}) store {}", collective::may_sync(r), home.display());
}
},
}
}
fn challenges_and_stdin(path: &str, rest: &[&str]) -> (Vec<conscience::Challenge>, String) {
let before = rest.first().and_then(|v| v.parse().ok());
let mut challenges = conscience::load(path.as_ref(), before);
borg::absorb::withhold_beside(path.as_ref(), &mut challenges, before);
let mut context = String::new();
let _ = std::io::stdin().read_to_string(&mut context);
(challenges, context)
}
fn learn_cmd(rest: &[&str]) {
borg::stats::set_purpose("learn");
let session = rest.iter().position(|a| *a == "--session").and_then(|i| rest.get(i + 1)).copied();
let mut others = Vec::new();
if learn::learning()
&& let Ok(cfg) = borg::collective::load()
{
let root = borg::collective::root_of(&learn::home());
let mut homes = std::collections::BTreeSet::new();
if let Some(s) = session
&& borg::review_fix::on()
{
homes.extend(borg::review_fix::record_session(&borg::review_fix::home_dir(), s, cfg.as_ref(), &root).1);
}
if borg::sweep::on() {
if let Some(s) = session {
homes.extend(borg::sweep::session_end(s, cfg.as_ref(), &root));
}
homes.extend(borg::sweep::idle(cfg.as_ref(), &root, learn::now_ms()));
}
others = homes.into_iter().filter(|h| *h != learn::home()).collect();
}
for home in &others {
learn_store(home, session);
}
let (new, merged, waiting) = learn_store(&learn::home(), session);
println!("{new} new lessons, {merged} merged into existing ones");
if waiting > 0 {
eprintln!("borg learn: the model gave no answer; {waiting} episodes still pending, re-run `borg learn` later");
}
}
fn learn_store(home: &std::path::Path, session: Option<&str>) -> (usize, usize, usize) {
let store = home.join("lessons.jsonl");
let from = borg::facets::len_of(&store);
let got = learn::learn_in_reporting(home, session, &learn::ask_claude, learn::learning(), learn::now_ms(), &mut learn::print_step);
facets_for_written(&store, from);
borg::vectors::refresh(&store);
borg::stats::rollup(home);
borg::outcome::publish(home);
if borg::outcome::path_beside(&store).exists() {
let _ = borg::select::summary::refit(&store);
}
got
}
fn check_cmd(rest: &[&str]) {
let flag = |f: &str| rest.iter().position(|a| *a == f).and_then(|i| rest.get(i + 1)).copied().unwrap_or_default();
let session = flag("--session");
if session.is_empty() {
eprintln!("borg check: --session ID is required (internal: the PostToolUse hook runs it in live mode)");
std::process::exit(2);
}
let input = json!({"session_id": session, "cwd": flag("--cwd"), "transcript_path": flag("--transcript")});
borg::hook::live_check(harness::current(), session, &input);
}
fn memory_cmd(path: &str, rest: &[&str]) {
let session = rest.iter().position(|a| *a == "--session").and_then(|i| rest.get(i + 1)).copied();
let path = std::path::Path::new(path);
let lessons = conscience::load_unweighted(path, None);
let outcomes = outcome::read_team(path);
let memory = borg::memory::derive(&outcomes, &lessons, session);
let weights = outcome::weights(&outcomes);
for c in &lessons {
let m = &memory[&c.id];
println!(
"{}",
json!({"id": c.id, "activation": m.activation, "changed": m.changed, "ignored": m.ignored, "corrected": m.corrected,
"vetoed": m.vetoed, "regretted": m.regretted, "tested": m.tested, "mean": m.mean(), "weight": m.weight(&c.id, session), "consolidated": m.consolidated, "dormant": m.dormant,
"outcome_weight": weights.get(&c.id).copied().unwrap_or(1.0)})
);
}
}
fn resolve_cmd(path: &str) {
let mut raw = String::new();
let _ = std::io::stdin().read_to_string(&mut raw);
let v: Value = serde_json::from_str(&raw).unwrap_or(Value::Null);
let all = conscience::load_unweighted(path.as_ref(), None);
let ids: Vec<&str> = v["asked"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
let asked: Vec<&conscience::Challenge> = ids.iter().filter_map(|i| all.iter().find(|c| c.id == *i)).collect();
let min = v["min_new"].as_u64().map_or(borg::memory::NOVEL_WORDS, |n| usize::try_from(n).unwrap_or(usize::MAX));
let s = |k: &str| v[k].as_str().unwrap_or_default();
let (changed, ignored) = borg::memory::resolve(&asked, &all, s("before"), s("edits"), s("final"), min);
println!("{}", json!({"changed": changed, "ignored": ignored}));
}
fn debug_cmd(args: &[&str]) {
if let Some(h) = command_help(borg::completions::DEBUG.iter(), args, "borg debug ") {
print!("{h}");
return;
}
match args {
["prefilter", path, rest @ ..] => {
let (challenges, context) = challenges_and_stdin(path, rest);
println!("{}", prefilter::features(&challenges, &context));
}
["memory", path, rest @ ..] => memory_cmd(path, rest),
["resolve", path] => resolve_cmd(path),
["recall", path, rest @ ..] => {
let (challenges, context) = challenges_and_stdin(path, rest);
for (score, c) in conscience::rank(&challenges, &conscience::work_text(&context)) {
println!("{} {score:.3}", c.id);
}
}
["expert", repo, rest @ ..] => std::process::exit(expert::main(repo, rest)),
["vectors", rest @ ..] if rest.contains(&"--query") => std::process::exit(borg::vectors::main(rest)),
["ope", rest @ ..] => std::process::exit(borg::ope::main(rest)),
["select", rest @ ..] => std::process::exit(borg::select::replay::main(rest)),
[] | ["help" | "-h" | "--help"] => print!("{}", help(atty(1), true)),
_ => {
eprint!("{}", help(atty(2), true));
std::process::exit(2);
}
}
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let args: Vec<&str> = args.iter().map(String::as_str).collect();
if let Some(cmd) = args.first().filter(|c| !borg::completions::known(c)) {
eprintln!("borg: unknown command {cmd} (borg help lists them)");
std::process::exit(2);
}
if let Some(h) = command_help(borg::completions::commands(), &args, "borg ") {
print!("{h}");
return;
}
match args.as_slice() {
["hook", name, event] => borg::hook::main(name, event),
["collective", rest @ ..] => collective_cmd(rest),
["mesh", rest @ ..] => exec("mesh", rest),
["web", rest @ ..] => exec("web", rest),
["learn", rest @ ..] => learn_cmd(rest),
["check", rest @ ..] => check_cmd(rest),
["vectors", rest @ ..] if rest.contains(&"--query") => {
eprintln!("borg vectors: unknown flag --query (borg debug vectors --query)");
std::process::exit(2);
}
["vectors", rest @ ..] => std::process::exit(borg::vectors::main(rest)),
["debug", rest @ ..] => debug_cmd(rest),
["memory", rest @ ..] => std::process::exit(borg::memory_export::main(rest)),
["setup", rest @ ..] => std::process::exit(borg::install::setup::main(rest)),
["completions", rest @ ..] => std::process::exit(borg::completions::main(rest)),
["reach", rest @ ..] => {
borg::stats::set_purpose("reach");
std::process::exit(borg::reach::main(rest))
}
["assimilate", rest @ ..] if rest.contains(&"--from-dir") => {
borg::stats::set_purpose("assimilate");
std::process::exit(borg::assimilate_dir::main(rest, &|prompt: &str, schema: &Value| learn::ask_model(prompt, schema)))
}
["facets", rest @ ..] => {
borg::stats::set_purpose("facets");
std::process::exit(borg::facets::main(rest, &|prompt: &str, schema: &Value| learn::ask_model(prompt, schema)))
}
["assimilate", rest @ ..] => {
borg::stats::set_purpose("assimilate");
std::process::exit(borg::assimilate::main(rest, &learn::ask_claude))
}
["queen", rest @ ..] => std::process::exit(borg::queen::main(rest)),
["notes", rest @ ..] => std::process::exit(borg::notes::main(rest, Some(borg::notes::regress_learn::after_notes))),
["regress", rest @ ..] => std::process::exit(borg::notes::regress_cli::main(rest)),
["review-fix", rest @ ..] => {
borg::stats::set_purpose("learn");
std::process::exit(borg::review_fix::main(rest, &learn::ask_claude))
}
["trust", rest @ ..] => std::process::exit(borg::trust::main(rest)),
["top", rest @ ..] => std::process::exit(borg::top::main(rest)),
["no", rest @ ..] => std::process::exit(borg::veto::main(rest)),
[cmd @ ("tell" | "ask" | "pause" | "resume" | "inbox"), rest @ ..] => std::process::exit(borg::msg::main(cmd, rest)),
["listen", rest @ ..] => std::process::exit(borg::listen::main(rest)),
["why", rest @ ..] => std::process::exit(borg::why::main(rest)),
[cmd @ ("install" | "uninstall" | "status" | "doctor"), rest @ ..] => std::process::exit(install::main(cmd, rest)),
["update", rest @ ..] => std::process::exit(borg::update::main(rest)),
["version" | "--version" | "-V"] => println!("borg {}", env!("CARGO_PKG_VERSION")),
[] | ["help" | "-h" | "--help"] => print!("{}", help(atty(1), false)),
_ => {
eprint!("{}", help(atty(2), false));
std::process::exit(2);
}
}
}