#!/usr/bin/env bash # create_session.sh — multi-agent-mux-create 의 부속 스크립트 # Usage: # bash create_session.sh --workspace --agent [--session ] [--wrapper] # # 동작: # 1) preflight: tmux/claude/agy 가용성, workspace 존재 # 2) tmux 세션 이름 결정 (--session 없으면 자동) # 3) tmux 세션 시작 (claude 는 wrapper 우선, agy 는 인라인) # 4) pane 메타 캡처 (pid, cmd, cwd) # 5) agent-sessions.yaml 에 tmux_sessions[] 엔트리 append # 6) 검증 출력 # # Exit codes: # 0 = success # 1 = preflight failure # 2 = invalid args # 3 = tmux session already exists (use multi-agent-mux-resume or delete first) # 4 = agent-sessions.yaml append failure set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" usage() { cat < --agent [options] Options: --workspace PATH project directory (required) --agent AGENT claude | agy | hermes (required) --session NAME tmux session name (default: derived from workspace) --wrapper force use of ~/.local/bin/ wrapper even if not present --dry-run print commands without executing --tmux-server NAME specify isolated tmux server name --submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt -h, --help this help EOF } WORKSPACE="" AGENT="" SESSION_NAME="" USE_WRAPPER=0 DRY_RUN=0 TMUX_SERVER_OPT="" SUBMIT_JOB_PROMPT="" while [ $# -gt 0 ]; do case "$1" in --workspace) WORKSPACE="$2"; shift 2 ;; --agent) AGENT="$2"; shift 2 ;; --session) SESSION_NAME="$2"; shift 2 ;; --wrapper) USE_WRAPPER=1; shift ;; --dry-run) DRY_RUN=1; shift ;; --tmux-server) TMUX_SERVER_OPT="$2"; shift 2 ;; --submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;; esac done if [ -n "$TMUX_SERVER_OPT" ]; then export TMUX_SERVER_NAME="$TMUX_SERVER_OPT" fi # Preflight [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; } [ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; } [ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; } command -v tmux >/dev/null || { echo "ERROR: tmux not installed" >&2; exit 1; } command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; } # Auth Check (OAuth check for agy, loggedIn check for claude, status for hermes) if [ "$AGENT" = "claude" ]; then if ! claude auth status 2>/dev/null | grep -q '"loggedIn":\s*true'; then echo "ERROR: claude not logged in. Run 'claude auth login' first." >&2 exit 1 fi elif [ "$AGENT" = "agy" ]; then if ! agy models >/dev/null 2>&1; then echo "ERROR: agy is not authenticated. Please log in first." >&2 exit 1 fi elif [ "$AGENT" = "hermes" ]; then if ! hermes status >/dev/null 2>&1; then echo "ERROR: hermes is not functional. Run 'hermes setup' first." >&2 exit 1 fi fi # 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A) if [ -z "$SESSION_NAME" ]; then SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")" fi # 이미 살아있으면 실패 if _tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo "ERROR: tmux session '$SESSION_NAME' already exists. Use multi-agent-mux-resume to attach, or multi-agent-mux-stop first." >&2 exit 3 fi # tmux 세션 띄우기 LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}" WRAPPER="$LOCAL_BIN/$SESSION_NAME" spawn() { case "$AGENT" in claude) if [ -x "$WRAPPER" ] || [ "$USE_WRAPPER" = "1" ]; then nohup "$WRAPPER" >/dev/null 2>&1 & disown else _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude --dangerously-skip-permissions" fi ;; agy) _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions" ;; hermes) _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes" ;; *) echo "ERROR: --agent must be claude, agy or hermes, got: $AGENT" >&2; exit 2 ;; esac } if [ "$DRY_RUN" = "1" ]; then echo "[dry-run] would spawn: tmux session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT)" exit 0 fi spawn # TUI 준비 대기 sleep 6 # pane 메타 캡처 PANE_PID=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "") PANE_CWD=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_path}' 2>/dev/null || echo "$WORKSPACE") PANE_CMD=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/dev/null || echo "$AGENT") TMUX_EPOCH=$(date +%s) NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ') # cmd_full 결정 case "$AGENT" in claude) CMD_FULL='claude --dangerously-skip-permissions' ;; agy) CMD_FULL='agy --dangerously-skip-permissions' ;; hermes) CMD_FULL='hermes' ;; esac # 시작 명령 local_tmux="tmux" if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then local_tmux="tmux -L $TMUX_SERVER_NAME" fi case "$AGENT" in claude) if [ -x "$WRAPPER" ]; then START_CMD="$WRAPPER # ~/.local/bin 의 래퍼" else START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"claude --dangerously-skip-permissions\"" fi ;; agy|hermes) START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" ;; esac # agent-sessions.yaml 에 append DELEGATE_JOB_ID="" if [ -n "$SUBMIT_JOB_PROMPT" ]; then delegate_agent="" if [ "$AGENT" = "claude" ]; then delegate_agent="claude-code" elif [ "$AGENT" = "hermes" ]; then delegate_agent="hermes-agent" else delegate_agent="antigravity-cli" fi agent_session="tmux:$SESSION_NAME" DELEGATE_JOB_ID=$(delegate_submit_job "$SUBMIT_JOB_PROMPT" "$delegate_agent" "$agent_session") echo "Submitted delegated job: $DELEGATE_JOB_ID" fi if [ ! -f "$AGENT_SESSIONS_YAML" ]; then mkdir -p "$(dirname "$AGENT_SESSIONS_YAML")" echo "tmux_sessions: []" > "$AGENT_SESSIONS_YAML" fi # atomic_dump_yaml: flock + temp+rename + .bak + schema validate (P0-B). # 모든 값은 환경변수로 전달 — heredoc interpolation 없음 (P1-B). # 자식 pid 는 bash 에서 pgrep 으로 미리 구함 (P2: 도구명 필터). CHILD_PID=0 if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ]; } && [ -n "$PANE_PID" ]; then CHILD_PID=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true) CHILD_PID="${CHILD_PID:-0}" fi atomic_dump_yaml "$AGENT_SESSIONS_YAML" \ SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \ TMUX_EPOCH="$TMUX_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \ CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \ TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" \ DELEGATE_JOB_ID="$DELEGATE_JOB_ID" <<'PYEOF' name = os.environ['SESSION_NAME'] agent = os.environ['AGENT'] pid = os.environ.get('PANE_PID', '') epoch = os.environ.get('TMUX_EPOCH', '') server_name = os.environ.get('TMUX_SERVER_NAME', 'default') server_opt = f"-L {server_name} " if server_name and server_name != 'default' else "" sessions = d.setdefault('tmux_sessions', []) # P0-D: 같은 이름 엔트리가 status=running 이면만 거부. terminated/archived 는 # 재사용 가능 — 낡은 엔트리를 제거하고 새로 append (create -> delete -> create). running_same = [s for s in sessions if s.get('name') == name and s.get('status') == 'running'] if running_same: print(f"ERROR: {name} already running in agent-sessions.yaml", flush=True) raise SystemExit(4) sessions[:] = [s for s in sessions if s.get('name') != name] entry = { 'name': name, 'status': 'running', 'tmux_session_created_at': os.environ['NOW_ISO'], 'tmux_session_epoch': int(epoch) if epoch.isdigit() else 0, 'tmux_server': server_name, 'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None, 'pane': { 'index': 0, 'pid': int(pid) if pid.isdigit() else 0, 'cmd': agent, 'cmd_full': os.environ['CMD_FULL'], 'cwd': os.environ['PANE_CWD'], }, 'start_command': os.environ['START_CMD'], 'attach_command': f'tmux {server_opt}attach -t {name}', 'kill_command': f'tmux {server_opt}kill-session -t {name}', } if agent == 'claude': entry['tui'] = { 'model': '(unknown — capture after first message)', 'provider': 'anthropic', 'plan': '(unknown)', 'account': '(unknown — read from claude auth status)', 'version': '(unknown — read from TUI)', } entry['claude_session_id_own'] = None entry['last_visible_status'] = "TUI started; awaiting first user message" elif agent == 'agy': cp = os.environ.get('CHILD_PID', '0') entry['child_pid'] = int(cp) if cp.isdigit() else 0 entry['agy_conversation_id_own'] = None entry['mcp_attachments'] = [ { 'name': 'stitch', 'transport': 'mcp-remote', 'endpoint': 'https://stitch.googleapis.com/mcp' } ] entry['last_visible_status'] = "TUI started; awaiting first user message" elif agent == 'hermes': cp = os.environ.get('CHILD_PID', '0') entry['child_pid'] = int(cp) if cp.isdigit() else 0 entry['hermes_conversation_id_own'] = None entry['last_visible_status'] = "TUI started; awaiting first user message" sessions.append(entry) snap = d.setdefault('snapshot', {}) snap['taken_at'] = os.environ['NOW_ISO'] snap['cwd'] = os.environ['PANE_CWD'] print(f"appended: {name}", flush=True) PYEOF echo echo "=== created ===" echo "tmux session: $SESSION_NAME (pane pid $PANE_PID, cmd $PANE_CMD, cwd $PANE_CWD)" if [ -n "$DELEGATE_JOB_ID" ]; then echo "delegate job: $DELEGATE_JOB_ID" delegate_publish_event "$DELEGATE_JOB_ID" started "multi-agent-mux session created" WD_PID=$(start_watchdog "$DELEGATE_JOB_ID" "$WORKSPACE") echo "watchdog PID: $WD_PID" fi echo "agent-sessions.yaml updated" echo if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then echo "Attach: tmux -L $TMUX_SERVER_NAME attach -t $SESSION_NAME" else echo "Attach: tmux attach -t $SESSION_NAME" fi echo "Delete: use multi-agent-mux-stop skill" echo "Resume: use multi-agent-mux-resume skill (after first message creates a session id)"