diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 4a8dd8d..2c21ffb 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -44,75 +44,172 @@ _TMUX_SKILLS_BIN_PATTERN="${_TMUX_SKILLS_BIN_PATTERN:-/.agents/skills/.bin}" TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" _resolve_real_tmux_path() { - if [ -z "${_REAL_TMUX_PATH:-}" ] || [[ "$_REAL_TMUX_PATH" == *"${_TMUX_SHIM_DIR_PATTERN}"* ]] || [[ "$_REAL_TMUX_PATH" == *"${_TMUX_SKILLS_BIN_PATTERN}"* ]]; then - local dir save_ifs="$IFS" - _REAL_TMUX_PATH="" - IFS=: - for dir in $PATH; do - if [[ "$dir" != *"${_TMUX_SHIM_DIR_PATTERN}"* ]] && [[ "$dir" != *"${_TMUX_SKILLS_BIN_PATTERN}"* ]] && [ -x "$dir/tmux" ]; then - _REAL_TMUX_PATH="$dir/tmux" - break - fi - done - IFS="$save_ifs" - if [ -z "$_REAL_TMUX_PATH" ]; then - _REAL_TMUX_PATH="tmux" - fi - export _REAL_TMUX_PATH - fi + _REAL_TMUX_PATH="tmux" + export _REAL_TMUX_PATH } _init_tmux_isolation() { - _resolve_real_tmux_path - if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then - local wrapper_dir="${TMPDIR:-/tmp}${_TMUX_SHIM_DIR_PATTERN}${TMUX_SERVER_NAME}" - if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then - mkdir -p "$wrapper_dir" - cat < "$wrapper_dir/tmux" + local wrapper_dir="$WORKSPACE_ROOT/.mam/shim" + mkdir -p "$wrapper_dir" + cat <<'EOF' > "$wrapper_dir/tmux" #!/usr/bin/env bash -if [ -z "\${TMUX_SERVER_NAME:-}" ] || [ "\$TMUX_SERVER_NAME" = "default" ]; then - exec "$_REAL_TMUX_PATH" "\$@" -else - exec "$_REAL_TMUX_PATH" -L "\$TMUX_SERVER_NAME" "\$@" +# Herdr-to-Tmux translation shim wrapper +set -euo pipefail + +wrapper_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +cmd="${1:-}" +if [ -z "$cmd" ]; then + echo "tmux shim: no command specified" >&2 + exit 1 fi -EOF - chmod +x "$wrapper_dir/tmux" - export PATH="$wrapper_dir:$PATH" - fi - else - # 격리 비활성화 시 shim 자동 cleanup (PATH에서 제거) - local new_path="" dir save_ifs="$IFS" - IFS=: - for dir in $PATH; do - if [[ "$dir" != *"${_TMUX_SHIM_DIR_PATTERN}"* ]] && [[ "$dir" != *"${_TMUX_SKILLS_BIN_PATTERN}"* ]]; then - if [ -z "$new_path" ]; then - new_path="$dir" - else - new_path="$new_path:$dir" - fi - fi +shift + +case "$cmd" in + has-session) + sess="" + while [ $# -gt 0 ]; do + case "$1" in + -t) sess="$2"; shift 2 ;; + *) shift ;; + esac done - IFS="$save_ifs" - export PATH="$new_path" + herdr agent get "$sess" >/dev/null 2>&1 + ;; + new-session) + name="" ws="" run_cmd="" + while [ $# -gt 0 ]; do + case "$1" in + -s) name="$2"; shift 2 ;; + -c) ws="$2"; shift 2 ;; + -d|-x|-y) shift ;; + *) run_cmd="$1"; shift ;; + esac + done + herdr workspace create --label "${TMUX_SERVER_NAME:-default}" --cwd "${ws:-.}" >/dev/null 2>&1 || true + # herdr agent start name workspace cwd argv + herdr agent start "$name" --workspace "${TMUX_SERVER_NAME:-default}" --cwd "${ws:-.}" -- $run_cmd + ;; + kill-session) + sess="" + while [ $# -gt 0 ]; do + case "$1" in + -t) sess="$2"; shift 2 ;; + *) shift ;; + esac + done + herdr session stop "$sess" >/dev/null 2>&1 || true + herdr session delete "$sess" >/dev/null 2>&1 || true + ;; + list-panes) + sess="" format="" + while [ $# -gt 0 ]; do + case "$1" in + -t) sess="$2"; shift 2 ;; + -F) format="$2"; shift 2 ;; + *) shift ;; + esac + done + info=$(herdr agent get "$sess" 2>/dev/null || true) + if [ -n "$info" ]; then + if [ "$format" = '#{pane_pid}|#{pane_current_path}|#{pane_current_command}' ]; then + python3 -c " +import sys, json +try: + d = json.loads(sys.stdin.read()) + p = d.get('pane', d) + print(f\"{p.get('pid', '')}|{p.get('cwd', '')}|{p.get('command', p.get('argv', ''))}\") +except Exception: + pass +" <<< "$info" + elif [ "$format" = '#{pane_pid}' ]; then + python3 -c "import sys, json; d = json.loads(sys.stdin.read()); print(d.get('pane', d).get('pid', ''))" <<< "$info" + elif [ "$format" = '#{pane_current_path}' ]; then + python3 -c "import sys, json; d = json.loads(sys.stdin.read()); print(d.get('pane', d).get('cwd', ''))" <<< "$info" + elif [ "$format" = '#{pane_current_command}' ]; then + python3 -c "import sys, json; d = json.loads(sys.stdin.read()); print(d.get('pane', d).get('command', d.get('pane', d).get('argv', '')))" <<< "$info" + fi + fi + ;; + capture-pane) + sess="" + while [ $# -gt 0 ]; do + case "$1" in + -t) sess="$2"; shift 2 ;; + *) shift ;; + esac + done + herdr agent read "$sess" --source visible --lines 100 2>/dev/null || true + ;; + send-keys) + sess="" key="" + while [ $# -gt 0 ]; do + case "$1" in + -t) sess="$2"; shift 2 ;; + *) key="$1"; shift ;; + esac + done + if [ "$key" = "C-m" ] || [ "$key" = "Enter" ]; then + key="Enter" + fi + herdr pane send-keys "$sess" "$key" >/dev/null 2>&1 || true + ;; + set-buffer) + text="" + while [ $# -gt 0 ]; do + case "$1" in + -b) buf="$2"; shift 2 ;; + *) text="$1"; shift ;; + esac + done + echo -n "$text" > "$wrapper_dir/tmp_buffer" + ;; + paste-buffer) + sess="" + while [ $# -gt 0 ]; do + case "$1" in + -t) sess="$2"; shift 2 ;; + *) shift ;; + esac + done + if [ -f "$wrapper_dir/tmp_buffer" ]; then + herdr agent send "$sess" "$(cat "$wrapper_dir/tmp_buffer")" >/dev/null 2>&1 || true + fi + ;; + delete-buffer) + rm -f "$wrapper_dir/tmp_buffer" + ;; + ls) + herdr session list --json 2>/dev/null | python3 -c " +import sys, json +try: + for s in json.load(sys.stdin): + print(f\"{s['name']}|{s.get('created_at_epoch', 0)}\") +except Exception: + pass +" || true + ;; + *) + true + ;; +esac +EOF + chmod +x "$wrapper_dir/tmux" + if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then + export PATH="$wrapper_dir:$PATH" fi } mam_tmux() { - _resolve_real_tmux_path - if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then - "$_REAL_TMUX_PATH" -L "$TMUX_SERVER_NAME" "$@" - else - "$_REAL_TMUX_PATH" "$@" - fi + _init_tmux_isolation + "$WORKSPACE_ROOT/.mam/shim/tmux" "$@" } _tmux() { - _init_tmux_isolation mam_tmux "$@" } tmux() { - _tmux "$@" + mam_tmux "$@" } # --------------------------------------------------------------------------- @@ -1239,3 +1336,6 @@ handle_startup_dialogs() { done return 0 } + +# Auto-initialize the herdr translation shim wrapper on library source +_init_tmux_isolation diff --git a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh index c0b19f7..e0d1db5 100755 --- a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh +++ b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh @@ -79,7 +79,7 @@ fi [ -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 herdr >/dev/null || { echo "ERROR: herdr 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) @@ -231,23 +231,7 @@ 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 +START_CMD="herdr agent start \"$SESSION_NAME\" --workspace \"${TMUX_SERVER_NAME:-default}\" --cwd \"$WORKSPACE\" -- $CMD_FULL" # If --onboard is specified, automatically build the onboarding prompt if [ "$ONBOARD" = "1" ] && [ -z "$SUBMIT_JOB_PROMPT" ]; then @@ -332,8 +316,8 @@ entry = { '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}', + 'attach_command': f'herdr session attach {name}', + 'kill_command': f'herdr session stop {name} && herdr session delete {name}', } # T5: isolation 블록 영속화 (all-L2) — resume/resolve/stop 이 재적용의 단일 소스로 사용 @@ -421,10 +405,6 @@ fi 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 "Attach: herdr session attach $SESSION_NAME" echo "Delete: use multi-agent-mux-stop skill" echo "Resume: use multi-agent-mux-resume skill (after first message creates a session id)" diff --git a/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh b/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh index 294d663..f5a8ac2 100755 --- a/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh +++ b/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh @@ -120,22 +120,13 @@ if [ -n "$ISO_ARGS" ]; then CMD_FULL="$CMD_FULL $ISO_ARGS" fi -# 4. Spawn new tmux session + run agent with the saved id -case "$AGENT" in - claude) - if [ -z "$ISO_ROOT" ] && [ -x "$HOME/.local/bin/canary-projects-multi-agent-mux-creator-claude" ]; then - START_CMD="tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$HOME/.local/bin/canary-projects-multi-agent-mux-creator-claude\"" - else - START_CMD="tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" - fi - eval "$START_CMD" - # auto-handle trust / bypass dialogs - handle_startup_dialogs "$SESSION_NAME" 20 - ;; - agy|hermes|cline) - eval "tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" - ;; -esac +# 4. Spawn new agent session via herdr +herdr workspace create --label "${TMUX_SERVER_NAME:-default}" --cwd "$WORKSPACE" >/dev/null 2>&1 || true +herdr agent start "$SESSION_NAME" --workspace "${TMUX_SERVER_NAME:-default}" --cwd "$WORKSPACE" -- $CMD_FULL +if [ "$AGENT" = "claude" ]; then + # auto-handle trust / bypass dialogs + handle_startup_dialogs "$SESSION_NAME" 20 +fi # Wait for TUI readiness or let it settle sleep 2 diff --git a/.agents/skills/multi-agent-mux-status/scripts/status.sh b/.agents/skills/multi-agent-mux-status/scripts/status.sh index f8005c9..26300fd 100755 --- a/.agents/skills/multi-agent-mux-status/scripts/status.sh +++ b/.agents/skills/multi-agent-mux-status/scripts/status.sh @@ -189,9 +189,9 @@ def get_job_status(s): sessions = d.get('tmux_sessions', []) -print(f"agent-sessions status — {drift['timestamp']} (tmux_confirmed={drift['tmux_confirmed']})") +print(f"agent-sessions status — {drift['timestamp']} (herdr_confirmed={drift['tmux_confirmed']})") print("=" * 136) -print(f"{'NAME':<44} {'SERVER':<12} {'YAML':<10} {'TMUX':<6} {'CMD':<6} {'RESUME':<8} {'JOB_ID':<10} {'JOB_STATUS':<12} DRIFT") +print(f"{'NAME':<44} {'WORKSPACE':<12} {'YAML':<10} {'HERDR':<6} {'CMD':<6} {'RESUME':<8} {'JOB_ID':<10} {'JOB_STATUS':<12} DRIFT") print("-" * 136) if not sessions: print("(no sessions registered)") @@ -199,12 +199,12 @@ for s in sessions: name = s.get('name', '?') server = s.get('tmux_server') or 'default' status = s.get('status', '?') - tmux = 'alive' if f"{name}|{server}" in alive else 'dead' + herdr = 'alive' if f"{name}|{server}" in alive else 'dead' cmd = (s.get('pane') or {}).get('cmd', '?') res = resume_on_disk(s) jid, jstatus = get_job_status(s) drs = ','.join(drift_by_name.get(name, [])) or '-' - print(f"{name:<44} {server:<12} {status:<10} {tmux:<6} {cmd:<6} {res:<8} {jid:<10} {jstatus:<12} {drs}") + print(f"{name:<44} {server:<12} {status:<10} {herdr:<6} {cmd:<6} {res:<8} {jid:<10} {jstatus:<12} {drs}") # drifts not tied to a registered row (e.g. class B unregistered, class D cache) known = {s.get('name') for s in sessions} extra = [dr for dr in drift.get('drifts', []) if dr['name'] not in known] @@ -213,5 +213,5 @@ if extra: for dr in extra: print(f" [{dr['class']}] {dr['msg']}") print("=" * 136) -print(f"alive tmux: {sorted(alive)}") +print(f"alive herdr: {sorted(alive)}") PYEOF diff --git a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh index ff6fefb..91dd74d 100755 --- a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh +++ b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh @@ -201,9 +201,9 @@ if [ "$PURGE" = "1" ] && [ "$TMUX_ALIVE" = "1" ]; then if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo "ERROR: session '$SESSION_NAME' is still alive after the kill chain." >&2 echo " Refusing registry removal — records preserved (no state was modified)." >&2 - echo " Diagnose the stuck TUI (tmux attach -t '$SESSION_NAME'), then re-run" >&2 + echo " Diagnose the stuck TUI (herdr session attach '$SESSION_NAME'), then re-run" >&2 echo " stop_session.sh --purge-conversation --yes (retry is safe/idempotent)." >&2 - delegate_publish_event "$DELEGATE_JOB_ID" error "purge aborted: tmux session still alive" + delegate_publish_event "$DELEGATE_JOB_ID" error "purge aborted: herdr session still alive" exit 4 fi fi diff --git a/deploy/INSTALL.md b/deploy/INSTALL.md index 2ba3abf..d75c6fb 100644 --- a/deploy/INSTALL.md +++ b/deploy/INSTALL.md @@ -8,7 +8,7 @@ MAM은 단일 워크스페이스 상에서 복수의 에이전트(Claude, Cline, ## 1. ⚙️ 사전 요구사항 MAM 스킬 및 스크립트들은 호스트 시스템의 다음 도구들에 의존합니다. 설치 전에 확인해 주세요. -* **tmux**: 에이전트를 백그라운드 격리 Pane에서 구동하기 위한 프로세스 컨테이너 +* **herdr**: 에이전트를 백그라운드 격리 Pane/Workspace에서 구동 및 관제하기 위한 프로세스 컨테이너 * **python3**: 세션 레지스트리(YAML/SQLite DB) 파싱 및 유효성 검사 (내장 `sqlite3` 모듈 필수) * **uuidgen**: 격리 세션 생성 시 고유의 UUID 할당 * **rsync**: 인스톨러(`deploy/install_mam.sh`)가 `.agents/` 오케스트레이터 및 스킬 폴더를 타겟 프로젝트에 복제하는 데 사용 (설치 시 필요) @@ -54,14 +54,14 @@ $ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ --role developer \ --session my-project-dev-claude \ --isolate \ - --tmux-server multi-agent-mux + --herdr-workspace multi-agent-mux ``` * `--isolate` 옵션을 주면 `.mam/agent_homes//` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다. ### 2) 세션 접속 (Attach) -백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다. (세션 생성 시 지정한 독립 격리 tmux 서버 소켓 `-L multi-agent-mux` 를 경유해 접속합니다.) +백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다. ```bash -$ tmux -L multi-agent-mux attach -t my-project-dev-claude +$ herdr session attach my-project-dev-claude ``` * **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다. @@ -81,8 +81,8 @@ $ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습 # 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동) # (주의: 만약 create 시 격리(--isolate) 세션으로 생성했다면, CLAUDE_CONFIG_DIR 환경변수를 YAML에 기록된 isolation.root 경로로 지정하여 띄워야 합니다. 상세 격리 복원 커맨드는 .agents/skills/multi-agent-mux-resume/SKILL.md 문서를 필독해 주세요.) -$ tmux -L multi-agent-mux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" \ - "claude --dangerously-skip-permissions -r $UUID; echo 'CLAUDE EXIT'; bash" +$ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \ + "claude --dangerously-skip-permissions -r $UUID" # 3단계: 레지스트리 세션 상태를 running 으로 동기화 갱신 $ bash .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh \ diff --git a/deploy/install_mam.sh b/deploy/install_mam.sh index 133fb76..ec7db8f 100755 --- a/deploy/install_mam.sh +++ b/deploy/install_mam.sh @@ -83,7 +83,7 @@ fi # 1. Dependency Checks log_info "Verifying host dependencies..." -DEPS=(tmux python3 rsync uuidgen) +DEPS=(herdr python3 rsync uuidgen) MISSING_DEPS=() for dep in "${DEPS[@]}"; do if ! command -v "$dep" &>/dev/null; then @@ -93,6 +93,10 @@ done if [ ${#MISSING_DEPS[@]} -ne 0 ]; then log_error "Missing required dependencies: ${MISSING_DEPS[*]}" + if [[ " ${MISSING_DEPS[*]} " == *" herdr "* ]]; then + log_error "To install herdr, please refer to the official installation guide:" + log_error " curl -fsSL https://herdr.dev/install.sh | sh" + fi log_error "Please install them before using MAM." exit 1 fi