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" && \
|
||||
|
||||
@@ -310,7 +310,7 @@ entry = {
|
||||
'role': role,
|
||||
'herdr_session_created_at': os.environ['NOW_ISO'],
|
||||
'herdr_session_epoch': int(epoch) if epoch.isdigit() else 0,
|
||||
'herdr_session': server_name,
|
||||
'herdr_server': server_name,
|
||||
'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None,
|
||||
'pane': {
|
||||
'index': 0,
|
||||
|
||||
@@ -138,19 +138,6 @@ cmd_submit() {
|
||||
- **Timeout**: $TIMEOUT s (Idle: $IDLE_TIMEOUT s)
|
||||
- **Output Report Path**: .mam/jobs/$JOB_ID/$AGENT-reports/report-final.md
|
||||
|
||||
## 🔔 Execution Commands
|
||||
On start run:
|
||||
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event started
|
||||
|
||||
On progress (optional):
|
||||
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event progress --detail '<short status>'
|
||||
|
||||
On success run:
|
||||
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event completed --detail '<one-line summary>'
|
||||
|
||||
On failure run:
|
||||
python3 .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py --registry-dir .mam/jobs --job $JOB_ID --event error --detail '<one-line reason>'
|
||||
|
||||
## 🔎 Task Description
|
||||
$PROMPT
|
||||
EOF
|
||||
@@ -197,8 +184,7 @@ EOF
|
||||
# an id from an earlier session is the #1 reason a delegated job sits idle and
|
||||
# times out (see SKILL.md "Wrong job_id propagated to the agent"). We make the
|
||||
# freshness explicit in the instruction header.
|
||||
# Keep this short and ASCII-only to prevent paste/wrap rendering issues in CLI REPLs.
|
||||
local instructions="Job $JOB_ID: Read .mam/jobs/$JOB_ID/brief.md and complete the task."
|
||||
local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths are documented in the task brief file at: .mam/jobs/$JOB_ID/brief.md. Please READ and follow .mam/jobs/$JOB_ID/brief.md to complete your work. Commands: start='$pub --event started', success='$pub --event completed --detail <summary>', error='$pub --event error --detail <reason>'."
|
||||
|
||||
run_agent "$JOB_ID" "$instructions"
|
||||
|
||||
@@ -454,7 +440,7 @@ run_agent() {
|
||||
# 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_session "$sess")"
|
||||
export HERDR_SERVER_NAME="$(resolve_herdr_workspace "$sess")"
|
||||
|
||||
if ! herdr has-session -t "$sess" 2>/dev/null; then
|
||||
echo "ERROR: 에이전트 세션 '$sess'이 존재하지 않습니다. 작업을 위임하기 전에 먼저 에이전트 세션을 기동해 주세요." >&2
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
---
|
||||
name: multi-agent-mux-loop
|
||||
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
|
||||
version: 1.0.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
environments: [terminal, herdr]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [agent, herdr, multi-agent, loop, planning, review, orchestrator]
|
||||
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-delegate-job]
|
||||
prereq_skills: [multi-agent-mux-create]
|
||||
---
|
||||
|
||||
# Multi-Agent Mux Loop — Autonomous Orchestration Loop
|
||||
|
||||
|
||||
> **Companion skills**: `multi-agent-mux-create` (start), `multi-agent-mux-resume` (re-attach), `multi-agent-mux-delegate-job` (delegate).
|
||||
> **Safety Guard**: `--max-loop` and `--plan-talk` restrict API cost runaways.
|
||||
> **Single source of truth**: `./.mam/agent-sessions.yaml`.
|
||||
|
||||
@@ -102,6 +102,10 @@ if [ "$ALL_REVIEWERS" = true ] && [ -n "$REVIEWER_LIST" ]; then
|
||||
log_warn "--all-reviewer takes precedence; ignoring --reviewer list ('$REVIEWER_LIST')."
|
||||
fi
|
||||
|
||||
if [ "$PLAN_TALK_TURNS" -gt 0 ] && [ "$PLAN_MODE" = false ]; then
|
||||
log_warn "--plan-talk was specified but --plan mode is not enabled. Discussion turns will be ignored."
|
||||
fi
|
||||
|
||||
# Verdict must occupy the report's last non-blank line — a standalone token
|
||||
# quoted mid-report (e.g. as a formatting example) never matches (P0-2).
|
||||
has_verdict() {
|
||||
@@ -159,7 +163,7 @@ import os, json
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
target_agent = os.environ.get('TARGET_AGENT')
|
||||
reviewers = [s.get('name') for s in d.get('herdr_sessions', [])
|
||||
if 'reviewer' in s.get('role', '').lower() and s.get('name') != target_agent]
|
||||
if 'reviewer' in (s.get('role') or '').lower() and s.get('status') == 'running' and s.get('name') != target_agent]
|
||||
print(','.join(reviewers))
|
||||
"
|
||||
}
|
||||
@@ -199,7 +203,7 @@ import os, json
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
planner = ''
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if 'planner' in s.get('role', '').lower():
|
||||
if 'planner' in (s.get('role') or '').lower() and s.get('status') == 'running':
|
||||
planner = s.get('name')
|
||||
break
|
||||
print(planner)
|
||||
@@ -220,8 +224,37 @@ log_info "Initializing multi-agent-mux-loop controller..."
|
||||
log_info "Target Agent: $TARGET_AGENT"
|
||||
log_info "Task Goal: $TASK"
|
||||
|
||||
# Validate that Target Agent is registered and running
|
||||
TARGET_STATUS=$(MAM_STATE_JSON="$(load_state_json)" TARGET="$TARGET_AGENT" python3 -c "
|
||||
import os, json
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
target = os.environ.get('TARGET')
|
||||
status = ''
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('name') == target:
|
||||
status = s.get('status')
|
||||
break
|
||||
print(status)
|
||||
")
|
||||
|
||||
if [ -z "$TARGET_STATUS" ]; then
|
||||
log_error "Target agent session '$TARGET_AGENT' is not registered in the session registry."
|
||||
exit 1
|
||||
elif [ "$TARGET_STATUS" != "running" ]; then
|
||||
log_error "Target agent session '$TARGET_AGENT' is not running (current status: '$TARGET_STATUS'). Please start it first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLANNER_SESSION=$(resolve_planner_session)
|
||||
log_info "Resolved Planner session: $PLANNER_SESSION"
|
||||
|
||||
if [ "$PLAN_MODE" = true ]; then
|
||||
if [ -z "$PLANNER_SESSION" ]; then
|
||||
log_error "Planner mode enabled (--plan) but no running session with a 'planner' role was found."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
CURRENT_PLAN=""
|
||||
CREATED_JOBS=()
|
||||
|
||||
@@ -329,7 +362,9 @@ else
|
||||
log_info "=== Phase 1: Self-Planning Mode (Direct Execution) ==="
|
||||
EXISTING_PLAN_FILE=""
|
||||
if [ -n "$PLANNER_SESSION" ]; then
|
||||
EXISTING_PLAN_FILE=".agents/reports/$PLANNER_SESSION/report-final.md"
|
||||
# Promoted plans are saved as plan-<job-id>.md by the Phase 3 promotion
|
||||
# step below, not report-final.md; pick the most recently promoted one.
|
||||
EXISTING_PLAN_FILE=$(ls -t ".agents/reports/$PLANNER_SESSION"/plan-*.md 2>/dev/null | head -n 1 || true)
|
||||
fi
|
||||
if [ -n "$EXISTING_PLAN_FILE" ] && [ -f "$EXISTING_PLAN_FILE" ]; then
|
||||
log_info "Found existing promoted plan at '$EXISTING_PLAN_FILE'. Loading plan..."
|
||||
@@ -385,8 +420,14 @@ if [ "$ALL_REVIEWERS" = true ]; then
|
||||
if [ -n "$RESOLVED_REVS" ]; then
|
||||
IFS=' ,' read -r -a REVIEWERS <<< "$RESOLVED_REVS"
|
||||
fi
|
||||
if [ "${#REVIEWERS[@]}" -eq 0 ]; then
|
||||
log_error "--all-reviewer was specified, but no running reviewer sessions were found."
|
||||
exit 1
|
||||
fi
|
||||
elif [ -n "$REVIEWER_LIST" ]; then
|
||||
IFS=' ,' read -r -a REVIEWERS <<< "$REVIEWER_LIST"
|
||||
# Strip whitespaces first to prevent empty array elements from trailing spaces
|
||||
CLEAN_REVS=$(echo "$REVIEWER_LIST" | tr -d '[:space:]')
|
||||
IFS=',' read -r -a REVIEWERS <<< "$CLEAN_REVS"
|
||||
fi
|
||||
|
||||
loop_count=1
|
||||
|
||||
@@ -121,7 +121,7 @@ _changed = False
|
||||
for s in d.get('herdr_sessions', []):
|
||||
if s.get('delegate_job_id') == _jid and s.get('status') == 'running':
|
||||
_name = s.get('name')
|
||||
_srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
_srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
if _event == 'completed':
|
||||
s['delegate_job_id'] = None
|
||||
print('MQTT Monitor: job completed on ' + str(_name) + ' — session kept alive', flush=True)
|
||||
@@ -131,8 +131,7 @@ for s in d.get('herdr_sessions', []):
|
||||
s['terminated_at'] = _now.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
s['terminated_at_epoch'] = int(_now.timestamp())
|
||||
s['termination_mode'] = 'auto-detected (MQTT ' + _event + ')'
|
||||
_shim = os.path.join(os.environ.get('WORKSPACE_ROOT', os.getcwd()), '.mam/shim/herdr')
|
||||
_cmd = [_shim] + (['-L', _srv] if _srv != 'default' else []) + ['kill-session', '-t', _name]
|
||||
_cmd = ['herdr'] + (['-L', _srv] if _srv != 'default' else []) + ['kill-session', '-t', _name]
|
||||
subprocess.run(_cmd, capture_output=True)
|
||||
print('MQTT Monitor: terminated + killed ' + str(_name) + ' on ' + str(_srv) + ' due to MQTT ' + _event, flush=True)
|
||||
_changed = True
|
||||
@@ -310,8 +309,6 @@ import yaml
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
home = os.environ['HOME_DIR']
|
||||
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
|
||||
workspace_root = os.environ.get('WORKSPACE_ROOT', os.getcwd())
|
||||
shim_herdr = os.path.join(workspace_root, '.mam/shim/herdr')
|
||||
|
||||
now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
@@ -345,12 +342,12 @@ unique_servers = {'default'}
|
||||
if 'HERDR_SERVER_NAME' in os.environ:
|
||||
unique_servers.add(os.environ['HERDR_SERVER_NAME'])
|
||||
for s in d.get('herdr_sessions', []):
|
||||
srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
unique_servers.add(srv)
|
||||
|
||||
try:
|
||||
for srv in sorted(unique_servers):
|
||||
cmd = [shim_herdr]
|
||||
cmd = ['herdr']
|
||||
if srv != 'default':
|
||||
cmd += ['-L', srv]
|
||||
cmd += ['ls', '-F', '#{session_name}|#{session_created}']
|
||||
@@ -372,7 +369,7 @@ except Exception:
|
||||
|
||||
def pane_meta(session, srv):
|
||||
try:
|
||||
cmd = [shim_herdr]
|
||||
cmd = ['herdr']
|
||||
if srv != 'default':
|
||||
cmd += ['-L', srv]
|
||||
cmd += ['list-panes', '-t', session, '-F',
|
||||
@@ -399,7 +396,7 @@ if herdr_confirmed:
|
||||
# (없으면 herdr-dead stopped 세션을 'terminated' 로 덮어써 resumable 플래그가 소실됨)
|
||||
if s.get('status') in ('terminated', 'archived', 'stopped'):
|
||||
continue
|
||||
srv = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
if (name, srv) not in alive_set:
|
||||
s['status'] = 'terminated'
|
||||
s['terminated_at'] = now_iso
|
||||
|
||||
@@ -37,7 +37,7 @@ if [ -z "$UUID" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
|
||||
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
|
||||
export HERDR_SERVER_NAME
|
||||
|
||||
# 2. If herdr is alive, print warning or attach.
|
||||
|
||||
@@ -33,7 +33,7 @@ done
|
||||
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
|
||||
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
||||
|
||||
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
|
||||
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
|
||||
export HERDR_SERVER_NAME
|
||||
|
||||
# --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시)
|
||||
|
||||
@@ -93,7 +93,7 @@ def get_job_status(s):
|
||||
sessions_detail = []
|
||||
for s in d.get('herdr_sessions', []):
|
||||
name = s.get('name', '?')
|
||||
server = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
server = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
jid, jstatus = get_job_status(s)
|
||||
pane = s.get('pane') or {}
|
||||
sessions_detail.append({
|
||||
@@ -197,7 +197,7 @@ if not sessions:
|
||||
print("(no sessions registered)")
|
||||
for s in sessions:
|
||||
name = s.get('name', '?')
|
||||
server = s.get('herdr_session') or s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
server = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
|
||||
status = s.get('status', '?')
|
||||
herdr = 'alive' if f"{name}|{server}" in alive else 'dead'
|
||||
cmd = (s.get('pane') or {}).get('cmd', '?')
|
||||
|
||||
@@ -82,7 +82,7 @@ if [ "$PURGE" = "1" ]; then
|
||||
trap 'rm -f "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"' EXIT
|
||||
fi
|
||||
|
||||
HERDR_SERVER_NAME="$(resolve_herdr_session "$SESSION_NAME")"
|
||||
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
|
||||
export HERDR_SERVER_NAME
|
||||
|
||||
# --agent 미지정 시 이름 suffix 로 fallback (P1-F)
|
||||
|
||||
Reference in New Issue
Block a user