diff --git a/CHANGELOG.md b/CHANGELOG.md index cee485e7..4e5e372b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Suggest roster-aware unused identity names during join onboarding (#178) + ## [1.1.1] - 2026-06-25 ### Added diff --git a/SKILL.md b/SKILL.md index 1137b2c7..454128cd 100644 --- a/SKILL.md +++ b/SKILL.md @@ -35,11 +35,13 @@ After this runs once, `~/.agents/skills/agmsg/` is populated and you can skip St ```bash ~/.agents/skills/agmsg/scripts/whoami.sh "$(pwd)" # type: claude-code, codex, gemini, antigravity, copilot -# Returns: agent=... / multiple=true ... / suggest=true ... / not_joined=true ... +# Returns: agent=... / multiple=true ... / suggest=true ... / not_joined=true ...; join-needed paths may include suggested= ``` ### Step 2a: If not in a team — join one +If whoami output includes `suggested=`, show those unused roster-aware identity names as options for the agent name. They are advisory; the user may still type a different name. + Ask the user for a team name and agent name, then run: ```bash diff --git a/scripts/data/name-pool.txt b/scripts/data/name-pool.txt new file mode 100644 index 00000000..c40f2d0c --- /dev/null +++ b/scripts/data/name-pool.txt @@ -0,0 +1,99 @@ +# Bundled identity-name pool for roster-aware onboarding suggestions. +# Keep entries lowercase ASCII [a-z0-9-]. These are agent identities, not tool +# types: prefer names like noether/lovelace over cc/codex. +ada +agnesi +albert +amalie +anaximander +archimedes +arete +artin +astrid +aurora +babbage +banach +bell +bernoulli +boole +brahe +cannon +cantor +carson +cartan +cassini +ceres +chandrasekhar +clarke +copernicus +curie +darwin +dedekind +dirac +easley +edison +emmy +erdos +euler +faraday +fibonacci +fleming +franklin +galileo +galois +gauss +germain +gosling +grace +halley +hamilton +hardy +hawking +heisenberg +hilbert +hopper +hypatia +ibn-sina +joule +katherine +kepler +khayyam +knuth +kovalevskaya +laplace +leavitt +leibniz +lovelace +margulis +maxwell +mendel +minkowski +mirzakhani +newton +noether +norton +ohm +pasteur +planck +poisson +ramanujan +riemann +rosalind +rubin +sagan +shannon +sonya +sophie +tao +tesla +thales +turing +tycho +vonneumann +wiles +wright +wu +yang +yukawa +zermelo +zhukovsky diff --git a/scripts/drivers/types/antigravity/template.md b/scripts/drivers/types/antigravity/template.md index 2182df78..8ad614ad 100644 --- a/scripts/drivers/types/antigravity/template.md +++ b/scripts/drivers/types/antigravity/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh antigravity "$(pwd)"` 4. Show the result and explain: @@ -63,10 +63,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=antigravity project= available_teams=` +`suggest=true agents= teams= type=antigravity project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh antigravity "$(pwd)"` diff --git a/scripts/drivers/types/claude-code/template.md b/scripts/drivers/types/claude-code/template.md index 7ba6fdb9..e67ddda9 100644 --- a/scripts/drivers/types/claude-code/template.md +++ b/scripts/drivers/types/claude-code/template.md @@ -21,7 +21,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -30,7 +30,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh claude-code "$(pwd)"` 4. Show the result and explain: @@ -74,10 +74,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=claude-code project= available_teams=` +`suggest=true agents= teams= type=claude-code project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh claude-code "$(pwd)"` diff --git a/scripts/drivers/types/codex/template.md b/scripts/drivers/types/codex/template.md index bd0f1c71..e2d8ca46 100644 --- a/scripts/drivers/types/codex/template.md +++ b/scripts/drivers/types/codex/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh codex "$(pwd)"` 4. Show the result and explain: @@ -68,10 +68,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=codex project= available_teams=` +`suggest=true agents= teams= type=codex project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh codex "$(pwd)"` diff --git a/scripts/drivers/types/copilot/template.md b/scripts/drivers/types/copilot/template.md index a5709643..0e34ec57 100644 --- a/scripts/drivers/types/copilot/template.md +++ b/scripts/drivers/types/copilot/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh copilot "$(pwd)"` 4. Show the result and explain: @@ -63,10 +63,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=copilot project= available_teams=` +`suggest=true agents= teams= type=copilot project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh copilot "$(pwd)"` diff --git a/scripts/drivers/types/cursor/template.md b/scripts/drivers/types/cursor/template.md index 59602189..4c957a2b 100644 --- a/scripts/drivers/types/cursor/template.md +++ b/scripts/drivers/types/cursor/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh cursor "$(pwd)"` 4. Show the result and explain: @@ -63,10 +63,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=cursor project= available_teams=` +`suggest=true agents= teams= type=cursor project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh cursor "$(pwd)"` diff --git a/scripts/drivers/types/gemini/template.md b/scripts/drivers/types/gemini/template.md index 353cf975..eea26dd1 100644 --- a/scripts/drivers/types/gemini/template.md +++ b/scripts/drivers/types/gemini/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh gemini "$(pwd)"` 4. Show the result and explain: @@ -63,10 +63,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=gemini project= available_teams=` +`suggest=true agents= teams= type=gemini project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh gemini "$(pwd)"` diff --git a/scripts/drivers/types/grok-build/template.md b/scripts/drivers/types/grok-build/template.md index ef008c7d..890fbcf3 100644 --- a/scripts/drivers/types/grok-build/template.md +++ b/scripts/drivers/types/grok-build/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh grok-build "$(pwd)"` 4. Show the result and explain: @@ -69,10 +69,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=grok-build project= available_teams=` +`suggest=true agents= teams= type=grok-build project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh grok-build "$(pwd)"` diff --git a/scripts/drivers/types/hermes/template.md b/scripts/drivers/types/hermes/template.md index f6f4c1b0..854878e3 100644 --- a/scripts/drivers/types/hermes/template.md +++ b/scripts/drivers/types/hermes/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh hermes "$(pwd)"` 4. Show the result and explain: @@ -47,10 +47,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=hermes project= available_teams=` +`suggest=true agents= teams= type=hermes project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh hermes "$(pwd)"` diff --git a/scripts/drivers/types/opencode/template.md b/scripts/drivers/types/opencode/template.md index d263bfdd..9e98006e 100644 --- a/scripts/drivers/types/opencode/template.md +++ b/scripts/drivers/types/opencode/template.md @@ -22,7 +22,7 @@ Four possible outputs: → Ask the user which agent name to use for this session, then go to **Execute**. **C) Not in a team:** -`not_joined=true available_teams=` (or `available_teams=none`) +`not_joined=true available_teams=` (or `available_teams=none`), optionally `suggested=` → Show the user the available teams from the output, then: > **First-time setup required.** @@ -31,7 +31,7 @@ Four possible outputs: > - **Agent name**: this agent's identity within the team 1. Ask: "Enter a team name (joins existing or creates new)" - 2. Ask: "Enter a name for this agent" + 2. If the output includes `suggested=`, show those unused roster-aware identity names as options. Ask: "Enter a name for this agent"; empty input may use the first suggested name. 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh opencode "$(pwd)"` 4. Show the result and explain: @@ -63,10 +63,10 @@ Four possible outputs: 6. Then check inbox for the newly joined team. **D) Suggestions for reuse:** -`suggest=true agents= teams= type=opencode project= available_teams=` +`suggest=true agents= teams= type=opencode project= available_teams=`, optionally `suggested=` → No exact registration exists for this project, but there are same-type agent names registered elsewhere. - 1. Show the suggested agent names to the user. + 1. Show the reuse candidates from `agents=` and the unused roster-aware names from `suggested=` (if present). 2. Ask whether to reuse one of those names or choose a new one. 3. Ask for the team name to join (existing or new). 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh opencode "$(pwd)"` diff --git a/scripts/lib/roster.sh b/scripts/lib/roster.sh new file mode 100644 index 00000000..04492814 --- /dev/null +++ b/scripts/lib/roster.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Read-only helpers for team rosters and onboarding name suggestions. + +_agmsg_roster_lib_dir() { + if [ -n "${BASH_SOURCE[0]:-}" ]; then + cd "$(dirname "${BASH_SOURCE[0]}")" && pwd + elif [ -n "${SKILL_DIR:-}" ]; then + cd "$SKILL_DIR/scripts/lib" && pwd + else + echo "Error: cannot resolve roster lib dir (BASH_SOURCE and SKILL_DIR empty)" >&2 + return 1 + fi +} + +_AGMSG_ROSTER_LIB_DIR="$(_agmsg_roster_lib_dir)" +_AGMSG_ROSTER_SCRIPTS_DIR="$(cd "$_AGMSG_ROSTER_LIB_DIR/.." && pwd)" +_AGMSG_ROSTER_SKILL_DIR="$(cd "$_AGMSG_ROSTER_SCRIPTS_DIR/.." && pwd)" + +# shellcheck disable=SC1091 +source "$_AGMSG_ROSTER_SCRIPTS_DIR/lib/storage.sh" + +agmsg_roster_config_path() { + local team="$1" + printf '%s\n' "$_AGMSG_ROSTER_SKILL_DIR/teams/$team/config.json" +} + +agmsg_roster_names() { + local team="$1" + local config_file + config_file="$(agmsg_roster_config_path "$team")" + [ -f "$config_file" ] || return 0 + + local cfg_sql + cfg_sql="$(agmsg_sql_readfile_path "$config_file")" + agmsg_sqlite_mem " + WITH raw(json) AS (SELECT CAST(readfile('$cfg_sql') AS TEXT)), + cfg(json) AS (SELECT CASE WHEN json_valid(json) THEN json END FROM raw) + SELECT key + FROM cfg, json_each(json_extract(cfg.json, '\$.agents')) + ORDER BY key; + " +} + +agmsg_name_pool_path() { + printf '%s\n' "$_AGMSG_ROSTER_SCRIPTS_DIR/data/name-pool.txt" +} + +agmsg_name_hash() { + printf '%s' "$1" | cksum | awk '{print $1}' +} + +agmsg_roster_contains() { + local needle="$1" + shift + local item + for item in "$@"; do + [ "$item" = "$needle" ] && return 0 + done + return 1 +} + +agmsg_pool_names() { + local pool + pool="$(agmsg_name_pool_path)" + [ -f "$pool" ] || return 0 + while IFS= read -r name || [ -n "$name" ]; do + name="${name%%#*}" + name="${name//[[:space:]]/}" + [ -n "$name" ] || continue + case "$name" in + *[!abcdefghijklmnopqrstuvwxyz0123456789-]*) continue ;; + *) printf '%s\n' "$name" ;; + esac + done < "$pool" +} + +agmsg_suggest_names() { + local team="$1" + local count="${2:-5}" + case "$count" in ''|*[!0-9]*) count=5 ;; esac + [ "$count" -gt 0 ] || count=5 + + local roster=() pool=() + local name + while IFS= read -r name; do + [ -n "$name" ] && roster+=("$name") + done < <(agmsg_roster_names "$team") + while IFS= read -r name; do + [ -n "$name" ] && pool+=("$name") + done < <(agmsg_pool_names) + + local pool_len="${#pool[@]}" + [ "$pool_len" -gt 0 ] || return 0 + + local offset idx emitted=0 candidate + offset=$(( $(agmsg_name_hash "$team") % pool_len )) + for ((idx = 0; idx < pool_len && emitted < count; idx++)); do + candidate="${pool[$(((offset + idx) % pool_len))]}" + if ! agmsg_roster_contains "$candidate" ${roster[@]+"${roster[@]}"}; then + printf '%s\n' "$candidate" + roster+=("$candidate") + emitted=$((emitted + 1)) + fi + done + + local suffix=2 base + while [ "$emitted" -lt "$count" ]; do + for base in "${pool[@]}"; do + candidate="${base}-${suffix}" + if ! agmsg_roster_contains "$candidate" ${roster[@]+"${roster[@]}"}; then + printf '%s\n' "$candidate" + roster+=("$candidate") + emitted=$((emitted + 1)) + [ "$emitted" -ge "$count" ] && break + fi + done + suffix=$((suffix + 1)) + done +} diff --git a/scripts/whoami.sh b/scripts/whoami.sh index aeeb80b9..cf5db1f4 100755 --- a/scripts/whoami.sh +++ b/scripts/whoami.sh @@ -96,11 +96,13 @@ TEAMS_DIR="$SCRIPT_DIR/../teams" source "$SCRIPT_DIR/lib/resolve-project.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/storage.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/roster.sh" PROJECT_PATH="$(agmsg_resolve_project "$PROJECT_PATH" "$AGENT_TYPE")" AGENT_TYPE_SQL=$(printf '%s' "$AGENT_TYPE" | sed "s/'/''/g") if [ ! -d "$TEAMS_DIR" ]; then - echo "not_joined=true available_teams=none" + echo "not_joined=true available_teams=none suggested=$(agmsg_suggest_names __new-team__ 5 | paste -sd, -)" exit 0 fi @@ -149,7 +151,9 @@ for config_file in "$TEAMS_DIR"/*/config.json; do done if [ -z "$EXACT_MATCHES" ] && [ -z "$SUGGESTED_MATCHES" ]; then - echo "not_joined=true available_teams=${ALL_TEAMS:-none}" + FIRST_TEAM="${ALL_TEAMS%%,*}" + SUGGESTED_NAMES="$(agmsg_suggest_names "${FIRST_TEAM:-__new-team__}" 5 | paste -sd, -)" + echo "not_joined=true available_teams=${ALL_TEAMS:-none} suggested=$SUGGESTED_NAMES" exit 0 fi @@ -157,7 +161,9 @@ if [ -z "$EXACT_MATCHES" ]; then # SUGGESTED_MATCHES is "team\tagent" per line; preserve that order. AGENT_NAMES=$(echo "$SUGGESTED_MATCHES" | cut -f2 | awk '!seen[$0]++' | paste -sd, -) TEAM_NAMES=$(echo "$SUGGESTED_MATCHES" | cut -f1 | awk '!seen[$0]++' | paste -sd, -) - echo "suggest=true agents=$AGENT_NAMES teams=$TEAM_NAMES type=$AGENT_TYPE project=$PROJECT_PATH available_teams=${ALL_TEAMS:-none}" + FIRST_TEAM="${TEAM_NAMES%%,*}" + SUGGESTED_NAMES="$(agmsg_suggest_names "$FIRST_TEAM" 5 | paste -sd, -)" + echo "suggest=true agents=$AGENT_NAMES teams=$TEAM_NAMES type=$AGENT_TYPE project=$PROJECT_PATH available_teams=${ALL_TEAMS:-none} suggested=$SUGGESTED_NAMES" exit 0 fi diff --git a/tests/test_roster.bats b/tests/test_roster.bats new file mode 100644 index 00000000..843d9038 --- /dev/null +++ b/tests/test_roster.bats @@ -0,0 +1,87 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + setup_test_env +} + +teardown() { + teardown_test_env +} + +@test "roster: lists team agent names" { + bash "$SCRIPTS/join.sh" myteam ada claude-code /tmp/proj-a + bash "$SCRIPTS/join.sh" myteam noether codex /tmp/proj-b + run bash -c "source '$SCRIPTS/lib/roster.sh'; agmsg_roster_names myteam" + [ "$status" -eq 0 ] + [[ "$output" =~ "ada" ]] + [[ "$output" =~ "noether" ]] +} + +@test "roster: missing team is empty and successful" { + run bash -c "source '$SCRIPTS/lib/roster.sh'; agmsg_roster_names nope" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "roster: suggestions are safe with empty roster under set -u" { + run bash -c "set -u; source '$SCRIPTS/lib/roster.sh'; agmsg_suggest_names emptyteam 5" + [ "$status" -eq 0 ] + [ "$(printf '%s\n' "$output" | sed '/^$/d' | wc -l | tr -d ' ')" -eq 5 ] +} + +@test "whoami: first run with no teams includes suggested names" { + run bash "$SCRIPTS/whoami.sh" /tmp/first-project claude-code + [ "$status" -eq 0 ] + [[ "$output" =~ "not_joined=true" ]] + [[ "$output" =~ "available_teams=none" ]] + [[ "$output" =~ "suggested=" ]] + [[ "$output" =~ suggested=[^[:space:]]+ ]] +} +@test "roster: suggestions do not collide with existing roster names" { + bash "$SCRIPTS/join.sh" myteam ada claude-code /tmp/proj-a + bash "$SCRIPTS/join.sh" myteam lovelace codex /tmp/proj-b + run bash -c "source '$SCRIPTS/lib/roster.sh'; agmsg_suggest_names myteam 5" + [ "$status" -eq 0 ] + [[ ! "$output" =~ (^|[[:space:]])ada($|[[:space:]]) ]] + [[ ! "$output" =~ (^|[[:space:]])lovelace($|[[:space:]]) ]] + [ "$(printf '%s\n' "$output" | sed '/^$/d' | wc -l | tr -d ' ')" -eq 5 ] +} + +@test "roster: suggestions are deterministic for the same roster" { + bash "$SCRIPTS/join.sh" myteam ada claude-code /tmp/proj-a + first="$(bash -c "source '$SCRIPTS/lib/roster.sh'; agmsg_suggest_names myteam 5")" + second="$(bash -c "source '$SCRIPTS/lib/roster.sh'; agmsg_suggest_names myteam 5")" + [ "$first" = "$second" ] +} + +@test "roster: suggestions use clean identity names" { + run bash -c "source '$SCRIPTS/lib/roster.sh'; agmsg_suggest_names myteam 10" + [ "$status" -eq 0 ] + while IFS= read -r name; do + [ -n "$name" ] || continue + [[ "$name" =~ ^[a-z0-9-]+$ ]] + [ "$name" != "codex" ] + [ "$name" != "cc" ] + done <<< "$output" +} + +@test "whoami: not_joined includes roster-aware suggested names" { + bash "$SCRIPTS/join.sh" myteam ada claude-code /tmp/other + run bash "$SCRIPTS/whoami.sh" /tmp/new-project claude-code + [ "$status" -eq 0 ] + [[ "$output" =~ "not_joined=true" ]] + [[ "$output" =~ "suggested=" ]] + [[ ! "$output" =~ "suggested=ada" ]] +} + +@test "whoami: reuse suggestions also include unused suggested names" { + bash "$SCRIPTS/join.sh" myteam ada claude-code /tmp/proj-a + run bash "$SCRIPTS/whoami.sh" /tmp/proj-b claude-code + [ "$status" -eq 0 ] + [[ "$output" =~ "suggest=true" ]] + [[ "$output" =~ "agents=ada" ]] + [[ "$output" =~ "suggested=" ]] + [[ ! "$output" =~ "suggested=ada" ]] +} diff --git a/tests/test_team.bats b/tests/test_team.bats index e39aba15..13c0a504 100644 --- a/tests/test_team.bats +++ b/tests/test_team.bats @@ -151,6 +151,7 @@ teardown() { run bash "$SCRIPTS/whoami.sh" /tmp/unknown claude-code [ "$status" -eq 0 ] [[ "$output" =~ "not_joined=true" ]] + [[ "$output" =~ "suggested=" ]] } @test "whoami: returns multiple when multiple identities" { @@ -167,6 +168,7 @@ teardown() { bash "$SCRIPTS/join.sh" team1 alice claude-code /tmp/other run bash "$SCRIPTS/whoami.sh" /tmp/nothere claude-code [[ "$output" =~ "available_teams=team1" ]] + [[ "$output" =~ "suggested=" ]] } @test "whoami: finds re-joined agent in another project registration" { @@ -185,6 +187,7 @@ teardown() { [[ "$output" =~ "suggest=true" ]] [[ "$output" =~ "agents=alice" ]] [[ "$output" =~ "available_teams=myteam" ]] + [[ "$output" =~ "suggested=" ]] } @test "whoami: auto-detects claude-code from CLAUDE_CODE_SESSION_ID env" {