- Resolve Herdr shim 5 routing & paste defects (ISSUE-1 ~ ISSUE-5):
* paste-buffer: use pane send-text without auto-enter, propagate rc=3 to send_keys_safe
* exact-match pane resolution: remove substring matching ('in tn') across all branches
* workspace scoping: introduce HERDR_WORKSPACE_ID and .mam/herdr_workspace_id persistence
* unified resolver: single _resolve_herdr_pane_id helper across shim commands
- Resolve dialog token false-positive on 'Yes, try it' tip and isolate fullscreen modal rejection
- Sync mock Herdr CLI contracts in tests/conftest.py
- Add contract tests H-15~H-23 and regression tests D-4~D-7 (412 tests, 100% PASS)
- Add multi-agent loop plans, review reports, and bug report
- Update framework and skill packages to v3.0.1
30 KiB
📐 구현 계획서: Herdr 셈(shim) 패인 라우팅 결함 4종 수정 (ISSUE-1/2/3/5)
- Job ID:
fae58b93 - Role: Planner (
planner-reviewer-claude-01) - 작성일: 2026-08-27
- 대상:
.agents/skills/lib.sh,tests/conftest.py,tests/test_herdr_shim_contract.py,tests/test_b19_headless_reconcile_fixes.py - 근거 문서:
bug_report.md(v1.0) - 기준 커밋:
4bbd03b(main)
0. 요약 (TL;DR)
bug_report.md의 5대 결함 중 ISSUE-4는 이미 커밋 4bbd03b에서 해결되어 회귀 테스트(test_agent_start_success_tokens_exclude_startup_timeout)로 고정되어 있다. 남은 ISSUE-1 / 2 / 3 / 5를 다음 순서로 처리한다.
- ISSUE-5 선행 — 셈 내부에 공용 헬퍼
_resolve_herdr_pane_id를 신설한다. 나머지 3개 이슈의 수정이 전부 이 헬퍼 안으로 수렴하므로 이것이 반드시 먼저다. - ISSUE-2 — 헬퍼 및
_resolve_herdr_target/has-session에서agent in tn부분 매칭을 전면 제거하고 엄격 일치로 대체. - ISSUE-3 —
HERDR_WORKSPACE_ID가 명시적으로 설정된 경우에만pane list --workspace로 하드 스코핑. - ISSUE-1 —
paste-buffer를pane send-text단독 삽입으로 교체(엔터 금지), 해결 실패 시 조용히 삼키지 말고 실패를 상위로 전달.
1. 사전 조사에서 확인된 사실 (계획의 전제)
계획 수립 중 실제 herdr 바이너리(/opt/homebrew/bin/herdr)와 현재 lib.sh를 직접 검증했다. 버그 리포트의 권고 코드를 그대로 옮기면 안 되는 지점이 3곳 있다.
1.1 ✅ herdr agent send 서브커맨드는 존재하지 않는다 (ISSUE-1의 진짜 뿌리)
$ herdr agent --help
Commands: list get read send-keys prompt rename focus wait attach start explain
현재 lib.sh:822의 paste-buffer 구현은 다음 한 줄이 전부다.
_real_herdr agent send "$sess" "$(cat "$buffer_dir/$buf")" >/dev/null 2>&1 || true
agent send는 CLI에 없으므로 이 호출은 항상 실패하고 || true가 실패를 삼킨다. 즉 현재 main에서 paste-buffer 경로는 텍스트를 단 한 글자도 주입하지 못하는 완전한 데드 코드다. 이것이 브리프의 "paste-buffer의 herdr agent send 부재"가 가리키는 실체이며, send_keys_safe의 폴백 경로 전체가 무력화되어 있음을 뜻한다.
참고:
send_keys_safe는agent prompt고속 경로가 성공하면 즉시 반환하므로(lib.sh:1785), 등록된 agent에 대해서는 이 결함이 드러나지 않는다. 결함이 표면화되는 조건은 정확히 버그 리포트가 기술한 상황 —agent prompt가 실패하는 라벨 전용 패인(agent 미등록) — 이다.
1.2 ⚠️ 버그 리포트의 pane_id 정규식은 실제 pane_id를 거부한다
버그 리포트 §3.1은 다음 검증을 제안한다.
if [[ "$pid" =~ ^w[0-9]+:p[0-9]+$ ]]; then
그러나 실제 서버가 반환하는 pane_id는 다음과 같다.
{"pane_id":"w1E:p1","workspace_id":"w1E","tab_id":"w1E:t1", ...}
워크스페이스 세그먼트는 w1E처럼 영문자를 포함한다. 권고 정규식을 그대로 쓰면 모든 실제 pane_id가 거부되어 헬퍼가 항상 실패하고, 결과적으로 ISSUE-1을 고친 뒤에도 주입이 되지 않는다.
→ 채택 정규식: ^w[A-Za-z0-9]+:p[A-Za-z0-9]+$
1.3 ⚠️ 실제 pane list 응답에는 label 키가 없을 수 있다
{"agent":"claude","agent_status":"working","cwd":"...","pane_id":"w1E:p1",
"tab_id":"w1E:t1","terminal_title":"...","workspace_id":"w1E"}
label은 herdr pane rename <PANE_ID> <LABEL>로 설정했을 때만 나타난다. 반면 agent list에는 name 필드가 있다("name":"planner-reviewer-claude-01"). 따라서 헬퍼의 매칭 우선순위는 label → name → agent 순으로 두되, 셋 다 완전 일치만 허용한다. agent 필드는 사실상 CLI 종류(claude/grok)이므로 tn이 그 값과 완전히 같은 경우에만 매칭되며, 이는 부분 매칭과 달리 오라우팅을 만들지 않는다.
1.4 ✅ pane list는 서버측 --workspace 필터를 지원한다
$ herdr pane list --help
Options:
--workspace <WORKSPACE_ID>
ISSUE-3의 스코핑은 파이썬 클라이언트 필터링만이 아니라 서버측 플래그로 1차 차단할 수 있다. 양쪽 모두 적용한다(플래그 미지원 구버전 herdr 대비 이중 방어).
1.5 ✅ pane read와 agent read의 출력 형식은 호환된다
둘 다 평문 텍스트를 반환하며, _pane_capture(lib.sh:1672)는 JSON 파싱 실패 시 원문을 그대로 반환하므로 capture-pane을 pane read로 전환해도 상위 로직이 깨지지 않는다.
1.6 ⚠️ 셈은 set -euo pipefail 아래에서 실행된다
셈 본문은 lib.sh:141의 cat <<'EOF' ~ lib.sh:959의 EOF 사이 히어독으로 생성되며 3번째 줄이 set -euo pipefail이다. 따라서 실패를 반환할 수 있는 새 헬퍼는 모든 호출부에서 || true로 감싸야 하며, 그렇지 않으면 셈이 조기 종료된다.
1.7 ✅ 테스트 목(mock)이 결함을 은폐하고 있다
tests/conftest.py:638의 목 herdr는 존재하지 않는 agent send를 성공으로 처리한다. 이 때문에 ISSUE-1이 테스트에서 전혀 드러나지 않았다. 목을 실제 CLI 계약에 맞추는 것이 이번 작업의 필수 선행 조건이다.
2. 변경 대상 목록
| # | 파일 | 위치 | 이슈 | 성격 |
|---|---|---|---|---|
| C1 | .agents/skills/lib.sh |
셈 히어독, _sanitize_herdr_agent_name 직후 (~L236) |
5 | 신규 헬퍼 _resolve_herdr_workspace_scope, _resolve_herdr_pane_id |
| C2 | .agents/skills/lib.sh |
_resolve_herdr_target (L249–286) |
2,3 | 부분 매칭 제거 + ws 필터 |
| C3 | .agents/skills/lib.sh |
has-session (L305–345) |
2,3,5 | 부분 매칭 제거 + ws 필터 + 헬퍼 폴백 |
| C4 | .agents/skills/lib.sh |
new-session (L434–530) |
3 | 해결된 workspace_id를 HERDR_WORKSPACE_ID로 export |
| C5 | .agents/skills/lib.sh |
kill-session (L567–603) |
5 | 인라인 파서 → 헬퍼 |
| C6 | .agents/skills/lib.sh |
capture-pane (L687–704) |
3,5 | 헬퍼 + pane read 경로 |
| C7 | .agents/skills/lib.sh |
send-keys (L705–744) |
2,3,5 | 인라인 파서 → 헬퍼 |
| C8 | .agents/skills/lib.sh |
paste-buffer (L794–827) |
1,5 | agent send → pane send-text, 엔터 금지, 실패 전파 |
| C9 | .agents/skills/lib.sh |
send_keys_safe (L1804–1806) |
1 | paste-buffer 종료 코드 확인 → rc 3 |
| C10 | tests/conftest.py |
목 herdr | 1,2,3 | pane send-text/pane read/pane rename 추가, pane list 병합·라벨·--workspace, agent send 제거 |
| T1 | tests/test_herdr_shim_contract.py |
신규 | 1,2,3,5 | H-15 ~ H-20 |
| T2 | tests/test_b19_headless_reconcile_fixes.py |
신규 | 1,2,5 | D-4 ~ D-7 |
list-panes(L605–686)의 인라인 파서는pane_id외에cwd/agent까지 한 번에 파싱하므로 헬퍼로 대체하지 않는다. ISSUE-5의 대상 목록에도 포함되어 있지 않다.
3. 상세 구현 설계
3.1 [C1] 공용 헬퍼 신설 (ISSUE-5)
lib.sh 셈 히어독 내부, _sanitize_herdr_agent_name 정의 직후(cmd="${1:-}" 앞)에 삽입한다. 이 위치여야 case 분기 전체에서 참조 가능하다.
# ---------------------------------------------------------------------------
# Workspace scoping (ISSUE-3).
#
# 스코핑은 HERDR_WORKSPACE_ID 가 "명시적으로" 설정된 경우에만 하드 필터로
# 동작한다. cwd 로부터 자동 추론하지 않는다 — 자동 추론은 다중 워크스페이스
# 오케스트레이션에서 정당한 교차 워크스페이스 조회를 조용히 막아버린다.
# 미설정 시에는 서버 전역 조회(기존 동작)를 유지한다.
# ---------------------------------------------------------------------------
_herdr_ws_scope() { printf '%s\n' "${HERDR_WORKSPACE_ID:-}"; }
# _resolve_herdr_pane_id <target> [workspace_id]
#
# 세션 이름 / 라벨을 실제 pane_id ("wN:pM") 로 해석한다.
# 엄격한 해석 순서 (부분 문자열 매칭은 어느 단계에서도 사용하지 않는다):
# 1. herdr agent get <sanitized_name>
# 2. herdr agent get <raw_name>
# 3. herdr pane list [--workspace WS] 에서
# 3-a. label 완전 일치
# 3-b. name 완전 일치
# 3-c. agent 완전 일치
# 성공 시 pane_id 를 stdout 에 출력하고 0, 실패 시 아무것도 출력하지 않고 1.
# 호출부는 반드시 `|| true` 로 감쌀 것 (셈은 set -e 하에서 동작한다).
_resolve_herdr_pane_id() {
local target="$1"
local target_ws="${2:-$(_herdr_ws_scope)}"
local sat pid=""
sat=$(_sanitize_herdr_agent_name "$target")
local cand
for cand in "$sat" "$target"; do
[ -n "$cand" ] || continue
pid=$(_real_herdr agent get "$cand" 2>/dev/null | TARGET_WS="$target_ws" python3 -c "
import sys, json, os
tws = os.environ.get('TARGET_WS', '')
try:
a = json.load(sys.stdin).get('result', {}).get('agent', {})
# ISSUE-3: 워크스페이스가 지정되면 다른 워크스페이스의 동명 agent 는 거부.
if tws and a.get('workspace_id') and a.get('workspace_id') != tws:
pass
else:
print(a.get('pane_id') or '')
except Exception:
pass
" 2>/dev/null || echo "")
[ -n "$pid" ] && break
done
if [ -z "$pid" ]; then
local ws_flag=()
[ -n "$target_ws" ] && ws_flag=(--workspace "$target_ws")
pid=$(_real_herdr pane list "${ws_flag[@]+"${ws_flag[@]}"}" 2>/dev/null \
| TARGET_NAME="$target" TARGET_SAN="$sat" TARGET_WS="$target_ws" python3 -c "
import sys, json, os
tn = os.environ.get('TARGET_NAME', '')
tsa = os.environ.get('TARGET_SAN', '')
tws = os.environ.get('TARGET_WS', '')
try:
panes = json.load(sys.stdin).get('result', {}).get('panes', [])
# 서버가 --workspace 를 무시하는 구버전일 수 있으므로 클라이언트에서 한 번 더 거른다.
if tws:
panes = [p for p in panes if p.get('workspace_id') == tws]
# ISSUE-2: 완전 일치만 허용. 'agent in tn' 부분 매칭은 사용하지 않는다.
for key in ('label', 'name', 'agent'):
for p in panes:
v = p.get(key)
if v and (v == tn or v == tsa):
pid = p.get('pane_id') or ''
if pid:
print(pid)
sys.exit(0)
except Exception:
pass
sys.exit(1)
" 2>/dev/null || echo "")
fi
# 실제 pane_id 는 'w1E:p1' 처럼 워크스페이스 세그먼트에 영문자를 포함한다.
# ^w[0-9]+:p[0-9]+$ 로 좁히면 모든 실제 pane_id 가 거부된다.
if [[ "$pid" =~ ^w[A-Za-z0-9]+:p[A-Za-z0-9]+$ ]]; then
printf '%s\n' "$pid"
return 0
fi
return 1
}
설계 근거
for key in ('label','name','agent')바깥 루프: 우선순위가 "패인 목록의 등장 순서"가 아니라 "필드의 신뢰도"로 결정된다. 안쪽/바깥쪽 루프를 뒤집으면 목록 첫 항목의agent매칭이 뒤쪽 항목의 정확한label매칭을 이겨버린다 — 이것이 ISSUE-2가 만든 오라우팅과 동일한 형태의 버그다.tsa(sanitized) 도 비교 대상에 포함:agent start가 이름을 sanitize해서 등록하므로, 라벨은 원본이고 등록명은 sanitize본인 혼재 상황을 커버한다. sanitize는 결정적 함수이므로 부분 매칭과 달리 충돌을 만들지 않는다.ws_flag배열 +${ws_flag[@]+...}:set -u하에서 빈 배열 전개가 unbound 오류를 내지 않도록 하는 표준 관용구.
3.2 [C2] _resolve_herdr_target 엄격화 (ISSUE-2, ISSUE-3)
lib.sh:262–275의 파이썬 블록에서 다음 술어를 제거한다.
if name == tn or (not name and agent and agent in tn): # ← 제거
교체:
tn = os.environ.get("TARGET_NAME", "")
tsa = os.environ.get("TARGET_SAN", "")
tws = os.environ.get("TARGET_WS", "")
...
agents = d.get("result", {}).get("agents", [])
if tws:
agents = [a for a in agents if a.get("workspace_id") == tws]
for a in agents:
name = a.get("name", "")
if name and (name == tn or name == tsa):
print(a.get("pane_id") or name)
sys.exit(0)
sys.exit(1)
pane_id or agent → pane_id or name으로 바꾼다. 기존 코드는 매칭에 실패한 항목의 CLI 종류(agent, 예: "claude")를 타깃으로 반환할 수 있었는데, 이는 agent prompt claude ...처럼 전혀 다른 대상에게 프롬프트를 던지는 경로다.
3.3 [C3] has-session 엄격화 + 패인 폴백 (ISSUE-2, ISSUE-3, ISSUE-5)
lib.sh:334의 다음 술어를 제거한다.
or (not an and a.get("agent") and a.get("agent") in tn) # ← 제거
남는 조건은 an == tn or an == stn이며, 여기에 HERDR_WORKSPACE_ID 필터를 추가한다.
그리고 agent 조회가 모두 실패했을 때 마지막 단계로 헬퍼를 호출한다.
if [ -n "$(_resolve_herdr_pane_id "$sess" 2>/dev/null || true)" ]; then
exit 0
fi
exit 1
의도적 동작 변경: 라벨만 붙은(agent 미등록) 패인도 이제 "세션 존재"로 판정된다. 이것이 정확히 버그 리포트가 보고한 실패 시나리오(label: reviewer-cline-01 패인에 주입 불가)의 해소 조건이다. _resolve_herdr_pane_id가 완전 일치만 허용하므로, 세션 이름 reviewer-creator-grok-01이 agent == "grok" 패인에 매칭될 일은 없다.
리스크: create_session.sh / reconcile.sh가 has-session 결과로 재생성 여부를 판단한다면, 라벨만 있고 실제 CLI가 죽은 패인을 "살아 있음"으로 오판할 수 있다. → 3.9의 회귀 검증 범위에 test_orc_onboard.py, test_tier3_integration.py, test_tier4_e2e.py를 명시적으로 포함한다.
3.4 [C4] HERDR_WORKSPACE_ID 전파 (ISSUE-3)
new-session 분기에서 existing_ws 또는 신규 ws_id가 확정된 직후(lib.sh:509 이후 ws_id 확정 지점) 다음을 추가한다.
if [ -n "${ws_id:-}" ]; then
export HERDR_WORKSPACE_ID="$ws_id"
fi
또한 agent start에 전달하는 env_flags에 --env HERDR_WORKSPACE_ID=$ws_id를 추가하여, 기동된 에이전트 프로세스가 상속한 셈 호출부터 자동으로 스코프가 걸리도록 한다.
채택하지 않은 대안: 셈이 $PWD/$WORKSPACE_ROOT의 cwd로부터 workspace_id를 자동 추론하는 방식. 추론이 성공하는 순간 교차 워크스페이스 조회가 조용히 막히고, 오케스트레이터가 다른 워크스페이스의 에이전트를 정당하게 다루는 경로가 원인 불명으로 깨진다. 스코핑은 명시적 옵트인이어야 진단 가능하다.
3.5 [C5]~[C7] 분기 리팩터링 (ISSUE-5)
kill-session — lib.sh:587–598의 이중 인라인 파이썬을 삭제.
agent_target=$(_sanitize_herdr_agent_name "$sess")
pane_id=$(_resolve_herdr_pane_id "$sess" 2>/dev/null || true)
if [ -n "$pane_id" ]; then
_real_herdr pane close "$pane_id" >/dev/null 2>&1 || true
fi
_real_herdr kill-session -t "$agent_target" >/dev/null 2>&1 \
|| _real_herdr kill-session -t "$sess" >/dev/null 2>&1 || true
capture-pane — 헬퍼로 pane_id를 얻으면 pane read, 아니면 기존 agent read 체인 유지.
agent_target=$(_sanitize_herdr_agent_name "$sess")
pane_id=$(_resolve_herdr_pane_id "$sess" 2>/dev/null || true)
if [ -n "$pane_id" ]; then
_real_herdr pane read "$pane_id" --source visible --lines 100 2>/dev/null || true
else
_real_herdr agent read "$agent_target" --source visible --lines 100 2>/dev/null \
|| _real_herdr agent read "$sess" --source visible --lines 100 2>/dev/null || true
fi
send-keys — lib.sh:726–738의 이중 인라인 파이썬을 삭제하고 헬퍼 호출로 대체. 폴백(pane send-keys "$agent_target" → "$sess")은 그대로 유지한다. C-m → Enter 정규화(L741–743)도 유지 — 이건 키 이름 번역이지 제출 정책이 아니다.
ISSUE-5의 "데드 파이프라인" 부분: 기존 인라인 파서는 except: pass로 항상 exit 0을 반환해 || 2차 폴백이 절대 실행되지 않았다. 신규 헬퍼는 sys.exit(1) + 정규식 검증 + return 1로 실패를 정확히 신호하므로 이 데드 코드가 구조적으로 제거된다.
3.6 [C8] paste-buffer 재작성 (ISSUE-1)
buffer_dir="${WORKSPACE_ROOT:+$WORKSPACE_ROOT/.mam/buffers}"
buffer_dir="${buffer_dir:-${TMPDIR:-/tmp}/mam_buffers}"
if [ ! -f "$buffer_dir/$buf" ]; then
echo "Error: buffer $buf not found ($buffer_dir/$buf)" >&2
exit 1
fi
pane_id=$(_resolve_herdr_pane_id "$sess" 2>/dev/null || true)
if [ -z "$pane_id" ]; then
# herdr 에는 `agent send` 서브커맨드가 없다. 여기서 조용히 성공을 반환하면
# send_keys_safe 가 아무것도 붙여넣지 않은 채 Enter 만 치게 된다.
echo "Error: paste-buffer could not resolve a pane for '$sess'" >&2
exit 1
fi
# 삽입 전용. Enter/C-m 제출은 전적으로 send_keys_safe 가 통제한다 (ISSUE-1).
# 여기서 `pane run` 을 쓰면 안 된다 — 텍스트와 Enter 를 한 번에 보내 이중 제출이 된다.
if ! _real_herdr pane send-text "$pane_id" "$(cat "$buffer_dir/$buf")" >/dev/null 2>&1; then
echo "Error: pane send-text failed for '$sess' ($pane_id)" >&2
exit 1
fi
불변식 (테스트로 고정): paste-buffer 분기 본문에는 Enter, C-m, pane run, agent prompt 중 어떤 것도 등장하지 않는다.
3.7 [C9] send_keys_safe의 붙여넣기 실패 전파 (ISSUE-1)
현재 lib.sh:1804–1806은 paste-buffer의 종료 코드를 버린다. 그리고 세션 이름에 cline|claude|agy|grok이 포함되면 붙여넣기 가시성 검증마저 건너뛴다(L1809–1812) — 즉 실제 운영 대상 전부에서 실패가 무성으로 삼켜진다.
_sks_herdr set-buffer -b "$sks_buf" "$text"
local _paste_rc=0
_sks_herdr paste-buffer -b "$sks_buf" -t "$sess" || _paste_rc=$?
_sks_herdr delete-buffer -b "$sks_buf" 2>/dev/null || true
if [ "$_paste_rc" != "0" ]; then
echo "send_keys_safe: paste-buffer failed rc=$_paste_rc ($sess)" >&2
return 3
fi
버퍼 정리(delete-buffer)는 조기 반환 앞에 둔다. 그렇지 않으면 실패 경로마다 버퍼가 누수되어 set-buffer의 A-3 GC 주석이 방어하는 바로 그 문제가 재발한다.
기존 반환 코드 계약(3 = paste not visible)을 재사용하므로 호출자 계약은 바뀌지 않는다.
3.8 [C10] 테스트 목(mock) 정합화 — tests/conftest.py
테스트 코드보다 먼저 처리해야 한다. 목이 실제 CLI와 어긋나 있는 한 어떤 테스트도 결함을 재현할 수 없다.
| 변경 | 위치 | 내용 |
|---|---|---|
| M1 | cmd1 == "agent", cmd2 == "send" (L638–664) |
핸들러 삭제 → 실제 CLI처럼 unknown subcommand로 exit 1. ISSUE-1 재현의 필수 조건 |
| M2 | cmd1 == "pane" |
send-text 핸들러 추가: pane_id로 대상 조회, sent_text 누적, buffer 갱신, sent_keys는 건드리지 않음 |
| M3 | cmd1 == "pane" |
read 핸들러 추가: 대상 패인의 buffer 평문 출력 |
| M4 | cmd1 == "pane" |
rename 핸들러 추가: state["panes"]의 해당 항목에 label 기록 |
| M5 | pane list (L253–281) |
현재는 agents가 하나라도 있으면 state["panes"]를 무시한다. → agent 유래 패인과 state["panes"]를 pane_id 기준으로 병합(dedupe)하고, label/name 필드를 그대로 실어 보낸다. 라벨 전용 패인 시나리오가 이 변경 없이는 표현 불가 |
| M6 | pane list |
--workspace 필터는 이미 구현되어 있음(L255–261). 유지 |
| M7 | agent get (L569) |
응답에 workspace_id가 이미 포함됨(L594). 유지 |
목의 _match_agent(L182)는 이미 엄격(완전 일치 / sanitize 일치)하므로 변경 불필요하다.
4. 테스트 계획
4.1 tests/test_herdr_shim_contract.py — 행위 테스트 (신규 H-15 ~ H-20)
기존 파일의 규약을 따른다: mam_sandbox / mock_herdr / mock_agents 픽스처로 셈을 실제 실행하고, mock_herdr_state.json의 calls 배열을 검증한다.
H-15 test_h15_paste_buffer_inserts_without_enter (ISSUE-1)
- 준비:
mock_agents로test-creator-claude기동. - 실행:
herdr set-buffer -b t1 "hello world"→herdr paste-buffer -b t1 -t test-creator-claude. - 단언:
calls에["pane","send-text",<pane_id>,"hello world"]가 정확히 1회.calls에["agent","send",...]가 0회 (M1로 이제 실패하게 되므로 회귀 감지).paste-buffer실행으로 발생한calls중pane send-keys/agent prompt/pane run이 0회 ← 이중 제출 방지의 핵심 단언.
H-16 test_h16_send_keys_safe_submits_exactly_once (ISSUE-1 종단)
agent prompt고속 경로를 강제로 실패시켜(존재하지 않는 세션명 또는 목의prompt실패 주입) 폴백 경로를 타게 한다.- 단언:
Enter/C-m키 전송 횟수 총합이 정확히 1. (현재 코드는paste-buffer자체가 죽어 0회, 버그 리포트가 기술한 패치 상태에서는 2회 — 양쪽 모두 이 테스트가 잡는다.)
H-17 test_h17_no_substring_cross_pane_routing (ISSUE-2) — 핵심 회귀 테스트
- 준비:
reviewer-creator-grok-01,worker-grok-02두 agent를 서로 다른 pane_id로 기동. - 실행:
herdr send-keys -t reviewer-creator-grok-01 C-m. - 단언:
pane send-keys의 대상 pane_id가reviewer-creator-grok-01의 것과 일치.worker-grok-02의 pane_id로 간 호출은 0회. - 추가: agent 등록 없이
agent: "grok"라벨 전용 패인만 두고herdr has-session -t reviewer-creator-grok-01→ exit 1이어야 한다(예전 부분 매칭이면 0).
H-18 test_h18_workspace_scoped_pane_resolution (ISSUE-3)
- 준비:
state["panes"]에 동일label: creator-agy-01을workspace_id: w1,w2에 각각 1개씩 시드. - 실행 A:
HERDR_WORKSPACE_ID=w2 herdr send-keys -t creator-agy-01 Enter→ 대상이w2의 pane_id. - 실행 B:
HERDR_WORKSPACE_ID=w1→ 대상이w1의 pane_id. - 실행 C:
HERDR_WORKSPACE_ID미설정 → 해석은 성공하되 실패하지 않음(기존 전역 동작 보존).
H-19 test_h19_single_resolver_helper_used_by_all_branches (ISSUE-5)
- 생성된 셈 파일(
$WORKSPACE_ROOT/.mam/shim/herdr)을 읽어:_resolve_herdr_pane_id()정의가 정확히 1회 등장.has-session/kill-session/capture-pane/send-keys/paste-buffer각 분기 본문에서_resolve_herdr_pane_id호출이 등장.result', {}).get('agent', {}).get('pane_id'형태의 인라인 파서 잔존 개수가 헬퍼 내부 1곳으로 한정.
bash -n으로 셈 구문 검증.
H-20 test_h20_pane_id_regex_accepts_alphanumeric_workspace (§1.2 회귀 방지)
- 헬퍼를 직접 호출해
w1E:p1,w10:p3형태가 통과하고notapane,w1:p, 빈 문자열이 거부되는지 확인. - 이 테스트가 없으면 버그 리포트 원문의
^w[0-9]+:p[0-9]+$가 나중에 다시 들어와도 아무도 모른다.
4.2 tests/test_b19_headless_reconcile_fixes.py — 소스/헬퍼 단위 테스트 (신규 D-4 ~ D-7)
기존 _run_lib_helpers() 헬퍼(L119–130)와 소스 문자열 검사 패턴을 재사용한다.
D-4 test_resolve_pane_id_fails_cleanly_under_set_e
set -euo pipefail아래에서_resolve_herdr_pane_id nonexistent || true가 셸을 죽이지 않고 빈 출력 + rc 1을 내는지.
D-5 test_send_keys_safe_returns_3_when_paste_buffer_fails (ISSUE-1)
_sks_herdr스텁:agent prompt→ rc 1,paste-buffer→ rc 1,send-keys호출은 파일에 기록.- 단언:
send_keys_saferc == 3, 기록 파일에C-m없음. - 추가 단언:
delete-buffer가 호출되었음(버퍼 누수 방지).
D-6 test_no_substring_matching_remains_in_lib_sh (ISSUE-2) — 소스 가드
lib.sh전문에서 정규식\bin tn\b및agent"\) in tn패턴 매치가 0건._resolve_herdr_pane_id본문에^w[A-Za-z0-9]+:p[A-Za-z0-9]+$가 존재.
D-7 test_paste_buffer_branch_never_submits (ISSUE-1) — 소스 가드
lib.sh에서paste-buffer)~ 다음;;구간을 잘라내어Enter,C-m,pane run,agent prompt문자열이 없음을 단언.- 행위 테스트(H-15)와 중복처럼 보이지만 층이 다르다: H-15는 목 경유라 목이 잘못되면 함께 침묵하고, D-7은 소스를 직접 본다.
4.3 회귀 범위
has-session 의미 변경(3.3)과 capture-pane 경로 변경(3.5)이 넓게 파급되므로, 다음을 우선 확인한 뒤 전체를 돌린다.
.venv/bin/python -m pytest tests/test_herdr_shim_contract.py \
tests/test_b19_headless_reconcile_fixes.py \
tests/test_b8_send_keys_verification.py \
tests/test_orc_onboard.py tests/test_workspace_scope.py \
tests/test_uuid_target.py tests/test_sanitize_and_mock_errors.py -q
이후 전체:
.venv/bin/python -m pytest -q
기준선 (실측): 작업 착수 시점(4bbd03b)에 test_herdr_shim_contract.py + test_b19_headless_reconcile_fixes.py + test_workspace_scope.py = 17 passed / 9.2s.
전체 스위트(pytest -q) = 397 passed / 502.00s (8분 21초). tier3/tier4 e2e가 herdr 목 프로세스를 다수 포크하는 것이 소요 시간의 대부분이다. 구현자는 다음을 전제로 시간을 배분할 것:
- 반복 개발 루프에서는 4.3의 우선 범위만 사용한다(약 10초).
- 전체 회귀는 S7에서 1회만, 백그라운드로 돌린다(약 8~9분).
- 완료 기준은 397 + 신규 10건 = 407 passed이다. 이보다 적으면 기존 테스트가 사라졌거나 무성 skip된 것이므로 반드시 원인을 규명할 것.
pytest-timeout은 이 저장소에 설치되어 있지 않다 —--timeout=플래그는unrecognized arguments로 즉시 실패한다. 필요하면requirements-dev.txt에 추가하거나 셸 레벨에서 제어할 것.
5. 실행 순서 (권장 커밋 단위)
| 단계 | 내용 | 검증 |
|---|---|---|
| S1 | [C10] conftest.py 목 정합화 (M1~M5) |
기존 스위트 실행 → 여기서 깨지는 테스트가 곧 은폐되어 있던 결함의 목록. 목록을 기록한다 |
| S2 | [C1] _resolve_herdr_pane_id / _herdr_ws_scope 신설 (호출부 변경 없음) |
bash -n, H-20, D-4 |
| S3 | [C2][C3] 부분 매칭 제거 (ISSUE-2) | H-17, D-6 |
| S4 | [C5][C6][C7] 분기 리팩터링 (ISSUE-5) | H-19, 4.3 우선 범위 |
| S5 | [C8][C9] paste-buffer 재작성 + 실패 전파 (ISSUE-1) |
H-15, H-16, D-5, D-7 |
| S6 | [C4] HERDR_WORKSPACE_ID 전파 (ISSUE-3) |
H-18 |
| S7 | 전체 회귀 | pytest -q 전량 그린 |
S2를 S3~S6보다 먼저 두는 이유: 헬퍼만 추가하고 아무도 호출하지 않는 상태는 정의상 무해하므로, 이 시점에 스위트가 깨지면 원인이 히어독 구문 오류 하나로 좁혀진다.
6. 리스크 및 완화
| # | 리스크 | 영향 | 완화 |
|---|---|---|---|
| R1 | 셈은 lib.sh 내부 히어독이라 편집 시 $, 백틱, 따옴표 이스케이프 사고가 나기 쉽다 |
셈 전체가 구문 오류로 죽어 모든 herdr 호출 실패 | <<'EOF'(따옴표 히어독)이므로 셸 확장은 일어나지 않음. 각 단계마다 _init_herdr_isolation 실행 후 생성물에 bash -n |
| R2 | has-session이 라벨 전용 패인을 "존재"로 판정 (3.3) |
create_session.sh가 죽은 패인을 재사용해 세션 재생성 실패 |
4.3 우선 회귀 범위에 test_orc_onboard.py 포함. 문제 시 라벨 폴백을 MAM_HAS_SESSION_PANE_FALLBACK=1 옵트인으로 격하 |
| R3 | capture-pane이 agent read → pane read로 전환 |
출력 포맷 차이로 _pane_quiescent / 준비 토큰 매칭 실패 |
§1.5에서 실기 검증 완료(양쪽 평문). test_b8_send_keys_verification.py로 회귀 확인 |
| R4 | HERDR_WORKSPACE_ID 하드 필터가 정당한 교차 워크스페이스 조회를 차단 |
다중 워크스페이스 오케스트레이션 기능 상실 | 자동 추론을 채택하지 않음(3.4). 미설정 = 기존 전역 동작. H-18 실행 C가 이를 고정 |
| R5 | 구버전 herdr가 pane list --workspace를 모름 |
플래그 오류로 조회 실패 | 클라이언트측 workspace_id 필터를 이중으로 유지(3.1). `2>/dev/null |
| R6 | paste-buffer 실패 전파(3.7)로 이전엔 "성공"이던 경로가 rc 3을 반환 |
상위 오케스트레이터가 새로 실패를 보게 됨 | 이는 의도된 결과다 — 기존 "성공"은 텍스트가 전달되지 않은 무성 실패였다. 다만 배포 노트에 명시 |
7. 완료 기준 (Definition of Done)
.agents/skills/lib.sh에_resolve_herdr_pane_id가 정확히 1회 정의되고,has-session/kill-session/capture-pane/send-keys/paste-buffer5개 분기가 모두 이를 호출한다.lib.sh전문에agent ... in tn형태의 부분 문자열 매칭이 0건이다.paste-buffer분기가pane send-text만 사용하고Enter/C-m/pane run/agent prompt를 사용하지 않는다.HERDR_WORKSPACE_ID가 설정되면 패인 해석이 해당 워크스페이스로 제한되고, 미설정 시 기존 전역 동작이 보존된다.tests/test_herdr_shim_contract.py에 H-15 ~ H-20,tests/test_b19_headless_reconcile_fixes.py에 D-4 ~ D-7이 추가되고 전부 통과한다..venv/bin/python -m pytest -q가 407 passed(기준선 397 + 신규 10)로 전량 그린. 실패가 남으면 원인과 함께 명시 보고(무성 skip 금지).bash -n이lib.sh및 생성된.mam/shim/herdr양쪽에서 통과한다.
8. 계획 범위 밖으로 남기는 항목
- ISSUE-4 — 커밋
4bbd03b에서 이미 수정 완료.test_agent_start_success_tokens_exclude_startup_timeout이 회귀를 고정하고 있어 추가 작업 없음. list-panes분기 — 인라인 파서를 유지한다.pane_id단독이 아니라cwd/agent를 함께 파싱하므로_resolve_herdr_pane_id로 대체 불가이며, ISSUE-5의 대상 목록에도 없다.bug_report.md의 업스트림 반영 — 본 작업은 이 저장소의lib.sh에 한정한다.multi-agent-mux업스트림 배포는 별도 릴리스 절차(VERSIONS.md,deploy/)를 따른다.