fix(lib): finalize herdr 0.7.4 contract refactor and layout policy
This commit is contained in:
+144
-43
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user