# ๐Ÿ› ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ: Herdr ํŒจ์ธ ๋ผ์šฐํŒ… ๋ฐ ์ง€์นจ ์ฃผ์ž… ์•ˆ์ „์„ฑ ๋ถ„์„ ๋ฆฌํฌํŠธ - **๋ฌธ์„œ ๋ฒ„์ „**: v1.0 - **์ž‘์„ฑ์ผ์ž**: 2026-08-27 - **๋Œ€์ƒ ๋ชจ๋“ˆ**: `.agents/skills/lib.sh` (Multi-Agent Mux ์ฝ”์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) - **๊ฒ€ํ†  ์ฐธ์—ฌ ๋ฆฌ๋ทฐ์–ด**: `planner-reviewer-claude-01` (Claude), `reviewer-creator-grok-01` (Grok), `reviewer-cline-01` (Cline) --- ## 1. ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ Multi-Agent Mux(MAM) ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ๋ฃจํ”„(`run_loop.sh`) ๊ตฌ๋™ ์ค‘, `herdr agent`๋กœ ์ž๋™ ๋“ฑ๋ก๋˜์ง€ ์•Š๊ณ  `herdr pane` ๋ผ๋ฒจ(`label: creator-agy-01`, `label: reviewer-cline-01`) ํ˜•ํƒœ๋กœ ๋™์ž‘ํ•˜๋Š” ์—์ด์ „ํŠธ ์„ธ์…˜์— ๋Œ€ํ•ด **ํ”„๋กฌํ”„ํŠธ ์ฃผ์ž…(`inject_instructions` / `send_keys_safe`)์ด ๋„๋‹ฌํ•˜์ง€ ๋ชปํ•˜๊ณ  ์„ธ์…˜์ด ๋ฌดํ•œ ๋Œ€๊ธฐ/ํƒ€์ž„์•„์›ƒ๋˜๋Š” ํ˜„์ƒ**์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์— ๋Œ€ํ•œ ์ž„์‹œ ๋ผ์šฐํŒ… ํŒจ์น˜ ํ›„ 3๊ฐœ ๋…๋ฆฝ ๋ฆฌ๋ทฐ์–ด ์„ธ์…˜(`claude`, `grok`, `cline`)์„ ํ†ตํ•ด ๊ต์ฐจ ํ”ผ์–ด ๋ฆฌ๋ทฐ๋ฅผ ์ˆ˜ํ–‰ํ•œ ๊ฒฐ๊ณผ, ์ถ”๊ฐ€์ ์ธ ํ•ต์‹ฌ ๋งน์ (P0~P2)์ด ๋ฐœ๊ตด๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ณธ ๋ฌธ์„œ๋Š” ํƒ€ ํ”„๋กœ์ ํŠธ ๋ฐ ์—…์ŠคํŠธ๋ฆผ(`multi-agent-mux`) ๋ฐ˜์˜์„ ์œ„ํ•ด ๋ถ„์„๋œ ๋ฌธ์ œ์ ๊ณผ ๊ถŒ๊ณ  ์ˆ˜์ •์•ˆ์„ ์ƒ์„ธํžˆ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. --- ## 2. ํ•ต์‹ฌ ๊ฒฐํ•จ ์ƒ์„ธ ๋ถ„์„ (Findings) ### [ISSUE-1] P0: `paste-buffer`์˜ ๋ถˆํ•„์š”ํ•œ `Enter` ์ „์†ก์œผ๋กœ ์ธํ•œ ์ด์ค‘ ์ œ์ถœ (Double Submit) - **์œ„์น˜**: `.agents/skills/lib.sh` `paste-buffer` (~880์ค„) ๋ฐ `send_keys_safe` (~1890์ค„) - **ํ˜„์ƒ**: `paste-buffer` ๊ตฌํ˜„๋ถ€์—์„œ `_real_herdr pane send-text` ํ˜ธ์ถœ ์งํ›„ `_real_herdr pane send-keys "$pane_id" Enter`๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ์ž‘์„ฑ๋จ. ๊ทธ๋Ÿฌ๋‚˜ ์ƒ์œ„ `send_keys_safe` ํ•จ์ˆ˜๋Š” ์ž์ฒด์ ์œผ๋กœ ํ…์ŠคํŠธ ์ฃผ์ž… ํ™•์ธ ํ›„ `_sks_herdr send-keys -t "$sess" C-m`์„ 1~3ํšŒ ์žฌ์‹œ๋„ํ•˜๋ฉฐ ์ œ์ถœํ•˜๋Š” **2๋‹จ๊ณ„(Paste $\rightarrow$ Submit) ํ”„๋กœํ† ์ฝœ**์„ ๊ฐ€์ง. - **๋ฌธ์ œ์ **: 1. `paste-buffer`๊ฐ€ ํ…์ŠคํŠธ๋ฅผ ๋„ฃ์œผ๋ฉด์„œ ์ด๋ฏธ 1์ฐจ ์—”ํ„ฐ๋ฅผ ์ณ๋ฒ„๋ฆผ. 2. `send_keys_safe`์˜ ํ›„์† `C-m` ๋ฃจํ”„๊ฐ€ ๋นˆ ํ”„๋กฌํ”„ํŠธ(Empty Enter)๋ฅผ ์ „์†กํ•˜๊ฑฐ๋‚˜, CLI์— ๋–  ์žˆ๋Š” ๋Œ€ํ™”์ฐฝ/์˜ต์…˜์„ ์˜๋„์น˜ ์•Š๊ฒŒ ์ž๋™ ์ˆ˜๋ฝํ•˜๋Š” ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ์œ ๋ฐœ. - **ํ•ด๊ฒฐ ๋ฐฉ์•ˆ**: `paste-buffer`๋Š” ์ˆœ์ˆ˜ํ•˜๊ฒŒ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅ ๋ฒ„ํผ์— ๋ฐฐ์น˜ํ•˜๋Š” ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋ฉฐ, ์—”ํ„ฐ ์ œ์ถœ(`Enter`/`C-m`)์€ `send_keys_safe` ๋‹จ๋…์œผ๋กœ ์ œ์–ดํ•ด์•ผ ํ•จ. --- ### [ISSUE-2] P0: `agent in tn` ๋ถ€๋ถ„ ๋งค์นญ์œผ๋กœ ์ธํ•œ ์˜ค๋ผ์šฐํŒ… (Cross-Pane Mis-Routing) - **์œ„์น˜**: `.agents/skills/lib.sh` `capture-pane`, `send-keys`, `paste-buffer` ๋‚ด๋ถ€ ํŒŒ์ด์ฌ ์Šคํฌ๋žฉ ์ฝ”๋“œ - **ํ˜„์ƒ**: ```python if p.get("label") == tn or (p.get("agent") and p.get("agent") in tn): print(p.get("pane_id", "")) sys.exit(0) ``` - **๋ฌธ์ œ์ **: ๋™์ผํ•œ CLI๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋‹ค์ค‘ ์—์ด์ „ํŠธ(์˜ˆ: `planner-reviewer-claude-01`๊ณผ `creator-claude-02`, ๋˜๋Š” `reviewer-creator-grok-01`๊ณผ `worker-grok-02`)๊ฐ€ ๋™์ผ ์„œ๋ฒ„์— ๊ณต์กดํ•  ๋•Œ, `p.get("agent") == "grok"`์ด `tn == "reviewer-creator-grok-01"`์˜ ๋ถ€๋ถ„ ๋ฌธ์ž์—ด(`"grok" in tn`)๋กœ ์ฐธ(True)์ด ๋˜์–ด, ๋ชฉ๋ก์—์„œ ๋จผ์ € ๋ฐœ๊ฒฌ๋œ ๋‹ค๋ฅธ ์—์ด์ „ํŠธ์˜ ํŒจ์ธ์œผ๋กœ ํ‚ค์™€ ํ…์ŠคํŠธ๊ฐ€ ์ž˜๋ชป ์ „์†ก๋จ. - **ํ•ด๊ฒฐ ๋ฐฉ์•ˆ**: ๋ถ€๋ถ„ ๋งค์นญ์„ ์ „๋ฉด ์ œ๊ฑฐํ•˜๊ณ , `p.get("label") == tn` (๋ผ๋ฒจ ์™„์ „ ์ผ์น˜) $\rightarrow$ `p.get("agent") == tn or p.get("name") == tn` (์ด๋ฆ„ ์™„์ „ ์ผ์น˜) ์ˆœ์„œ๋กœ ์—„๊ฒฉํ•œ ์ผ์น˜๋งŒ ํ—ˆ์šฉ. --- ### [ISSUE-3] P1: `pane list` ์กฐํšŒ ์‹œ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฏธ๊ฒฉ๋ฆฌ๋กœ ์ธํ•œ ํŒจ์ธ ์ถฉ๋Œ (Cross-Workspace Collision) - **์œ„์น˜**: `has-session`, `kill-session`, `capture-pane`, `send-keys`, `paste-buffer` - **ํ˜„์ƒ**: `herdr pane list`์˜ ๊ฒฐ๊ณผ๋Š” ์„œ๋ฒ„ ์ „์ฒด ์ „์—ญ(Server-Global) ๋ชฉ๋ก์ž„. - **๋ฌธ์ œ์ **: `MULTI_AGENT_RULES.md`์˜ ํ‘œ์ค€ ๋ช…๋ช… ๊ทœ์น™์— ๋”ฐ๋ผ ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ๊ฐ€ `creator-agy-01`, `reviewer-cline-01` ๋“ฑ ๋™์ผํ•œ ๊ธฐ๋ณธ ๋ผ๋ฒจ์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ๋‹ค๋ฅธ ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ ํŒจ์ธ์„ ์กฐํšŒํ•˜์—ฌ ํ…์ŠคํŠธ๋ฅผ ํƒˆ์ทจํ•˜๊ฑฐ๋‚˜ ๋ช…๋ น์„ ์ž˜๋ชป ์ฃผ์ž…ํ•  ์œ„ํ—˜์ด ์žˆ์Œ. - **ํ•ด๊ฒฐ ๋ฐฉ์•ˆ**: ํ˜„์žฌ ์„ธ์…˜์˜ `workspace_id`(์˜ˆ: `resolve_herdr_workspace` ๋˜๋Š” `p.get("workspace_id") == target_ws`)๋ฅผ ์กฐ๊ฑด์— ํฌํ•จํ•˜์—ฌ ๋™์ผ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋‚ด์˜ ํŒจ์ธ๋งŒ ์‹๋ณ„ํ•˜๋„๋ก ์Šค์ฝ”ํ”„ ์ œํ•œ. --- ### [ISSUE-4] P2: `agent start` ์„ฑ๊ณต ํŒ์ • ์ •๊ทœ์‹์˜ ํƒ€์ž„์•„์›ƒ ์˜ค์ธ - **์œ„์น˜**: `.agents/skills/lib.sh` `new-session` (~560์ค„) - **ํ˜„์ƒ**: ```bash if echo "$res" | grep -qE "agent_started|agent_not_ready|timed out waiting for agent startup"; then success=1 ``` - **๋ฌธ์ œ์ **: `timed out waiting for agent startup` ๋ฐ `agent_not_ready`๋Š” ์‹ค์ œ๋กœ๋Š” ๊ธฐ๋™ ์‹คํŒจ ๋˜๋Š” ์ง€์—ฐ ์ƒํƒœ์ž„์—๋„ `success=1`๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ์ฆ‰์‹œ ์žฌ์‹œ๋„ ๋ฃจํ”„๋ฅผ ๋น ์ ธ๋‚˜๊ฐ. ์ด๋กœ ์ธํ•ด TUI๊ฐ€ ์•„์ง ๋œจ์ง€ ์•Š์€ ๋นˆ ์…ธ ์ƒํƒœ์—์„œ ํ”„๋กฌํ”„ํŠธ ์ฃผ์ž…์ด ์‹œ๋„๋˜์–ด ์‹คํŒจ๋ฅผ ์ดˆ๋ž˜ํ•จ. - **ํ•ด๊ฒฐ ๋ฐฉ์•ˆ**: ์„ฑ๊ณต ํŒ์ •์€ `agent_started` (๋ฐ ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ํ™•์ธ ์‘๋‹ต)๋งŒ ์ธ์ •ํ•˜๊ณ , `agent_not_ready`/ํƒ€์ž„์•„์›ƒ ์‹œ์—๋Š” ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„๋ฅผ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์—„๊ฒฉํ™”. --- ### [ISSUE-5] P2: ์ค‘๋ณต๋œ ํŒŒ์ด์ฌ ์›๋ผ์ด๋„ˆ ํŒŒ์„œ ๋ฐ ๋ฐ๋“œ ํŒŒ์ดํ”„๋ผ์ธ - **์œ„์น˜**: 5๊ฐœ ์ด์ƒ์˜ ๋ช…๋ น ๋ถ„๊ธฐ(`has-session`, `kill-session`, `capture-pane`, `send-keys`, `paste-buffer`) - **ํ˜„์ƒ**: ๊ฐ ๋ถ„๊ธฐ๋งˆ๋‹ค ๋…๋ฆฝ์ ์œผ๋กœ ํŒŒ์ด์ฌ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ๋˜์–ด ๊ตฌํ˜„์ด ์ œ๊ฐ๊ฐ์œผ๋กœ ๋ถ„๊ธฐ(Drift)๋˜์—ˆ๊ณ , `python3 -c "... except: pass"`๊ฐ€ ํ•ญ์ƒ exit code 0์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ bash์˜ `||` ์ฒด์ธ์ด 2์ฐจ ํด๋ฐฑ์œผ๋กœ ๋„˜์–ด๊ฐ€์ง€ ์•Š๋Š” ๋ฐ๋“œ ์ฝ”๋“œ๊ฐ€ ์กด์žฌํ•จ. - **ํ•ด๊ฒฐ ๋ฐฉ์•ˆ**: ๋‹จ์ผ ๊ณต์šฉ ํ—ฌํผ ํ•จ์ˆ˜(`_resolve_herdr_pane_id`)๋ฅผ ์„ ์–ธํ•˜์—ฌ ๋ชจ๋“  ๋ถ„๊ธฐ๊ฐ€ ์ผ๊ด€๋œ ๋‹จ์ผ ๋กœ์ง์„ ํ˜ธ์ถœํ•˜๋„๋ก ๋ฆฌํŒฉํ„ฐ๋ง. --- ## 3. ๊ถŒ๊ณ  ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ฐœ์„  ์ฝ”๋“œ (Recommended Implementation) ### 3.1 ๋‹จ์ผ ๊ณต์šฉ ํ—ฌํผ ํ•จ์ˆ˜ ์„ ์–ธ (`_resolve_herdr_pane_id`) ```bash # _resolve_herdr_pane_id [workspace_id] # Resolves the concrete pane_id ("wN:pN") for a given session name or label. # Strict resolution order: # 1. herdr agent get # 2. herdr agent get # 3. herdr pane list (scoped by workspace_id, exact match on label, then exact match on agent/name) _resolve_herdr_pane_id() { local target="$1" local target_ws="${2:-${HERDR_WORKSPACE_ID:-}}" local sat sat=$(_sanitize_herdr_agent_name "$target") # 1 & 2. Try native agent get local pid="" pid=$(_real_herdr agent get "$sat" 2>/dev/null | python3 -c " import sys, json try: d = json.load(sys.stdin) print(d.get('result', {}).get('agent', {}).get('pane_id') or '') except Exception: pass " 2>/dev/null || echo "") if [ -z "$pid" ]; then pid=$(_real_herdr agent get "$target" 2>/dev/null | python3 -c " import sys, json try: d = json.load(sys.stdin) print(d.get('result', {}).get('agent', {}).get('pane_id') or '') except Exception: pass " 2>/dev/null || echo "") fi # 3. Fallback to pane list with exact matching and optional workspace scoping if [ -z "$pid" ]; then pid=$(_real_herdr pane list 2>/dev/null | TARGET_NAME="$target" TARGET_WS="$target_ws" python3 -c " import sys, json, os, re tn = os.environ.get('TARGET_NAME', '') tws = os.environ.get('TARGET_WS', '') try: data = json.load(sys.stdin) panes = data.get('result', {}).get('panes', []) # Filter by workspace if provided if tws: panes = [p for p in panes if p.get('workspace_id') == tws] # Priority 1: Exact label match for p in panes: if p.get('label') == tn: pid = p.get('pane_id', '') if pid: print(pid) sys.exit(0) # Priority 2: Exact agent name match (NO substring matching) for p in panes: if p.get('agent') == tn or p.get('name') == tn: pid = p.get('pane_id', '') if pid: print(pid) sys.exit(0) except Exception: pass sys.exit(1) " 2>/dev/null || echo "") fi # Validate pane_id format (e.g. w1:p2) if [[ "$pid" =~ ^w[0-9]+:p[0-9]+$ ]]; then echo "$pid" return 0 fi return 1 } ``` ### 3.2 ๋ถ„๊ธฐ๋ณ„ Shim ๊ฐ„์†Œํ™” ๋ฐ ์ •ํ•ฉ์„ฑ ๋ณด์žฅ ```bash # capture-pane capture-pane) sess="" # ... parse -t ... pane_id=$(_resolve_herdr_pane_id "$sess") if [ -n "$pane_id" ]; then _real_herdr pane read "$pane_id" --source visible --lines 100 2>/dev/null || true else _real_herdr agent read "$sess" --source visible --lines 100 2>/dev/null || true fi ;; # send-keys send-keys) sess="" key="" # ... parse -t and key ... [ "$key" = "C-m" ] && key="Enter" pane_id=$(_resolve_herdr_pane_id "$sess") if [ -n "$pane_id" ]; then _real_herdr pane send-keys "$pane_id" "$key" >/dev/null 2>&1 || true else _real_herdr pane send-keys "$sess" "$key" >/dev/null 2>&1 || true fi ;; # paste-buffer (NO Enter, insert-only) paste-buffer) # ... parse -b and -t ... pane_id=$(_resolve_herdr_pane_id "$sess") if [ -f "$buffer_dir/$buf" ]; then if [ -n "$pane_id" ]; then # Insert text only. Enter submission is strictly delegated to send_keys_safe. _real_herdr pane send-text "$pane_id" "$(cat "$buffer_dir/$buf")" >/dev/null 2>&1 || true else _real_herdr agent send "$sess" "$(cat "$buffer_dir/$buf")" >/dev/null 2>&1 || true fi fi ;; ``` --- ## 4. ๊ฒฐ๋ก  ๋ฐ ํ–ฅํ›„ ์กฐ์น˜ - ๋ณธ ๋ฒ„๊ทธ ๋ฆฌํฌํŠธ์— ๋ช…์‹œ๋œ 5๋Œ€ ํ•ต์‹ฌ ๊ฒฐํ•จ(P0: ์ด์ค‘ ์ œ์ถœ, P0: substring ์˜ค๋ผ์šฐํŒ…, P1: ์›Œํฌ์ŠคํŽ˜์ด์Šค ๋ฏธ๊ฒฉ๋ฆฌ, P2: ํƒ€์ž„์•„์›ƒ ์„ฑ๊ณต ์˜ค์ธ, P2: ์ค‘๋ณต ํŒŒ์„œ)์€ ๋…๋ฆฝ์ ์ธ 3์ธ ๋ฆฌ๋ทฐ์–ด์˜ ์ •๋ฐ€ ๊ต์ฐจ ๊ฒ€์ฆ์„ ํ†ตํ•ด ๊ทธ ์œ„ํ—˜์„ฑ๊ณผ ์žฌํ˜„ ๊ฒฝ๋กœ๊ฐ€ ๋ช…๋ฐฑํžˆ ์ž…์ฆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. - ๋ณธ ๋ฆฌํฌํŠธ์˜ ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ `multi-agent-mux` ์—…์ŠคํŠธ๋ฆผ ์ €์žฅ์†Œ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์œ ์ง€๋ณด์ˆ˜ ํ”„๋กœ์ ํŠธ์—์„œ `lib.sh` ๋ฆฌํŒฉํ„ฐ๋ง ํŒจ์น˜๋ฅผ ๊ณต์‹ ๋ฐฐํฌํ•  ๊ฒƒ์„ ๊ฐ•๋ ฅํžˆ ๊ถŒ๊ณ ํ•ฉ๋‹ˆ๋‹ค.