#!/usr/bin/env bash # create_session.sh — multi-agent-mux-create 의 부속 스크립트 # Usage: # bash create_session.sh --workspace --agent --role [--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 --role [options] Options: --workspace PATH project directory (required) --agent AGENT claude | agy | hermes | cline (required) --role ROLE assigned role (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 --onboard automatically submit a project alignment/orientation job to the new agent --no-isolate disable state isolation (shares global configuration/history) [default: isolated mode is always active] -h, --help this help EOF } WORKSPACE="" AGENT="" ROLE="" SESSION_NAME="" USE_WRAPPER=0 DRY_RUN=0 TMUX_SERVER_OPT="" SUBMIT_JOB_PROMPT="" ONBOARD=0 ISOLATE=1 while [ $# -gt 0 ]; do case "$1" in --workspace) WORKSPACE="$2"; shift 2 ;; --agent) AGENT="$2"; shift 2 ;; --role) ROLE="$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 ;; --onboard) ONBOARD=1; shift ;; --isolate) ISOLATE=1; shift ;; # legacy compatibility --no-isolate) ISOLATE=0; shift ;; -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; } [ -n "$ROLE" ] || { echo "ERROR: --role 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 elif [ "$AGENT" = "cline" ]; then if ! cline history --json >/dev/null 2>&1; then echo "ERROR: cline is not functional or configured." >&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 # T3: 세션 격리 프로비저닝 (all-L2) — 격리 홈 생성 + auth/설정 심링크 시딩 # (implementation_plan.session_isolation.md Rev.3 / Phase 0 실측 매트릭스 기준) ISOLATION_UUID="" ISOLATION_ROOT="" ISOLATION_LEVER="" ISOLATION_SEEDED="" if [ "$ISOLATE" = "1" ]; then command -v uuidgen >/dev/null || { echo "ERROR: uuidgen not found (required for --isolate)" >&2; exit 1; } WORKSPACE_ABS="$(cd "$WORKSPACE" && pwd)" ISOLATION_UUID="$(uuidgen)" ISOLATION_ROOT="$WORKSPACE_ABS/.mam/agent_homes/$ISOLATION_UUID" ISOLATION_LEVER="$(isolation_lever "$AGENT")" if [ "$DRY_RUN" = "1" ]; then echo "[dry-run] would provision isolation: lever=$ISOLATION_LEVER root=$ISOLATION_ROOT" else ISOLATION_SEEDED="$(provision_isolation "$AGENT" "$ISOLATION_ROOT")" echo "isolation: lever=$ISOLATION_LEVER root=$ISOLATION_ROOT" fi fi # tmux 세션 띄우기 LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}" WRAPPER="$LOCAL_BIN/$SESSION_NAME" # cmd_full 결정 — T4: isolation 디스패치(env prefix / CLI args)를 시작 명령에 주입. # 격리 미사용 시 기존 문자열과 byte-identical (V4 회귀 0). ISO_ENV_PREFIX="" ISO_CMD_ARGS="" if [ -n "$ISOLATION_ROOT" ]; then ISO_ENV_PREFIX="$(isolation_env_prefix "$AGENT" "$ISOLATION_ROOT")" ISO_CMD_ARGS="$(isolation_cmd_args "$AGENT" "$ISOLATION_ROOT")" fi # Resolve absolute path of the agent command to prevent tmux PATH inheritance issues (especially on macOS) RESOLVED_BIN="$AGENT" if [ "$AGENT" = "cline" ]; then if command -v cline >/dev/null 2>&1; then RESOLVED_BIN="$(command -v cline)" fi else if command -v "$AGENT" >/dev/null 2>&1; then RESOLVED_BIN="$(command -v "$AGENT")" fi fi # On macOS, clear quarantine attribute for the agent binary to prevent Gatekeeper hangs if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then xattr -d com.apple.quarantine "$RESOLVED_BIN" 2>/dev/null || true fi case "$AGENT" in claude) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;; agy) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;; hermes) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN}" ;; cline) CMD_FULL="${RESOLVED_BIN} -i${ISO_CMD_ARGS:+ $ISO_CMD_ARGS}" ;; esac spawn() { case "$AGENT" in claude) # 격리 시 wrapper 경로는 env 주입을 운반하지 못하므로 인라인 spawn 강제 if [ "$ISOLATE" != "1" ] && { { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$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" "$CMD_FULL" fi ;; agy|hermes|cline) _tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL" ;; *) echo "ERROR: --agent must be claude, agy, hermes or cline, 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 # Trap for rolling back/cleaning up tmux session if script exits due to error cleanup_tmux_on_error() { local exit_code=$? if [ $exit_code -ne 0 ]; then echo "⚠️ Error occurred during initialization. Rolling back and killing tmux session '$SESSION_NAME'..." >&2 _tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true # T3 rollback: 이 세션용으로 프로비저닝한 격리 홈 제거 (경로 가드 후 rm) if [ -n "$ISOLATION_ROOT" ] && [ -d "$ISOLATION_ROOT" ]; then case "$ISOLATION_ROOT" in */.mam/agent_homes/*) rm -rf "$ISOLATION_ROOT" ;; esac fi fi } trap cleanup_tmux_on_error EXIT # TUI 준비 대기 if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2 exit 1 fi # 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 은 spawn 전에 isolation 디스패치를 반영해 확정됨 — T4) 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 [ "$ISOLATE" != "1" ] && [ -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\" \"$CMD_FULL\"" fi ;; agy|hermes|cline) START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" ;; esac # If --onboard is specified, automatically build the onboarding prompt if [ "$ONBOARD" = "1" ] && [ -z "$SUBMIT_JOB_PROMPT" ]; then SUBMIT_JOB_PROMPT="You are a newly spawned $ROLE Team Leader agent in this workspace. To align yourself with the project context, perform the following tasks: 1. Read the project documentation at README.md and the multi-agent protocol guidelines at .agents/MULTI_AGENT_RULES.md to understand the design rules. 2. Run 'git status' and 'git diff' to analyze the current modifications and active work in the repository. 3. Read .mam/agent-sessions.yaml to see other running agent sessions, their roles, and confirm your own assigned role: $ROLE. 4. Once you have fully comprehended the project state, publish a completed event (using publish_event.py) with the detail 'Onboarding complete; aligned with role $ROLE'." fi # 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" elif [ "$AGENT" = "cline" ]; then delegate_agent="cline-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" ] || [ "$AGENT" = "cline" ]; } && [ -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" ROLE="$ROLE" \ ISOLATION_UUID="$ISOLATION_UUID" ISOLATION_ROOT="$ISOLATION_ROOT" \ ISOLATION_LEVER="$ISOLATION_LEVER" ISOLATION_SEEDED="$ISOLATION_SEEDED" <<'PYEOF' name = os.environ['SESSION_NAME'] agent = os.environ['AGENT'] role = os.environ['ROLE'] 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', 'role': role, '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}', } # T5: isolation 블록 영속화 (all-L2) — resume/resolve/stop 이 재적용의 단일 소스로 사용 iso_uuid = os.environ.get('ISOLATION_UUID', '') if iso_uuid: entry['isolation'] = { 'uuid': iso_uuid, 'root': os.environ.get('ISOLATION_ROOT', ''), 'lever': os.environ.get('ISOLATION_LEVER', ''), 'seeded': [x for x in os.environ.get('ISOLATION_SEEDED', '').split(',') if x], } 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" elif agent == 'cline': cp = os.environ.get('CHILD_PID', '0') entry['child_pid'] = int(cp) if cp.isdigit() else 0 entry['cline_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" # Construct instructions matching multi-agent-mux-delegate-job format py_bin="$(_delegate_py_bin)" pub="$py_bin .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --job $DELEGATE_JOB_ID" instructions="Your job_id is \"$DELEGATE_JOB_ID\" (the one just registered for THIS delegation). On start run: $pub --event started. On progress (optional): $pub --event progress --detail ''. On success run: $pub --event completed --detail ''. On failure run: $pub --event error --detail ''. Task: $SUBMIT_JOB_PROMPT" # Inject instructions into the tmux pane rc=0 inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" || rc=$? if [ "$rc" -ne 0 ]; then delegate_publish_event "$DELEGATE_JOB_ID" error "instruction injection failed (prompt-lock, rc=$rc)" exit 1 fi delegate_publish_event "$DELEGATE_JOB_ID" started "multi-agent-mux session created and instructions injected" WD_PID=$(start_watchdog "$DELEGATE_JOB_ID" "$WORKSPACE") echo "watchdog PID: $WD_PID" fi # Clear cleanup trap as registration was fully successful trap - EXIT 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)"