Align conftest mock herdr and unit test assertions with Claude's simplified session-based isolation and native herdr command updates
This commit is contained in:
+159
-66
@@ -22,7 +22,7 @@ if [ -z "${BASH_VERSION:-}" ]; then
|
||||
return 1 2>/dev/null || exit 1
|
||||
fi
|
||||
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORKSPACE_ROOT="$(cd "$SKILL_DIR/../.." && pwd)"
|
||||
WORKSPACE_ROOT="${WORKSPACE_ROOT:-$(cd "$SKILL_DIR/../.." && pwd)}"
|
||||
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
|
||||
|
||||
# Add common Homebrew and local binary paths to PATH to ensure they are available in non-interactive shells
|
||||
@@ -100,6 +100,46 @@ while [ "${1:-}" = "-L" ]; do
|
||||
shift 2
|
||||
done
|
||||
|
||||
# Herdr's real isolation boundary is `--session <name>` (a whole separate
|
||||
# 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
|
||||
_MAM_SESSION=""
|
||||
else
|
||||
_mam_session_running=$("$REAL_HERDR" session list --json 2>/dev/null | MAM_SESS="$_MAM_SESSION" python3 -c "
|
||||
import sys, json, os
|
||||
try:
|
||||
d = json.loads(sys.stdin.read())
|
||||
name = os.environ.get('MAM_SESS', '')
|
||||
print('1' if any(s.get('name') == name and s.get('running') for s in d.get('sessions', [])) else '0')
|
||||
except Exception:
|
||||
print('0')
|
||||
" 2>/dev/null || echo "0")
|
||||
if [ "$_mam_session_running" != "1" ]; then
|
||||
# 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).
|
||||
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
|
||||
sleep 0.25
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
_real_herdr() {
|
||||
if [ -n "$_MAM_SESSION" ]; then
|
||||
"$REAL_HERDR" --session "$_MAM_SESSION" "$@"
|
||||
else
|
||||
"$REAL_HERDR" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
wrapper_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
cmd="${1:-}"
|
||||
if [ -z "$cmd" ]; then
|
||||
@@ -125,7 +165,7 @@ case "$cmd" in
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
"$REAL_HERDR" agent get "$sess" >/dev/null 2>&1
|
||||
_real_herdr agent get "$sess" >/dev/null 2>&1
|
||||
;;
|
||||
new-session)
|
||||
name="" ws="" run_cmd=""
|
||||
@@ -154,8 +194,7 @@ case "$cmd" in
|
||||
if [ -z "$run_cmd" ]; then
|
||||
run_cmd="${SHELL:-/bin/bash}"
|
||||
fi
|
||||
"$REAL_HERDR" workspace create --label "${HERDR_SERVER_NAME:-default}" --cwd "${ws:-.}" >/dev/null 2>&1 || true
|
||||
|
||||
|
||||
parsed=$(python3 -c "
|
||||
import sys, shlex
|
||||
cmd = sys.argv[1]
|
||||
@@ -177,26 +216,22 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
|
||||
final_cmd=$(echo "$parsed" | tail -n +2)
|
||||
fi
|
||||
|
||||
ws_label="${HERDR_SERVER_NAME:-default}"
|
||||
ws_id="w1"
|
||||
if [ "$ws_label" != "default" ]; then
|
||||
ws_id=$("$REAL_HERDR" workspace list 2>/dev/null | WS_LABEL="$ws_label" python3 -c "
|
||||
import sys, os, json
|
||||
# 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.
|
||||
ws_id=$(_real_herdr workspace create --cwd "${ws:-.}" --no-focus 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
ws_label = os.environ.get('WS_LABEL', '')
|
||||
data = json.loads(sys.stdin.read())
|
||||
res = data.get('result', data)
|
||||
for ws in res.get('workspaces', []):
|
||||
if ws.get('label') == ws_label or ws.get('workspace_id') == ws_label:
|
||||
print(ws.get('workspace_id'))
|
||||
sys.exit(0)
|
||||
d = json.loads(sys.stdin.read())
|
||||
print(d.get('result', {}).get('workspace', {}).get('workspace_id', ''))
|
||||
except Exception:
|
||||
pass
|
||||
")
|
||||
ws_id="${ws_id:-w1}"
|
||||
fi
|
||||
ws_id="${ws_id:-w1}"
|
||||
|
||||
eval "\"$REAL_HERDR\" agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $env_flags -- $final_cmd"
|
||||
eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $env_flags -- $final_cmd"
|
||||
;;
|
||||
kill-session)
|
||||
sess=""
|
||||
@@ -213,8 +248,20 @@ except Exception:
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
"$REAL_HERDR" session stop "$sess" >/dev/null 2>&1 || true
|
||||
"$REAL_HERDR" session delete "$sess" >/dev/null 2>&1 || true
|
||||
# NOTE: herdr's `session` verb operates on whole server instances (see
|
||||
# `herdr session list`), not individual agent panes — `session stop/delete
|
||||
# "$sess"` with a MAM session name always fails (silently, via `|| true`).
|
||||
# Resolve the real pane_id via `agent get` and close just that pane instead.
|
||||
pane_id=$(_real_herdr agent get "$sess" 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
print(json.load(sys.stdin).get('result', {}).get('agent', {}).get('pane_id', ''))
|
||||
except Exception:
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
if [ -n "$pane_id" ]; then
|
||||
_real_herdr pane close "$pane_id" >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
list-panes)
|
||||
sess="" format=""
|
||||
@@ -239,26 +286,57 @@ except Exception:
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
info=$("$REAL_HERDR" agent get "$sess" 2>/dev/null || true)
|
||||
# NOTE: `herdr agent get <target>` returns {"result": {"agent": {...}}} —
|
||||
# there is no top-level "pane" key, and no "pid" field at all (only
|
||||
# "pane_id", herdr's own wN:pN identifier). The real OS pid requires a
|
||||
# second call to `pane process-info --pane <pane_id>`.
|
||||
info=$(_real_herdr agent get "$sess" 2>/dev/null || true)
|
||||
pane_id="" cwd="" cmd=""
|
||||
if [ -n "$info" ]; then
|
||||
if [ "$format" = '#{pane_pid}|#{pane_current_path}|#{pane_current_command}' ]; then
|
||||
python3 -c "
|
||||
parsed=$(python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.loads(sys.stdin.read())
|
||||
p = d.get('pane', d)
|
||||
print(f\"{p.get('pid', '')}|{p.get('cwd', '')}|{p.get('command', p.get('argv', ''))}\")
|
||||
a = json.loads(sys.stdin.read()).get('result', {}).get('agent', {})
|
||||
except Exception:
|
||||
a = {}
|
||||
print(a.get('pane_id', ''))
|
||||
print(a.get('cwd', ''))
|
||||
print(a.get('agent', ''))
|
||||
" <<< "$info")
|
||||
pane_id=$(sed -n '1p' <<< "$parsed")
|
||||
cwd=$(sed -n '2p' <<< "$parsed")
|
||||
cmd=$(sed -n '3p' <<< "$parsed")
|
||||
fi
|
||||
pid=""
|
||||
if [ -n "$pane_id" ]; then
|
||||
case "$format" in
|
||||
*pane_pid*)
|
||||
pid=$(_real_herdr pane process-info --pane "$pane_id" 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
pi = json.loads(sys.stdin.read()).get('result', {}).get('process_info', {})
|
||||
fg = pi.get('foreground_processes') or []
|
||||
print(fg[0]['pid'] if fg else pi.get('shell_pid', ''))
|
||||
except Exception:
|
||||
pass
|
||||
" <<< "$info"
|
||||
elif [ "$format" = '#{pane_pid}' ]; then
|
||||
python3 -c "import sys, json; d = json.loads(sys.stdin.read()); print(d.get('pane', d).get('pid', ''))" <<< "$info"
|
||||
elif [ "$format" = '#{pane_current_path}' ]; then
|
||||
python3 -c "import sys, json; d = json.loads(sys.stdin.read()); print(d.get('pane', d).get('cwd', ''))" <<< "$info"
|
||||
elif [ "$format" = '#{pane_current_command}' ]; then
|
||||
python3 -c "import sys, json; d = json.loads(sys.stdin.read()); print(d.get('pane', d).get('command', d.get('pane', d).get('argv', '')))" <<< "$info"
|
||||
fi
|
||||
")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
case "$format" in
|
||||
'#{pane_pid}|#{pane_current_path}|#{pane_current_command}')
|
||||
printf '%s|%s|%s\n' "$pid" "$cwd" "$cmd"
|
||||
;;
|
||||
'#{pane_pid}')
|
||||
printf '%s\n' "$pid"
|
||||
;;
|
||||
'#{pane_current_path}')
|
||||
printf '%s\n' "$cwd"
|
||||
;;
|
||||
'#{pane_current_command}')
|
||||
printf '%s\n' "$cmd"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
capture-pane)
|
||||
sess=""
|
||||
@@ -275,7 +353,7 @@ except Exception:
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
"$REAL_HERDR" agent read "$sess" --source visible --lines 100 2>/dev/null || true
|
||||
_real_herdr agent read "$sess" --source visible --lines 100 2>/dev/null || true
|
||||
;;
|
||||
send-keys)
|
||||
sess="" key=""
|
||||
@@ -295,7 +373,18 @@ except Exception:
|
||||
if [ "$key" = "C-m" ] || [ "$key" = "Enter" ]; then
|
||||
key="Enter"
|
||||
fi
|
||||
"$REAL_HERDR" pane send-keys "$sess" "$key" >/dev/null 2>&1 || true
|
||||
# `pane send-keys` requires a real pane_id ("wN:pN"), not an agent name —
|
||||
# resolve it via `agent get` first (agent-level commands accept names).
|
||||
pane_id=$(_real_herdr agent get "$sess" 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
print(json.load(sys.stdin).get('result', {}).get('agent', {}).get('pane_id', ''))
|
||||
except Exception:
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
if [ -n "$pane_id" ]; then
|
||||
_real_herdr pane send-keys "$pane_id" "$key" >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
set-buffer)
|
||||
text=""
|
||||
@@ -330,14 +419,14 @@ except Exception:
|
||||
esac
|
||||
done
|
||||
if [ -f "$wrapper_dir/tmp_buffer" ]; then
|
||||
"$REAL_HERDR" agent send "$sess" "$(cat "$wrapper_dir/tmp_buffer")" >/dev/null 2>&1 || true
|
||||
_real_herdr agent send "$sess" "$(cat "$wrapper_dir/tmp_buffer")" >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
delete-buffer)
|
||||
rm -f "$wrapper_dir/tmp_buffer"
|
||||
;;
|
||||
ls)
|
||||
"$REAL_HERDR" agent list 2>/dev/null | python3 -c "
|
||||
_real_herdr agent list 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
@@ -353,7 +442,7 @@ except Exception:
|
||||
" || true
|
||||
;;
|
||||
*)
|
||||
"$REAL_HERDR" "$cmd" "$@"
|
||||
_real_herdr "$cmd" "$@"
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
@@ -424,10 +513,15 @@ print(json.dumps(d, ensure_ascii=False))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# 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"
|
||||
local raw_label
|
||||
raw_label=$(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
|
||||
name = os.environ['SESSION_NAME']
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
@@ -436,26 +530,7 @@ for s in d.get('herdr_sessions', []):
|
||||
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
|
||||
sys.exit(0)
|
||||
print(os.environ.get('HERDR_SERVER_NAME', 'default'))
|
||||
")
|
||||
if [ "$raw_label" = "default" ]; then
|
||||
echo "w1"
|
||||
else
|
||||
local ws_id
|
||||
ws_id=$(herdr workspace list 2>/dev/null | RAW_LABEL="$raw_label" python3 -c "
|
||||
import sys, os, json
|
||||
try:
|
||||
raw_label = os.environ.get('RAW_LABEL', '')
|
||||
data = json.loads(sys.stdin.read())
|
||||
res = data.get('result', data)
|
||||
for ws in res.get('workspaces', []):
|
||||
if ws.get('label') == raw_label or ws.get('workspace_id') == raw_label:
|
||||
print(ws.get('workspace_id'))
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
pass
|
||||
")
|
||||
echo "${ws_id:-w1}"
|
||||
fi
|
||||
"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -489,6 +564,10 @@ derive_session_name() {
|
||||
fi
|
||||
slug="$(printf '%s-%s' "$parent" "$work" | tr '[:upper:]' '[:lower:]' | tr '_' '-')"
|
||||
slug="$(printf '%s' "$slug" | tr -cd 'a-zA-Z0-9-')"
|
||||
slug="$(printf '%s' "$slug" | sed 's/^-*//')"
|
||||
if [ -z "$slug" ]; then
|
||||
slug="ws"
|
||||
fi
|
||||
printf '%s-creator-%s' "$slug" "$agent"
|
||||
}
|
||||
|
||||
@@ -1440,8 +1519,18 @@ _pane_dialog_open() {
|
||||
send_keys_safe() {
|
||||
local sess="$1" text="$2" job_id="${3:-adhoc}"
|
||||
local marker pre_submit deadline try
|
||||
# Verification token: last 24 chars of the last non-empty line (multi-line safe).
|
||||
marker=$(printf '%s' "$text" | tr -d '\r' | awk 'NF {line=$0} END {print line}' | tail -c 24)
|
||||
# Verification token: last 24 *characters* (not bytes — `tail -c` can split a
|
||||
# multi-byte UTF-8 char, e.g. Korean, producing a marker that can never match
|
||||
# the properly-decoded rendered pane text) of the last non-empty line.
|
||||
marker=$(printf '%s' "$text" | tr -d '\r' | awk 'NF {line=$0} END {print line}' | python3 -c "import sys; print(sys.stdin.read().rstrip('\n')[-24:], end='')")
|
||||
# Whitespace-normalized copy for matching: some TUIs (e.g. cline's own
|
||||
# message rendering) don't just soft-wrap with a bare newline — they add a
|
||||
# leading-space "hanging indent" on the continuation line too, so removing
|
||||
# only '\n' still leaves an extra space that breaks an exact literal match.
|
||||
# Matching with all whitespace collapsed out sidesteps wrap formatting
|
||||
# entirely, whatever shape it takes.
|
||||
local marker_norm
|
||||
marker_norm=$(printf '%s' "$marker" | tr -d '[:space:]')
|
||||
|
||||
_pane_quiescent "$sess" || { echo "send_keys_safe: pane never quiesced ($sess)" >&2; return 1; }
|
||||
|
||||
@@ -1470,7 +1559,11 @@ send_keys_safe() {
|
||||
pane_content=$(_pane_capture "$sess")
|
||||
if printf '%s\n' "$pane_content" | grep -Eq "Pasted text|paste again to expand"; then
|
||||
was_popup=1
|
||||
elif ! printf '%s\n' "$pane_content" | grep -Fq "$marker"; then
|
||||
# Strip ALL whitespace before matching — the rendered pane soft-wraps long
|
||||
# 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 ! printf '%s' "$pane_content" | tr -d '[:space:]' | grep -Fq "$marker_norm"; then
|
||||
echo "send_keys_safe: paste not visible ($sess)" >&2
|
||||
return 3
|
||||
fi
|
||||
@@ -1485,7 +1578,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" ] && ! _pane_tail "$sess" 3 | grep -Fq "$marker" && [ "$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" && \
|
||||
|
||||
Reference in New Issue
Block a user