use std::path::{Path, PathBuf};
use crate::conscience::Challenge;
use crate::learn::{Episode, Lesson, append_jsonl};
pub const REPO: &str = "repo";
pub const PERSONAL: &str = "personal";
pub const PERSONAL_FILE: &str = "personal.jsonl";
pub fn personal_path(home: &Path) -> PathBuf {
home.join(PERSONAL_FILE)
}
pub fn personal_lessons(home: &Path) -> Vec<Lesson> {
personal_all(home).into_iter().filter(|l| l.question != MOVED).collect()
}
pub fn personal_all(home: &Path) -> Vec<Lesson> {
crate::learn::lessons(&personal_path(home))
}
pub fn personal_ids(home: &Path) -> std::collections::HashSet<String> {
personal_all(home).into_iter().map(|l| l.id).collect()
}
pub const MOVED: &str = "(moved to another collective)";
pub fn deliverable(c: &Challenge, repos: &[String]) -> bool {
match c.reach.as_str() {
PERSONAL => false,
REPO => repos.iter().any(|r| r.eq_ignore_ascii_case(&c.repo)),
_ => true,
}
}
pub fn narrower<'a>(a: &'a str, b: &'a str) -> &'a str {
let rank = |r: &str| match r {
PERSONAL => 2,
REPO => 1,
_ => 0,
};
if rank(b) > rank(a) { b } else { a }
}
pub const PERSONAL_TOMBSTONE: &str = "(personal to its engineer)";
pub const WITHDRAWN: &str = "(personal preference, moved to a private collective)";
pub fn only(engineers: &[String], me: &str) -> bool {
only_with(engineers, me, crate::collective::aliases())
}
pub fn only_with(engineers: &[String], me: &str, aliases: &crate::collective::Aliases) -> bool {
!me.trim().is_empty() && !engineers.is_empty() && engineers.iter().all(|x| aliases.same(x.trim(), me.trim()))
}
fn learned_here(l: &Lesson) -> bool {
l.source.is_empty() || l.source == "queen"
}
#[derive(Debug, Clone)]
pub enum Placed {
Here(Box<Lesson>),
Personal { lesson: Box<Lesson>, withdraw: Option<Box<Lesson>> },
}
impl Placed {
pub fn lesson(&self) -> &Lesson {
match self {
Placed::Here(l) | Placed::Personal { lesson: l, .. } => l,
}
}
}
pub fn place(l: &Lesson, e: &Episode, mine: bool, team: Option<&Lesson>) -> Placed {
if !mine && l.reach != PERSONAL {
return Placed::Here(Box::new(l.clone()));
}
let personal = |l: Lesson| Box::new(Lesson { reach: PERSONAL.into(), ..l });
match team {
Some(t) if !mine && !(only(&t.engineers, &e.engineer) && learned_here(t)) => Placed::Personal { lesson: personal(own(l, e)), withdraw: None },
Some(_) if !mine => Placed::Personal { lesson: personal(l.clone()), withdraw: Some(Box::new(tombstone(l))) },
_ => Placed::Personal { lesson: personal(l.clone()), withdraw: None },
}
}
fn own(l: &Lesson, e: &Episode) -> Lesson {
Lesson {
id: format!("L{}", e.id),
origin_ms: e.ms,
repo: e.repo.clone(),
question: l.question.clone(),
situations: l.situations.clone(),
consequence: l.consequence.clone(),
support: 1,
episodes: vec![e.id.clone()],
version: 1,
engineers: Some(e.engineer.clone()).filter(|x| !x.is_empty()).into_iter().collect(),
area: l.area.clone(),
..Lesson::default()
}
}
pub fn write(home: &Path, p: &Placed) -> std::io::Result<()> {
match p {
Placed::Here(l) => append_jsonl(&home.join("lessons.jsonl"), &**l),
Placed::Personal { lesson, withdraw } => {
append_jsonl(&personal_path(home), &**lesson)?;
match withdraw {
Some(t) => append_jsonl(&home.join("lessons.jsonl"), &**t),
None => Ok(()),
}
}
}
}
pub fn tombstone(l: &Lesson) -> Lesson {
Lesson {
version: l.version.max(1) + 1,
question: PERSONAL_TOMBSTONE.into(),
situations: Vec::new(),
consequence: String::new(),
history: Vec::new(),
facets: Vec::new(),
reach: PERSONAL.into(),
..l.clone()
}
}
pub const TEAM_OPTION: &str = "team: everyone working on this code should hear it";
pub const PERSONAL_OPTION: &str = "personal: only for me, kept in this collective";
const PLAN_CHARS: usize = 110;
fn repo_option(repo: &str) -> String {
format!("repo: only in {}", if repo.is_empty() { "the repo it was learned in" } else { repo })
}
fn verdict(text: &str) -> &'static str {
let t = text.trim().to_lowercase();
if t.starts_with("personal") || t.contains("private") || t.contains("only me") {
PERSONAL
} else if t.starts_with("repo") {
REPO
} else {
""
}
}
pub fn apply_one(home: &Path, l: &Lesson, to: &str, me: &str) -> Result<(), String> {
match to {
PERSONAL | REPO if !only(&l.engineers, me) => Err(format!("{} is not yours alone (another engineer or none recorded): it stays with the team", l.id)),
PERSONAL => write(home, &Placed::Personal { lesson: Box::new(Lesson { reach: PERSONAL.into(), ..l.clone() }), withdraw: Some(Box::new(tombstone(l))) })
.map_err(|e| e.to_string()),
REPO => append_jsonl(&home.join("lessons.jsonl"), &Lesson { version: l.version + 1, reach: REPO.into(), ..l.clone() }).map_err(|e| e.to_string()),
_ => Ok(()),
}
}
fn me() -> String {
crate::learn::engineer(&std::env::current_dir().unwrap_or_default().display().to_string())
}
pub fn answer(store: &Path, e: &crate::queen::Entry, text: &str) -> Result<(), String> {
let id = e.evidence.first().and_then(|x| x.strip_prefix("lesson ")).ok_or("this question names no lesson")?;
let l = crate::learn::lessons(&store.join("lessons.jsonl")).into_iter().find(|l| l.id == id).ok_or(format!("lesson {id} is not in this collective"))?;
apply_one(store, &l, verdict(text), &me())
}
const PROMPT: &str = "A coding agent learned this lesson from an engineer's correction. Decide its reach: who should hear it. \
Ask: would a teammate working on the same code get this correction too?
- personal: how this one engineer likes the agent to work with them, not a property of the code: commit message style or \
length, AI attribution or co-author trailers, whether and when to commit, push, branch or open a PR, what to stage, how to \
report or ask, language and tone, their own tooling habits.
- repo: a convention of the repo it was learned in that other repos do not share (its branch or deploy workflow, a tool or \
command it mandates).
- team: an engineering concern about the code, design, correctness or verification that any teammate should hear.
Set confident to false when the lesson mixes these or you cannot tell. why: one short sentence.";
fn schema() -> serde_json::Value {
serde_json::json!({"type": "object", "additionalProperties": false, "properties": {
"reach": {"type": "string", "enum": ["team", "repo", "personal"]}, "confident": {"type": "boolean"}, "why": {"type": "string"}},
"required": ["reach", "confident", "why"]})
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Verdict {
pub id: String,
pub repo: String,
pub question: String,
pub reach: String,
pub confident: bool,
pub why: String,
}
fn candidate(l: &Lesson, me: &str) -> bool {
l.reach.is_empty() && learned_here(l) && only(&l.engineers, me)
}
pub fn candidates(home: &Path, me: &str) -> Vec<Lesson> {
crate::learn::lessons(&home.join("lessons.jsonl")).into_iter().filter(|l| candidate(l, me)).collect()
}
pub type Ask = dyn Fn(&str, &serde_json::Value) -> Option<(serde_json::Value, Option<f64>)> + Sync;
pub fn classify(lessons: &[Lesson], jobs: usize, ask: &Ask) -> (Vec<Verdict>, f64) {
use std::sync::atomic::{AtomicUsize, Ordering};
let out = std::sync::Mutex::new((Vec::new(), 0.0));
let (next, failures, answers) = (AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0));
std::thread::scope(|s| {
for _ in 0..jobs.max(1) {
s.spawn(|| {
loop {
if failures.load(Ordering::SeqCst) >= crate::runner::MAX_FAILURES && answers.load(Ordering::SeqCst) == 0 {
break;
}
let i = next.fetch_add(1, Ordering::Relaxed);
let Some(l) = lessons.get(i) else { break };
let prompt = format!("{PROMPT}\n\nLesson (learned in repo {}):\nQuestion: {}\nConsequence: {}\n", l.repo, l.question, l.consequence);
let Some((v, cost)) = ask(&prompt, &schema()) else {
failures.fetch_add(1, Ordering::SeqCst);
continue;
};
answers.fetch_add(1, Ordering::SeqCst);
let reach = v["reach"].as_str().filter(|r| [REPO, PERSONAL].contains(r)).unwrap_or_default();
let verdict = Verdict {
id: l.id.clone(),
repo: l.repo.clone(),
question: l.question.clone(),
reach: reach.into(),
confident: v["confident"].as_bool().unwrap_or(false),
why: v["why"].as_str().unwrap_or_default().into(),
};
let mut g = out.lock().unwrap();
g.0.push(verdict);
g.1 += cost.unwrap_or(0.0);
}
});
}
});
let (mut v, cost) = out.into_inner().unwrap();
v.sort_by(|a, b| a.id.cmp(&b.id));
(v, cost)
}
#[derive(Debug, Default, PartialEq)]
pub struct Applied {
pub personal: usize,
pub scoped: usize,
pub asked: usize,
}
pub fn apply(home: &Path, plan: &[Verdict], me: &str, now: i64) -> Result<Applied, String> {
let lessons = crate::learn::lessons(&home.join("lessons.jsonl"));
let mut done = Applied::default();
for v in plan {
let Some(l) = lessons.iter().find(|l| l.id == v.id && candidate(l, me)) else { continue };
match (v.confident, v.reach.as_str()) {
(true, PERSONAL) => {
apply_one(home, l, PERSONAL, me)?;
done.personal += 1;
}
(true, REPO) => {
apply_one(home, l, REPO, me)?;
done.scoped += 1;
}
(true, _) => {}
(false, _) => {
let key = crate::queen::decision_key(crate::queen::Kind::Reach, &l.repo, &l.id);
let default = match v.reach.as_str() {
PERSONAL => PERSONAL_OPTION.to_string(),
REPO => repo_option(&l.repo),
_ => TEAM_OPTION.to_string(),
};
let e = crate::queen::Entry {
id: format!("q{}", &key[..8]),
decision_key: key,
kind: crate::queen::Kind::Reach,
question: format!("Who should hear this lesson? \"{}\"", crate::stats::excerpt(&l.question, crate::queen::QUOTE_CHARS)),
options: vec![TEAM_OPTION.into(), repo_option(&l.repo), PERSONAL_OPTION.into()],
default,
evidence: vec![format!("lesson {}", l.id), format!("model: {}", v.why)],
session: "reach".into(),
cwd: String::new(),
repo: l.repo.clone(),
engineer: l.engineers.first().cloned().unwrap_or_default(),
ms: now,
answer: None,
answered_by: None,
answered_ms: None,
disagreeing: Vec::new(),
dismissed: None,
dismissed_ms: None,
payload: None,
};
if crate::queen::seed(home, &e).map_err(|e| e.to_string())? {
done.asked += 1;
}
}
}
}
Ok(done)
}
pub fn main(args: &[&str]) -> i32 {
let flag = |name: &str| args.iter().position(|a| *a == name).and_then(|i| args.get(i + 1)).copied();
let apply_it = args.contains(&"--apply");
let cwd = std::env::current_dir().unwrap_or_default().display().to_string();
let cfg = match crate::collective::load() {
Ok(c) => c,
Err(e) => {
eprintln!("borg reach: invalid collectives config: {e}");
return 1;
}
};
let crate::collective::Route::Collective { name, home, .. } = crate::collective::route(cfg.as_ref(), &cwd, &crate::learn::home()) else {
eprintln!("borg reach: {cwd} belongs to no collective");
return 1;
};
let plan_path = flag("--plan").map(PathBuf::from).unwrap_or_else(|| home.join("reach-plan.json"));
if apply_it {
let plan: Vec<Verdict> = match std::fs::read_to_string(&plan_path).ok().and_then(|t| serde_json::from_str(&t).ok()) {
Some(p) => p,
None => {
eprintln!("borg reach: no plan at {} (run `borg reach` first)", plan_path.display());
return 1;
}
};
return match apply(&home, &plan, &me(), crate::learn::now_ms()) {
Ok(a) => {
println!(
"{name}: {} kept personal (for you only, in {name}), {} limited to their repo, {} queued for the queen (`borg queen`)",
a.personal, a.scoped, a.asked
);
0
}
Err(e) => {
eprintln!("borg reach: {e}");
1
}
};
}
let todo = candidates(&home, &me());
let jobs = flag("--jobs").and_then(|j| j.parse().ok()).unwrap_or(crate::runner::JOBS);
eprintln!("borg reach: classifying {} lessons of {name} (one model call each, {jobs} at a time)", todo.len());
let (plan, cost) = classify(&todo, jobs, &|p: &str, s: &serde_json::Value| crate::learn::ask_model(p, s));
for (label, pick) in [("personal: for you only, kept in this collective", PERSONAL), ("repo: deliver only in its repo", REPO)] {
let hits: Vec<&Verdict> = plan.iter().filter(|v| v.confident && v.reach == pick).collect();
println!("\n{label} ({})", hits.len());
for v in hits {
println!(" {} {} ({})", v.id, crate::stats::excerpt(&v.question, PLAN_CHARS), v.why);
}
}
let unsure: Vec<&Verdict> = plan.iter().filter(|v| !v.confident).collect();
println!("\nunsure: a question for the queen ({})", unsure.len());
for v in unsure {
println!(" {} [{}?] {}", v.id, if v.reach.is_empty() { "team" } else { &v.reach }, crate::stats::excerpt(&v.question, PLAN_CHARS));
}
let team = plan.iter().filter(|v| v.confident && v.reach.is_empty()).count();
println!("\nteam: unchanged ({team}); {} of {} classified, ${cost:.2}", plan.len(), todo.len());
match std::fs::write(&plan_path, serde_json::to_string_pretty(&plan).unwrap_or_default()) {
Ok(()) => {
println!("plan: {} (apply with `borg reach --apply --plan {}`)", plan_path.display(), plan_path.display());
0
}
Err(e) => {
eprintln!("borg reach: cannot write {}: {e}", plan_path.display());
1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lesson(reach: &str, repo: &str) -> Challenge {
Challenge { id: "L".into(), reach: reach.into(), repo: repo.into(), ..Default::default() }
}
#[test]
fn personal_is_never_delivered_and_repo_only_in_its_repo() {
let here = ["app".to_string()];
assert!(deliverable(&lesson("", "other"), &here), "team lessons go everywhere");
assert!(!deliverable(&lesson(PERSONAL, "app"), &here));
assert!(deliverable(&lesson(REPO, "App"), &here));
assert!(!deliverable(&lesson(REPO, "other"), &here));
assert!(!deliverable(&lesson(REPO, "app"), &[]), "unknown repo: a repo convention stays out");
}
fn tmp(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("borg-reach-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn learned(id: &str, version: u32, reach: &str) -> Lesson {
Lesson { id: id.into(), question: "never add a co-author trailer?".into(), consequence: "c".into(), version, reach: reach.into(), ..Default::default() }
}
fn by(l: Lesson, engineers: &[&str]) -> Lesson {
Lesson { engineers: engineers.iter().map(|e| e.to_string()).collect(), ..l }
}
fn episode(id: &str, engineer: &str) -> Episode {
Episode { id: id.into(), ms: 7, repo: "app".into(), engineer: engineer.into(), ..Default::default() }
}
fn latest(path: &Path, id: &str) -> Option<Lesson> {
crate::learn::lessons(path).into_iter().find(|l| l.id == id)
}
#[test]
fn a_personal_lesson_stays_in_its_store_and_only_its_own_engineer_withdraws_a_team_one() {
let base = tmp("place");
let (team, other) = (crate::collective::store(&base, "work"), crate::collective::store(&base, "private"));
std::fs::create_dir_all(&team).unwrap();
let (lessons, personal) = (team.join("lessons.jsonl"), personal_path(&team));
let p = place(&learned("L1", 1, PERSONAL), &episode("e1", "ann@x"), false, None);
write(&team, &p).unwrap();
assert!(crate::learn::lessons(&lessons).is_empty());
assert_eq!(latest(&personal, "L1").map(|l| (l.version, l.reach)), Some((1, PERSONAL.to_string())));
assert!(!other.exists(), "never written to another collective");
let mine = by(learned("L2", 2, ""), &["ann@x"]);
append_jsonl(&lessons, &mine).unwrap();
let p = place(&by(learned("L2", 3, PERSONAL), &["ann@x"]), &episode("e2", "ann@x"), false, Some(&mine));
write(&team, &p).unwrap();
let t = latest(&lessons, "L2").unwrap();
assert_eq!((t.version, t.reach.as_str(), t.question.as_str(), t.consequence.as_str()), (4, PERSONAL, PERSONAL_TOMBSTONE, ""));
assert_eq!(latest(&personal, "L2").map(|l| l.version), Some(3));
let c = Challenge { id: t.id.clone(), reach: t.reach.clone(), ..Default::default() };
assert!(!deliverable(&c, &["app".into()]), "the tombstone is never delivered");
let shared = by(learned("L3", 1, ""), &["bob@x"]);
append_jsonl(&lessons, &shared).unwrap();
let before = std::fs::read_to_string(&lessons).unwrap();
let p = place(&by(learned("L3", 2, PERSONAL), &["bob@x", "ann@x"]), &episode("e3", "ann@x"), false, Some(&shared));
write(&team, &p).unwrap();
assert_eq!(std::fs::read_to_string(&lessons).unwrap(), before, "the teammate's lesson is untouched");
assert!(latest(&personal, "L3").is_none(), "no personal copy of it");
let own = latest(&personal, "Le3").unwrap();
assert_eq!((own.engineers, own.episodes, own.support), (vec!["ann@x".to_string()], vec!["e3".to_string()], 1));
assert!(matches!(place(&by(learned("L2", 3, PERSONAL), &["ann@x"]), &episode("e4", ""), false, Some(&mine)), Placed::Personal { withdraw: None, .. }));
let nobody = learned("L4", 1, "");
let imported = Lesson { source: "import".into(), ..by(learned("L5", 1, ""), &["ann@x"]) };
for t in [&nobody, &imported] {
let p = place(&Lesson { id: t.id.clone(), ..learned("x", 2, PERSONAL) }, &episode("e7", "ann@x"), false, Some(t));
assert!(matches!(&p, Placed::Personal { lesson, withdraw: None } if lesson.id == "Le7"), "{}: {p:?}", t.id);
}
assert!(
matches!(place(&learned("L1", 2, ""), &episode("e5", "ann@x"), true, None), Placed::Personal { lesson, withdraw: None } if lesson.reach == PERSONAL)
);
assert!(matches!(place(&learned("L9", 1, ""), &episode("e6", "ann@x"), false, None), Placed::Here(l) if l.reach.is_empty()));
}
#[test]
fn a_plan_makes_personal_scopes_and_asks_and_answers_apply_it() {
let base = tmp("plan");
let team = crate::collective::store(&base, "work");
std::fs::create_dir_all(&team).unwrap();
for (id, q) in [("La", "no co-author trailer?"), ("Lb", "stage branch refspec?"), ("Lc", "lock order?"), ("Ld", "report style?")] {
let l = Lesson { id: id.into(), question: q.into(), repo: "app".into(), version: 1, engineers: vec!["ann@x".into()], ..Default::default() };
append_jsonl(&team.join("lessons.jsonl"), &l).unwrap();
}
let fake = |p: &str, _: &serde_json::Value| {
let p = p.split("Question: ").nth(1).unwrap_or_default();
let (reach, confident) = if p.contains("trailer") {
("personal", true)
} else if p.contains("refspec") {
("repo", true)
} else if p.contains("report") {
("personal", false)
} else {
("team", true)
};
Some((serde_json::json!({"reach": reach, "confident": confident, "why": "w"}), Some(0.01)))
};
let (plan, cost) = classify(&candidates(&team, "ann@x"), 2, &fake);
assert_eq!((plan.len(), (cost * 100.0).round()), (4, 4.0));
let done = apply(&team, &plan, "ann@x", 1000).unwrap();
assert_eq!(done, Applied { personal: 1, scoped: 1, asked: 1 });
let lessons = team.join("lessons.jsonl");
assert_eq!(latest(&lessons, "La").unwrap().reach, PERSONAL);
assert_eq!(latest(&personal_path(&team), "La").unwrap().question, "no co-author trailer?", "kept in this store");
assert_eq!(latest(&lessons, "Lb").unwrap().reach, REPO);
assert_eq!(latest(&lessons, "Lc").unwrap().version, 1, "team: untouched");
assert!(candidates(&team, "ann@x").iter().all(|l| l.id == "Lc" || l.id == "Ld"), "personal and scoped ones are no candidates any more");
let q = crate::queen::entries(&team).into_iter().find(|e| e.kind == crate::queen::Kind::Reach).unwrap();
assert_eq!(q.options.len(), 3);
let l = latest(&lessons, "Ld").unwrap();
apply_one(&team, &l, verdict(&q.options[2]), "ann@x").unwrap();
assert_eq!(latest(&lessons, "Ld").unwrap().reach, PERSONAL);
assert_eq!((verdict(&q.options[0]), verdict(&q.options[1])), ("", REPO));
}
#[test]
fn reach_never_considers_a_lesson_another_engineer_is_on() {
let base = tmp("teammate");
let team = crate::collective::store(&base, "work");
let lessons = team.join("lessons.jsonl");
for (id, engineers) in [("Lmine", vec!["ann@x"]), ("Lbob", vec!["bob@x"]), ("Lboth", vec!["ann@x", "bob@x"]), ("Lnone", vec![])] {
append_jsonl(&lessons, &by(learned(id, 1, ""), &engineers)).unwrap();
}
let ids = |me: &str| {
let mut v = candidates(&team, me).into_iter().map(|l| l.id).collect::<Vec<_>>();
v.sort();
v
};
assert_eq!(ids("ann@x"), ["Lmine"], "a lesson nobody is recorded on is not provably ann's");
assert!(ids("").is_empty(), "an unknown engineer is nobody");
let plan: Vec<Verdict> = ["Lbob", "Lboth"]
.iter()
.map(|id| Verdict { id: id.to_string(), repo: String::new(), question: String::new(), reach: PERSONAL.into(), confident: true, why: String::new() })
.collect();
let before = std::fs::read_to_string(&lessons).unwrap();
assert_eq!(apply(&team, &plan, "ann@x", 1).unwrap(), Applied::default());
assert_eq!(std::fs::read_to_string(&lessons).unwrap(), before);
assert!(!personal_path(&team).exists());
for to in [PERSONAL, REPO] {
for id in ["Lboth", "Lnone"] {
assert!(apply_one(&team, &latest(&lessons, id).unwrap(), to, "ann@x").is_err(), "{id} {to}");
}
}
assert_eq!(std::fs::read_to_string(&lessons).unwrap(), before);
}
#[test]
fn a_dead_model_is_asked_only_until_the_failure_guard() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let lessons: Vec<Lesson> = (0..10).map(|i| Lesson { id: format!("L{i}"), ..Lesson::default() }).collect();
let calls = Arc::new(AtomicUsize::new(0));
let counted = Arc::clone(&calls);
let dead = move |_: &str, _: &serde_json::Value| {
counted.fetch_add(1, Ordering::SeqCst);
None
};
assert!(classify(&lessons, 1, &dead).0.is_empty());
assert_eq!(calls.load(Ordering::SeqCst), crate::runner::MAX_FAILURES);
let calls = Arc::new(AtomicUsize::new(0));
let counted = Arc::clone(&calls);
let flaky = move |_: &str, _: &serde_json::Value| {
(counted.fetch_add(1, Ordering::SeqCst) == 0).then(|| (serde_json::json!({"reach": "team", "confident": true, "why": "w"}), None))
};
assert_eq!(classify(&lessons, 1, &flaky).0.len(), 1);
assert_eq!(calls.load(Ordering::SeqCst), lessons.len(), "every lesson asked");
}
#[test]
fn merges_never_widen() {
assert_eq!(narrower("", REPO), REPO);
assert_eq!(narrower(PERSONAL, ""), PERSONAL);
assert_eq!(narrower(REPO, PERSONAL), PERSONAL);
assert_eq!(narrower("", ""), "");
}
}