refactor: update multi-agent rules to TMUX & refactor MAM skills scripts

- Update .agents/MULTI_AGENT_RULES.md & .ko.md to align with TMUX concepts
- Add .agents/INSTALL.md guide for MAM setup and workflows
- Clean up legacy markdown reports under .agents/reports/
- Refactor MAM scripts in .agents/skills/ (rename resolve_herdr_session to resolve_herdr_workspace)
- Improve workspace pane splitting/re-use logic in lib.sh
- Reduce paste safety sleep in send_keys_safe and support linking ~/.claude.json
This commit is contained in:
2026-07-22 06:57:32 +09:00
parent e4b1fb3329
commit 36a087af58
49 changed files with 258 additions and 4361 deletions
+47 -63
View File
@@ -58,6 +58,12 @@ _resolve_real_herdr_path() {
_init_herdr_isolation() {
local wrapper_dir="$WORKSPACE_ROOT/.mam/shim"
mkdir -p "$wrapper_dir"
if [ -x "$wrapper_dir/herdr" ]; then
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
export PATH="$wrapper_dir:$PATH"
fi
return 0
fi
local tmp_file
tmp_file=$(mktemp "$wrapper_dir/herdr.XXXXXX")
@@ -117,7 +123,7 @@ except Exception:
# Headless bootstrap avoids herdr's "nested herdr is disabled" guard that
# blocks a normal interactive `herdr --session <name>` launch from inside
# an existing herdr pane (which is how MAM's own agents usually run).
setsid "$REAL_HERDR" --session "$_MAM_SESSION" server </dev/null >/dev/null 2>&1 &
nohup "$REAL_HERDR" --session "$_MAM_SESSION" server >/dev/null 2>&1 &
disown 2>/dev/null || true
for _mam_wait_i in $(seq 1 40); do
[ -S "${HOME:-$HOME_DIR}/.config/herdr/sessions/$_MAM_SESSION/herdr.sock" ] && break
@@ -127,8 +133,12 @@ except Exception:
fi
_real_herdr() {
if [ -n "$_MAM_SESSION" ]; then
"$REAL_HERDR" --session "$_MAM_SESSION" "$@"
local session="${HERDR_SERVER_NAME:-}"
if [ "$session" = "default" ]; then
session=""
fi
if [ -n "$session" ]; then
"$REAL_HERDR" --session "$session" "$@"
else
"$REAL_HERDR" "$@"
fi
@@ -210,52 +220,35 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
final_cmd=$(echo "$parsed" | tail -n +2)
fi
# Isolation now comes from `_MAM_SESSION` (a real, separate herdr
# session/server) rather than a workspace label match — just create a
# fresh workspace inside whatever session is active (default or
# isolated) and place the agent there. No cross-session label lookup
# needed since `--session` already scopes everything.
# Resolve ws_id and split direction:
# - If we have up to 3 agents, split 'right' (columns)
# - If we have 4 or more agents, split 'down' (rows)
res=$(_real_herdr workspace list 2>/dev/null | WS_CWD="${ws:-.}" python3 -c "
import sys, json, os
# Check if there is an existing workspace inside this session (to reuse and split view)
existing_ws=$(_real_herdr workspace list 2>/dev/null | python3 -c "
import sys, json
try:
target_label = os.path.basename(os.path.abspath(os.environ.get('WS_CWD', '.')))
wlist = json.loads(sys.stdin.read()).get('result', {}).get('workspaces', [])
match = None
for w in wlist:
if w.get('label') == target_label:
match = w
break
if not match and wlist:
match = wlist[0]
if match:
ws_id = match.get('workspace_id', 'w1')
pane_count = match.get('pane_count', 0)
split_dir = 'right' if pane_count < 3 else 'down'
print(f'{ws_id}\t{split_dir}')
d = json.loads(sys.stdin.read())
wss = d.get('result', {}).get('workspaces', [])
if wss:
print(wss[0].get('workspace_id', ''))
except Exception:
pass
")
ws_id=$(echo "$res" | cut -f1)
split_dir=$(echo "$res" | cut -f2)
if [ -z "$ws_id" ]; then
split_flag=""
if [ -n "$existing_ws" ]; then
ws_id="$existing_ws"
split_flag="--split right"
else
ws_id=$(_real_herdr workspace create --cwd "${ws:-.}" --no-focus 2>/dev/null | python3 -c "
import sys, json
try:
d = json.loads(sys.stdin.read()).get('result', {}).get('workspace', {}).get('workspace_id', '')
print(d)
d = json.loads(sys.stdin.read())
print(d.get('result', {}).get('workspace', {}).get('workspace_id', ''))
except Exception:
pass
")
split_dir="right"
ws_id="${ws_id:-w1}"
fi
ws_id="${ws_id:-w1}"
split_dir="${split_dir:-right}"
eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --split \"$split_dir\" --cwd \"${ws:-.}\" $env_flags -- $final_cmd"
eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $split_flag $env_flags -- $final_cmd"
;;
kill-session)
sess=""
@@ -363,7 +356,7 @@ except Exception:
esac
;;
capture-pane)
sess="" src="visible" lines="100"
sess=""
while [ $# -gt 0 ]; do
case "$1" in
-t)
@@ -374,22 +367,10 @@ except Exception:
sess="$2"
shift 2
;;
-S)
# tmux `-S -N` (start N lines above the visible viewport) maps to
# herdr `--source recent --lines N`. Non-negative or non-numeric
# values keep the default visible viewport read.
if [ $# -ge 2 ] && [[ "$2" =~ ^-[0-9]+$ ]]; then
src="recent"
lines="${2#-}"
shift 2
else
shift
fi
;;
*) shift ;;
esac
done
_real_herdr agent read "$sess" --source "$src" --lines "$lines" 2>/dev/null || true
_real_herdr agent read "$sess" --source visible --lines 100 2>/dev/null || true
;;
send-keys)
sess="" key=""
@@ -483,11 +464,7 @@ except Exception:
esac
EOF
chmod +x "$tmp_file"
if [ -f "$wrapper_dir/herdr" ] && cmp -s "$tmp_file" "$wrapper_dir/herdr"; then
rm -f "$tmp_file"
else
mv -f "$tmp_file" "$wrapper_dir/herdr"
fi
mv -f "$tmp_file" "$wrapper_dir/herdr"
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
export PATH="$wrapper_dir:$PATH"
fi
@@ -507,9 +484,9 @@ herdr() {
}
# ---------------------------------------------------------------------------
# resolve_herdr_session <session_name>
# resolve_herdr_server <session_name>
#
# Query agent-sessions.yaml to find the herdr_session associated with a session.
# Query agent-sessions.yaml to find the herdr_server associated with a session.
# Fallback to HERDR_SERVER_NAME or 'default' if not registered or field is missing.
# Prints the resolved server name on stdout.
# ---------------------------------------------------------------------------
@@ -553,7 +530,13 @@ print(json.dumps(d, ensure_ascii=False))
PYEOF
}
resolve_herdr_session() {
# Despite the name (kept for caller compatibility — resume/stop/update_yaml_resumed
# all do `HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"`), this
# returns the isolated herdr *session* name to use for this MAM session row, not
# a workspace id. Real isolation is `--session <name>` (see `_MAM_SESSION` in the
# generated wrapper) — a workspace label match provides no actual isolation
# since agent/pane commands are server-global regardless of workspace.
resolve_herdr_workspace() {
local session_name="$1"
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
import sys, os, json
@@ -561,7 +544,7 @@ name = os.environ['SESSION_NAME']
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default')
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
sys.exit(0)
print(os.environ.get('HERDR_SERVER_NAME', 'default'))
"
@@ -1200,7 +1183,8 @@ provision_isolation() {
mkdir -p "$root"
case "$agent" in
claude)
ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded=".credentials.json"
if [ -e "$HOME/.claude.json" ]; then ln -sfn "$HOME/.claude.json" "$root/.claude.json"; seeded=".claude.json"; fi
ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded="${seeded:+$seeded,}.credentials.json"
if [ -e "$HOME/.claude/settings.json" ]; then ln -sfn "$HOME/.claude/settings.json" "$root/settings.json"; seeded="$seeded,settings.json"; fi
if [ -d "$HOME/.claude/plugins" ]; then ln -sfn "$HOME/.claude/plugins" "$root/plugins"; seeded="$seeded,plugins"; fi
;;
@@ -1588,7 +1572,7 @@ send_keys_safe() {
_sks_herdr send-keys -t "$sess" C-m
return 0
fi
sleep "${SKS_PASTE_SLEEP:-1.5}"
sleep 0.5
local pane_content was_popup=0
pane_content=$(_pane_capture "$sess")
if printf '%s\n' "$pane_content" | grep -Eq "Pasted text|paste again to expand"; then
@@ -1597,7 +1581,7 @@ send_keys_safe() {
# lines at the terminal width (and some TUIs add indentation on the
# continuation line too), which can split/reformat the marker across two
# visual lines and make a literal grep miss it even though the paste landed.
elif [ -n "$marker_norm" ] && ! printf '%s' "$pane_content" | tr -d '[:space:]' | grep -Fq "$marker_norm"; then
elif ! printf '%s' "$pane_content" | tr -d '[:space:]' | grep -Fq "$marker_norm"; then
echo "send_keys_safe: paste not visible ($sess)" >&2
return 3
fi
@@ -1612,7 +1596,7 @@ send_keys_safe() {
if printf '%s\n' "$cur_content" | grep -Eq "● |✽ |[A-Za-z]+ing…|[A-Za-z]+ing\.\.\.|esc to interrupt"; then
return 0
fi
if [ "$was_popup" = "0" ] && { [ -z "$marker_norm" ] || ! _pane_tail "$sess" 3 | tr -d '[:space:]' | grep -Fq "$marker_norm"; } && [ "$cur_content" != "$pre_submit" ]; then
if [ "$was_popup" = "0" ] && ! _pane_tail "$sess" 3 | tr -d '[:space:]' | grep -Fq "$marker_norm" && [ "$cur_content" != "$pre_submit" ]; then
return 0
elif [ "$was_popup" = "1" ] && \
! printf '%s\n' "$cur_content" | grep -Eq "Pasted text|paste again to expand" && \
@@ -310,7 +310,7 @@ entry = {
'role': role,
'herdr_session_created_at': os.environ['NOW_ISO'],
'herdr_session_epoch': int(epoch) if epoch.isdigit() else 0,
'herdr_session': server_name,
'herdr_server': server_name,
'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None,
'pane': {
'index': 0,
@@ -138,19 +138,6 @@ cmd_submit() {
- **Timeout**: $TIMEOUT s (Idle: $IDLE_TIMEOUT s)
- **Output Report Path**: .mam/jobs/$JOB_ID/$AGENT-reports/report-final.md
## 🔔 Execution Commands
On start run:
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event started
On progress (optional):
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event progress --detail '<short status>'
On success run:
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event completed --detail '<one-line summary>'
On failure run:
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event error --detail '<one-line reason>'
## 🔎 Task Description
$PROMPT
EOF
@@ -197,8 +184,7 @@ EOF
# an id from an earlier session is the #1 reason a delegated job sits idle and
# times out (see SKILL.md "Wrong job_id propagated to the agent"). We make the
# freshness explicit in the instruction header.
# Keep this short and ASCII-only to prevent paste/wrap rendering issues in CLI REPLs.
local instructions="Job $JOB_ID: Read .mam/jobs/$JOB_ID/brief.md and complete the task."
local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths are documented in the task brief file at: .mam/jobs/$JOB_ID/brief.md. Please READ and follow .mam/jobs/$JOB_ID/brief.md to complete your work. Commands: start='$pub --event started', success='$pub --event completed --detail <summary>', error='$pub --event error --detail <reason>'."
run_agent "$JOB_ID" "$instructions"
@@ -454,7 +440,7 @@ run_agent() {
# the caller having exported HERDR_SERVER_NAME by hand. This is what lets
# delegation reach an agent living in an isolated herdr session (e.g. one
# created with --herdr-server) instead of silently looking in "default".
export HERDR_SERVER_NAME="$(resolve_herdr_session "$sess")"
export HERDR_SERVER_NAME="$(resolve_herdr_workspace "$sess")"
if ! herdr has-session -t "$sess" 2>/dev/null; then
echo "ERROR: 에이전트 세션 '$sess'이 존재하지 않습니다. 작업을 위임하기 전에 먼저 에이전트 세션을 기동해 주세요." >&2
@@ -1,5 +1,21 @@
---
name: multi-agent-mux-loop
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
version: 1.0.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, herdr]
metadata:
hermes:
tags: [agent, herdr, multi-agent, loop, planning, review, orchestrator]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-delegate-job]
prereq_skills: [multi-agent-mux-create]
---
# Multi-Agent Mux Loop — Autonomous Orchestration Loop
> **Companion skills**: `multi-agent-mux-create` (start), `multi-agent-mux-resume` (re-attach), `multi-agent-mux-delegate-job` (delegate).
> **Safety Guard**: `--max-loop` and `--plan-talk` restrict API cost runaways.
> **Single source of truth**: `./.mam/agent-sessions.yaml`.
@@ -102,6 +102,10 @@ if [ "$ALL_REVIEWERS" = true ] && [ -n "$REVIEWER_LIST" ]; then
log_warn "--all-reviewer takes precedence; ignoring --reviewer list ('$REVIEWER_LIST')."
fi
if [ "$PLAN_TALK_TURNS" -gt 0 ] && [ "$PLAN_MODE" = false ]; then
log_warn "--plan-talk was specified but --plan mode is not enabled. Discussion turns will be ignored."
fi
# Verdict must occupy the report's last non-blank line — a standalone token
# quoted mid-report (e.g. as a formatting example) never matches (P0-2).
has_verdict() {
@@ -159,7 +163,7 @@ import os, json
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
target_agent = os.environ.get('TARGET_AGENT')
reviewers = [s.get('name') for s in d.get('herdr_sessions', [])
if 'reviewer' in s.get('role', '').lower() and s.get('name') != target_agent]
if 'reviewer' in (s.get('role') or '').lower() and s.get('status') == 'running' and s.get('name') != target_agent]
print(','.join(reviewers))
"
}
@@ -199,7 +203,7 @@ import os, json
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
planner = ''
for s in d.get('herdr_sessions', []):
if 'planner' in s.get('role', '').lower():
if 'planner' in (s.get('role') or '').lower() and s.get('status') == 'running':
planner = s.get('name')
break
print(planner)
@@ -220,8 +224,37 @@ log_info "Initializing multi-agent-mux-loop controller..."
log_info "Target Agent: $TARGET_AGENT"
log_info "Task Goal: $TASK"
# Validate that Target Agent is registered and running
TARGET_STATUS=$(MAM_STATE_JSON="$(load_state_json)" TARGET="$TARGET_AGENT" python3 -c "
import os, json
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
target = os.environ.get('TARGET')
status = ''
for s in d.get('herdr_sessions', []):
if s.get('name') == target:
status = s.get('status')
break
print(status)
")
if [ -z "$TARGET_STATUS" ]; then
log_error "Target agent session '$TARGET_AGENT' is not registered in the session registry."
exit 1
elif [ "$TARGET_STATUS" != "running" ]; then
log_error "Target agent session '$TARGET_AGENT' is not running (current status: '$TARGET_STATUS'). Please start it first."
exit 1
fi
PLANNER_SESSION=$(resolve_planner_session)
log_info "Resolved Planner session: $PLANNER_SESSION"
if [ "$PLAN_MODE" = true ]; then
if [ -z "$PLANNER_SESSION" ]; then
log_error "Planner mode enabled (--plan) but no running session with a 'planner' role was found."
exit 1
fi
fi
CURRENT_PLAN=""
CREATED_JOBS=()
@@ -329,7 +362,9 @@ else
log_info "=== Phase 1: Self-Planning Mode (Direct Execution) ==="
EXISTING_PLAN_FILE=""
if [ -n "$PLANNER_SESSION" ]; then
EXISTING_PLAN_FILE=".agents/reports/$PLANNER_SESSION/report-final.md"
# Promoted plans are saved as plan-<job-id>.md by the Phase 3 promotion
# step below, not report-final.md; pick the most recently promoted one.
EXISTING_PLAN_FILE=$(ls -t ".agents/reports/$PLANNER_SESSION"/plan-*.md 2>/dev/null | head -n 1 || true)
fi
if [ -n "$EXISTING_PLAN_FILE" ] && [ -f "$EXISTING_PLAN_FILE" ]; then
log_info "Found existing promoted plan at '$EXISTING_PLAN_FILE'. Loading plan..."
@@ -385,8 +420,14 @@ if [ "$ALL_REVIEWERS" = true ]; then
if [ -n "$RESOLVED_REVS" ]; then
IFS=' ,' read -r -a REVIEWERS <<< "$RESOLVED_REVS"
fi
if [ "${#REVIEWERS[@]}" -eq 0 ]; then
log_error "--all-reviewer was specified, but no running reviewer sessions were found."
exit 1
fi
elif [ -n "$REVIEWER_LIST" ]; then
IFS=' ,' read -r -a REVIEWERS <<< "$REVIEWER_LIST"
# Strip whitespaces first to prevent empty array elements from trailing spaces
CLEAN_REVS=$(echo "$REVIEWER_LIST" | tr -d '[:space:]')
IFS=',' read -r -a REVIEWERS <<< "$CLEAN_REVS"
fi
loop_count=1
@@ -121,7 +121,7 @@ _changed = False
for s in d.get('herdr_sessions', []):
if s.get('delegate_job_id') == _jid and s.get('status') == 'running':
_name = s.get('name')
_srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
_srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
if _event == 'completed':
s['delegate_job_id'] = None
print('MQTT Monitor: job completed on ' + str(_name) + ' — session kept alive', flush=True)
@@ -131,8 +131,7 @@ for s in d.get('herdr_sessions', []):
s['terminated_at'] = _now.strftime('%Y-%m-%dT%H:%M:%SZ')
s['terminated_at_epoch'] = int(_now.timestamp())
s['termination_mode'] = 'auto-detected (MQTT ' + _event + ')'
_shim = os.path.join(os.environ.get('WORKSPACE_ROOT', os.getcwd()), '.mam/shim/herdr')
_cmd = [_shim] + (['-L', _srv] if _srv != 'default' else []) + ['kill-session', '-t', _name]
_cmd = ['herdr'] + (['-L', _srv] if _srv != 'default' else []) + ['kill-session', '-t', _name]
subprocess.run(_cmd, capture_output=True)
print('MQTT Monitor: terminated + killed ' + str(_name) + ' on ' + str(_srv) + ' due to MQTT ' + _event, flush=True)
_changed = True
@@ -310,8 +309,6 @@ import yaml
yaml_path = os.environ['YAML_PATH']
home = os.environ['HOME_DIR']
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
workspace_root = os.environ.get('WORKSPACE_ROOT', os.getcwd())
shim_herdr = os.path.join(workspace_root, '.mam/shim/herdr')
now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
@@ -345,12 +342,12 @@ unique_servers = {'default'}
if 'HERDR_SERVER_NAME' in os.environ:
unique_servers.add(os.environ['HERDR_SERVER_NAME'])
for s in d.get('herdr_sessions', []):
srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
unique_servers.add(srv)
try:
for srv in sorted(unique_servers):
cmd = [shim_herdr]
cmd = ['herdr']
if srv != 'default':
cmd += ['-L', srv]
cmd += ['ls', '-F', '#{session_name}|#{session_created}']
@@ -372,7 +369,7 @@ except Exception:
def pane_meta(session, srv):
try:
cmd = [shim_herdr]
cmd = ['herdr']
if srv != 'default':
cmd += ['-L', srv]
cmd += ['list-panes', '-t', session, '-F',
@@ -399,7 +396,7 @@ if herdr_confirmed:
# (없으면 herdr-dead stopped 세션을 'terminated' 로 덮어써 resumable 플래그가 소실됨)
if s.get('status') in ('terminated', 'archived', 'stopped'):
continue
srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
if (name, srv) not in alive_set:
s['status'] = 'terminated'
s['terminated_at'] = now_iso
@@ -37,7 +37,7 @@ if [ -z "$UUID" ]; then
exit 1
fi
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
# 2. If herdr is alive, print warning or attach.
@@ -33,7 +33,7 @@ done
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
# --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시)
@@ -93,7 +93,7 @@ def get_job_status(s):
sessions_detail = []
for s in d.get('herdr_sessions', []):
name = s.get('name', '?')
server = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
server = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
jid, jstatus = get_job_status(s)
pane = s.get('pane') or {}
sessions_detail.append({
@@ -197,7 +197,7 @@ if not sessions:
print("(no sessions registered)")
for s in sessions:
name = s.get('name', '?')
server = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
server = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
status = s.get('status', '?')
herdr = 'alive' if f"{name}|{server}" in alive else 'dead'
cmd = (s.get('pane') or {}).get('cmd', '?')
@@ -82,7 +82,7 @@ if [ "$PURGE" = "1" ]; then
trap 'rm -f "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"' EXIT
fi
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
# --agent 미지정 시 이름 suffix 로 fallback (P1-F)