//! Human given names for minted agents. //! //! A team used to come back as `planner`, `coder`, `tester`, `reviewer`, //! `committer` — the roster read as a list of job tickets, and the UI showed //! the same word twice (name on top, role underneath). A crew you keep should //! read like people: Meredith, Vijay, Tomasz, Amara. //! //! The role is not lost — it stays in `job_title`, which is what the mission //! machinery binds on. Only the display identity changes. //! //! Names are drawn from many naming traditions on purpose: this workforce is //! not from one place. They are given names only — no surnames — so nobody //! reads a claw as a specific real person. /// Given names, deliberately wide. Kept as one flat list rather than grouped by /// origin: grouping invites picking "one from each", which is a worse kind of /// tokenism than simply having a broad pool and drawing from it evenly. /// /// Size is a product decision, not an aesthetic one. Every mission now mints /// its own crew and nothing retires them, so the roster grows by the team size /// per mission — at ~5 a mission a 70-name pool starts emitting "Amara 2" /// inside twenty missions. This pool carries a few hundred so a workspace runs /// for a long time before any name repeats at all. pub const NAMES: &[&str] = &[ // A "Aarav", "Abebe", "Adaora", "Adrian", "Agnieszka", "Ahmad", "Aiko", "Ainhoa", "Alejandro", "Alina", "Amara", "Amina", "Anders", "Andrea", "Anjali", "Annika", "Antoine", "Arjun", "Astrid", "Ayo", "Ayesha", "Aziz", // B–C "Beatriz", "Bilal", "Bjorn", "Blessing", "Bogdan", "Camila", "Carlos", "Catalina", "Chidi", "Chiara", "Chioma", "Cyrus", // D–E "Dagny", "Damir", "Daniela", "Dilnoza", "Dmitri", "Ebele", "Eduardo", "Eero", "Ekaterina", "Elena", "Elias", "Emeka", "Enrique", "Esi", "Esther", "Eun-ji", "Ewa", // F–G "Fabio", "Farida", "Fatou", "Felipe", "Fernanda", "Freya", "Gabriel", "Georgi", "Giulia", "Grace", "Gunnar", "Gulnara", // H–I "Hana", "Hasan", "Heidi", "Hina", "Hiroshi", "Ibrahim", "Idris", "Ilya", "Imani", "Ingrid", "Iris", "Isabela", "Ivan", "Iwona", // J–K "Jaromir", "Javier", "Jing", "Joana", "Johan", "Josefina", "Junko", "Kaito", "Kalinda", "Karim", "Katarzyna", "Kenji", "Khalid", "Kiran", "Klara", "Kwame", "Kyoko", // L–M "Lakshmi", "Lars", "Laila", "Leilani", "Lena", "Liam", "Linnea", "Lucia", "Lukas", "Madhavi", "Maja", "Malik", "Marisol", "Mateo", "Matteo", "Mei", "Meredith", "Milena", "Mira", "Mohan", "Mira-Lynn", "Mateusz", // N–O "Nadia", "Nasrin", "Neelam", "Niamh", "Nikolai", "Nilufar", "Nkechi", "Noor", "Nuria", "Oksana", "Oleksii", "Olamide", "Omar", "Oskar", "Osei", // P–R "Paloma", "Panagiotis", "Pedro", "Petra", "Priya", "Rafael", "Rania", "Ravi", "Reza", "Renata", "Rin", "Robert", "Rosalind", "Rustam", // S "Sadia", "Salome", "Samir", "Sanjay", "Sara", "Seong-min", "Sipho", "Sofia", "Solveig", "Soren", "Suvi", "Svetlana", // T–U "Tadeusz", "Takeshi", "Tamar", "Tariq", "Thandiwe", "Thi", "Tim", "Tomasz", "Tove", "Tuva", "Ulrika", "Uma", "Usman", // V–Z "Valentina", "Vera", "Vijay", "Vikram", "Wanjiru", "Wei", "Wiktor", "Yara", "Yasmin", "Yohannes", "Yuki", "Yusuf", "Zainab", "Zara", "Zoltan", "Zuzanna", ]; /// Pick a name not already in `taken`. /// /// `seed` spreads the starting point so a workspace does not always begin at /// "Amara" — it is an offset into the list, not randomness, so the choice is /// reproducible for a given (seed, taken) pair and therefore testable. /// /// When every name is taken it appends a numeric suffix — `Amara 2` — rather /// than returning `None` and forcing the caller to invent something. Running /// out is a nice problem (70+ concurrent agents in one workspace) and a /// duplicate display name is far less harmful than a failed mission launch. pub fn pick(taken: &[String], seed: u64) -> String { let start = (seed % NAMES.len() as u64) as usize; for i in 0..NAMES.len() { let candidate = NAMES[(start + i) % NAMES.len()]; if !taken.iter().any(|t| t.eq_ignore_ascii_case(candidate)) { return candidate.to_string(); } } // Second pass with a suffix. `round` starts at 2 so the first repeat reads // "Amara 2", which is how a person would disambiguate two colleagues. for round in 2..1000 { for i in 0..NAMES.len() { let candidate = format!("{} {}", NAMES[(start + i) % NAMES.len()], round); if !taken.iter().any(|t| t.eq_ignore_ascii_case(&candidate)) { return candidate; } } } // Unreachable in practice; still not a panic. format!("Agent {seed}") } #[cfg(test)] mod tests { use super::*; #[test] fn names_are_unique_and_non_empty() { let mut seen = std::collections::HashSet::new(); for n in NAMES { assert!(!n.trim().is_empty(), "empty name in the pool"); assert!(seen.insert(n.to_ascii_lowercase()), "duplicate in pool: {n}"); } // Every mission mints its own crew and nothing retires them, so the // pool is consumed for the life of the workspace, not recycled. At ~5 // per mission this is ~35 missions before the first numeric suffix. assert!(NAMES.len() >= 150, "pool too small for one crew per mission"); } /// A crew should not read as an alphabetical run. /// /// With the role index as the seed, every crew started at the top of the /// pool and took the next free names — the first real mission hired Aarav, /// Abebe, Adaora, Adrian, Agnieszka. Unique and correct, and obviously /// generated. Callers now seed from the claw's uuid tail, so this checks /// that well-spread seeds actually land in different regions of the pool /// rather than clustering at one end. #[test] fn spread_seeds_do_not_produce_an_alphabetical_run() { let index_of = |n: &str| NAMES.iter().position(|c| *c == n).expect("name in pool"); let seeds = [ 0x9e37_79b9_7f4a_7c15u64, 0x1234_5678_9abc_def0, 0xfeed_face_dead_beef, 0x0f0f_0f0f_f0f0_f0f0, 0xa5a5_5a5a_c3c3_3c3c, ]; let mut taken: Vec = Vec::new(); let mut positions = Vec::new(); for s in seeds { let n = pick(&taken, s); positions.push(index_of(&n) as i64); taken.push(n); } // Adjacent picks landing within a couple of slots of each other is the // clustering signature; require the crew to span a real distance. let (min, max) = ( *positions.iter().min().unwrap(), *positions.iter().max().unwrap(), ); assert!( max - min > (NAMES.len() as i64) / 3, "crew clustered in one region of the pool: {positions:?}" ); } /// The scenario the operator actually asked for: consecutive missions must /// not hand back the same names. Reuse is off, so mission two staffs from /// what mission one left. #[test] fn consecutive_missions_get_different_crews() { let mut roster: Vec = Vec::new(); let mut crews: Vec> = Vec::new(); for mission in 0..6u64 { let mut crew = Vec::new(); for role in 0..5u64 { let n = pick(&roster, mission * 5 + role); roster.push(n.clone()); crew.push(n); } crews.push(crew); } for (i, a) in crews.iter().enumerate() { for (j, b) in crews.iter().enumerate().skip(i + 1) { let shared: Vec<_> = a.iter().filter(|n| b.contains(n)).collect(); assert!( shared.is_empty(), "missions {i} and {j} share {shared:?} — crews must be distinct" ); } } // And no duplicates anywhere on the roster. let uniq: std::collections::HashSet<_> = roster.iter().collect(); assert_eq!(uniq.len(), roster.len(), "a name was issued twice"); } #[test] fn pick_avoids_taken_names() { let taken: Vec = NAMES.iter().take(10).map(|s| s.to_string()).collect(); let got = pick(&taken, 0); assert!( !taken.iter().any(|t| t.eq_ignore_ascii_case(&got)), "picked a name already taken: {got}" ); } #[test] fn pick_is_case_insensitive_about_taken() { // A name already on the roster in a different case is still taken — // "meredith" and "Meredith" are the same colleague. let taken = vec![NAMES[0].to_ascii_lowercase()]; assert_ne!(pick(&taken, 0).to_ascii_lowercase(), taken[0]); } #[test] fn seed_spreads_the_starting_point() { // Different seeds should not all hand back the same first name, or a // fresh workspace always opens with the same roster. let a = pick(&[], 0); let b = pick(&[], 7); assert_ne!(a, b, "seed had no effect on the choice"); } #[test] fn exhausting_the_pool_suffixes_rather_than_failing() { let taken: Vec = NAMES.iter().map(|s| s.to_string()).collect(); let got = pick(&taken, 0); assert!( !taken.iter().any(|t| t.eq_ignore_ascii_case(&got)), "must not reuse a taken name" ); assert!(got.ends_with(" 2"), "expected a suffixed name, got {got}"); } #[test] fn a_full_team_gets_distinct_names() { // The actual scenario: mint five roles into an empty workspace and get // five different people, not five "planner"s. let mut taken: Vec = Vec::new(); for i in 0..5 { let n = pick(&taken, i); assert!(!taken.contains(&n), "repeated {n} within one team"); taken.push(n); } assert_eq!(taken.len(), 5); } }