Files
skills/common/misc/tmux-launch-agent/detect-agent
T
gitadmin c92ee69440 refactor: extract tmux-launch-agent from implement-issue/setup-skills, overhaul PKM skills
- New common/misc/tmux-launch-agent skill with detect-agent script
  and agents-seed.md (moved from engineering/setup-skills)
- implement-issue simplified: delegates agent dispatch to
  tmux-launch-agent instead of inlining it
- setup-skills stripped of Section E (agent CLI config)
- New in-progress/agent-handoff skill using tmux-launch-agent
- PKM skills simplified: conversation-summary and pkm-curation
  rewritten with leaner process, crit streamlined
- knowledge-gardener removed (moved to common/in-progress/)
- All READMEs updated to match
2026-07-06 11:29:16 -04:00

354 lines
9.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# detect-agent — Detect which AI agent CLI launched this script.
#
# Checks environment variables and walks the parent process tree to
# identify the originating coding agent. Designed for agent-agnostic
# tools (skills, scripts, hooks) that need to adjust behaviour based
# on which agent invoked them.
#
# Usage:
# detect-agent # print name (e.g. "pi", "opencode")
# detect-agent --name # same as default
# detect-agent --json # JSON: {"agent":"pi","detected":true}
# detect-agent --verbose # human-readable with debug info
# detect-agent --list-agents # list all known agent names
#
# Exit codes:
# 0 agent detected
# 1 unknown / not detected
#
# Supported agents (add your own at the top of each definition map):
# pi, opencode, aider, claude, codex, cursor, windsurf, continue,
# github-copilot, goose
#
# Detection order:
# 1. Environment variable signatures (most reliable)
# 2. Walk parent process tree skipping shells
# 3. Check /proc/$PPID/{comm,cmdline} as fallback
#
set -euo pipefail
########################################################################
# Agent definitions
########################################################################
# Each entry: "<env-var>:<match-value>:<agent-name>"
# If match-value is "*", any non-empty value matches.
# If match-value is a literal, the env var must equal it.
AGENT_ENV_SIGS=(
"PI_CODING_AGENT:true:pi"
"OPENCODE_CONFIG:*:opencode"
"OPENCODE_CONFIG_DIR:*:opencode"
"OPENCODE_API_KEY:*:opencode"
"CLAUDE_CODE_AGENT_RULE_DISABLED:*:claude"
"CLAUDE_CODE_SSE_PORT:*:claude"
"AIDER_DARK_MODE:*:aider"
"CODEX_API_KEY:*:codex"
"CURSOR_AGENT_RULE_DISABLED:*:cursor"
"WINDSURF_AGENT_RULE_DISABLED:*:windsurf"
"CONTINUE_ON_ERROR:*:continue"
)
# Key = /proc/PID/comm (truncated to 15 chars on Linux).
# Value = canonical agent name.
declare -A AGENT_COMM
AGENT_COMM=(
[pi]="pi"
[opencode]="opencode"
[aider]="aider"
[claude]="claude"
[codex]="codex"
[cursor]="cursor"
[windsurf]="windsurf"
[continue]="continue"
[github-copilot]="github-copilot"
[copilot]="github-copilot"
[goose]="goose"
)
# For agents running under an interpreter (node, python, etc.).
# Key = substring to search in cmdline, Value = agent name.
declare -A AGENT_CMDLINE
AGENT_CMDLINE=(
["opencode"]="opencode"
["/pi "]="pi"
["/aider"]="aider"
["/claude"]="claude"
["/codex"]="codex"
["cursor"]="cursor"
["windsurf"]="windsurf"
["continue.dev"]="continue"
["github-copilot"]="github-copilot"
["goose"]="goose"
)
# Shells to skip when walking ancestors.
SHELL_COMMS=(
bash zsh fish sh dash ksh mksh posh ash tcsh csh
tmux:server tmux screen
sudo doas
)
SHELL_CMDLINE_SUBSTRINGS=(
"/bash" "/zsh" "/fish" "/sh" "/dash" "/ksh"
)
########################################################################
# Helpers
########################################################################
comm_of() {
local pid="$1"
cat "/proc/${pid}/comm" 2>/dev/null || true
}
cmdline_of() {
local pid="$1"
tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true
}
is_shell() {
local pid="$1"
local c; c="$(comm_of "$pid")" || true
[[ -z "$c" ]] && return 1
for s in "${SHELL_COMMS[@]}"; do
[[ "$c" == "$s" ]] && return 0
done
local cl; cl="$(cmdline_of "$pid")" || true
for s in "${SHELL_CMDLINE_SUBSTRINGS[@]}"; do
[[ "$cl" == *"$s"* ]] && return 0
done
return 1
}
walk_to_agent_comm() {
local pid="$1"
local c
while [[ "$pid" -gt 1 ]]; do
c="$(comm_of "$pid")" || true
[[ -z "$c" ]] && break
if ! is_shell "$pid"; then
echo "$c"
return 0
fi
pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
[[ -z "$pid" ]] && break
done
return 1
}
walk_to_agent_cmdline() {
local pid="$1"
local cl
while [[ "$pid" -gt 1 ]]; do
if ! is_shell "$pid"; then
cl="$(cmdline_of "$pid")" || true
if [[ -n "$cl" ]]; then
echo "$cl"
return 0
fi
fi
pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
[[ -z "$pid" ]] && break
done
return 1
}
check_env_sigs() {
local var val agent
for sig in "${AGENT_ENV_SIGS[@]}"; do
IFS=':' read -r var val agent <<< "$sig"
if [[ "$val" == "*" ]]; then
if [[ -n "${!var:-}" ]]; then
echo "$agent"
return 0
fi
else
if [[ "${!var:-}" == "$val" ]]; then
echo "$agent"
return 0
fi
fi
done
return 1
}
match_cmdline() {
local cl="$1"
local needle agent
for needle in "${!AGENT_CMDLINE[@]}"; do
agent="${AGENT_CMDLINE[$needle]}"
if [[ "$cl" == *"$needle"* ]]; then
echo "$agent"
return 0
fi
done
return 1
}
match_comm() {
local c="$1"
local comm_name
for comm_name in "${!AGENT_COMM[@]}"; do
if [[ "$c" == "$comm_name" ]]; then
echo "${AGENT_COMM[$comm_name]}"
return 0
fi
done
return 1
}
########################################################################
# Main detection logic
########################################################################
detect() {
local agent=""
# Phase 1 — environment variables (most reliable)
agent="$(check_env_sigs)" || true
[[ -n "$agent" ]] && echo "$agent" && return 0
# Phase 2 — walk ancestors, check comm
local ancestor_comm
ancestor_comm="$(walk_to_agent_comm "$PPID")" || true
if [[ -n "$ancestor_comm" ]]; then
agent="$(match_comm "$ancestor_comm")" || true
[[ -n "$agent" ]] && echo "$agent" && return 0
# comm matched but not in our map; try its cmdline
local ancestor_pid="$PPID"
while [[ "$ancestor_pid" -gt 1 ]]; do
local ac; ac="$(comm_of "$ancestor_pid")" || true
if [[ "$ac" == "$ancestor_comm" ]]; then
local ancestor_cl
ancestor_cl="$(cmdline_of "$ancestor_pid")" || true
if [[ -n "$ancestor_cl" ]]; then
agent="$(match_cmdline "$ancestor_cl")" || true
[[ -n "$agent" ]] && echo "$agent" && return 0
fi
break
fi
ancestor_pid="$(awk '/^PPid:/{print $2}' "/proc/${ancestor_pid}/status" 2>/dev/null || true)"
done
fi
# Phase 3 — walk ancestors, check cmdline (catches node-based agents)
local ancestor_cl
ancestor_cl="$(walk_to_agent_cmdline "$PPID")" || true
if [[ -n "$ancestor_cl" ]]; then
agent="$(match_cmdline "$ancestor_cl")" || true
[[ -n "$agent" ]] && echo "$agent" && return 0
fi
# Phase 4 — fallback: check immediate PPID directly
local ppid_comm; ppid_comm="$(comm_of "$PPID")" || true
agent="$(match_comm "$ppid_comm")" || true
[[ -n "$agent" ]] && echo "$agent" && return 0
local ppid_cl; ppid_cl="$(cmdline_of "$PPID")" || true
agent="$(match_cmdline "$ppid_cl")" || true
[[ -n "$agent" ]] && echo "$agent" && return 0
return 1
}
########################################################################
# Output
########################################################################
mode="${1:---name}"
case "$mode" in
--list-agents)
echo "Known agents (by env-var / process-name):"
echo ""
echo "Environment variable signatures:"
for sig in "${AGENT_ENV_SIGS[@]}"; do
IFS=':' read -r var val agent <<< "$sig"
printf " %-12s %-8s %s=%s\n" "$agent" "(env)" "$var" "$val"
done
echo ""
echo "Process comm signatures:"
for comm_name in "${!AGENT_COMM[@]}"; do
printf " %-12s %-8s %s\n" "${AGENT_COMM[$comm_name]}" "(comm)" "$comm_name"
done
echo ""
echo "Cmdline substring signatures:"
for needle in "${!AGENT_CMDLINE[@]}"; do
printf " %-12s %-8s *%s*\n" "${AGENT_CMDLINE[$needle]}" "(cmdline)" "$needle"
done
;;
--json)
agent="$(detect)" || agent="null"
if [[ "$agent" != "null" ]]; then
printf '{"agent":"%s","detected":true}\n' "$agent"
else
printf '{"agent":null,"detected":false}\n'
exit 1
fi
;;
--verbose)
echo "=== Agent Detection ==="
agent="$(detect)" || true
if [[ -n "$agent" ]]; then
echo "Detected agent: $agent"
else
echo "Detected agent: (unknown)"
fi
echo ""
echo "Relevant environment variables:"
for sig in "${AGENT_ENV_SIGS[@]}"; do
IFS=':' read -r var val _ <<< "$sig"
# Skip entries that aren't real variable names (e.g. patterns with *).
[[ "$var" == *\** ]] && continue
if [[ -n "${!var:-}" ]]; then
echo " $var=${!var}"
fi
done
[[ -z "${PI_CODING_AGENT:-}" ]] && echo " PI_CODING_AGENT=<unset>"
echo ""
echo "Process tree (up from PPID=$PPID):"
_dbg_pid="$PPID"
_dbg_depth=0
while [[ "$_dbg_pid" -gt 1 && "$_dbg_depth" -lt 15 ]]; do
_dbg_c="$(comm_of "$_dbg_pid")" || true
_dbg_cl="$(cmdline_of "$_dbg_pid")" || true
if [[ -n "$_dbg_c" ]]; then
printf " [%d] %s" "$_dbg_depth" "$_dbg_c"
if [[ -n "$_dbg_cl" ]]; then
printf " <- %s" "${_dbg_cl:0:100}"
fi
if [[ -n "$agent" ]]; then
_dbg_mc="$(match_comm "$_dbg_c")" || true
if [[ "$_dbg_mc" == "$agent" ]]; then
printf " <-- DETECTED"
fi
fi
echo ""
fi
_dbg_pid="$(awk '/^PPid:/{print $2}' "/proc/${_dbg_pid}/status" 2>/dev/null || true)"
_dbg_depth=$((_dbg_depth + 1))
done
;;
--name|*)
agent="$(detect)" || agent=""
if [[ -n "$agent" ]]; then
echo "$agent"
else
echo "unknown"
exit 1
fi
;;
esac