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:
+158
-65
@@ -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,7 +194,6 @@ 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
|
||||
@@ -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" && \
|
||||
|
||||
@@ -66,6 +66,8 @@ When running multiple agent sessions alongside other workflows (e.g., cmux, Kanb
|
||||
|
||||
To prevent this, you can run this skill inside an **isolated herdr server** using the `HERDR_SERVER_NAME` environment variable or the `--herdr-server <name>` flag (opt-in).
|
||||
|
||||
Under the hood this now maps to a real, separate herdr **session** (`herdr --session <name>` — its own socket, its own `agent list`/`workspace list`, completely invisible to the default session and vice versa), not just a workspace label inside the same server. `lib.sh`'s shim bootstraps the named session's server headlessly (`herdr --session <name> server`, backgrounded) the first time it's needed, and scopes every subsequent herdr call to it automatically — this headless bootstrap is what lets it work even when the skill itself is running from inside another herdr-managed pane (a plain interactive `herdr --session <name>` launch is blocked there by herdr's "nested herdr is disabled" guard; headless `server` mode isn't).
|
||||
|
||||
### How to use
|
||||
1. **Via Environment Variable**:
|
||||
```bash
|
||||
@@ -95,8 +97,9 @@ tmc ls # Lists only your multi-agent sessions
|
||||
```
|
||||
|
||||
### Safety Rules (Pitfall 29 Summary)
|
||||
- Never use global server termination commands like `herdr kill-server` or `herdr kill-session -a` as they will destroy all sessions on that server (including your own workspace sessions if they share the server).
|
||||
- By using an isolated server via `HERDR_SERVER_NAME`, your agent sessions are completely separated from your default user workspace, ensuring 0% interference.
|
||||
- Never use global server termination commands like `herdr server stop` as they will destroy every workspace/agent on that server (including your own workspace sessions if they share the server). (`kill-server`/`kill-session -a` are tmux-era names that don't exist in herdr's real CLI — see Pitfalls below.)
|
||||
- By using an isolated server via `HERDR_SERVER_NAME`, your agent sessions are completely separated from your default user workspace, ensuring 0% interference — this is now backed by a genuinely separate `herdr` session/socket, not merely a workspace label.
|
||||
- To deliberately tear down an *entire* isolated group at once (all its workspaces and agents), use `herdr session stop <HERDR_SERVER_NAME>` followed by `herdr session delete <HERDR_SERVER_NAME>` — this only affects that named session, never the default one.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -137,7 +140,9 @@ sleep 6
|
||||
PANE_PID=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}')
|
||||
PANE_CWD=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_path}')
|
||||
PANE_CMD=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}')
|
||||
HERDR_EPOCH=$(herdr list-sessions -F '#{session_created}' -t "$SESSION_NAME" 2>/dev/null | head -1)
|
||||
# `herdr list-sessions` doesn't exist (real or shimmed) — we just spawned this
|
||||
# session ourselves, so stamp the epoch locally instead of round-tripping herdr.
|
||||
HERDR_EPOCH=$(date +%s)
|
||||
```
|
||||
|
||||
## Registering the session in agent-sessions.yaml
|
||||
@@ -162,9 +167,13 @@ After spawn, append a new `herdr_sessions[]` entry to `.mam/agent-sessions.yaml`
|
||||
plan: <from TUI status>
|
||||
account: <from TUI status>
|
||||
version: <from TUI status>
|
||||
start_command: <the exact herdr new-session command used>
|
||||
attach_command: "herdr attach -t <SESSION_NAME>"
|
||||
kill_command: "herdr kill-session -t <SESSION_NAME>"
|
||||
start_command: "HERDR_SERVER_NAME=<herdr_server> herdr new-session -d -s <SESSION_NAME> -x 140 -y 40 -c <WORKSPACE> <CMD_FULL>"
|
||||
attach_command: "HERDR_SERVER_NAME=<herdr_server> herdr agent attach <SESSION_NAME>"
|
||||
kill_command: "HERDR_SERVER_NAME=<herdr_server> herdr kill-session -t <SESSION_NAME>"
|
||||
# All three require `source .agents/skills/lib.sh` first — `new-session`/`kill-session`
|
||||
# are tmux-compat pseudo-commands the shim translates, and `HERDR_SERVER_NAME` is what
|
||||
# the shim reads to route to the right isolated herdr *session* (real `herdr` has no
|
||||
# env-var-based scoping of its own; `herdr_server: default` needs no prefix at all).
|
||||
```
|
||||
|
||||
`cmd_full` per agent (this is the actual command line in the pane, not the resume command):
|
||||
@@ -196,11 +205,15 @@ The script handles the YAML append, pane capture, and the `last_visible_status`
|
||||
After spawn + YAML append:
|
||||
|
||||
```bash
|
||||
# 1. herdr session is alive
|
||||
herdr has-session -t "$SESSION_NAME" && echo OK || echo MISSING
|
||||
# 1. herdr session is alive (real native command — no lib.sh needed)
|
||||
herdr agent get "$SESSION_NAME" >/dev/null 2>&1 && echo OK || echo MISSING
|
||||
|
||||
# 2. pane has the expected cmd + cwd
|
||||
herdr list-panes -t "$SESSION_NAME" -F 'cmd=#{pane_current_command} cwd=#{pane_current_path}'
|
||||
herdr agent get "$SESSION_NAME" | python3 -c "
|
||||
import sys, json
|
||||
a = json.load(sys.stdin)['result']['agent']
|
||||
print(f\"cmd={a['agent']} cwd={a['cwd']}\")
|
||||
"
|
||||
|
||||
# 3. agent-sessions.yaml has the new entry
|
||||
python3 -c "
|
||||
@@ -211,13 +224,15 @@ assert '$SESSION_NAME' in names, 'session not registered'
|
||||
print('OK:', names)
|
||||
"
|
||||
|
||||
# 4. Optional: check the TUI status via capture-pane
|
||||
herdr capture-pane -t "$SESSION_NAME" -p -S -20 # TUI ready = agent banner visible, no dialog text
|
||||
# 4. Optional: check the TUI status (real native command)
|
||||
herdr agent read "$SESSION_NAME" --source visible --lines 20 # TUI ready = agent banner visible, no dialog text
|
||||
```
|
||||
|
||||
> `herdr has-session` / `herdr list-panes` / `herdr capture-pane` above are tmux-compat pseudo-commands only understood after `source .agents/skills/lib.sh` (see `Workflow`) — the real `herdr` binary has no such subcommands. The block above uses the real `herdr agent get`/`herdr agent read` equivalents so it also works standalone.
|
||||
|
||||
## When NOT to use this skill
|
||||
|
||||
- **Resuming an old conversation** → `multi-agent-mux-resume`
|
||||
- **Killing an existing session** → `multi-agent-mux-stop`
|
||||
- **Just attaching to an existing session** → `herdr attach -t <name>` (no skill needed)
|
||||
- **Just attaching to an existing session** → `herdr agent attach <name>` (no skill needed)
|
||||
- **One-shot print mode (claude -p "...")** → no herdr needed; use `claude-code` skill's print mode
|
||||
|
||||
@@ -231,7 +231,11 @@ HERDR_EPOCH=$(date +%s)
|
||||
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
# 시작 명령 (CMD_FULL 은 spawn 전에 isolation 디스패치를 반영해 확정됨 — T4)
|
||||
START_CMD="herdr agent start \"$SESSION_NAME\" --workspace \"${HERDR_SERVER_NAME:-default}\" --cwd \"$WORKSPACE\" -- $CMD_FULL"
|
||||
# NOTE: this must match what `spawn()` actually ran above — env-var-driven
|
||||
# isolation (HERDR_SERVER_NAME picked up by the lib.sh shim), not a
|
||||
# `--workspace <label>` flag (real `herdr agent start --workspace` wants an
|
||||
# actual workspace id like "w2", which HERDR_SERVER_NAME is not).
|
||||
START_CMD="HERDR_SERVER_NAME=${HERDR_SERVER_NAME:-default} herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
|
||||
|
||||
# If --onboard is specified, automatically build the onboarding prompt
|
||||
if [ "$ONBOARD" = "1" ] && [ -z "$SUBMIT_JOB_PROMPT" ]; then
|
||||
@@ -316,8 +320,13 @@ entry = {
|
||||
'cwd': os.environ['PANE_CWD'],
|
||||
},
|
||||
'start_command': os.environ['START_CMD'],
|
||||
'attach_command': f'herdr session attach {name}',
|
||||
'kill_command': f'herdr session stop {name} && herdr session delete {name}',
|
||||
# NOTE: `herdr session attach/stop/delete` operate on whole herdr
|
||||
# *sessions* (server instances, e.g. "default") — NOT on an individual
|
||||
# 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}',
|
||||
}
|
||||
|
||||
# T5: isolation 블록 영속화 (all-L2) — resume/resolve/stop 이 재적용의 단일 소스로 사용
|
||||
|
||||
@@ -92,4 +92,5 @@ Job lifecycle execution events are persistently mirrored to an append-only log u
|
||||
- **Subscribe-Before-Publish**: The subscriber must be running before the agent starts publishing. The `submit` command handles this automatically by launching the subscriber in the background first.
|
||||
- **Fresh job_id Propagation**: Make sure the worker agent receives the correct `JOB_ID` generated for the current run, rather than reusing stale IDs from previous sessions.
|
||||
- **Brief delivery via file path**: For long or complex prompts, write the instructions to a file (e.g. `/tmp/task-brief.md`) and pass a short prompt pointing to the file path to prevent terminal buffer overflows.
|
||||
- **Prompts injected into a live agent session MUST be English, ASCII-only, and short** — this is exactly what `--prompt`/the `instructions` string sent to `run_agent()` end up as. Any Korean (or other non-ASCII) content the task needs to convey must go in a markdown brief file (e.g. `.mam/jobs/<id>/brief.md`, written in Korean is fine) that the injected prompt merely tells the agent to read. Two independent bugs in `send_keys_safe`'s paste-verification (in `lib.sh`) made this matter in practice: (a) its marker was taken with a byte-based `tail -c 24`, which can slice a multi-byte UTF-8 (e.g. Korean) character in half; (b) the rendered pane soft-wraps long lines at the terminal width, which can split the marker across two visual lines. Both are now fixed at the source (character-safe truncation + newline-stripped matching before comparison), but keeping injected prompts short/English/file-referencing is still the cheapest way to avoid ever exercising this edge case at all — it's also simply what `submit`'s own default instruction template already does (see `Core Commands` above).
|
||||
- **Batch Grouping**: Group non-overlapping tasks into batches to parallelize execution across multiple agent sessions, reducing overhead.
|
||||
|
||||
@@ -23,6 +23,13 @@ elif [[ -f "$SCRIPT_DIR/../../.env" ]]; then
|
||||
set -a; source "$SCRIPT_DIR/../../.env"; set +a
|
||||
fi
|
||||
|
||||
# Source EARLY (before any herdr usage in run_agent) — this is what turns
|
||||
# plain `herdr` into the tmux-compat shim (herdr() function) and provides
|
||||
# resolve_herdr_workspace/send_keys_safe. Sourcing it late meant the
|
||||
# has-session pre-flight check below used to hit the real herdr binary with
|
||||
# a nonexistent subcommand and always fail.
|
||||
source "$SCRIPT_DIR/../lib.sh"
|
||||
|
||||
# Pick an interpreter: prefer a project .venv, else python3.
|
||||
pick_python() {
|
||||
local py_bin
|
||||
@@ -429,19 +436,19 @@ run_agent() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local _herdr="herdr"
|
||||
if [ -n "${HERDR_SERVER_NAME:-}" ]; then
|
||||
_herdr="herdr -L $HERDR_SERVER_NAME"
|
||||
fi
|
||||
# Auto-resolve isolation the same way resume/stop/create do — don't rely on
|
||||
# the caller having exported HERDR_SERVER_NAME by hand. This is what lets
|
||||
# delegation reach an agent living in an isolated herdr session (e.g. one
|
||||
# created with --herdr-server) instead of silently looking in "default".
|
||||
export HERDR_SERVER_NAME="$(resolve_herdr_workspace "$sess")"
|
||||
|
||||
if ! $_herdr has-session -t "$sess" 2>/dev/null; then
|
||||
if ! herdr has-session -t "$sess" 2>/dev/null; then
|
||||
echo "ERROR: 에이전트 세션 '$sess'이 존재하지 않습니다. 작업을 위임하기 전에 먼저 에이전트 세션을 기동해 주세요." >&2
|
||||
echo " 팁: 'multi-agent-mux-resume' 또는 'multi-agent-mux-create'를 통해 에이전트를 먼저 생성할 수 있습니다." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check role suitability
|
||||
source "$SCRIPT_DIR/../lib.sh"
|
||||
local sess_role job_role
|
||||
sess_role=$(SESS_NAME="$sess" MAM_STATE_JSON="$(load_state_json)" "$PY" -c "
|
||||
import os, json
|
||||
@@ -499,7 +506,11 @@ else:
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "작업이 세션 '$sess'에 전송되었습니다. (연결하려면: $_herdr session attach $sess)"
|
||||
# NOTE: `herdr session attach` operates on whole herdr *sessions* (server
|
||||
# instances), not an individual agent by its MAM name — `agent attach` is
|
||||
# the real command for that. HERDR_SERVER_NAME is inlined so the printed
|
||||
# command is copy-pasteable in a fresh shell that hasn't sourced lib.sh.
|
||||
echo "작업이 세션 '$sess'에 전송되었습니다. (연결하려면: HERDR_SERVER_NAME=$HERDR_SERVER_NAME herdr agent attach $sess — lib.sh를 source한 셸에서 실행)"
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ metadata:
|
||||
Dispatch a **Kanban worker** (in `goal_mode`) that:
|
||||
|
||||
1. Every ~30s polls the actual state of:
|
||||
- `herdr ls` (which sessions are alive)
|
||||
- `herdr list-panes -t <session> ...` (pane cmd, cwd, pid)
|
||||
- `herdr agent list` (which sessions are alive)
|
||||
- `herdr agent get <session>` (pane cmd, cwd)
|
||||
- `~/.claude/projects/<workspace-key>/*.jsonl` mtime + first-line sessionId
|
||||
- `~/.gemini/antigravity-cli/cache/last_conversations.json` (agy workspace → conversation mapping)
|
||||
- `~/.gemini/antigravity-cli/conversations/<uuid>.db` mtime (agy)
|
||||
@@ -47,7 +47,7 @@ Dispatch a **Kanban worker** (in `goal_mode`) that:
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- One-off interactive session — just check `herdr ls` and read the YAML
|
||||
- One-off interactive session — just check `herdr agent list` and read the YAML
|
||||
- A single, short session — overhead > benefit
|
||||
- You don't have a Kanban dispatcher running
|
||||
|
||||
@@ -69,7 +69,7 @@ hermes kanban create \
|
||||
You are the agent-sessions monitor. Every 30 seconds, do:
|
||||
|
||||
1. Read .mam/agent-sessions.yaml
|
||||
2. Run `herdr ls` and `herdr list-panes -F 'session=#{session_name} pid=#{pane_pid} cmd=#{pane_current_command} cwd=#{pane_current_path}'`
|
||||
2. Run `herdr agent list` and `herdr agent get <session>` for each tracked session name (these are real native herdr commands — do not use tmux-era names like `herdr ls`/`herdr list-panes` outside a shell that has sourced `.agents/skills/lib.sh`)
|
||||
3. For each session in the YAML, check the corresponding herdr state
|
||||
4. For each herdr session matching `*-creator-claude` or `*-creator-agy` that's not in the YAML, register it
|
||||
5. For any drift, call `kanban_comment` with the diff
|
||||
|
||||
@@ -413,6 +413,11 @@ if herdr_confirmed:
|
||||
name = t['name']
|
||||
if name in yaml_session_names:
|
||||
continue
|
||||
workspace_root = os.environ.get('WORKSPACE_ROOT')
|
||||
if not workspace_root:
|
||||
workspace_root = os.path.abspath(os.path.join(os.path.dirname(yaml_path), '..'))
|
||||
if os.path.exists(os.path.join(workspace_root, '.mam', f"purging-{name}")):
|
||||
continue
|
||||
if name.endswith('-creator-claude'):
|
||||
agent = 'claude'
|
||||
elif name.endswith('-creator-agy'):
|
||||
@@ -445,7 +450,7 @@ if herdr_confirmed:
|
||||
'pane': {'index': 0, 'pid': pm['pid'], 'cmd': agent, 'cmd_full': cmd_full, 'cwd': pm['cwd']},
|
||||
# P2: cwd 인용
|
||||
'start_command': f'herdr {server_opt}new-session -d -s "{name}" -x 140 -y 40 -c "{pm["cwd"]}" "{cmd_full}"',
|
||||
'attach_command': f'herdr {server_opt}attach -t {name}',
|
||||
'attach_command': f'herdr {server_opt}agent attach {name}',
|
||||
'kill_command': f'herdr {server_opt}kill-session -t {name}',
|
||||
'last_visible_status': 'running',
|
||||
'last_visible_note': 'auto-registered by monitor',
|
||||
|
||||
@@ -81,7 +81,7 @@ export HERDR_SERVER_NAME="$(resolve_herdr_server "$SESSION_NAME")"
|
||||
# 2. If herdr is alive, attach. Done.
|
||||
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||||
echo "herdr '$SESSION_NAME' already running. Attaching..."
|
||||
exec herdr attach -t "$SESSION_NAME"
|
||||
exec herdr agent attach "$SESSION_NAME"
|
||||
fi
|
||||
|
||||
# 3. Resolve isolation settings for this session (T4/T5 re-apply)
|
||||
@@ -160,8 +160,9 @@ esac
|
||||
bash .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh \
|
||||
--session "$SESSION_NAME" --uuid "$UUID"
|
||||
|
||||
# 5. Attach
|
||||
herdr attach -t "$SESSION_NAME"
|
||||
# 5. Attach (real native command — "attach" isn't in the lib.sh tmux-compat shim,
|
||||
# and real herdr has no `-t` flag here, only a positional target)
|
||||
herdr agent attach "$SESSION_NAME"
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
@@ -175,8 +176,12 @@ herdr attach -t "$SESSION_NAME"
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# 1. herdr alive with the right cmd
|
||||
herdr list-panes -t "$SESSION_NAME" -F 'cmd=#{pane_current_command} cwd=#{pane_current_path}'
|
||||
# 1. herdr alive with the right cmd (real native command, no lib.sh needed)
|
||||
herdr agent get "$SESSION_NAME" | python3 -c "
|
||||
import sys, json
|
||||
a = json.load(sys.stdin)['result']['agent']
|
||||
print(f\"cmd={a['agent']} cwd={a['cwd']}\")
|
||||
"
|
||||
|
||||
# 2. agent-sessions.yaml updated
|
||||
python3 -c "
|
||||
@@ -187,14 +192,19 @@ print(f' status: {s[\"status\"]}')
|
||||
print(f' pane.cmd_full: {s[\"pane\"][\"cmd_full\"]}')
|
||||
"
|
||||
|
||||
# 3. TUI shows resumed conversation (capture-pane to verify)
|
||||
# 3. TUI shows resumed conversation (real native command, no lib.sh needed)
|
||||
sleep 5
|
||||
herdr capture-pane -t "$SESSION_NAME" -p -S -30
|
||||
herdr agent read "$SESSION_NAME" --source visible --lines 30
|
||||
# look for the previous message at top of the buffer (claude) or last_visible_status set (agy)
|
||||
```
|
||||
|
||||
> `herdr list-panes` / `herdr capture-pane` here are tmux-compat pseudo-commands that only
|
||||
> work after `source .agents/skills/lib.sh` (as done in `Workflow` above) — the real `herdr`
|
||||
> binary doesn't have those subcommands. This block uses the real `herdr agent get`/`herdr agent read`
|
||||
> equivalents instead so it also works standalone.
|
||||
|
||||
## When NOT to use this skill
|
||||
|
||||
- **No saved session yet** → `multi-agent-mux-create`
|
||||
- **Killing an existing session** → `multi-agent-mux-stop`
|
||||
- **Just attaching** → `herdr attach -t <name>` (no skill needed)
|
||||
- **Just attaching** → `herdr agent attach <name>` (no skill needed)
|
||||
|
||||
@@ -46,8 +46,8 @@ The script:
|
||||
1. Calls `reconcile.sh --once --emit-diff --dry-run` (read-only; no YAML mutation) for the drift snapshot
|
||||
2. Loads `agent-sessions.yaml` (read-only) to enrich the table
|
||||
3. For each row in `herdr_sessions[]`:
|
||||
- herdr alive? (via `herdr has-session -t <name>`)
|
||||
- pane cmd, cwd (via `herdr list-panes`)
|
||||
- herdr alive? (via `herdr agent get <name>`, real native command — the script sources `lib.sh` internally, which is what lets it also spell this as `herdr has-session -t <name>`)
|
||||
- pane cmd, cwd (via `herdr agent get <name>`, likewise shimmed as `herdr list-panes` internally)
|
||||
- resume UUID on disk? (claude: `$CLAUDE_PROJECT_DIR/<key>/<uuid>.jsonl` with default `~/.claude/projects/`; agy: `$HOME_DIR/.gemini/antigravity-cli/conversations/<uuid>.db` with default `~/.gemini/...`)
|
||||
4. For each herdr session matching `*-creator-*` not in YAML → flag as "unregistered"
|
||||
5. Prints a table (default) or JSON (with `--json`)
|
||||
@@ -99,7 +99,7 @@ lab-paper-pdf2md-creator-claude default running alive clau
|
||||
| Class | Detection | Meaning |
|
||||
|---|---|---|
|
||||
| `A` | YAML `running`, herdr dead | session died without going through `multi-agent-mux-stop`. *Could* auto-terminate but won't — that's `multi-agent-mux-monitor`'s job. |
|
||||
| `B` | herdr alive, not in YAML | ad-hoc session someone started without `multi-agent-mux-create`. Suggest: "use multi-agent-mux-create to register, or herdr kill-session to clean up." |
|
||||
| `B` | herdr alive, not in YAML | ad-hoc session someone started without `multi-agent-mux-create`. Suggest: "use multi-agent-mux-create to register, or multi-agent-mux-stop to clean up." |
|
||||
| `C` | YAML has `claude_session_id_own: null` AND a new *.jsonl exists | new session id materialized; suggest: "run multi-agent-mux-resume or reconcile to register it." |
|
||||
| `D` | YAML has UUID in `agent_identities`, but the on-disk artifact is gone | stale UUID; user should `multi-agent-mux-stop --purge-conversation` to clean up. |
|
||||
|
||||
|
||||
@@ -111,8 +111,9 @@ The script:
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# 1. herdr gone
|
||||
herdr has-session -t "$SESSION_NAME" 2>/dev/null && echo "STILL ALIVE" || echo "OK: herdr gone"
|
||||
# 1. herdr gone (real native command — `herdr has-session` is a lib.sh
|
||||
# tmux-compat pseudo-command and needs `source .agents/skills/lib.sh` first)
|
||||
herdr agent get "$SESSION_NAME" >/dev/null 2>&1 && echo "STILL ALIVE" || echo "OK: herdr gone"
|
||||
|
||||
# 2. YAML has stopped entry
|
||||
python3 -c "
|
||||
@@ -131,6 +132,6 @@ print(f' preserved: pane.pid={s[\"pane\"][\"pid\"]}, cmd={s[\"pane\"][\"cmd\"]}
|
||||
|
||||
## When NOT to use this skill
|
||||
|
||||
- **Just detaching** → `herdr detach` (Ctrl-B d) or just close the terminal. The herdr session keeps running.
|
||||
- **Stopping the agent inside but keeping herdr** → send `Ctrl-C` or `/exit` (claude) / `Ctrl-D` (agy) via `herdr send-keys`. The herdr session stays but the agent process is gone.
|
||||
- **Just detaching** → there's no `herdr detach` CLI command; press the herdr detach keybinding inside the pane, or just close the terminal. The herdr session keeps running.
|
||||
- **Stopping the agent inside but keeping herdr** → send `Ctrl-C` or `/exit` (claude) / `Ctrl-D` (agy) via `herdr agent send <target> <text>` (real native command; `herdr send-keys` is a lib.sh tmux-compat pseudo-command that needs `source .agents/skills/lib.sh` first). The herdr session stays but the agent process is gone.
|
||||
- **Replacing an existing session with a new one** → `multi-agent-mux-stop` first, then `multi-agent-mux-create`.
|
||||
|
||||
@@ -67,9 +67,21 @@ while [ $# -gt 0 ]; do
|
||||
*) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$AGENT" ]; then
|
||||
case "$AGENT" in
|
||||
claude|agy|hermes|cline) ;;
|
||||
*) echo "ERROR: invalid agent type '$AGENT'. Allowed types are: claude, agy, hermes, cline." >&2; exit 2 ;;
|
||||
esac
|
||||
fi
|
||||
[ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; usage; exit 2; }
|
||||
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
||||
|
||||
# Implement the purging-<session> file lock mechanism
|
||||
if [ "$PURGE" = "1" ]; then
|
||||
touch "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"
|
||||
trap 'rm -f "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"' EXIT
|
||||
fi
|
||||
|
||||
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
|
||||
export HERDR_SERVER_NAME
|
||||
|
||||
|
||||
+45
-5
@@ -103,6 +103,11 @@ def save_state():
|
||||
save_state()
|
||||
|
||||
args = sys.argv[1:]
|
||||
if args and args[0] == "--session":
|
||||
if len(args) > 1:
|
||||
args = args[2:]
|
||||
else:
|
||||
args = args[1:]
|
||||
if not args:
|
||||
sys.exit(0)
|
||||
|
||||
@@ -310,7 +315,18 @@ elif cmd1 == "agent":
|
||||
"command": agent_data.get("command", ""),
|
||||
"argv": agent_data.get("command", "")
|
||||
}
|
||||
print(json.dumps({"pane": pane_info}))
|
||||
print(json.dumps({
|
||||
"pane": pane_info,
|
||||
"result": {
|
||||
"agent": {
|
||||
"agent": agent_data.get("agent", "claude"),
|
||||
"status": agent_data.get("status", "running"),
|
||||
"cwd": agent_data.get("cwd", ""),
|
||||
"pane_id": agent_data.get("pane_id", "w1:p1"),
|
||||
"workspace_id": agent_data.get("workspace_id", "w1")
|
||||
}
|
||||
}
|
||||
}))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.stderr.write("Agent " + name + " not found\\\\n")
|
||||
@@ -372,22 +388,46 @@ elif cmd1 == "session":
|
||||
sys.exit(1)
|
||||
|
||||
elif cmd1 == "pane":
|
||||
if len(args) < 4:
|
||||
if len(args) < 3:
|
||||
sys.exit(1)
|
||||
cmd2 = args[1]
|
||||
if cmd2 == "send-keys":
|
||||
if len(args) < 4:
|
||||
sys.exit(1)
|
||||
name = args[2]
|
||||
key = args[3]
|
||||
agents = state.get("agents", {})
|
||||
if name in agents:
|
||||
agents[name]["sent_keys"] = agents[name].get("sent_keys", []) + [key]
|
||||
actual_name = name
|
||||
for a_name, data in agents.items():
|
||||
if data.get("pane_id") == name:
|
||||
actual_name = a_name
|
||||
break
|
||||
if actual_name in agents:
|
||||
agents[actual_name]["sent_keys"] = agents[actual_name].get("sent_keys", []) + [key]
|
||||
if key in ("Enter", "C-m"):
|
||||
agents[name]["buffer"] = agents[name].get("buffer", "") + "\\nesc to interrupt"
|
||||
agents[actual_name]["buffer"] = agents[actual_name].get("buffer", "") + "\\nesc to interrupt"
|
||||
state["agents"] = agents
|
||||
save_state()
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
elif cmd2 == "process-info":
|
||||
pane_id = ""
|
||||
if "--pane" in args:
|
||||
pane_id = args[args.index("--pane") + 1]
|
||||
pid = 9999
|
||||
for name, data in state.get("agents", {}).items():
|
||||
if data.get("pane_id") == pane_id or pane_id == "w1:p1":
|
||||
pid = data.get("pid", 9999)
|
||||
break
|
||||
res = {
|
||||
"process_info": {
|
||||
"foreground_processes": [{"pid": pid}],
|
||||
"shell_pid": pid
|
||||
}
|
||||
}
|
||||
print(json.dumps({"result": res}))
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd1 == "ls":
|
||||
if "-F" in args:
|
||||
|
||||
@@ -108,13 +108,13 @@ def test_resume_resolve_herdr_workspace_default(mam_sandbox):
|
||||
"""Test resolve_herdr_workspace fallback behavior when session is not in YAML."""
|
||||
res = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "non-existent-session")
|
||||
assert res.returncode == 0
|
||||
assert res.stdout.strip() == "w1"
|
||||
assert res.stdout.strip() == "default"
|
||||
|
||||
def test_resume_resolve_herdr_workspace_env(mam_sandbox):
|
||||
"""Test resolve_herdr_workspace fallback to HERDR_SERVER_NAME env var."""
|
||||
res = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "non-existent-session", env={"HERDR_SERVER_NAME": "custom_server"})
|
||||
assert res.returncode == 0
|
||||
assert res.stdout.strip() == "w1" # default mapping to w1 unless resolved from workspace list
|
||||
assert res.stdout.strip() == "custom_server"
|
||||
|
||||
def test_resume_find_workspace_uuid_empty(mam_sandbox):
|
||||
"""Test find_workspace_uuid returns empty string for non-existent workspace."""
|
||||
|
||||
Reference in New Issue
Block a user