#!/usr/bin/env bash # agent-roster — list other live Claude agents working in this project. # # The single source of "who else is live in this project." Both launchers # (ai --helper) and the in-session startup check call this rather than # reimplementing the scan, so concurrent-agent detection has one definition. # # Scan (stateless): enumerate running Claude processes (pgrep -x claude), read # each one's working directory from /proc//cwd, keep those whose cwd is # the project root or inside it, and drop the scanner's own process ancestry # (walk parent pids from /proc/self up). What remains is the set of *other* # live agents in this project. # # Usage: agent-roster [project-root] (default: $PWD) # Output: one "pidcwd" line per other agent # Exit: 0 = alone (no other agents) # 1 = one or more other agents (and printed) # 2 = roster unavailable (no /proc; non-Linux or absent) # # Known limits, accepted for v1: a session not running as a local process on # this machine (a cloud session against the same checkout) is invisible, and # the match is on process cwd, so an agent started from outside the project # tree isn't seen. Both are edge shapes the operator created deliberately. # # The boundary (pgrep, /proc, self pid) is injectable so the filtering logic # is testable without spawning real agents: ROSTER_PGREP, ROSTER_PROC, # ROSTER_SELF_PID. Production defaults need no environment. set -euo pipefail PGREP="${ROSTER_PGREP:-pgrep}" PROC="${ROSTER_PROC:-/proc}" SELF_PID="${ROSTER_SELF_PID:-$$}" root="${1:-$PWD}" root="${root%/}" # Linux /proc is the substrate. Absent (non-Linux, or unreadable) means the # scan can't run; say so explicitly rather than reporting a false "alone". if [ ! -d "$PROC" ]; then echo "agent-roster: roster unavailable (no $PROC; non-Linux or absent)" >&2 exit 2 fi # pgrep is the enumeration boundary. Without it the scan can't run, and the # no-match exit code (1) below is indistinguishable from "tool missing" once # swallowed, so check up front rather than report a false "alone". if ! command -v "$PGREP" >/dev/null 2>&1; then echo "agent-roster: roster unavailable ($PGREP not found)" >&2 exit 2 fi # Build the scanner's ancestry set: SELF_PID and every parent up to init. # A Claude found by pgrep that lands in this set is the current session (or its # launcher chain), not another agent. ancestry=" " pid="$SELF_PID" while [ -n "$pid" ] && [ "$pid" != "0" ] && [ "$pid" != "1" ]; do ancestry="${ancestry}${pid} " status="$PROC/$pid/status" [ -r "$status" ] || break pid="$(awk '/^PPid:/{print $2; exit}' "$status")" done found=0 while read -r candidate; do [ -n "$candidate" ] || continue case "$ancestry" in *" $candidate "*) continue ;; # scanner's own ancestry esac # cwd may be gone if the process exited between pgrep and here; skip it. cwd="$(readlink "$PROC/$candidate/cwd" 2>/dev/null)" || continue [ -n "$cwd" ] || continue # Keep only agents at or inside the project root. The trailing slashes make # the prefix test exact, so /foo/project-other doesn't match /foo/project. case "$cwd/" in "$root"/*) ;; *) continue ;; esac printf '%s\t%s\n' "$candidate" "$cwd" found=1 done < <("$PGREP" -x claude 2>/dev/null || true) [ "$found" -eq 1 ] && exit 1 exit 0