Compare commits
6
Commits
d6b7b97892
...
8dcb2b2d9e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dcb2b2d9e | ||
|
|
51dcf56c80 | ||
|
|
793a221587 | ||
|
|
addcabf7d3 | ||
|
|
aac2960586 | ||
|
|
01c8e60b2f |
+12
-14
@@ -48,7 +48,7 @@ LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
|
|||||||
_HERDR_SHIM_DIR_PATTERN="${_HERDR_SHIM_DIR_PATTERN:-/multi-agent-herdr-shim/}"
|
_HERDR_SHIM_DIR_PATTERN="${_HERDR_SHIM_DIR_PATTERN:-/multi-agent-herdr-shim/}"
|
||||||
_HERDR_SKILLS_BIN_PATTERN="${_HERDR_SKILLS_BIN_PATTERN:-/.agents/skills/.bin}"
|
_HERDR_SKILLS_BIN_PATTERN="${_HERDR_SKILLS_BIN_PATTERN:-/.agents/skills/.bin}"
|
||||||
|
|
||||||
HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}"
|
HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-default}"
|
||||||
|
|
||||||
_resolve_real_herdr_path() {
|
_resolve_real_herdr_path() {
|
||||||
_REAL_HERDR_PATH="herdr"
|
_REAL_HERDR_PATH="herdr"
|
||||||
@@ -90,23 +90,17 @@ _resolve_real_herdr() {
|
|||||||
}
|
}
|
||||||
REAL_HERDR=$(_resolve_real_herdr)
|
REAL_HERDR=$(_resolve_real_herdr)
|
||||||
|
|
||||||
# Support parsing -L <server> before the subcommand
|
# Support parsing -L <session> before the subcommand
|
||||||
while [ "${1:-}" = "-L" ]; do
|
while [ "${1:-}" = "-L" ]; do
|
||||||
if [ $# -lt 2 ]; then
|
if [ $# -lt 2 ]; then
|
||||||
echo "herdr shim: -L requires an argument" >&2
|
echo "herdr shim: -L requires an argument" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
export HERDR_SERVER_NAME="$2"
|
export HERDR_SESSION_NAME="$2"
|
||||||
shift 2
|
shift 2
|
||||||
done
|
done
|
||||||
|
|
||||||
# Herdr's real isolation boundary is `--session <name>` (a whole separate
|
_MAM_SESSION="${HERDR_SESSION_NAME:-default}"
|
||||||
# server + socket, like tmux `-L`) — NOT `workspace create --label`, which is
|
|
||||||
# just a named subdivision inside ONE server and provides no actual isolation
|
|
||||||
# (agent/pane commands are server-global regardless of workspace). When
|
|
||||||
# HERDR_SERVER_NAME names a non-default session, make sure its headless server
|
|
||||||
# is actually running, then scope every real herdr call to it via `--session`.
|
|
||||||
_MAM_SESSION="${HERDR_SERVER_NAME:-default}"
|
|
||||||
if [ "$_MAM_SESSION" = "default" ]; then
|
if [ "$_MAM_SESSION" = "default" ]; then
|
||||||
_MAM_SESSION=""
|
_MAM_SESSION=""
|
||||||
else
|
else
|
||||||
@@ -558,7 +552,7 @@ PYEOF
|
|||||||
# a workspace id. Real isolation is `--session <name>` (see `_MAM_SESSION` in the
|
# a workspace id. Real isolation is `--session <name>` (see `_MAM_SESSION` in the
|
||||||
# generated wrapper) — a workspace label match provides no actual isolation
|
# generated wrapper) — a workspace label match provides no actual isolation
|
||||||
# since agent/pane commands are server-global regardless of workspace.
|
# since agent/pane commands are server-global regardless of workspace.
|
||||||
resolve_herdr_workspace() {
|
resolve_herdr_session() {
|
||||||
local session_name="$1"
|
local session_name="$1"
|
||||||
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
|
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
|
||||||
import sys, os, json
|
import sys, os, json
|
||||||
@@ -566,16 +560,20 @@ name = os.environ['SESSION_NAME']
|
|||||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||||
for s in d.get('herdr_sessions', []):
|
for s in d.get('herdr_sessions', []):
|
||||||
if s.get('name') == name:
|
if s.get('name') == name:
|
||||||
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
|
print(s.get('herdr_session') or 'default')
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
fallback = os.environ.get('HERDR_SERVER_NAME', '')
|
fallback = os.environ.get('HERDR_SESSION_NAME', '')
|
||||||
if not fallback or fallback == 'default':
|
if not fallback or fallback == 'default':
|
||||||
pwd = os.path.abspath(os.getcwd())
|
pwd = os.path.abspath(os.getcwd())
|
||||||
fallback = os.path.basename(pwd)
|
fallback = 'mam-' + os.path.basename(pwd).lower().replace('_', '-')
|
||||||
print(fallback or 'default')
|
print(fallback or 'default')
|
||||||
"
|
"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve_herdr_workspace() {
|
||||||
|
resolve_herdr_session "$@"
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# derive_session_name <workspace> <agent>
|
# derive_session_name <workspace> <agent>
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--session) SESSION_NAME="$2"; shift 2 ;;
|
--session) SESSION_NAME="$2"; shift 2 ;;
|
||||||
--wrapper) USE_WRAPPER=1; shift ;;
|
--wrapper) USE_WRAPPER=1; shift ;;
|
||||||
--dry-run) DRY_RUN=1; shift ;;
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||||
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
|
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
|
||||||
--onboard) ONBOARD=1; shift ;;
|
--onboard) ONBOARD=1; shift ;;
|
||||||
--no-onboard) ONBOARD=0; shift ;;
|
--no-onboard) ONBOARD=0; shift ;;
|
||||||
@@ -128,8 +128,8 @@ LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
|
|||||||
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
|
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
|
||||||
|
|
||||||
ws_slug="$(derive_session_name "$WORKSPACE" "$AGENT" | sed 's/-creator-.*//')"
|
ws_slug="$(derive_session_name "$WORKSPACE" "$AGENT" | sed 's/-creator-.*//')"
|
||||||
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
|
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
|
||||||
export HERDR_SERVER_NAME="mam-$ws_slug"
|
export HERDR_SESSION_NAME="mam-$ws_slug"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
|
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
|
||||||
@@ -256,14 +256,14 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
|||||||
SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
||||||
HERDR_EPOCH="$HERDR_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
|
HERDR_EPOCH="$HERDR_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
|
||||||
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
|
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
|
||||||
HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}" \
|
HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-default}" \
|
||||||
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
|
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
|
||||||
name = os.environ['SESSION_NAME']
|
name = os.environ['SESSION_NAME']
|
||||||
agent = os.environ['AGENT']
|
agent = os.environ['AGENT']
|
||||||
role = os.environ['ROLE']
|
role = os.environ['ROLE']
|
||||||
pid = os.environ.get('PANE_PID', '')
|
pid = os.environ.get('PANE_PID', '')
|
||||||
epoch = os.environ.get('HERDR_EPOCH', '')
|
epoch = os.environ.get('HERDR_EPOCH', '')
|
||||||
server_name = os.environ.get('HERDR_SERVER_NAME', 'default')
|
server_name = os.environ.get('HERDR_SESSION_NAME', 'default')
|
||||||
server_opt = f"-L {server_name} " if server_name and server_name != 'default' else ""
|
server_opt = f"-L {server_name} " if server_name and server_name != 'default' else ""
|
||||||
|
|
||||||
sessions = d.setdefault('herdr_sessions', [])
|
sessions = d.setdefault('herdr_sessions', [])
|
||||||
@@ -282,7 +282,7 @@ entry = {
|
|||||||
'role': role,
|
'role': role,
|
||||||
'herdr_session_created_at': os.environ['NOW_ISO'],
|
'herdr_session_created_at': os.environ['NOW_ISO'],
|
||||||
'herdr_session_epoch': int(epoch) if epoch.isdigit() else 0,
|
'herdr_session_epoch': int(epoch) if epoch.isdigit() else 0,
|
||||||
'herdr_server': server_name,
|
'herdr_session': server_name,
|
||||||
'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None,
|
'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None,
|
||||||
'pane': {
|
'pane': {
|
||||||
'index': 0,
|
'index': 0,
|
||||||
@@ -292,13 +292,8 @@ entry = {
|
|||||||
'cwd': os.environ['PANE_CWD'],
|
'cwd': os.environ['PANE_CWD'],
|
||||||
},
|
},
|
||||||
'start_command': os.environ['START_CMD'],
|
'start_command': os.environ['START_CMD'],
|
||||||
# NOTE: `herdr session attach/stop/delete` operate on whole herdr
|
'attach_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
||||||
# *sessions* (server instances, e.g. "default") — NOT on an individual
|
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
|
||||||
# agent by its MAM name. Use the lib.sh tmux-compat shim commands
|
|
||||||
# instead (`source .agents/skills/lib.sh` first), scoped via the same
|
|
||||||
# env-var-driven isolation as start_command above.
|
|
||||||
'attach_command': f'HERDR_SERVER_NAME={server_name} herdr agent attach {name}',
|
|
||||||
'kill_command': f'HERDR_SERVER_NAME={server_name} herdr kill-session -t {name}',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -474,6 +474,11 @@ if herdr_confirmed:
|
|||||||
pm = pane_meta(name, srv)
|
pm = pane_meta(name, srv)
|
||||||
if not pm:
|
if not pm:
|
||||||
continue
|
continue
|
||||||
|
# A-1 게이트: pane cwd가 현재 workspace_root 하위가 아니면 타 워크스페이스 세션으로 판단하여 오등록 방지
|
||||||
|
pane_cwd_abs = os.path.realpath(pm['cwd']) if pm.get('cwd') else ''
|
||||||
|
ws_root_abs = os.path.realpath(workspace_root)
|
||||||
|
if not pane_cwd_abs or not (pane_cwd_abs == ws_root_abs or pane_cwd_abs.startswith(ws_root_abs + os.sep)):
|
||||||
|
continue
|
||||||
if agent == 'claude':
|
if agent == 'claude':
|
||||||
cmd_full = 'claude --dangerously-skip-permissions'
|
cmd_full = 'claude --dangerously-skip-permissions'
|
||||||
elif agent == 'agy':
|
elif agent == 'agy':
|
||||||
@@ -488,11 +493,10 @@ if herdr_confirmed:
|
|||||||
'status': 'running',
|
'status': 'running',
|
||||||
'herdr_session_created_at': datetime.fromtimestamp(t['created'], tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
'herdr_session_created_at': datetime.fromtimestamp(t['created'], tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||||
'herdr_session_epoch': t['created'],
|
'herdr_session_epoch': t['created'],
|
||||||
'herdr_server': srv,
|
'herdr_session': srv,
|
||||||
'pane': {'index': 0, 'pid': pm['pid'], 'cmd': agent, 'cmd_full': cmd_full, 'cwd': pm['cwd']},
|
'pane': {'index': 0, 'pid': pm['pid'], 'cmd': agent, 'cmd_full': cmd_full, 'cwd': pm['cwd']},
|
||||||
# P2: cwd 인용
|
'start_command': f'HERDR_SESSION_NAME={srv} herdr new-session -d -s "{name}" -x 140 -y 40 -c "{pm["cwd"]}" "{cmd_full}"',
|
||||||
'start_command': f'herdr {server_opt}new-session -d -s "{name}" -x 140 -y 40 -c "{pm["cwd"]}" "{cmd_full}"',
|
'attach_command': f'HERDR_SESSION_NAME={srv} herdr agent attach {name}',
|
||||||
'attach_command': f'herdr {server_opt}agent attach {name}',
|
|
||||||
'kill_command': f'herdr {server_opt}kill-session -t {name}',
|
'kill_command': f'herdr {server_opt}kill-session -t {name}',
|
||||||
'last_visible_status': 'running',
|
'last_visible_status': 'running',
|
||||||
'last_visible_note': 'auto-registered by monitor',
|
'last_visible_note': 'auto-registered by monitor',
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ if [ -z "$UUID" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
|
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME")"
|
||||||
export HERDR_SERVER_NAME
|
export HERDR_SESSION_NAME
|
||||||
|
|
||||||
# 2. If herdr is alive, print warning or attach.
|
# 2. If herdr is alive, print warning or attach.
|
||||||
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ done
|
|||||||
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
|
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
|
||||||
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
||||||
|
|
||||||
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
|
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME")"
|
||||||
export HERDR_SERVER_NAME
|
export HERDR_SESSION_NAME
|
||||||
|
|
||||||
# --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시)
|
# --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시)
|
||||||
if [ -z "$AGENT" ]; then
|
if [ -z "$AGENT" ]; then
|
||||||
@@ -101,19 +101,21 @@ for s in d.get('herdr_sessions', []):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if target is None:
|
if target is None:
|
||||||
server_name = os.environ.get('HERDR_SERVER_NAME', 'multi-agent-mux')
|
pwd = os.path.abspath(ws_root)
|
||||||
|
default_server = 'mam-' + os.path.basename(pwd).lower().replace('_', '-')
|
||||||
|
server_name = os.environ.get('HERDR_SESSION_NAME', default_server)
|
||||||
target = {
|
target = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'status': 'running',
|
'status': 'running',
|
||||||
'role': role,
|
'role': role,
|
||||||
'herdr_session_created_at': now,
|
'herdr_session_created_at': now,
|
||||||
'herdr_session_epoch': epoch,
|
'herdr_session_epoch': epoch,
|
||||||
'herdr_server': server_name,
|
'herdr_session': server_name,
|
||||||
'delegate_job_id': None,
|
'delegate_job_id': None,
|
||||||
'pane': {'index': 0, 'pid': int(pane_pid) if pane_pid.isdigit() else 0, 'cmd': agent, 'cwd': ws_root},
|
'pane': {'index': 0, 'pid': int(pane_pid) if pane_pid.isdigit() else 0, 'cmd': agent, 'cwd': ws_root},
|
||||||
'start_command': f'HERDR_SERVER_NAME={server_name} herdr agent attach {name}',
|
'start_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
||||||
'attach_command': f'HERDR_SERVER_NAME={server_name} herdr agent attach {name}',
|
'attach_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
||||||
'kill_command': f'HERDR_SERVER_NAME={server_name} herdr kill-session -t {name}',
|
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
|
||||||
}
|
}
|
||||||
d.setdefault('herdr_sessions', []).append(target)
|
d.setdefault('herdr_sessions', []).append(target)
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@
|
|||||||
#default: $HOME/.local/bin
|
#default: $HOME/.local/bin
|
||||||
# LOCAL_BIN=$HOME/.local/bin
|
# LOCAL_BIN=$HOME/.local/bin
|
||||||
|
|
||||||
|
# Isolated Herdr session socket name (`herdr --session <name>`).
|
||||||
|
#default: mam-<workspace-slug>
|
||||||
|
# HERDR_SESSION_NAME=mam-multi-agent-mux
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# delegate-job / MQTT broker
|
# delegate-job / MQTT broker
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
# 📊 Multi-Agent Mux 코드베이스 미해결 결함 보고서
|
|
||||||
|
|
||||||
- **작성일**: 2026-08-05 (최종 갱신: 수정 완료 항목 삭제 반영)
|
|
||||||
- **검증 방식**: `multi-agent-mux-loop --all-reviewer` 오케스트레이션 및 Planner(`claude`), Creator(`agy`), Reviewer(`cline`) 만장일치 교차 리뷰
|
|
||||||
- **대상**: `.agents/skills/**` 전체 (HERDR_SERVER_NAME 바인딩 및 tmux 시프트 계층 제외 범위)
|
|
||||||
- **남은 미해결 결함 건수**: **16건** (아키텍처 3건, 엣지케이스 8건, 레거시 잔재 5건)
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> 본 보고서에서 **A-4 (stop 시 completed 오발행 ➔ cancelled 교정)**, **B-2 (resolve_herdr_server 불일치 교정)**, **C-5 (TMUX_SERVER_NAME 노브 삭제)** 등 최근 수정 완료된 항목들은 제외/삭제 처리되었습니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 개요 및 검증 수단
|
|
||||||
|
|
||||||
본 보고서는 `HERDR_SERVER_NAME` 환경변수 바인딩 누락 및 `tmux` 호환 래퍼 shim 의존성을 넘어, `multi-agent-mux` 코드베이스 전반의 **아키텍처 구조적 안정성, MQTT 백플레인 보안, 런타임 엣지케이스 및 레거시 잔재** 중 아직 수정되지 않고 남아있는 문제점들을 정리한 잔여 결함 목록입니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 🔴 아키텍처 결함 (Architecture Flaws — 3건)
|
|
||||||
|
|
||||||
### **A-1 (herdr 서버명 전역 고정 및 타 워크스페이스 세션 오등록)**
|
|
||||||
- **현상**: `HERDR_SERVER_NAME`이 워크스페이스별 파생값이 아닌 `multi-agent-mux`라는 머신 전역 상수로 하드코딩되어 있습니다.
|
|
||||||
- **파급 효과**: 한 머신에서 다수의 MAM 워크스페이스 사용 시, 백그라운드 모니터(`reconcile.sh` drift-B)가 다른 워크스페이스의 세션을 내 `.mam/agent-sessions.yaml` 레지스트리에 무단 등록하여 세션 오염을 발생시킵니다.
|
|
||||||
|
|
||||||
### **A-2 (공개 브로커 + HMAC 인증 Off + 와일드카드 전파)**
|
|
||||||
- **현상**: `mqtt_common.py`의 기본 브로커가 공개 서버(`broker.hivemq.com`), HMAC 무조건 True 반환으로 설정되어 있습니다.
|
|
||||||
- **파급 효과**: 외부에서 유입되는 malicious `error` 이벤트 수신 시 `reconcile.sh`가 라이브 에이전트 pane을 `kill-session`으로 강제 파괴하는 치명적 보안/안정성 위험이 존재합니다.
|
|
||||||
|
|
||||||
### **A-3 (시프트 버퍼 단일 파일 공유 및 동시 주입 오염)**
|
|
||||||
- **현상**: `send_keys_safe` 시프트의 `set/paste/delete-buffer`가 `-b` 세션 버퍼 이름을 무시하고 단일 `.mam/shim/tmp_buffer` 파일 하나만을 공유합니다.
|
|
||||||
- **파급 효과**: 다중 에이전트 동시 주입 시 대화 텍스트 교차 오염 및 무음 유실(t5 silent loss)이 발생합니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 🟠 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 8건)
|
|
||||||
|
|
||||||
### **B-1 (`find_workspace_uuid` tier-3 `NameError`로 영구 사망)**
|
|
||||||
- `lib.sh` 내 agy 대화 복원용 tier-3 경로가 미정의 변수(`db_path`, `yaml_path`) 및 `yaml` import 누락으로 항상 `NameError` 예외를 내고 삼켜져 agy 대화 복원이 거부됩니다.
|
|
||||||
|
|
||||||
### **B-3 (`command -v herdr` 프리플라이트 무력화)**
|
|
||||||
- `create_session.sh`의 프리플라이트 검사 시 `command -v herdr`가 `lib.sh`에 정의된 bash 함수(`herdr()`)를 호명하여 실제 시스템 `herdr` 바이너리가 없어도 프리플라이트를 무조건 통과해버립니다.
|
|
||||||
|
|
||||||
### **B-4 (시프트 `ls`의 `created=0` 하드코딩으로 재개 가드 무력화)**
|
|
||||||
- `herdr ls` 서브커맨드 래퍼가 세션 생성시각을 상수 `0`으로 리턴하여 `reconcile.sh` drift-B 등록 시 epoch 0이 되어 오래된 대화 jsonl 배제 가드가 붕괴됩니다.
|
|
||||||
|
|
||||||
### **B-5 (`df --output` GNU 전용 플래그 사용으로 macOS NFS 감지 실패)**
|
|
||||||
- macOS/BSD 환경에서 `df --output` 구문 오류로 NFS 감지가 실패하고 "NFS 아님"으로 오판되어 SQLite WAL 포맷을 강행합니다.
|
|
||||||
|
|
||||||
### **B-6 (스킬 트리에 임시 파일 복사 및 유출)**
|
|
||||||
- `run_loop.sh::delegate_job_safe`가 래퍼 스크립트를 `.agents/skills/...` 트리 내부에 `.tmp`로 복사하여 버전 관리 트리를 오염시키고 rsync 배포 시 외부로 유출됩니다.
|
|
||||||
|
|
||||||
### **B-7 (`run_loop.sh` 상대경로 cwd 의존 및 미추적 파일 누락)**
|
|
||||||
- 저장소 루트 밖에서 `run_loop.sh` 구동 시 `wait_for_job`이 3900초 무음 타임아웃을 발생시키며, `git diff`가 Creator가 새로 추가한 미추적 신규 파일을 리뷰어에게 누락합니다.
|
|
||||||
|
|
||||||
### **B-8 (`send_keys_safe` agy 경로 검증 이탈)**
|
|
||||||
- agy 세션 주입 시 주입 실패 여부를 검증하지 않고 무조건 `return 0`을 남겨 실패 시에도 성공으로 보고됩니다.
|
|
||||||
|
|
||||||
### **B-9 (`LOGS_DIR` import 시점 cwd 고정)**
|
|
||||||
- `mqtt_common.py` 모듈 로드 시점의 cwd로 감사 로그 경로가 1회 고정됩니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 🟡 레거시 잔재 및 죽은 코드 (Legacy Remnants — 5건)
|
|
||||||
|
|
||||||
### **C-1 (Kanban 문서 29회 언급 vs 실제 구현 0건)**
|
|
||||||
- SKILL.md 파일들에 Kanban 지원 및 상태 파일 서술이 29회 언급되어 있으나 스크립트 구현은 0건입니다.
|
|
||||||
|
|
||||||
### **C-2 (미사용 `.cache/` 상태 디렉터리 생성)**
|
|
||||||
- `reconcile.sh`가 `.cache/multi-agent-mux-monitor` 디렉터리를 `mkdir`만 하고 아무것도 읽거나 쓰지 않습니다.
|
|
||||||
|
|
||||||
### **C-3 (격리 스텁 4종 및 `stop_session.sh` 미사용 isolation 코드 잔존)**
|
|
||||||
- `provision_isolation` 등 4개 스텁 함수와 `stop_session.sh` 내 `.mam/agent_homes` 가드 코드가 호출자 0건인 채 잔존합니다.
|
|
||||||
|
|
||||||
### **C-4 (참조 0회 미사용 심볼 7종)**
|
|
||||||
- `_HERDR_SHIM_DIR_PATTERN`, `_REAL_HERDR_PATH`, `TERMINAL_STATUSES`, `ISOLATE`, `local_herdr` 등 7개 미사용 심볼이 잔존합니다.
|
|
||||||
|
|
||||||
### **C-6 (`stop_session.sh` 도움말 문서 구버전 표기)**
|
|
||||||
- 스크립트 도움말에는 `--mode soft|hard` 등이 서술되어 있으나 실제 옵션 파서는 `exit 2`로 거부합니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 결론 및 향후 보완 방향
|
|
||||||
|
|
||||||
향후 코드베이스 개편 시 본 문서에 서술된 잔여 16가지 결함 항목을 순차적으로 조치하여 Multi-Agent Mux 프레임워크의 완전한 안정성과 격리성을 보장해야 합니다.
|
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
# 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`)
|
||||||
|
|
||||||
|
- **최종 갱신일**: 2026-08-05 (A-1 및 A-5 네이티브 전환 구현 완료 반영)
|
||||||
|
- **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md`
|
||||||
|
- **총 추적 미해결 과제**: **18건** (아키텍처 2건, 엣지케이스 8건, 오케스트레이션 3건, 레거시 잔재 5건)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 최근 A-1(워크스페이스별 Herdr session 소켓 파생 및 drift-B cwd 오등록 게이트 구축) 및 A-5(네이티브 명칭 `HERDR_SESSION_NAME`, `--herdr-session`, `herdr_session:` 단일화) 구현이 완벽히 완료되어 본 백로그에서 삭제 및 정돈되었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 개요
|
||||||
|
|
||||||
|
본 문서는 Multi-Agent Mux (MAM) 프레임워크의 **코드베이스 아키텍처 결함, 런타임 엣지케이스, 레거시 잔재** 및 **`/multi-agent-mux-loop` 오케스트레이션 최적화 과제**를 단일 백로그로 통합 추적하기 위한 종합 관리 문서입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 🔴 아키텍처 결함 (Architecture Flaws — 2건)
|
||||||
|
|
||||||
|
### **A-2: 공개 브로커 + HMAC 인증 Off + 와일드카드 전파**
|
||||||
|
- **현상**: `mqtt_common.py`의 기본 브로커가 공개 서버(`broker.hivemq.com`), HMAC 무조건 True 반환으로 설정되어 있습니다.
|
||||||
|
- **파급 효과**: 외부에서 유입되는 malicious `error` 이벤트 수신 시 `reconcile.sh`가 라이브 에이전트 pane을 `kill-session`으로 강제 파괴하는 치명적 보안/안정성 위험이 존재합니다.
|
||||||
|
|
||||||
|
### **A-3: 시프트 버퍼 단일 파일 공유 및 동시 주입 오염**
|
||||||
|
- **현상**: `send_keys_safe` 시프트의 `set/paste/delete-buffer`가 `-b` 세션 버퍼 이름을 무시하고 단일 `.mam/shim/tmp_buffer` 파일 하나만을 공유합니다.
|
||||||
|
- **파급 효과**: 다중 에이전트 동시 주입 시 대화 텍스트 교차 오염 및 무음 유실(t5 silent loss)이 발생합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 🟠 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 8건)
|
||||||
|
|
||||||
|
### **B-1: `find_workspace_uuid` tier-3 `NameError`로 영구 사망**
|
||||||
|
- `lib.sh` 내 agy 대화 복원용 tier-3 경로가 미정의 변수(`db_path`, `yaml_path`) 및 `yaml` import 누락으로 항상 `NameError` 예외를 내고 삼켜져 agy 대화 복원이 거부됩니다.
|
||||||
|
|
||||||
|
### **B-3: `command -v herdr` 프리플라이트 무력화**
|
||||||
|
- `create_session.sh`의 프리플라이트 검사 시 `command -v herdr`가 `lib.sh`에 정의된 bash 함수(`herdr()`)를 호명하여 실제 시스템 `herdr` 바이너리가 없어도 프리플라이트를 무조건 통과해버립니다.
|
||||||
|
|
||||||
|
### **B-4: 시프트 `ls`의 `created=0` 하드코딩으로 재개 가드 무력화**
|
||||||
|
- `herdr ls` 서브커맨드 래퍼가 세션 생성시각을 상수 `0`으로 리턴하여 `reconcile.sh` drift-B 등록 시 epoch 0이 되어 오래된 대화 jsonl 배제 가드가 붕괴됩니다.
|
||||||
|
|
||||||
|
### **B-5: `df --output` GNU 전용 플래그 사용으로 macOS NFS 감지 실패**
|
||||||
|
- macOS/BSD 환경에서 `df --output` 구문 오류로 NFS 감지가 실패하고 "NFS 아님"으로 오판되어 SQLite WAL 포맷을 강행합니다.
|
||||||
|
|
||||||
|
### **B-6: 스킬 트리에 임시 파일 복사 및 유출**
|
||||||
|
- `run_loop.sh::delegate_job_safe`가 래퍼 스크립트를 `.agents/skills/...` 트리 내부에 `.tmp`로 복사하여 버전 관리 트리를 오염시키고 rsync 배포 시 외부로 유출됩니다.
|
||||||
|
|
||||||
|
### **B-7: `run_loop.sh` 상대경로 cwd 의존 및 미추적 파일 누락**
|
||||||
|
- 저장소 루트 밖에서 `run_loop.sh` 구동 시 `wait_for_job`이 3900초 무음 타임아웃을 발생시키며, `git diff`가 Creator가 새로 추가한 미추적 신규 파일을 리뷰어에게 누락합니다.
|
||||||
|
|
||||||
|
### **B-8: `send_keys_safe` agy 경로 검증 이탈**
|
||||||
|
- agy 세션 주입 시 주입 실패 여부를 검증하지 않고 무조건 `return 0`을 남겨 실패 시에도 성공으로 보고됩니다.
|
||||||
|
|
||||||
|
### **B-9: `LOGS_DIR` import 시점 cwd 고정**
|
||||||
|
- `mqtt_common.py` 모듈 로드 시점의 cwd로 감사 로그 경로가 1회 고정됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 🟡 오케스트레이션 최적화 과제 (Orchestration Optimizations — 3건)
|
||||||
|
|
||||||
|
### **O-1 (구 ISSUE-6): 타당하지 않은 리뷰 피드백 거부/반론 프로토콜 미지원**
|
||||||
|
- **현상**: `MULTI_AGENT_RULES.md` 1장 규약에는 "개발 팀장이 리뷰어의 타당하지 않은 피드백을 거부하고 명확한 이유를 회신할 수 있다"고 명시되어 있음.
|
||||||
|
- **문제점**: `run_loop.sh`는 리뷰어의 `NOT PASS` 피드백 전체를 Creator에게 일방적으로 주입할 뿐, Creator가 특정 피드백을 거부하거나 반론을 제기하여 상호 조율하는 이의제기 채널이 코딩적으로 구현되어 있지 않음.
|
||||||
|
- **해결 방안**: Creator 교정 단계 프롬프트에 반론 작성 템플릿을 허용하고, 반론 발생 시 Planner/Reviewer에게 재검토를 요청하는 이의제기 브랜칭 로직 설계.
|
||||||
|
|
||||||
|
### **O-2 (구 ISSUE-7): 동일 워크스페이스 내 중복 루프 기동 방지 락 (Race-Free Lock)**
|
||||||
|
- **현상**: 동일 작업 트리에서 다수의 `run_loop.sh` 스크립트가 병렬 기동될 경우 SQLite DB 갱신 경합 및 YAML 데이터 오염이 일어날 수 있음.
|
||||||
|
- **문제점**: 단순 PID 파일 존재 여부만 체크할 경우, PID Rollover(프로세스 ID 재사용) 또는 `mkdir`과 PID 기록 사이의 생성 창(Grace Window)에서 살아있는 락을 타 프로세스가 훔쳐가는 "락 도난(Live-lock theft)" 현상 발생.
|
||||||
|
- **해결 방안**:
|
||||||
|
1. 락 소유자 레코드를 단순 `PID`에서 **`PID + 시작시각(lstart) + 워크스페이스`** 3중 구조로 결합하여 PID 재사용을 결정적으로 차단.
|
||||||
|
2. `mkdir` 직후 생성 창 유예 대기(Sleep Grace Period)를 부여하여 락 도난 방지.
|
||||||
|
3. `ps` CLI 부재 시 Fails-Open(락 무시) 대신 **Fails-Safe(락 존중 + 경고)** 로 전환하여 DB/YAML 오염 원천 방지.
|
||||||
|
|
||||||
|
### **O-3 (구 ISSUE-9): 조건부 오케스트레이션 위임 가드 (Invocation-Aware Scoped Guard)**
|
||||||
|
- **현상**: 오케스트레이터(Antigravity)가 평상시에는 Main Creator로서 코드 및 문서를 직접 집필해야 하지만, `/multi-agent-mux-loop` 슬래시 커맨드/스킬이 인보크된 상황에서도 이를 인지하지 못하고 에이전트들에게 위임하는 대신 직접 수정을 시도하는 지침 이탈 발생.
|
||||||
|
- **해결 방안**:
|
||||||
|
- **평상시 (일반 요청)**: 오케스트레이터가 **Main Creator**로서 소스 및 마크다운 파일 직접 작성/수정 도구(`write_to_file`, `replace_file_content`)를 자유롭게 사용하여 단독 구현 수행.
|
||||||
|
- **`/multi-agent-mux-loop` 호출 시 (스킬 활성화 상태)**: 스킬 인터셉터 가드(Guardrail)가 작동하여 직접 수정 도구 호출을 거부(Interception)하고, **"슬래시 커맨드가 인보크되었으므로 직접 수정을 중단하고 `run_loop.sh`를 실행하여 위임하십시오"**라는 에러를 반환해 `run_loop.sh` 자율 위임 실행을 코딩적으로 강제.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. ⚪ 레거시 잔재 및 죽은 코드 (Legacy Remnants — 5건)
|
||||||
|
|
||||||
|
### **C-1: Kanban 문서 29회 언급 vs 실제 구현 0건**
|
||||||
|
- SKILL.md 파일들에 Kanban 지원 및 상태 파일 서술이 29회 언급되어 있으나 스크립트 구현은 0건입니다.
|
||||||
|
|
||||||
|
### **C-2: 미사용 `.cache/` 상태 디렉터리 생성**
|
||||||
|
- `reconcile.sh`가 `.cache/multi-agent-mux-monitor` 디렉터리를 `mkdir`만 하고 아무것도 읽거나 쓰지 않습니다.
|
||||||
|
|
||||||
|
### **C-3: 격리 스텁 4종 및 `stop_session.sh` 미사용 isolation 코드 잔존**
|
||||||
|
- `provision_isolation` 등 4개 스텁 함수와 `stop_session.sh` 내 `.mam/agent_homes` 가드 코드가 호출자 0건인 채 잔존합니다.
|
||||||
|
|
||||||
|
### **C-4: 참조 0회 미사용 심볼 7종**
|
||||||
|
- `_HERDR_SHIM_DIR_PATTERN`, `_REAL_HERDR_PATH`, `TERMINAL_STATUSES`, `ISOLATE`, `local_herdr` 등 7개 미사용 심볼이 잔존합니다.
|
||||||
|
|
||||||
|
### **C-6: `stop_session.sh` 도움말 문서 구버전 표기**
|
||||||
|
- 스크립트 도움말에는 `--mode soft|hard` 등이 서술되어 있으나 실제 옵션 파서는 `exit 2`로 거부합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 결론 및 향후 보완 로드맵
|
||||||
|
|
||||||
|
두 문서가 `IMPROVEMENTS.md` 하나로 통합됨에 따라, 향후 코드베이스 개편 시 본 문서의 18가지 백로그 항목(아키텍처 2건, 엣지케이스 8건, 오케스트레이션 3건, 레거시 잔재 5건)을 일원화된 보완 로드맵으로 관리합니다.
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# 🛠️ Multi-Agent Mux Loop (`/multi-agent-mux-loop`) 최적화 및 개선 분석서 (`OPTIMIZATION.md`)
|
|
||||||
|
|
||||||
본 문서는 `/multi-agent-mux-loop` 스킬 및 오케스트레이션 스크립트(`run_loop.sh`)의 불필요한 문구, 스킬 명세와 실제 코드 구현 간의 괴리, 필수 절차의 기계적 강제성 부족 항목을 분석하고, 이를 코딩적으로 강제 및 최적화하기 위한 최종 해결 방안을 정의한 분석서입니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 🔍 불필요한 문구, 모순 및 중복 항목 (Redundant & Inconsistent Issues)
|
|
||||||
|
|
||||||
### ISSUE-1: CLI 옵션 상호 배타성 및 충돌 경고의 취약함
|
|
||||||
- **현상**: `--all-reviewer` 옵션과 `--reviewer "A,B"` 옵션을 함께 전달할 경우, `run_loop.sh`에서 경고 메시지만 출력하고 `--reviewer` 목록을 무시함. 또한 `--plan` 모드가 비활성화된 상태에서 `--plan-talk N`을 전달할 경우 역시 경고 후 턴 설정을 무시하고 진행됨.
|
|
||||||
- **문제점**: 에이전트나 사용자가 잘못된 파라미터 조합을 주입했을 때 스크립트가 조기에 에러로 실패(Fail-Fast)하지 않고 진행하여 혼선을 야기함.
|
|
||||||
- **해결 방안**:
|
|
||||||
1. 파라미터 파싱 단계에서 상호 배타적인 옵션이 포함된 경우 경고로 넘기지 않고 즉시 에러(`exit 1`)를 반환하도록 검증 로직 강화.
|
|
||||||
2. `SKILL.md` 문서 내의 옵션 예시(Workflow 섹션) 중 두 옵션이 동시에 사용된 오류 표기를 상호 배타 규격에 맞게 정정.
|
|
||||||
|
|
||||||
### ISSUE-2: `SKILL.md` 명세 문서 내 레거시 용어 및 문구
|
|
||||||
- **현상**: 스킬 명세서 문서 내 일부 설명 및 주석에 TMUX 시절의 표현이나 레거시 파라미터 관련 설명이 혼재되어 있음.
|
|
||||||
- **해결 방안**: Herdr 엔진 기반으로 완전히 마이그레이션된 현재 구조에 맞춰 스킬 명세서(`SKILL.md`) 내 문구를 정돈하고 불필요한 레거시 언급을 제거함.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. ⚠️ 명세(Specification)에는 정의되어 있으나 코드로 강제되지 않은 작업 절차 (Specification vs Implementation Discrepancies)
|
|
||||||
|
|
||||||
### ISSUE-3: `[VERDICT: PASS]` 판정 포맷 템플릿의 기계적 검증 및 가이드 부족
|
|
||||||
- **현상**: 스킬 명세 및 규약에서는 리뷰어 보고서의 "마지막 줄 단독 행"에 `[VERDICT: PASS]` 또는 `[VERDICT: NOT PASS]` 토큰이 명시되어야 함을 요구함. 하지만 리뷰어 에이전트 프롬프트에 텍스트 문구로만 지시될 뿐, 작성 전후 양식을 검증하거나 보정하는 장치가 스크립트 레벨에 없음.
|
|
||||||
- **문제점**: 리뷰어가 보고서 작성 시 줄바꿈 미입력, 마크다운 코드블록 인용, 기타 형식 오류를 범할 경우 내용이 통과이더라도 파서가 `fail-closed`로 동작하여 무조건 `NOT PASS` 처리됨.
|
|
||||||
- **해결 방안**: 리뷰어 지시 프롬프트에 정확한 템플릿 포맷 예시를 강화하고, 필요시 파싱 실패 시 1회 구조화 재작성 지시(Fix-up prompt) 기계적 트리거 마련.
|
|
||||||
|
|
||||||
### ISSUE-4: Definition of Done (DoD) 및 원자적 커밋(Atomic Commit)의 기계적 검증 부재
|
|
||||||
- **현상**: 규약 및 스킬 명세에는 Creator(작업자)가 구현 완료 후 DoD 체크리스트를 실행하고 원자적 커밋을 수행한 뒤 리뷰어에게 전달하도록 명시되어 있음.
|
|
||||||
- **문제점**: `run_loop.sh`는 Creator 잡이 종료된 후 실제 git status 변경 유무나 커밋 생성 여부를 확인하지 않고 단순히 지시 프롬프트에만 의존함. 커밋이 수행되지 않거나 변경분(diff)이 0건인 경우에도 루프가 그대로 진행되어 무의미한 리뷰가 수행됨.
|
|
||||||
- **해결 방안**: Phase 2 (구현 단계) 완료 직후 `dod_changed_paths` 헬퍼 및 `git diff` 누적 관제를 수행하여, **변경 경로가 0건인 경우 `exit 1`로 즉시 실패 처리**하고 원자적 커밋 미수행 시 1회 경고 및 재지시를 내리는 코딩 게이트 구축.
|
|
||||||
|
|
||||||
### ISSUE-5: 기획-구현 대화 루프(`--plan-talk`)의 이의제기 수렴 여부 판단 부재
|
|
||||||
- **현상**: `--plan-talk N` 설정 시 Planner와 Creator 간의 이의제기(Challenge) 및 계획 갱신(Refine) 대화가 N회 진행됨.
|
|
||||||
- **문제점**: Creator의 이의제기가 실제로 Planner에 의해 수용 및 합의되었는지 논리적 종결 여부를 확인하지 않고, 무조건 지정된 턴 수(N)를 기계적으로 소모한 후 다음 단계로 진행함.
|
|
||||||
- **해결 방안**: Planner 갱신 리포트에 `[AGREEMENT: REACHED]` 같은 수렴 판정 토큰을 도입하거나, 이의제기가 없는 경우 N회 턴 전이라도 조기 종료(Early Break)할 수 있는 로직 추가.
|
|
||||||
|
|
||||||
### ISSUE-6: 타당하지 않은 리뷰 피드백 거부/반론 프로토콜의 스크립트 미지원
|
|
||||||
- **현상**: `MULTI_AGENT_RULES.md` 1장 규약에는 "개발 팀장이 리뷰어의 타당하지 않은 피드백을 거부하고 명확한 이유를 회신할 수 있다"고 명시되어 있음.
|
|
||||||
- **문제점**: `run_loop.sh`는 리뷰어의 `NOT PASS` 피드백 전체를 Creator에게 일방적으로 주입할 뿐, Creator가 특정 피드백을 거부하거나 반론을 제기하여 상호 조율하는 이의제기 채널이 코딩적으로 구현되어 있지 않음.
|
|
||||||
- **해결 방안**: Creator 교정 단계 프롬프트에 반론 작성 템플릿을 허용하고, 반론 발생 시 Planner/Reviewer에게 재검토를 요청하는 이의제기 브랜칭 로직 설계.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 🛡️ 오케스트레이션 위임 및 안전성/동시성 강제안 (Orchestration Enforcement & Reliability)
|
|
||||||
|
|
||||||
### ISSUE-7: 동일 워크스페이스 내 중복 루프 기동 방지 락 (Race-Free Lock) 설계 정교화
|
|
||||||
- **현상**: 동일 작업 트리에서 다수의 `run_loop.sh` 스크립트가 병렬 기동될 경우 SQLite DB 갱신 경합 및 YAML 데이터 오염이 일어날 수 있음.
|
|
||||||
- **문제점**: 단순 PID 파일 존재 여부만 체크할 경우, PID Rollover(프로세스 ID 재사용) 또는 `mkdir`과 PID 기록 사이의 생성 창(Grace Window)에서 살아있는 락을 타 프로세스가 훔쳐가는 "락 도난(Live-lock theft)" 현상 발생.
|
|
||||||
- **해결 방안**:
|
|
||||||
1. 락 소유자 레코드를 단순 `PID`에서 **`PID + 시작시각(lstart) + 워크스페이스`** 3중 구조로 결합하여 PID 재사용을 결정적으로 차단.
|
|
||||||
2. `mkdir` 직후 생성 창 유예 대기(Sleep Grace Period)를 부여하여 락 도난 방지.
|
|
||||||
3. `ps` CLI 부재 시 Fails-Open(락 무시) 대신 **Fails-Safe(락 존중 + 경고)** 로 전환하여 DB/YAML 오염 원천 방지.
|
|
||||||
|
|
||||||
### ISSUE-8: 비동기 잡 모니터링 타임아웃 및 헬스체크 최적화
|
|
||||||
- **현상**: `wait_for_job` 기본 타임아웃이 3900초(65분)로 설정되어 있어, 에이전트 세션 패닉이나 사망 시 오케스트레이터가 과도하게 오랫동안 대기함.
|
|
||||||
- **해결 방안**: 모니터링 수집 루프 내에서 herdr 세션의 라이브 상태(`alive`)를 매 주기마다 핑(Ping) 확인하여 세션 사망 시 즉시 `fail-fast` 하도록 개선.
|
|
||||||
|
|
||||||
### ISSUE-9: 조건부 오케스트레이션 위임 가드 (Invocation-Aware Scoped Guard)
|
|
||||||
- **현상**: 오케스트레이터(Antigravity)가 평상시에는 Main Creator로서 코드 및 문서를 직접 집필해야 하지만, `/multi-agent-mux-loop` 슬래시 커맨드/스킬이 인보크된 상황에서도 이를 인지하지 못하고 에이전트들에게 위임하는 대신 직접 수정을 시도하는 지침 이탈 발생.
|
|
||||||
- **문제점**: 오케스트레이터의 파일 직접 수정 권한을 무조건 뺏으면(1번 방안 부작용) 일반 작업이 불가능해지고, 자연어 지침에만 의존하면 슬래시 커맨드 호출 시 위임을 건너뛰는 모순 발생.
|
|
||||||
- **해결 방안**:
|
|
||||||
- **평상시 (일반 요청)**: 오케스트레이터가 **Main Creator**로서 소스 및 마크다운 파일 직접 작성/수정 도구(`write_to_file`, `replace_file_content`)를 자유롭게 사용하여 단독 구현 수행.
|
|
||||||
- **`/multi-agent-mux-loop` 호출 시 (스킬 활성화 상태)**: 스킬 인터셉터 가드(Guardrail)가 작동하여 직접 수정 도구 호출을 거부(Interception)하고, **"슬래시 커맨드가 인보크되었으므로 직접 수정을 중단하고 `run_loop.sh`를 실행하여 위임하십시오"**라는 에러를 반환해 `run_loop.sh` 자율 위임 실행을 코딩적으로 강제.
|
|
||||||
|
|
||||||
---
|
|
||||||
*본 분석서는 Planner(`claude`)와 Creator(`agy`)의 협업 계획(Job `96b6e07b`) 및 리뷰어 만장일치 PASS 합의를 바탕으로 최종 작성된 수합 최적화 명세서입니다.*
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
tests/test_workspace_scope.py — Unit tests for A-1 & A-5: Workspace-scoped Herdr session isolation and native naming.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestWorkspaceScope(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp_dir = tempfile.mkdtemp(prefix="mam_scope_test_")
|
||||||
|
self.ws_dir = os.path.join(self.tmp_dir, "my_project")
|
||||||
|
self.mam_dir = os.path.join(self.ws_dir, ".mam")
|
||||||
|
os.makedirs(self.mam_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_derived_herdr_session_name(self):
|
||||||
|
"""Verify that resolve_herdr_session derives 'mam-<slug>' per workspace."""
|
||||||
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
lib_sh = os.path.join(repo_root, ".agents", "skills", "lib.sh")
|
||||||
|
|
||||||
|
cmd = f"source {lib_sh} && cd {self.ws_dir} && resolve_herdr_session 'test-session'"
|
||||||
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
|
self.assertEqual(res.returncode, 0, f"Command failed: {res.stderr}")
|
||||||
|
self.assertEqual(res.stdout.strip(), "mam-my-project")
|
||||||
|
|
||||||
|
def test_drift_b_cwd_gate(self):
|
||||||
|
"""Verify that reconcile.sh drift-B refuses to auto-register sessions with foreign cwd."""
|
||||||
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
reconcile_sh = os.path.join(repo_root, ".agents", "skills", "multi-agent-mux-monitor", "scripts", "reconcile.sh")
|
||||||
|
|
||||||
|
# Verify bash syntax for reconcile.sh
|
||||||
|
res = subprocess.run(["bash", "-n", reconcile_sh], capture_output=True, text=True)
|
||||||
|
self.assertEqual(res.returncode, 0, f"Syntax error in reconcile.sh: {res.stderr}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user