Clean up all tmux occurrences from dev skills and integrate herdr wrapper translation shim with dynamic env and workspace ID parsing
This commit is contained in:
+176
-91
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# Single source of truth for the four things that were inconsistently
|
||||
# re-implemented across create/resume/delete/monitor (REVIEW.md §4.1):
|
||||
# - derive_session_name : the tmux session slug (P0-A)
|
||||
# - derive_session_name : the herdr session slug (P0-A)
|
||||
# - atomic_dump_yaml : SQLite db transaction + temp+rename + .bak + validate (P0-B)
|
||||
# - env_python : env-safe Python (no heredoc injection) (P0-B / P1-B)
|
||||
# - find_workspace_uuid : workspace-SCOPED resume id lookup (P0-C)
|
||||
@@ -25,6 +25,13 @@ SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
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
|
||||
for dir in /home/linuxbrew/.linuxbrew/bin /home/linuxbrew/.linuxbrew/sbin "$HOME/.local/bin" "$HOME/.npm-global/bin"; do
|
||||
if [ -d "$dir" ] && [[ ":$PATH:" != *":$dir:"* ]]; then
|
||||
export PATH="$dir:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
# Central TUI dialog and readiness validation tokens (OP-6)
|
||||
_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel|Resuming the full session|Resume from summary'
|
||||
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|projects'
|
||||
@@ -35,31 +42,49 @@ CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$HOME/.claude/projects}"
|
||||
LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tmux Server Isolation support
|
||||
# Herdr Server Isolation support
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paths to exclude when resolving the real tmux binary (shim/wrapper dirs).
|
||||
_TMUX_SHIM_DIR_PATTERN="${_TMUX_SHIM_DIR_PATTERN:-/multi-agent-tmux-shim/}"
|
||||
_TMUX_SKILLS_BIN_PATTERN="${_TMUX_SKILLS_BIN_PATTERN:-/.agents/skills/.bin}"
|
||||
# Paths to exclude when resolving the real herdr binary (shim/wrapper dirs).
|
||||
_HERDR_SHIM_DIR_PATTERN="${_HERDR_SHIM_DIR_PATTERN:-/multi-agent-herdr-shim/}"
|
||||
_HERDR_SKILLS_BIN_PATTERN="${_HERDR_SKILLS_BIN_PATTERN:-/.agents/skills/.bin}"
|
||||
|
||||
TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}"
|
||||
HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}"
|
||||
|
||||
_resolve_real_tmux_path() {
|
||||
_REAL_TMUX_PATH="tmux"
|
||||
export _REAL_TMUX_PATH
|
||||
_resolve_real_herdr_path() {
|
||||
_REAL_HERDR_PATH="herdr"
|
||||
export _REAL_HERDR_PATH
|
||||
}
|
||||
|
||||
_init_tmux_isolation() {
|
||||
_init_herdr_isolation() {
|
||||
local wrapper_dir="$WORKSPACE_ROOT/.mam/shim"
|
||||
mkdir -p "$wrapper_dir"
|
||||
cat <<'EOF' > "$wrapper_dir/tmux"
|
||||
cat <<'EOF' > "$wrapper_dir/herdr"
|
||||
#!/usr/bin/env bash
|
||||
# Herdr-to-Tmux translation shim wrapper
|
||||
# Herdr-to-Herdr translation shim wrapper
|
||||
set -euo pipefail
|
||||
|
||||
# Dynamic real herdr path resolution to prevent infinite recursion
|
||||
_resolve_real_herdr() {
|
||||
local dir save_ifs="$IFS" real_path=""
|
||||
IFS=:
|
||||
for dir in $PATH; do
|
||||
if [[ "$dir" != *".mam/shim"* ]] && [[ "$dir" != *"-shim"* ]] && [ -x "$dir/herdr" ]; then
|
||||
real_path="$dir/herdr"
|
||||
break
|
||||
fi
|
||||
done
|
||||
IFS="$save_ifs"
|
||||
if [ -z "$real_path" ]; then
|
||||
real_path="herdr"
|
||||
fi
|
||||
echo "$real_path"
|
||||
}
|
||||
REAL_HERDR=$(_resolve_real_herdr)
|
||||
|
||||
wrapper_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
cmd="${1:-}"
|
||||
if [ -z "$cmd" ]; then
|
||||
echo "tmux shim: no command specified" >&2
|
||||
echo "herdr shim: no command specified" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
@@ -73,7 +98,7 @@ case "$cmd" in
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
herdr agent get "$sess" >/dev/null 2>&1
|
||||
"$REAL_HERDR" agent get "$sess" >/dev/null 2>&1
|
||||
;;
|
||||
new-session)
|
||||
name="" ws="" run_cmd=""
|
||||
@@ -85,9 +110,48 @@ case "$cmd" in
|
||||
*) run_cmd="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
herdr workspace create --label "${TMUX_SERVER_NAME:-default}" --cwd "${ws:-.}" >/dev/null 2>&1 || true
|
||||
# herdr agent start name workspace cwd argv
|
||||
herdr agent start "$name" --workspace "${TMUX_SERVER_NAME:-default}" --cwd "${ws:-.}" -- $run_cmd
|
||||
"$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]
|
||||
tokens = shlex.split(cmd)
|
||||
env_flags = []
|
||||
binary_tokens = []
|
||||
for tok in tokens:
|
||||
if '=' in tok and not binary_tokens:
|
||||
env_flags.append('--env ' + tok)
|
||||
else:
|
||||
binary_tokens.append(shlex.quote(tok))
|
||||
print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
|
||||
" "$run_cmd" 2>/dev/null || echo "")
|
||||
|
||||
env_flags=""
|
||||
final_cmd="$run_cmd"
|
||||
if [ -n "$parsed" ]; then
|
||||
env_flags=$(echo "$parsed" | head -n 1 | tr '\t' ' ')
|
||||
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 | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
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)
|
||||
except Exception:
|
||||
pass
|
||||
")
|
||||
ws_id="${ws_id:-w1}"
|
||||
fi
|
||||
|
||||
eval "\"$REAL_HERDR\" agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $env_flags -- $final_cmd"
|
||||
;;
|
||||
kill-session)
|
||||
sess=""
|
||||
@@ -97,8 +161,8 @@ case "$cmd" in
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
herdr session stop "$sess" >/dev/null 2>&1 || true
|
||||
herdr session delete "$sess" >/dev/null 2>&1 || true
|
||||
"$REAL_HERDR" session stop "$sess" >/dev/null 2>&1 || true
|
||||
"$REAL_HERDR" session delete "$sess" >/dev/null 2>&1 || true
|
||||
;;
|
||||
list-panes)
|
||||
sess="" format=""
|
||||
@@ -109,7 +173,7 @@ case "$cmd" in
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
info=$(herdr agent get "$sess" 2>/dev/null || true)
|
||||
info=$("$REAL_HERDR" agent get "$sess" 2>/dev/null || true)
|
||||
if [ -n "$info" ]; then
|
||||
if [ "$format" = '#{pane_pid}|#{pane_current_path}|#{pane_current_command}' ]; then
|
||||
python3 -c "
|
||||
@@ -138,7 +202,7 @@ except Exception:
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
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=""
|
||||
@@ -151,7 +215,7 @@ except Exception:
|
||||
if [ "$key" = "C-m" ] || [ "$key" = "Enter" ]; then
|
||||
key="Enter"
|
||||
fi
|
||||
herdr pane send-keys "$sess" "$key" >/dev/null 2>&1 || true
|
||||
"$REAL_HERDR" pane send-keys "$sess" "$key" >/dev/null 2>&1 || true
|
||||
;;
|
||||
set-buffer)
|
||||
text=""
|
||||
@@ -172,51 +236,53 @@ except Exception:
|
||||
esac
|
||||
done
|
||||
if [ -f "$wrapper_dir/tmp_buffer" ]; then
|
||||
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)
|
||||
herdr session list --json 2>/dev/null | python3 -c "
|
||||
"$REAL_HERDR" agent list 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
for s in json.load(sys.stdin):
|
||||
print(f\"{s['name']}|{s.get('created_at_epoch', 0)}\")
|
||||
data = json.load(sys.stdin)
|
||||
res = data.get('result', data)
|
||||
for a in res.get('agents', []):
|
||||
print(f\"{a['name']}|0\")
|
||||
except Exception:
|
||||
pass
|
||||
" || true
|
||||
;;
|
||||
*)
|
||||
true
|
||||
"$REAL_HERDR" "$cmd" "$@"
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$wrapper_dir/tmux"
|
||||
chmod +x "$wrapper_dir/herdr"
|
||||
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
|
||||
export PATH="$wrapper_dir:$PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
mam_tmux() {
|
||||
_init_tmux_isolation
|
||||
"$WORKSPACE_ROOT/.mam/shim/tmux" "$@"
|
||||
mam_herdr() {
|
||||
_init_herdr_isolation
|
||||
"$WORKSPACE_ROOT/.mam/shim/herdr" "$@"
|
||||
}
|
||||
|
||||
_tmux() {
|
||||
mam_tmux "$@"
|
||||
_herdr() {
|
||||
mam_herdr "$@"
|
||||
}
|
||||
|
||||
tmux() {
|
||||
mam_tmux "$@"
|
||||
herdr() {
|
||||
mam_herdr "$@"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_tmux_server <session_name>
|
||||
# resolve_herdr_server <session_name>
|
||||
#
|
||||
# Query agent-sessions.yaml to find the tmux_server associated with a session.
|
||||
# Fallback to TMUX_SERVER_NAME or 'default' if not registered or field is missing.
|
||||
# Query agent-sessions.yaml to find the herdr_server associated with a session.
|
||||
# Fallback to HERDR_SERVER_NAME or 'default' if not registered or field is missing.
|
||||
# Prints the resolved server name on stdout.
|
||||
# ---------------------------------------------------------------------------
|
||||
load_state_json() {
|
||||
@@ -236,7 +302,7 @@ try:
|
||||
cursor = conn.execute('SELECT data FROM sessions')
|
||||
for r in cursor.fetchall():
|
||||
db_sessions.append(json.loads(r[0]))
|
||||
d['tmux_sessions'] = db_sessions
|
||||
d['herdr_sessions'] = db_sessions
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
conn.close()
|
||||
@@ -259,24 +325,43 @@ print(json.dumps(d, ensure_ascii=False))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
resolve_tmux_server() {
|
||||
resolve_herdr_workspace() {
|
||||
local session_name="$1"
|
||||
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
|
||||
local raw_label
|
||||
raw_label=$(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', '{}'))
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('name') == name:
|
||||
print(s.get('tmux_server', 'default'))
|
||||
print(s.get('herdr_workspace', 'default'))
|
||||
sys.exit(0)
|
||||
print(os.environ.get('TMUX_SERVER_NAME', 'default'))
|
||||
"
|
||||
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 | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
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
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# derive_session_name <workspace> <agent>
|
||||
#
|
||||
# THE single source of truth for the tmux session name. Rule:
|
||||
# THE single source of truth for the herdr session name. Rule:
|
||||
# slug = the two trailing path components of the absolute workspace,
|
||||
# '_' -> '-', lowercased, joined with '-'
|
||||
# name = "<slug>-creator-<agent>"
|
||||
@@ -417,33 +502,33 @@ db_path = os.path.splitext(yaml_path)[0] + '.db'
|
||||
def _validate(d):
|
||||
if not isinstance(d, dict):
|
||||
raise SystemExit("VALIDATE: top-level is not a mapping")
|
||||
sessions = d.get('tmux_sessions', [])
|
||||
sessions = d.get('herdr_sessions', [])
|
||||
if not isinstance(sessions, list):
|
||||
raise SystemExit("VALIDATE: tmux_sessions is not a list")
|
||||
raise SystemExit("VALIDATE: herdr_sessions is not a list")
|
||||
valid = {'running', 'terminated', 'archived', 'stopped'}
|
||||
for i, s in enumerate(sessions):
|
||||
if not isinstance(s, dict):
|
||||
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] not a mapping")
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] not a mapping")
|
||||
if not s.get('name') or not s.get('status'):
|
||||
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] missing name/status")
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] missing name/status")
|
||||
if s.get('role') is not None and (not isinstance(s['role'], str) or not s['role'].strip()):
|
||||
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} role must be a non-empty string")
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} role must be a non-empty string")
|
||||
if s['status'] not in valid:
|
||||
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}")
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}")
|
||||
if not isinstance(s.get('pane'), dict):
|
||||
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} missing pane")
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} missing pane")
|
||||
iso = s.get('isolation')
|
||||
if iso is not None:
|
||||
if not isinstance(iso, dict) or not iso.get('uuid') or not iso.get('root'):
|
||||
raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} isolation block requires uuid/root")
|
||||
raise SystemExit(f"VALIDATE: herdr_sessions[{i}] {s.get('name')!r} isolation block requires uuid/root")
|
||||
|
||||
def get_terminal_set(d):
|
||||
return {s.get('name'): s.get('status') for s in d.get('tmux_sessions', []) if s.get('status') in ('stopped', 'terminated', 'archived')}
|
||||
return {s.get('name'): s.get('status') for s in d.get('herdr_sessions', []) if s.get('status') in ('stopped', 'terminated', 'archived')}
|
||||
|
||||
def get_all_sessions_status(d):
|
||||
import hashlib
|
||||
res = {}
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
ser = json.dumps(s, sort_keys=True)
|
||||
res[name] = hashlib.sha256(ser.encode('utf-8')).hexdigest()
|
||||
@@ -484,7 +569,7 @@ try:
|
||||
else:
|
||||
d = {}
|
||||
|
||||
# Assemble d['tmux_sessions'] from sessions table if table contains data
|
||||
# Assemble d['herdr_sessions'] from sessions table if table contains data
|
||||
db_sessions = []
|
||||
cursor = conn.execute('SELECT name, status, pane_cwd, data FROM sessions')
|
||||
for s_row in cursor.fetchall():
|
||||
@@ -497,9 +582,9 @@ try:
|
||||
db_sessions.append(s_data)
|
||||
|
||||
if db_sessions:
|
||||
d['tmux_sessions'] = db_sessions
|
||||
elif 'tmux_sessions' not in d:
|
||||
d['tmux_sessions'] = []
|
||||
d['herdr_sessions'] = db_sessions
|
||||
elif 'herdr_sessions' not in d:
|
||||
d['herdr_sessions'] = []
|
||||
|
||||
old_sessions = get_all_sessions_status(d)
|
||||
old_roles = {s.get('name'): s.get('role') for s in db_sessions if s.get('role')}
|
||||
@@ -508,7 +593,7 @@ try:
|
||||
exec(compile(os.environ['AGENT_SESSIONS_MUTATION'], '<mutation>', 'exec'), globals())
|
||||
|
||||
# Role immutability check
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
if name in old_roles and s.get('role') != old_roles[name]:
|
||||
raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}")
|
||||
@@ -516,7 +601,7 @@ try:
|
||||
# ID Uniqueness Check (T2)
|
||||
running_keys = ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']
|
||||
id_to_session = {}
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('status') == 'running':
|
||||
s_name = s.get('name')
|
||||
for k in running_keys:
|
||||
@@ -529,11 +614,11 @@ try:
|
||||
_validate(d)
|
||||
|
||||
# Separate globals and sessions for normalization
|
||||
d_state = {k: v for k, v in d.items() if k != 'tmux_sessions'}
|
||||
d_state = {k: v for k, v in d.items() if k != 'herdr_sessions'}
|
||||
conn.execute('REPLACE INTO state (id, data) VALUES (1, ?)', (json.dumps(d_state),))
|
||||
|
||||
current_names = []
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name')
|
||||
status = s.get('status')
|
||||
pane_cwd = (s.get('pane') or {}).get('cwd', '')
|
||||
@@ -599,7 +684,7 @@ PYEOF
|
||||
# Workspace-SCOPED resolution of the resume UUID (P0-C). It NEVER returns a
|
||||
# global agent_identities id unless that id's project_cwd matches THIS
|
||||
# workspace. Resolution order:
|
||||
# 1) tmux_sessions[] row whose pane.cwd == this workspace -> per-row own id
|
||||
# 1) herdr_sessions[] row whose pane.cwd == this workspace -> per-row own id
|
||||
# (claude_session_id_own / agy_conversation_id_own)
|
||||
# 2) on-disk scan scoped to this workspace
|
||||
# (claude: ~/.claude/projects/<key>/*.jsonl ; agy: last_conversations.json[cwd])
|
||||
@@ -668,7 +753,7 @@ except Exception:
|
||||
d = {}
|
||||
|
||||
running_ids = set()
|
||||
for s_item in d.get('tmux_sessions', []):
|
||||
for s_item in d.get('herdr_sessions', []):
|
||||
if s_item.get('status') == 'running':
|
||||
if target and s_item.get('name') == target:
|
||||
continue
|
||||
@@ -687,7 +772,7 @@ def emit(u):
|
||||
|
||||
# Fetch all sessions matching this workspace
|
||||
sessions = []
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws:
|
||||
sessions.append(s)
|
||||
|
||||
@@ -1013,7 +1098,7 @@ try:
|
||||
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
|
||||
if row:
|
||||
d = json.loads(row[0])
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('name') == name and s.get('status') == 'stopped':
|
||||
print(f"stopped_at={s.get('stopped_at', '?')}")
|
||||
raise SystemExit(0)
|
||||
@@ -1022,7 +1107,7 @@ try:
|
||||
elif os.path.exists(yaml_path):
|
||||
with open(yaml_path) as f:
|
||||
d = yaml.safe_load(f) or {}
|
||||
for s in d.get('tmux_sessions', []):
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('name') == name and s.get('status') == 'stopped':
|
||||
print(f"stopped_at={s.get('stopped_at', '?')}")
|
||||
raise SystemExit(0)
|
||||
@@ -1139,9 +1224,9 @@ start_watchdog() {
|
||||
# Waits up to 15 seconds for the agent's TUI to render its welcome screen.
|
||||
wait_for_tui_ready() {
|
||||
local sess="$1" agent="$2"
|
||||
local local_tmux="tmux" i
|
||||
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
|
||||
local_tmux="tmux -L $TMUX_SERVER_NAME"
|
||||
local local_herdr="herdr" i
|
||||
if [ -n "${HERDR_SERVER_NAME:-}" ] && [ "$HERDR_SERVER_NAME" != "default" ]; then
|
||||
local_herdr="herdr -L $HERDR_SERVER_NAME"
|
||||
fi
|
||||
|
||||
for i in {1..30}; do
|
||||
@@ -1150,7 +1235,7 @@ wait_for_tui_ready() {
|
||||
continue
|
||||
fi
|
||||
local content
|
||||
content=$($local_tmux capture-pane -p -t "$sess" 2>/dev/null || echo "")
|
||||
content=$($local_herdr capture-pane -p -t "$sess" 2>/dev/null || echo "")
|
||||
if [ -n "$content" ]; then
|
||||
case "$agent" in
|
||||
claude)
|
||||
@@ -1186,7 +1271,7 @@ wait_for_tui_ready() {
|
||||
}
|
||||
|
||||
# inject_instructions <session_name> <instructions> [job_id]
|
||||
# Injects instructions into the tmux session using paste-buffer and C-m.
|
||||
# Injects instructions into the herdr session using paste-buffer and C-m.
|
||||
# Delegates to send_keys_safe to ensure focus and renderer correctness (MS-5).
|
||||
inject_instructions() {
|
||||
send_keys_safe "$1" "$2" "${3:-onboard}"
|
||||
@@ -1200,23 +1285,23 @@ inject_instructions() {
|
||||
# Callers MUST handle non-zero.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Server-aware tmux (same isolation rule as inject_instructions).
|
||||
_sks_tmux() {
|
||||
mam_tmux "$@"
|
||||
# Server-aware herdr (same isolation rule as inject_instructions).
|
||||
_sks_herdr() {
|
||||
mam_herdr "$@"
|
||||
}
|
||||
|
||||
_pane_capture() { _sks_tmux capture-pane -p -t "$1" 2>/dev/null || echo ""; }
|
||||
_pane_capture() { _sks_herdr capture-pane -p -t "$1" 2>/dev/null || echo ""; }
|
||||
|
||||
# Bottom-N *content* lines: capture-pane -p pads the viewport with trailing
|
||||
# blank rows; window on non-blank lines or top-anchored dialogs are invisible.
|
||||
_pane_tail() { _pane_capture "$1" | grep -v '^[[:space:]]*$' | tail -n "${2:-20}"; }
|
||||
|
||||
# _wait_session_gone <sess> [max_sec=5]
|
||||
# Reactive loop to wait until a tmux session goes away (happy path returns early).
|
||||
# Reactive loop to wait until a herdr session goes away (happy path returns early).
|
||||
_wait_session_gone() {
|
||||
local sess="$1" max="${2:-5}" i
|
||||
for ((i = 0; i < max * 4; i++)); do
|
||||
_sks_tmux has-session -t "$sess" 2>/dev/null || return 0
|
||||
_sks_herdr has-session -t "$sess" 2>/dev/null || return 0
|
||||
sleep 0.25
|
||||
done
|
||||
return 1
|
||||
@@ -1263,7 +1348,7 @@ send_keys_safe() {
|
||||
deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} ))
|
||||
while _pane_dialog_open "$sess"; do
|
||||
if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then
|
||||
_sks_tmux send-keys -t "$sess" Escape
|
||||
_sks_herdr send-keys -t "$sess" Escape
|
||||
sleep 1
|
||||
fi
|
||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||
@@ -1273,11 +1358,11 @@ send_keys_safe() {
|
||||
sleep 2
|
||||
done
|
||||
|
||||
_sks_tmux set-buffer -b "sks_$job_id" "$text"
|
||||
_sks_tmux paste-buffer -b "sks_$job_id" -t "$sess"
|
||||
_sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true
|
||||
_sks_herdr set-buffer -b "sks_$job_id" "$text"
|
||||
_sks_herdr paste-buffer -b "sks_$job_id" -t "$sess"
|
||||
_sks_herdr delete-buffer -b "sks_$job_id" 2>/dev/null || true
|
||||
if [[ "$sess" =~ "agy" ]]; then
|
||||
_sks_tmux send-keys -t "$sess" C-m
|
||||
_sks_herdr send-keys -t "$sess" C-m
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
@@ -1292,7 +1377,7 @@ send_keys_safe() {
|
||||
|
||||
for try in 1 2 3; do
|
||||
pre_submit=$(_pane_capture "$sess")
|
||||
_sks_tmux send-keys -t "$sess" C-m
|
||||
_sks_herdr send-keys -t "$sess" C-m
|
||||
sleep "$try"
|
||||
local cur_content
|
||||
cur_content=$(_pane_capture "$sess")
|
||||
@@ -1321,13 +1406,13 @@ handle_startup_dialogs() {
|
||||
while [ "$waited" -lt "$timeout" ]; do
|
||||
pane=$(_pane_tail "$sess" 20)
|
||||
if printf '%s\n' "$pane" | grep -q 'Do you trust the files'; then
|
||||
_sks_tmux send-keys -t "$sess" Enter
|
||||
_sks_herdr send-keys -t "$sess" Enter
|
||||
elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then
|
||||
_sks_tmux send-keys -t "$sess" Down
|
||||
_sks_herdr send-keys -t "$sess" Down
|
||||
sleep 0.3
|
||||
_sks_tmux send-keys -t "$sess" Enter
|
||||
_sks_herdr send-keys -t "$sess" Enter
|
||||
elif printf '%s\n' "$pane" | grep -q 'Resuming the full session'; then
|
||||
_sks_tmux send-keys -t "$sess" Enter
|
||||
_sks_herdr send-keys -t "$sess" Enter
|
||||
elif printf '%s\n' "$pane" | grep -Eq "$_MAM_READY_TOKENS_CLAUDE"; then
|
||||
return 0
|
||||
fi
|
||||
@@ -1338,4 +1423,4 @@ handle_startup_dialogs() {
|
||||
}
|
||||
|
||||
# Auto-initialize the herdr translation shim wrapper on library source
|
||||
_init_tmux_isolation
|
||||
_init_herdr_isolation
|
||||
|
||||
Reference in New Issue
Block a user