aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/agent-roster
blob: f32b744e86e2fff6d952bdbce91b87ef14c9c877 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/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/<pid>/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 "pid<TAB>cwd" 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