#!/usr/bin/env bash # orc_onboard.sh — Register/remove orchestrator session UUID in agent-sessions.yaml (O-4) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" # shellcheck disable=SC1091 source "$REPO_ROOT/.agents/skills/lib.sh" MODE="add" TARGET_UUID="" AUTODETECT=true DRY_RUN=false PURGE=false YES=false AGENT_HINT="auto" usage() { cat <<'EOF' Usage: orc_onboard.sh [OPTIONS] Options: --uuid Register specified orchestrator session UUID --remove Remove specified UUID from orchestrator_uuids list --purge-conversation With --remove: also delete the conversation's on-disk artifacts through its agent adapter (irreversible; refused while a live process still references it) --yes Skip the --purge-conversation confirmation --agent Adapter for --purge-conversation (claude|agy|hermes| grok|opencode|codex); default: auto-detect from disk --list List all registered orchestrator UUIDs --no-autodetect Disable process ancestry auto-detection when --uuid is omitted --dry-run Preview changes without updating state registry EOF exit 2 } while [ $# -gt 0 ]; do case "$1" in --uuid) if [ $# -lt 2 ] || [ -z "$2" ]; then usage; fi TARGET_UUID="$2" MODE="add" shift 2 ;; --remove) if [ $# -lt 2 ] || [ -z "$2" ]; then usage; fi TARGET_UUID="$2" MODE="remove" shift 2 ;; --list) MODE="list" shift ;; --purge-conversation) PURGE=true shift ;; --yes) YES=true shift ;; --agent) if [ $# -lt 2 ] || [ -z "$2" ]; then usage; fi AGENT_HINT="$2" shift 2 ;; --no-autodetect) AUTODETECT=false shift ;; --dry-run) DRY_RUN=true shift ;; -h|--help) usage ;; *) usage ;; esac done is_valid_id() { local val="$1" local uuid_re='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' if [[ "$val" =~ $uuid_re ]]; then return 0 fi return 1 } detect_nearest_agent() { if [ "${MAM_AUTODETECT_FORCE_FAIL:-0}" = "1" ]; then return 3 fi local curr=$$ local agent_family="" local cmd_line="" local detected_id="" while [ -n "$curr" ] && [ "$curr" -gt 1 ]; do cmd_line=$(ps -p "$curr" -o command= 2>/dev/null || true) if [ -z "$cmd_line" ]; then break fi # Extract tokens before any leading hyphen option local tokens=() for tok in $cmd_line; do if [[ "$tok" =~ ^- ]]; then break fi tokens+=("$tok") done # Check for agent family match in path tokens local match="" for tok in "${tokens[@]}"; do local base base="$(basename "$tok")" case "$base" in claude|agy|hermes|grok|opencode|codex) match="$base" break ;; esac done if [ -n "$match" ]; then agent_family="$match" # 1. Parse CLI argv for flags (handles both space and equals form) case "$agent_family" in claude) detected_id=$(echo "$cmd_line" | grep -oE '(-r|--session-id)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(-r|--session-id)[[:space:]=]+//' || true) ;; agy) detected_id=$(echo "$cmd_line" | grep -oE '--conversation[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^--conversation[[:space:]=]+//' || true) ;; hermes) detected_id=$(echo "$cmd_line" | grep -oE '(--resume|--session)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--resume|--session)[[:space:]=]+//' || true) ;; grok) detected_id=$(echo "$cmd_line" | grep -oE '(--session-id|--resume)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session-id|--resume)[[:space:]=]+//' || true) ;; opencode) detected_id=$(echo "$cmd_line" | grep -oE '(--session|-s)[[:space:]=]+[^[:space:]]+' | head -n 1 | sed -E 's/^(--session|-s)[[:space:]=]+//' || true) ;; codex) detected_id=$(echo "$cmd_line" | grep -oE 'resume[[:space:]]+.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' | head -n 1 || true) ;; esac if [ -n "$detected_id" ] && is_valid_id "$detected_id"; then echo "$detected_id" return 0 fi # 2. Check family-matched environment variable local env_var="" case "$agent_family" in claude) env_var="${CLAUDE_CODE_SESSION_ID:-}" ;; agy) env_var="${ANTIGRAVITY_CONVERSATION_ID:-}" ;; hermes) env_var="${HERMES_SESSION_ID:-}" ;; grok) env_var="${GROK_SESSION_ID:-}" ;; opencode) env_var="${OPENCODE_SESSION_ID:-}" ;; codex) env_var="" ;; esac if [ -n "$env_var" ] && is_valid_id "$env_var"; then echo "$env_var" return 0 fi # Stop at nearest agent ancestor break fi curr=$(ps -p "$curr" -o ppid= 2>/dev/null | tr -d ' ' || true) done return 3 } export PYTHONPATH="$REPO_ROOT/.agents/skills:${PYTHONPATH:-}" if [ "$MODE" = "list" ]; then python3 -c ' import sys, json try: d = json.loads(sys.stdin.read() or "{}") except Exception: d = {} uuids = d.get("orchestrator_uuids", []) for u in uuids: print(u) ' <<< "$(load_state_json)" exit 0 fi if [ -z "$TARGET_UUID" ]; then if [ "$AUTODETECT" = true ]; then TARGET_UUID=$(detect_nearest_agent || true) fi if [ -z "$TARGET_UUID" ]; then echo "ERROR: Could not detect orchestrator session ID." >&2 exit 3 fi fi if ! is_valid_id "$TARGET_UUID"; then echo "ERROR: Invalid UUID or session ID format '$TARGET_UUID'." >&2 exit 2 fi if [ "$PURGE" = true ] && [ "$MODE" != "remove" ]; then echo "ERROR: --purge-conversation only applies together with --remove ." >&2 exit 2 fi if [ "$MODE" = "add" ]; then # Check if running sub-agent session row currently owns this UUID check_owner_rc=0 python3 -c ' import sys, json try: d = json.loads(sys.stdin.read() or "{}") except Exception: d = {} target = sys.argv[1] for s in d.get("herdr_sessions", []): if s.get("status") == "running": for k in ["claude_session_id_own", "agy_conversation_id_own", "hermes_conversation_id_own", "grok_session_id_own"]: if s.get(k) == target: sys.exit(1) sys.exit(0) ' "$TARGET_UUID" <<< "$(load_state_json)" || check_owner_rc=$? if [ "$check_owner_rc" -eq 1 ]; then echo "ERROR: ID '$TARGET_UUID' is currently owned by a running session." >&2 exit 1 fi fi if [ "$DRY_RUN" = true ]; then if [ "$PURGE" = true ]; then echo "[dry-run] would remove orchestrator uuid: $TARGET_UUID and purge its conversation artifacts (agent: $AGENT_HINT)" else echo "[dry-run] would $MODE orchestrator uuid: $TARGET_UUID" fi exit 0 fi # Issue #72 (U2): no registry write while an update runs - a rollback would # restore the registry without it. Held until the write is done (lib.sh # mam_update_admit). mam_update_admit || exit 1 # Issue #13: the orchestrator has no herdr_sessions row, so stop_session.sh # --purge-conversation can never reach its conversation. Purge FIRST, through # the adapter (`lib_py.agents purge`): a refusal because the conversation is # still in use leaves the registration untouched. PURGE_JSON="" if [ "$PURGE" = true ]; then if [ "$YES" != true ]; then echo "DANGER: --purge-conversation will DELETE orchestrator conversation $TARGET_UUID from disk." >&2 echo " This cannot be undone: no future resume of that conversation." >&2 echo " Re-run with --yes to confirm." >&2 exit 4 fi purge_rc=0 PURGE_JSON="$("$(_delegate_py_bin)" -m lib_py.agents purge "$AGENT_HINT" "$TARGET_UUID" "$WORKSPACE_ROOT")" || purge_rc=$? case "$purge_rc" in 0) ;; 4) echo "ERROR: conversation $TARGET_UUID is still in use (a live process references it); nothing purged, registration unchanged." >&2 exit 5 ;; 5) echo "WARN: no on-disk conversation found for $TARGET_UUID (agent: $AGENT_HINT); nothing to purge." >&2 PURGE_JSON="" ;; *) echo "ERROR: purge failed (rc=$purge_rc): $PURGE_JSON" >&2 exit 1 ;; esac fi MAM_TARGET_UUID="$TARGET_UUID" MAM_OP_MODE="$MODE" atomic_dump_yaml "$AGENT_SESSIONS_YAML" <<'PYEOF' import os, sys, json target = os.environ["MAM_TARGET_UUID"] op = os.environ["MAM_OP_MODE"] uuids = d.get("orchestrator_uuids", []) if not isinstance(uuids, list): uuids = [] if op == "add": if target not in uuids: uuids.append(target) elif op == "remove": uuids = [u for u in uuids if u != target] d["orchestrator_uuids"] = uuids PYEOF mam_update_admit_release case "$MODE" in add) verb="added" ;; remove) verb="removed" ;; *) verb="${MODE}ed" ;; esac echo "Successfully $verb orchestrator UUID: $TARGET_UUID" if [ -n "$PURGE_JSON" ]; then printf '%s' "$PURGE_JSON" | python3 -c ' import json, sys d = json.loads(sys.stdin.read() or "{}") for p in d.get("purged", []): print("purged: %s" % p) residual = d.get("residual", []) for p in residual: print("WARN residual: %s" % p) if not residual: print("residual: none (agent: %s)" % d.get("agent")) ' fi