fix(lib): finalize herdr 0.7.4 contract refactor and layout policy

This commit is contained in:
2026-08-13 20:50:03 +09:00
parent 2bd59fce9b
commit 657a749f43
7 changed files with 1159 additions and 107 deletions
+144 -43
View File
@@ -114,8 +114,7 @@ _init_herdr_isolation() {
export PATH="$wrapper_dir:$PATH"
fi
local tmp_file
tmp_file=$(mktemp "$wrapper_dir/herdr.XXXXXX")
local tmp_file="$wrapper_dir/herdr.tmp.$$.$RANDOM"
cat <<'EOF' > "$tmp_file"
#!/usr/bin/env bash
# Herdr-to-Herdr translation shim wrapper
@@ -167,9 +166,11 @@ except Exception:
# blocks a normal interactive `herdr --session <name>` launch from inside
# an existing herdr pane (which is how MAM's own agents usually run).
nohup "$REAL_HERDR" --session "$_MAM_SESSION" server >/dev/null 2>&1 &
_mam_server_pid=$!
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
kill -0 "$_mam_server_pid" 2>/dev/null || break
sleep 0.25
done
fi
@@ -287,51 +288,133 @@ kind = sys.argv[2]
try:
tokens = shlex.split(cmd)
if tokens and (tokens[0] == kind or tokens[0].endswith('/' + kind)):
tokens = tokens[1:]
if len(tokens) > 1 and not tokens[1].startswith('-'):
tokens = tokens[1:]
print(' '.join(shlex.quote(t) for t in tokens))
except Exception:
print(cmd)
" "$final_cmd" "$kind" 2>/dev/null || echo "$final_cmd")
# Check if there is an existing workspace inside this session (to reuse and split view)
existing_ws=$(_real_herdr workspace list 2>/dev/null | TARGET_CWD="${ws:-.}" python3 -c "
# W1: Resolve existing_ws by querying pane list for matching CWD (since WorkspaceInfo has no cwd key)
existing_ws=$(_real_herdr pane list 2>/dev/null | TARGET_CWD="${ws:-.}" python3 -c "
import sys, json, os
target_ws = os.environ.get('TARGET_CWD', '')
try:
target_abs = os.path.realpath(target_ws) if target_ws else ''
d = json.loads(sys.stdin.read())
wss = d.get('result', {}).get('workspaces', [])
matched_id = ''
for w in wss:
w_cwd = os.path.realpath(w.get('cwd', '')) if w.get('cwd') else ''
if target_abs and w_cwd == target_abs:
matched_id = w.get('workspace_id', '')
panes = d.get('result', {}).get('panes', [])
matched_ws = ''
for p in panes:
p_cwd = os.path.realpath(p.get('cwd', '')) if p.get('cwd') else ''
if target_abs and p_cwd == target_abs and p.get('workspace_id'):
matched_ws = p.get('workspace_id')
break
print(matched_id)
print(matched_ws)
except Exception:
pass
")
" 2>/dev/null || echo "")
ws_id=""
split_arg=""
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 "
# W2a: Determine split direction policy via pane layout
sample_pane=$(_real_herdr pane list 2>/dev/null | TARGET_WS="$existing_ws" python3 -c "
import sys, json, os
target_ws = os.environ.get('TARGET_WS', '')
try:
d = json.loads(sys.stdin.read())
panes = d.get('result', {}).get('panes', [])
for p in panes:
if p.get('workspace_id') == target_ws and p.get('pane_id'):
print(p.get('pane_id'))
break
except Exception:
pass
" 2>/dev/null || echo "")
split_dir=""
if [ -n "$sample_pane" ]; then
split_dir=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null | MAM_MIN_COLS="${MAM_MIN_PANE_COLS:-60}" MAM_MIN_ROWS="${MAM_MIN_PANE_ROWS:-20}" python3 -c "
import sys, json, os
min_cols = int(os.environ.get('MAM_MIN_COLS', 60))
min_rows = int(os.environ.get('MAM_MIN_ROWS', 20))
try:
d = json.loads(sys.stdin.read()).get('result', {})
focused_id = d.get('focused_pane_id', '')
panes = d.get('panes', [])
anchor = None
for p in panes:
if p.get('pane_id') == focused_id:
anchor = p.get('rect', {})
break
if not anchor and panes:
anchor = panes[0].get('rect', {})
if anchor:
w = anchor.get('width', 0)
h = anchor.get('height', 0)
if w // 2 >= min_cols:
print('right')
elif h // 2 >= min_rows:
print('down')
else:
print('overflow')
except Exception:
pass
" 2>/dev/null || echo "")
fi
if [ "$split_dir" = "right" ] || [ "$split_dir" = "down" ]; then
ws_id="$existing_ws"
split_arg="--split $split_dir"
elif [ "$split_dir" = "overflow" ]; then
# W2b: Overflow threshold reached — force create fresh workspace
existing_ws=""
else
ws_id="$existing_ws"
split_arg="--split right"
fi
fi
if [ -z "$existing_ws" ]; then
ws_json=$(_real_herdr workspace create --cwd "${ws:-.}" --no-focus 2>/dev/null || echo "")
ws_id=$(echo "$ws_json" | python3 -c "
import sys, json
try:
d = json.loads(sys.stdin.read())
print(d.get('result', {}).get('workspace', {}).get('workspace_id', ''))
res = d.get('result', {})
w_obj = res.get('workspace', {})
print(w_obj.get('workspace_id') or res.get('workspace_id', ''))
except Exception:
pass
")
ws_id="${ws_id:-w1}"
" 2>/dev/null || echo "")
fi
# Attempt --kind syntax first, fall back to explicit binary name if required by herdr CLI
res=$(eval "_real_herdr agent start \"$name\" --kind \"$kind\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $split_flag $env_flags -- $final_cmd" 2>&1 || true)
if ! echo "$res" | grep -q "agent_started"; then
res=$(eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $split_flag $env_flags -- $kind $final_cmd" 2>&1 || true)
# W5/W6: Backoff retries (0.5 -> 1 -> 2), abort immediately on usage or unknown flag errors
res=""
success=0
backoffs=(0.5 1 2)
for i in $(seq 0 2); do
if [ -n "$ws_id" ]; then
res=$(eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $split_arg $env_flags -- $final_cmd" 2>&1 || true)
else
res=$(eval "_real_herdr agent start \"$name\" --cwd \"${ws:-.}\" $split_arg $env_flags -- $final_cmd" 2>&1 || true)
fi
if echo "$res" | grep -q "agent_started"; then
success=1
break
fi
if echo "$res" | grep -qiE "^usage:|unknown option|unknown flag"; then
break
fi
if [ "$i" -lt 2 ]; then
sleep "${backoffs[$i]}"
fi
done
if [ "$success" -ne 1 ]; then
echo "$res" >&2
exit 1
fi
;;
kill-session)
@@ -363,6 +446,7 @@ except Exception:
if [ -n "$pane_id" ]; then
_real_herdr pane close "$pane_id" >/dev/null 2>&1 || true
fi
_real_herdr kill-session -t "$sess" >/dev/null 2>&1 || true
;;
list-panes)
sess="" format=""
@@ -397,7 +481,10 @@ except Exception:
parsed=$(python3 -c "
import sys, json
try:
a = json.loads(sys.stdin.read()).get('result', {}).get('agent', {})
raw = sys.stdin.read()
if '{' in raw:
raw = raw[raw.index('{'):]
a = json.loads(raw).get('result', {}).get('agent', {})
except Exception:
a = {}
print(a.get('pane_id', ''))
@@ -425,18 +512,21 @@ except Exception:
esac
fi
case "$format" in
'#{pane_pid}|#{pane_current_path}|#{pane_current_command}')
*'#{pane_pid}'*'#{pane_current_path}'*|*#{pane_pid}*#{pane_current_path}*)
printf '%s|%s|%s\n' "$pid" "$cwd" "$cmd"
;;
'#{pane_pid}')
*'#{pane_pid}'*|*#{pane_pid}*)
printf '%s\n' "$pid"
;;
'#{pane_current_path}')
*'#{pane_current_path}'*|*#{pane_current_path}*)
printf '%s\n' "$cwd"
;;
'#{pane_current_command}')
*'#{pane_current_command}'*|*#{pane_current_command}*)
printf '%s\n' "$cmd"
;;
*)
printf '%s|%s|%s\n' "$pid" "$cwd" "$cmd"
;;
esac
;;
capture-pane)
@@ -485,6 +575,8 @@ except Exception:
" 2>/dev/null)
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
;;
set-buffer)
@@ -524,10 +616,13 @@ except Exception:
# a DOT-prefixed temp ('.buf_sks_....tmp'), which 'buf_sks_*' cannot match.
# Without it a temp orphaned by SIGKILL between write and rename would leak
# forever, exactly like the buffers this sweep exists to reclaim.
find "$wrapper_dir" \( -name 'buf_sks_*' -o -name '.buf_sks_*' \) \
buffer_dir="${WORKSPACE_ROOT:+$WORKSPACE_ROOT/.mam/buffers}"
buffer_dir="${buffer_dir:-${TMPDIR:-/tmp}/mam_buffers}"
mkdir -p "$buffer_dir" 2>/dev/null || true
find "$buffer_dir" \( -name 'buf_sks_*' -o -name '.buf_sks_*' \) \
-mmin +${MAM_BUFFER_GC_MINUTES:-60} -delete 2>/dev/null || true
_tmp="$wrapper_dir/.$buf.$$.tmp"
if ! { echo -n "$text" > "$_tmp" && mv -f "$_tmp" "$wrapper_dir/$buf"; }; then
_tmp="$buffer_dir/.$buf.$$.tmp"
if ! { echo -n "$text" > "$_tmp" && mv -f "$_tmp" "$buffer_dir/$buf"; }; then
rm -f "$_tmp"
echo "Error: failed to write buffer $buf" >&2
exit 1
@@ -558,10 +653,12 @@ except Exception:
*) shift ;;
esac
done
if [ -f "$wrapper_dir/$buf" ]; then
_real_herdr agent send "$sess" "$(cat "$wrapper_dir/$buf")" >/dev/null 2>&1 || true
buffer_dir="${WORKSPACE_ROOT:+$WORKSPACE_ROOT/.mam/buffers}"
buffer_dir="${buffer_dir:-${TMPDIR:-/tmp}/mam_buffers}"
if [ -f "$buffer_dir/$buf" ]; then
_real_herdr agent send "$sess" "$(cat "$buffer_dir/$buf")" >/dev/null 2>&1 || true
else
echo "Error: buffer $buf not found" >&2
echo "Error: buffer $buf not found ($buffer_dir/$buf)" >&2
exit 1
fi
;;
@@ -581,7 +678,9 @@ except Exception:
*) shift ;;
esac
done
rm -f "$wrapper_dir/$buf"
buffer_dir="${WORKSPACE_ROOT:+$WORKSPACE_ROOT/.mam/buffers}"
buffer_dir="${buffer_dir:-${TMPDIR:-/tmp}/mam_buffers}"
rm -f "$buffer_dir/$buf"
;;
ls)
format=""
@@ -695,8 +794,8 @@ for name, pid in rows:
;;
esac
EOF
chmod +x "$tmp_file"
mv -f "$tmp_file" "$wrapper_dir/herdr"
chmod +x "$tmp_file" 2>/dev/null || true
mv -f "$tmp_file" "$wrapper_dir/herdr" 2>/dev/null || rm -f "$tmp_file" 2>/dev/null || true
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
export PATH="$wrapper_dir:$PATH"
fi
@@ -783,8 +882,10 @@ ws = os.environ.get('TARGET_WS', '').strip()
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_server') or s.get('herdr_workspace') or 'default')
sys.exit(0)
val = s.get('herdr_session') or s.get('herdr_server') or s.get('herdr_workspace')
if val and val != 'default':
print(val)
sys.exit(0)
fallback = ''
if ws:
abs_ws = os.path.abspath(ws)
@@ -1916,7 +2017,7 @@ start_watchdog() {
# and ensure it runs with $workdir as cwd to anchor relative log paths.
local orig_pwd="$PWD"
cd "$workdir" || return 1
nohup bash "$monitor_script" --subscribe --idle-timeout 0 >> "$log_file" 2>&1 &
nohup bash "$monitor_script" --subscribe --idle-timeout 0 </dev/null >> "$log_file" 2>&1 &
pid=$!
cd "$orig_pwd" || return 1
fi
@@ -2129,7 +2230,7 @@ send_keys_safe() {
local cur_content
cur_content=$(_pane_capture "$sess")
# Hardened Submission Checks
if printf '%s\n' "$cur_content" | grep -Eq "● |✽ |[A-Za-z]+ing…|[A-Za-z]+ing\.\.\.|esc to interrupt"; then
if printf '%s\n' "$cur_content" | grep -Fq "esc to interrupt" || printf '%s\n' "$cur_content" | grep -Eq "● |✽ |[A-Za-z]+ing"; then
return 0
fi
if [ "$was_popup" = "0" ] && ! _pane_tail "$sess" 3 | tr -d '[:space:]' | grep -Fq "$marker_norm" && [ "$cur_content" != "$pre_submit" ]; then
@@ -505,7 +505,6 @@ if herdr_confirmed:
pm = pane_meta(name, srv)
if not pm:
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)):