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:
2026-07-22 06:57:32 +09:00
parent e4b1fb3329
commit 36a087af58
49 changed files with 258 additions and 4361 deletions
+47 -63
View File
@@ -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" && \