refactor: update multi-agent rules to TMUX & refactor MAM skills scripts
- Update .agents/MULTI_AGENT_RULES.md & .ko.md to align with TMUX concepts - Add .agents/INSTALL.md guide for MAM setup and workflows - Clean up legacy markdown reports under .agents/reports/ - Refactor MAM scripts in .agents/skills/ (rename resolve_herdr_session to resolve_herdr_workspace) - Improve workspace pane splitting/re-use logic in lib.sh - Reduce paste safety sleep in send_keys_safe and support linking ~/.claude.json
This commit is contained in:
+47
-63
@@ -58,6 +58,12 @@ _resolve_real_herdr_path() {
|
||||
_init_herdr_isolation() {
|
||||
local wrapper_dir="$WORKSPACE_ROOT/.mam/shim"
|
||||
mkdir -p "$wrapper_dir"
|
||||
if [ -x "$wrapper_dir/herdr" ]; then
|
||||
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
|
||||
export PATH="$wrapper_dir:$PATH"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmp_file
|
||||
tmp_file=$(mktemp "$wrapper_dir/herdr.XXXXXX")
|
||||
@@ -117,7 +123,7 @@ except Exception:
|
||||
# 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).
|
||||
setsid "$REAL_HERDR" --session "$_MAM_SESSION" server </dev/null >/dev/null 2>&1 &
|
||||
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
|
||||
@@ -127,8 +133,12 @@ except Exception:
|
||||
fi
|
||||
|
||||
_real_herdr() {
|
||||
if [ -n "$_MAM_SESSION" ]; then
|
||||
"$REAL_HERDR" --session "$_MAM_SESSION" "$@"
|
||||
local session="${HERDR_SERVER_NAME:-}"
|
||||
if [ "$session" = "default" ]; then
|
||||
session=""
|
||||
fi
|
||||
if [ -n "$session" ]; then
|
||||
"$REAL_HERDR" --session "$session" "$@"
|
||||
else
|
||||
"$REAL_HERDR" "$@"
|
||||
fi
|
||||
@@ -210,52 +220,35 @@ print('\t'.join(env_flags) + '\n' + ' '.join(binary_tokens))
|
||||
final_cmd=$(echo "$parsed" | tail -n +2)
|
||||
fi
|
||||
|
||||
# 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.
|
||||
# Resolve ws_id and split direction:
|
||||
# - If we have up to 3 agents, split 'right' (columns)
|
||||
# - If we have 4 or more agents, split 'down' (rows)
|
||||
res=$(_real_herdr workspace list 2>/dev/null | WS_CWD="${ws:-.}" python3 -c "
|
||||
import sys, json, os
|
||||
# Check if there is an existing workspace inside this session (to reuse and split view)
|
||||
existing_ws=$(_real_herdr workspace list 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
target_label = os.path.basename(os.path.abspath(os.environ.get('WS_CWD', '.')))
|
||||
wlist = json.loads(sys.stdin.read()).get('result', {}).get('workspaces', [])
|
||||
match = None
|
||||
for w in wlist:
|
||||
if w.get('label') == target_label:
|
||||
match = w
|
||||
break
|
||||
if not match and wlist:
|
||||
match = wlist[0]
|
||||
if match:
|
||||
ws_id = match.get('workspace_id', 'w1')
|
||||
pane_count = match.get('pane_count', 0)
|
||||
split_dir = 'right' if pane_count < 3 else 'down'
|
||||
print(f'{ws_id}\t{split_dir}')
|
||||
d = json.loads(sys.stdin.read())
|
||||
wss = d.get('result', {}).get('workspaces', [])
|
||||
if wss:
|
||||
print(wss[0].get('workspace_id', ''))
|
||||
except Exception:
|
||||
pass
|
||||
")
|
||||
ws_id=$(echo "$res" | cut -f1)
|
||||
split_dir=$(echo "$res" | cut -f2)
|
||||
|
||||
if [ -z "$ws_id" ]; then
|
||||
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 "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.loads(sys.stdin.read()).get('result', {}).get('workspace', {}).get('workspace_id', '')
|
||||
print(d)
|
||||
d = json.loads(sys.stdin.read())
|
||||
print(d.get('result', {}).get('workspace', {}).get('workspace_id', ''))
|
||||
except Exception:
|
||||
pass
|
||||
")
|
||||
split_dir="right"
|
||||
ws_id="${ws_id:-w1}"
|
||||
fi
|
||||
ws_id="${ws_id:-w1}"
|
||||
split_dir="${split_dir:-right}"
|
||||
|
||||
eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --split \"$split_dir\" --cwd \"${ws:-.}\" $env_flags -- $final_cmd"
|
||||
eval "_real_herdr agent start \"$name\" --workspace \"$ws_id\" --cwd \"${ws:-.}\" $split_flag $env_flags -- $final_cmd"
|
||||
;;
|
||||
kill-session)
|
||||
sess=""
|
||||
@@ -363,7 +356,7 @@ except Exception:
|
||||
esac
|
||||
;;
|
||||
capture-pane)
|
||||
sess="" src="visible" lines="100"
|
||||
sess=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-t)
|
||||
@@ -374,22 +367,10 @@ except Exception:
|
||||
sess="$2"
|
||||
shift 2
|
||||
;;
|
||||
-S)
|
||||
# tmux `-S -N` (start N lines above the visible viewport) maps to
|
||||
# herdr `--source recent --lines N`. Non-negative or non-numeric
|
||||
# values keep the default visible viewport read.
|
||||
if [ $# -ge 2 ] && [[ "$2" =~ ^-[0-9]+$ ]]; then
|
||||
src="recent"
|
||||
lines="${2#-}"
|
||||
shift 2
|
||||
else
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
_real_herdr agent read "$sess" --source "$src" --lines "$lines" 2>/dev/null || true
|
||||
_real_herdr agent read "$sess" --source visible --lines 100 2>/dev/null || true
|
||||
;;
|
||||
send-keys)
|
||||
sess="" key=""
|
||||
@@ -483,11 +464,7 @@ except Exception:
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$tmp_file"
|
||||
if [ -f "$wrapper_dir/herdr" ] && cmp -s "$tmp_file" "$wrapper_dir/herdr"; then
|
||||
rm -f "$tmp_file"
|
||||
else
|
||||
mv -f "$tmp_file" "$wrapper_dir/herdr"
|
||||
fi
|
||||
mv -f "$tmp_file" "$wrapper_dir/herdr"
|
||||
if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then
|
||||
export PATH="$wrapper_dir:$PATH"
|
||||
fi
|
||||
@@ -507,9 +484,9 @@ herdr() {
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_herdr_session <session_name>
|
||||
# resolve_herdr_server <session_name>
|
||||
#
|
||||
# Query agent-sessions.yaml to find the herdr_session associated with a session.
|
||||
# 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.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -553,7 +530,13 @@ print(json.dumps(d, ensure_ascii=False))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
resolve_herdr_session() {
|
||||
# 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"
|
||||
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
|
||||
import sys, os, json
|
||||
@@ -561,7 +544,7 @@ name = os.environ['SESSION_NAME']
|
||||
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_workspace') or s.get('herdr_server') or 'default')
|
||||
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
|
||||
sys.exit(0)
|
||||
print(os.environ.get('HERDR_SERVER_NAME', 'default'))
|
||||
"
|
||||
@@ -1200,7 +1183,8 @@ provision_isolation() {
|
||||
mkdir -p "$root"
|
||||
case "$agent" in
|
||||
claude)
|
||||
ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded=".credentials.json"
|
||||
if [ -e "$HOME/.claude.json" ]; then ln -sfn "$HOME/.claude.json" "$root/.claude.json"; seeded=".claude.json"; fi
|
||||
ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded="${seeded:+$seeded,}.credentials.json"
|
||||
if [ -e "$HOME/.claude/settings.json" ]; then ln -sfn "$HOME/.claude/settings.json" "$root/settings.json"; seeded="$seeded,settings.json"; fi
|
||||
if [ -d "$HOME/.claude/plugins" ]; then ln -sfn "$HOME/.claude/plugins" "$root/plugins"; seeded="$seeded,plugins"; fi
|
||||
;;
|
||||
@@ -1588,7 +1572,7 @@ send_keys_safe() {
|
||||
_sks_herdr send-keys -t "$sess" C-m
|
||||
return 0
|
||||
fi
|
||||
sleep "${SKS_PASTE_SLEEP:-1.5}"
|
||||
sleep 0.5
|
||||
local pane_content was_popup=0
|
||||
pane_content=$(_pane_capture "$sess")
|
||||
if printf '%s\n' "$pane_content" | grep -Eq "Pasted text|paste again to expand"; then
|
||||
@@ -1597,7 +1581,7 @@ send_keys_safe() {
|
||||
# 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 [ -n "$marker_norm" ] && ! printf '%s' "$pane_content" | tr -d '[:space:]' | grep -Fq "$marker_norm"; then
|
||||
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
|
||||
@@ -1612,7 +1596,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" ] && { [ -z "$marker_norm" ] || ! _pane_tail "$sess" 3 | tr -d '[:space:]' | grep -Fq "$marker_norm"; } && [ "$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