Three `create --onboard` at once into one folder (the control root, so no control-plane prefix reaches the pane and the shim reuses an idle pane): the shim's new-session chooses a pane - an idle one in a workspace on the same cwd, a split, a fresh workspace's root pane - and then claims it with `agent start`; two shims could choose the pane one of them had just made, and the later start failed with agent_pane_busy (one create in three). mam_herdr now runs every new-session call holding one lock per server: a flock on fd 9, opened for the block and taken by lib_py.pane_lock, which also bounds the wait (MAM_PANE_CLAIM_WAIT_S, default 120). It is released with the block or with the process, however it ends. The shim heredoc is unchanged (S11). Other servers' creates are not held. Serialising the claims exposed a second race: a create's end-of-run `reconcile.sh --once` adopted the pane of another create that had not yet written its row, and that create refused its own name as already running (exit 4). Such a row - registered by the monitor, no job, for a pane created after this create began - is now replaced, not refused. Tests: real herdr - shim-level and create-level three-way races into one folder (the real herdr's pane-creating replies held back so the window is hit on every run; both fail on the base with the orchestrator's error), the different-folder test kept; pane_lock unit tests (per-server file, timeout, kill -9 holder, handover, knob); the adoption rule and its three refusals; shim contract h26 (held lock: no pane calls). The herdr box gets a ~/.zshrc: without it every first `agent start` typed into zsh's first-run menu and timed out at 30 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
772 lines
36 KiB
Bash
Executable File
772 lines
36 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# create_session.sh — multi-agent-mux-create 의 부속 스크립트
|
|
# Usage:
|
|
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
|
|
#
|
|
# 동작:
|
|
# 1) preflight: herdr/claude/agy 가용성, workspace 존재
|
|
# 2) herdr 세션 이름 결정 (--session 없으면 자동)
|
|
# 3) herdr 세션 시작 (claude 는 wrapper 우선, agy 는 인라인)
|
|
# 4) pane 메타 캡처 (pid, cmd, cwd)
|
|
# 5) agent-sessions.yaml 에 herdr_sessions[] 엔트리 append
|
|
# 6) 검증 출력
|
|
#
|
|
# Exit codes:
|
|
# 0 = success
|
|
# 1 = preflight failure
|
|
# 2 = invalid args
|
|
# 3 = herdr session already exists (use multi-agent-mux-resume or delete first)
|
|
# 4 = agent-sessions.yaml append failure
|
|
# 42 = agent TUI never became ready and the process is gone
|
|
# 43 = rollback could not kill the herdr session; it is still running (#23)
|
|
# 44 = the onboarding job ended with error (or was cancelled); the session is kept (#50)
|
|
# 45 = the onboarding job did not end within MAM_ONBOARD_WAIT_S; the session is kept (#50)
|
|
set -euo pipefail
|
|
|
|
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
_lib_sh="$(cd "$_script_dir/../.." && pwd)/lib.sh"
|
|
[ -f "$_lib_sh" ] || _lib_sh="${WORKSPACE_ROOT:-$PWD}/.agents/skills/lib.sh"
|
|
source "$_lib_sh"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 --workspace <path> --agent <claude|agy|hermes|grok|opencode|codex> --role <role> [options]
|
|
|
|
Options:
|
|
--workspace PATH project directory (required)
|
|
--agent AGENT claude | agy | hermes | grok | opencode | codex (required)
|
|
--role ROLE assigned role (required)
|
|
--session NAME herdr session name (default: derived from workspace)
|
|
--wrapper force use of ~/.local/bin/<session> wrapper even if not present
|
|
--dry-run print commands without executing
|
|
--herdr-session NAME specify isolated herdr session name (alias: --herdr-server)
|
|
--herdr-server NAME specify isolated herdr session name (legacy alias)
|
|
--herdr-workspace NAME workspace label recorded in the registry
|
|
(flag > \$HERDR_WORKSPACE > workspace slug without mam-).
|
|
A label only — it never selects a herdr socket;
|
|
use --herdr-session for that.
|
|
--submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt
|
|
--onboard automatically submit a project alignment/orientation job to the new agent
|
|
--no-auto-trust do not answer a start-up directory-trust dialog (default: answer it)
|
|
--no-onboard disable automatic onboarding job submission
|
|
--no-isolate legacy flag (no-op; all sessions use global configuration)
|
|
--isolate legacy flag (no-op; all sessions use global configuration)
|
|
-h, --help this help
|
|
EOF
|
|
}
|
|
|
|
WORKSPACE=""
|
|
AGENT=""
|
|
ROLE=""
|
|
SESSION_NAME=""
|
|
USE_WRAPPER=0
|
|
DRY_RUN=0
|
|
HERDR_SERVER_OPT=""
|
|
HERDR_WORKSPACE_OPT=""
|
|
SUBMIT_JOB_PROMPT=""
|
|
ONBOARD=1
|
|
# Issue #24: answer a start-up directory-trust dialog for the --workspace the
|
|
# operator named. --no-auto-trust leaves those unanswered (notices are always
|
|
# answered); codex in particular loads project-local config, hooks and exec
|
|
# policy once trusted. MAM_AUTO_TRUST=0 in the environment is the same switch
|
|
# (the skill docs say so); an explicit --auto-trust/--no-auto-trust wins over it
|
|
# (merge review, codex probe: the export below used to overwrite the caller's 0).
|
|
AUTO_TRUST="${MAM_AUTO_TRUST:-1}"
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--workspace) WORKSPACE="$2"; shift 2 ;;
|
|
--agent) AGENT="$2"; shift 2 ;;
|
|
--role) ROLE="$2"; shift 2 ;;
|
|
--session|--name) SESSION_NAME="$2"; shift 2 ;;
|
|
--wrapper) USE_WRAPPER=1; shift ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
|
--herdr-workspace) HERDR_WORKSPACE_OPT="$2"; shift 2 ;;
|
|
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
|
|
--onboard) ONBOARD=1; shift ;;
|
|
--no-onboard) ONBOARD=0; shift ;;
|
|
--auto-trust) AUTO_TRUST=1; shift ;;
|
|
--no-auto-trust) AUTO_TRUST=0; shift ;;
|
|
--isolate) echo "NOTE: --isolate/--no-isolate is a no-op — config-home isolation was removed; sessions always use global config." >&2; shift ;; # legacy compatibility
|
|
--no-isolate) echo "NOTE: --isolate/--no-isolate is a no-op — config-home isolation was removed; sessions always use global config." >&2; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
export MAM_AUTO_TRUST="$AUTO_TRUST"
|
|
|
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
|
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
|
fi
|
|
|
|
|
|
# Preflight
|
|
[ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; }
|
|
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; }
|
|
case "$AGENT" in
|
|
claude|agy|hermes|grok|opencode|codex) ;;
|
|
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT" >&2; exit 2 ;;
|
|
esac
|
|
[ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; }
|
|
[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; }
|
|
# B-3: `command -v herdr` matches lib.sh's herdr() function and `type -P herdr`
|
|
# matches the .mam/shim wrapper, so both pass with no herdr installed.
|
|
has_real_herdr || { echo "ERROR: herdr not installed" >&2; exit 1; }
|
|
command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; }
|
|
|
|
# Auth Check (OAuth check for agy, loggedIn check for claude, status for hermes)
|
|
if [ "$AGENT" = "claude" ]; then
|
|
if ! claude auth status 2>/dev/null | grep -q '"loggedIn":\s*true'; then
|
|
echo "ERROR: claude not logged in. Run 'claude auth login' first." >&2
|
|
exit 1
|
|
fi
|
|
elif [ "$AGENT" = "agy" ]; then
|
|
# Fast, non-blocking check: if token or credentials exist on disk, assume authenticated to prevent keyring hang
|
|
if [ -f "$HOME/.gemini/oauth_creds.json" ] || [ -f "$HOME/.gemini/antigravity-cli/antigravity-oauth-token" ]; then
|
|
true
|
|
elif ! agy models >/dev/null 2>&1; then
|
|
echo "ERROR: agy is not authenticated. Please log in first." >&2
|
|
exit 1
|
|
fi
|
|
elif [ "$AGENT" = "hermes" ]; then
|
|
if ! hermes status >/dev/null 2>&1; then
|
|
echo "ERROR: hermes is not functional. Run 'hermes setup' first." >&2
|
|
exit 1
|
|
fi
|
|
elif [ "$AGENT" = "grok" ]; then
|
|
if [ -f "$HOME/.grok/auth.json" ] || [ -n "$XAI_API_KEY" ]; then
|
|
true
|
|
elif ! grok --version >/dev/null 2>&1; then
|
|
echo "ERROR: grok is not functional or configured." >&2
|
|
exit 1
|
|
fi
|
|
elif [ "$AGENT" = "opencode" ]; then
|
|
if [ -f "$HOME/.local/share/opencode/auth.json" ] || [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${OPENAI_API_KEY:-}" ] || [ -n "${GEMINI_API_KEY:-}" ] || [ -n "${GROQ_API_KEY:-}" ]; then
|
|
true
|
|
elif ! opencode --version >/dev/null 2>&1; then
|
|
echo "ERROR: opencode is not functional or configured." >&2
|
|
exit 1
|
|
fi
|
|
elif [ "$AGENT" = "codex" ]; then
|
|
codex_home="${CODEX_HOME:-$HOME/.codex}"
|
|
if [ -f "$codex_home/auth.json" ] || [ -n "${OPENAI_API_KEY:-}" ]; then
|
|
true
|
|
elif ! codex --version >/dev/null 2>&1; then
|
|
echo "ERROR: codex is not functional or configured." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# 세션 이름 — lib.sh::derive_session_name 이 단일 소스 (P0-A)
|
|
if [ -z "$SESSION_NAME" ]; then
|
|
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT" "$ROLE")"
|
|
fi
|
|
|
|
# 이미 살아있으면 실패
|
|
if _herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
echo "ERROR: herdr session '$SESSION_NAME' already exists. Use multi-agent-mux-resume to attach, or multi-agent-mux-stop first." >&2
|
|
exit 3
|
|
fi
|
|
|
|
# Config-home isolation was removed in favor of global config + process/UUID isolation.
|
|
|
|
# herdr 세션 띄우기
|
|
LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
|
|
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
|
|
|
|
ws_slug="$(derive_workspace_slug "$WORKSPACE")"
|
|
# 플래그 > 환경변수 > 워크스페이스 슬러그 (C-3: HERDR_SESSION_NAME 과 대칭).
|
|
# D5: resolve_herdr_workspace 를 쓰지 않는다 — 동명 terminated 행 위에 재생성할 때
|
|
# 낡은 pane.cwd 에서 파생된 라벨을 물려받기 때문 (create 는 사실을 세우는 쪽).
|
|
export MAM_WS_LABEL="${HERDR_WORKSPACE_OPT:-${HERDR_WORKSPACE:-${ws_slug#mam-}}}"
|
|
if [ -z "$HERDR_SERVER_OPT" ]; then
|
|
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
|
|
export HERDR_SESSION_NAME="$ws_slug"
|
|
fi
|
|
fi
|
|
|
|
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
|
|
RESOLVED_BIN="$AGENT"
|
|
if command -v "$AGENT" >/dev/null 2>&1; then
|
|
RESOLVED_BIN="$(command -v "$AGENT")"
|
|
fi
|
|
|
|
# On macOS, clear quarantine attribute for the agent binary to prevent Gatekeeper hangs
|
|
if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
|
|
xattr -d com.apple.quarantine "$RESOLVED_BIN" 2>/dev/null || true
|
|
fi
|
|
|
|
SESSION_UUID=""
|
|
if [ "$AGENT" = "claude" ] || [ "$AGENT" = "grok" ]; then
|
|
SESSION_UUID="$(mam_gen_uuid)"
|
|
fi
|
|
|
|
# Retrieve agent facts once
|
|
eval "$("$(_delegate_py_bin)" -m lib_py.agents facts "$AGENT" 2>/dev/null || true)"
|
|
|
|
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "$SESSION_UUID" "${USE_WRAPPER:-0}" 2>/dev/null || true)"
|
|
if [ -z "$CMD_FULL" ]; then
|
|
case "$AGENT" in
|
|
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;;
|
|
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
|
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
|
|
grok) CMD_FULL="${RESOLVED_BIN} --permission-mode bypassPermissions" ;;
|
|
opencode) CMD_FULL="${RESOLVED_BIN} --auto --agent build" ;;
|
|
codex) CMD_FULL="${RESOLVED_BIN} --dangerously-bypass-approvals-and-sandbox" ;;
|
|
esac
|
|
fi
|
|
|
|
# Issue #21 (item 3): the control-plane binding. Persisted on the row always;
|
|
# forwarded to the pane (leading K=V tokens that lib.sh's new-session turns
|
|
# into --env) only when it differs from what --workspace alone implies.
|
|
# lib.sh splits those flags unquoted (and, with env flags, never reuses an
|
|
# idle pane - `agent start --` would pass them to the agent as arguments), so a
|
|
# binding with whitespace that would have to be forwarded is refused.
|
|
# A binding that cannot be computed is not swallowed (Issue #21 phase 2, A12):
|
|
# for a worktree (--workspace != control root) that would silently put the
|
|
# session on the wrong control plane, so refuse; for the control root itself
|
|
# warn and record the row without control_plane.
|
|
cp_rc=0
|
|
CP_JSON="$(mam_control_plane_json "$WORKSPACE")" || cp_rc=$?
|
|
if [ "$cp_rc" -ne 0 ] || [ -z "$CP_JSON" ]; then
|
|
if [ "$(mam_abs_workspace "$WORKSPACE")" != "$(mam_abs_workspace "$WORKSPACE_ROOT")" ]; then
|
|
echo "ERROR: cannot compute the control-plane binding (rc=$cp_rc) for --workspace '$WORKSPACE' (control root '$WORKSPACE_ROOT'); refusing to start a session that would use the wrong control plane" >&2
|
|
exit 2
|
|
fi
|
|
echo "WARNING: control-plane binding unavailable (rc=$cp_rc); the row is recorded without control_plane" >&2
|
|
CP_JSON=""
|
|
fi
|
|
CP_PREFIX=""
|
|
if [ -n "$CP_JSON" ]; then
|
|
cp_rc=0
|
|
CP_PREFIX="$(mam_control_plane_prefix "$CP_JSON")" || cp_rc=$?
|
|
if [ "$cp_rc" -eq 3 ]; then
|
|
echo "ERROR: control-plane path contains whitespace; cannot forward to the pane" >&2
|
|
exit 2
|
|
fi
|
|
fi
|
|
|
|
spawn() {
|
|
if [ -z "$HERDR_SERVER_OPT" ]; then
|
|
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
|
|
export HERDR_SESSION_NAME="$ws_slug"
|
|
fi
|
|
fi
|
|
case "$AGENT" in
|
|
claude)
|
|
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
|
|
SESSION_UUID=""
|
|
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "" "true" 2>/dev/null || echo "${RESOLVED_BIN} --dangerously-skip-permissions")"
|
|
[ -z "$CP_PREFIX" ] || echo "WARNING: the ~/.local/bin wrapper path bypasses herdr; the control-plane binding is recorded but not forwarded" >&2
|
|
nohup "$WRAPPER" >/dev/null 2>&1 &
|
|
disown
|
|
else
|
|
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
|
|
fi
|
|
;;
|
|
agy|hermes|grok|codex)
|
|
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
|
|
;;
|
|
opencode)
|
|
if [ -z "${OPENCODE_PERMISSION:-}" ]; then
|
|
export OPENCODE_PERMISSION='{"*":"allow"}'
|
|
fi
|
|
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
|
|
;;
|
|
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT" >&2; exit 2 ;;
|
|
esac
|
|
}
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo "[dry-run] would spawn: herdr session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT, herdr_session=${HERDR_SESSION_NAME:-default}, herdr_workspace=${MAM_WS_LABEL})"
|
|
exit 0
|
|
fi
|
|
|
|
HERDR_EPOCH=$(date +%s)
|
|
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
|
|
|
spawn
|
|
|
|
# _mark_row_rolled_back <status> <note>
|
|
# ISSUE-23: the YAML row and the job registration are both written before the
|
|
# prompt is injected, so a rollback that only kills the pane leaves a row that
|
|
# says `running` pointing at a failed job - which is what status.sh keeps
|
|
# reporting (symptom 5). Mark the row instead of deleting it: a deleted row
|
|
# takes the evidence with it.
|
|
#
|
|
# <status> stays inside the schema's vocabulary (running/terminated/archived/
|
|
# stopped - atomic_yaml.py rejects anything else): `terminated` when the
|
|
# session is really gone, and `running` when the kill failed, because it IS
|
|
# still running and saying otherwise would be the same lie in reverse. What
|
|
# makes the failed case legible is the note, the cleared job id and exit 43.
|
|
_mark_row_rolled_back() {
|
|
local status="$1" note="$2"
|
|
[ "${ROW_APPENDED:-0}" = "1" ] || return 0
|
|
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
|
SESSION_NAME="$SESSION_NAME" ROLLBACK_STATUS="$status" ROLLBACK_NOTE="$note" <<'PYEOF' || true
|
|
name = os.environ['SESSION_NAME']
|
|
status = os.environ['ROLLBACK_STATUS']
|
|
note = os.environ['ROLLBACK_NOTE']
|
|
for s in d.get('herdr_sessions', []):
|
|
if s.get('name') == name:
|
|
s['status'] = status
|
|
s['last_visible_status'] = note
|
|
s['delegate_job_id'] = None
|
|
break
|
|
PYEOF
|
|
}
|
|
|
|
# _mark_row_note <note>
|
|
# Issue #50: last_visible_status only. The row stays `running` with its
|
|
# delegate_job_id: the session is alive and the job record is the evidence.
|
|
_mark_row_note() {
|
|
local note="$1"
|
|
[ "${ROW_APPENDED:-0}" = "1" ] || return 0
|
|
atomic_dump_yaml "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" ROW_NOTE="$note" <<'PYEOF' || true
|
|
name = os.environ['SESSION_NAME']
|
|
for s in d.get('herdr_sessions', []):
|
|
if s.get('name') == name:
|
|
s['last_visible_status'] = os.environ['ROW_NOTE']
|
|
break
|
|
PYEOF
|
|
}
|
|
|
|
# Trap for rolling back/cleaning up herdr session if script exits due to error
|
|
cleanup_herdr_on_error() {
|
|
local exit_code=$?
|
|
[ $exit_code -eq 0 ] && return 0
|
|
echo "⚠️ Error occurred during initialization (exit code $exit_code). Rolling back and killing herdr session '$SESSION_NAME'..." >&2
|
|
|
|
# ISSUE-23: the kill result used to be discarded (`2>/dev/null || true`), so
|
|
# a kill that closed nothing still printed "rolled back" while the session
|
|
# kept running. Check it, then confirm with has-session.
|
|
local kill_rc=0
|
|
_herdr kill-session -t "$SESSION_NAME" || kill_rc=$?
|
|
local alive=0
|
|
_herdr has-session -t "$SESSION_NAME" >/dev/null 2>&1 && alive=1
|
|
|
|
# Release the onboarding job: `cancelled` is a terminal transition the
|
|
# registry knows, and it says what happened - the job was never really
|
|
# dispatched. Best effort; a failure here must not stop the rollback.
|
|
# `completed`/`error`/`cancelled` are terminal and published exactly once, so
|
|
# a job this run already closed is left alone - a second one is refused by
|
|
# the registry anyway ("conflicting cancelled after terminal error").
|
|
if [ -n "${DELEGATE_JOB_ID:-}" ] && [ "${JOB_TERMINALIZED:-0}" != "1" ]; then
|
|
delegate_publish_event "$DELEGATE_JOB_ID" cancelled "rolled back by create_session" "${DELEGATE_JOB_TOKEN:-}" "${DELEGATE_JOB_ATTEMPT:-}" || true
|
|
JOB_TERMINALIZED=1
|
|
fi
|
|
|
|
if [ "$alive" = "1" ]; then
|
|
_mark_row_rolled_back "running" "rollback-failed"
|
|
echo "ERROR: rollback could not kill herdr session '$SESSION_NAME' (kill rc=$kill_rc) - it is still running." >&2
|
|
echo " Clean it up with: HERDR_SESSION_NAME=${HERDR_SESSION_NAME:-default} herdr kill-session -t $SESSION_NAME" >&2
|
|
exit 43
|
|
fi
|
|
_mark_row_rolled_back "terminated" "rolled-back"
|
|
}
|
|
trap cleanup_herdr_on_error EXIT
|
|
|
|
if [ -z "$HERDR_SERVER_OPT" ]; then
|
|
RESOLVED_SERVER="$(resolve_herdr_session "$SESSION_NAME" "$WORKSPACE")"
|
|
export HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-$RESOLVED_SERVER}"
|
|
fi
|
|
|
|
# TUI 준비 대기
|
|
TUI_READY_RC=0
|
|
wait_for_tui_ready "$SESSION_NAME" "$AGENT" || TUI_READY_RC=$?
|
|
LAST_VISIBLE_STATUS_OVERRIDE=""
|
|
|
|
if [ "$TUI_READY_RC" -ne 0 ]; then
|
|
# 1. Diagnostic dump
|
|
DIAG_DIR="${WORKSPACE:-$PWD}/.mam/diagnostics"
|
|
mkdir -p "$DIAG_DIR" 2>/dev/null || true
|
|
DIAG_FILE="$DIAG_DIR/${SESSION_NAME}-$(date +%s).txt"
|
|
{
|
|
echo "=== MAM Diagnostic Dump ==="
|
|
echo "Date: $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
echo "MAM Version: ${MAM_VERSION:-unknown}"
|
|
echo "Session: $SESSION_NAME"
|
|
echo "Agent: $AGENT"
|
|
echo "ReturnCode: $TUI_READY_RC"
|
|
echo "--- Pane Raw Capture ---"
|
|
_pane_capture "$SESSION_NAME" 2>/dev/null || true
|
|
} > "$DIAG_FILE" 2>/dev/null || true
|
|
if [ ! -f "$DIAG_FILE" ]; then
|
|
echo "WARNING: Failed to write diagnostic dump to $DIAG_FILE" >&2
|
|
fi
|
|
|
|
# 2. Check pane PID liveness
|
|
PANE_PID_CHECK=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
|
|
ALIVE=false
|
|
if [ -n "$PANE_PID_CHECK" ] && [[ "$PANE_PID_CHECK" =~ ^[0-9]+$ ]]; then
|
|
if kill -0 "$PANE_PID_CHECK" 2>/dev/null; then
|
|
ALIVE=true
|
|
fi
|
|
fi
|
|
|
|
if [ "$ALIVE" = true ] && [ "${MAM_KILL_ON_READY_TIMEOUT:-0}" != "1" ]; then
|
|
echo "WARNING: agent TUI never became ready within timeout, but process (PID $PANE_PID_CHECK) is alive. Preserving session for diagnosis." >&2
|
|
LAST_VISIBLE_STATUS_OVERRIDE="tui-ready-timeout"
|
|
SUBMIT_JOB_PROMPT="" # disable instruction injection on unready session
|
|
else
|
|
echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2
|
|
exit 42
|
|
fi
|
|
fi
|
|
|
|
# Issue #24: every agent, not only claude. The signatures and accept keys come
|
|
# from the agent adapter, so an agy/grok/codex trust prompt is answered here
|
|
# too instead of sitting until the readiness wait gives up.
|
|
if [ -z "$LAST_VISIBLE_STATUS_OVERRIDE" ]; then
|
|
handle_startup_dialogs "$SESSION_NAME" 15 "$AGENT"
|
|
fi
|
|
|
|
# pane 메타 캡처
|
|
PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
|
|
PANE_CWD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_path}' 2>/dev/null || echo "$WORKSPACE")
|
|
PANE_CMD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/dev/null || echo "$AGENT")
|
|
|
|
# 시작 명령
|
|
# NOTE: this must match what `spawn()` actually ran above — env-var-driven
|
|
# herdr server shim (HERDR_SESSION_NAME picked up by the lib.sh shim).
|
|
START_CMD="HERDR_SESSION_NAME=${HERDR_SESSION_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 session is ready)
|
|
if [ "$ONBOARD" = "1" ] && [ -z "$SUBMIT_JOB_PROMPT" ] && [ -z "$LAST_VISIBLE_STATUS_OVERRIDE" ]; then
|
|
SUBMIT_JOB_PROMPT="You are a newly spawned $ROLE Team Leader agent in this workspace. Without making any non-standard pre-preparations or modifying files directly, proceed immediately to align yourself with the project context by performing the following tasks:
|
|
1. Read the project documentation at README.md and the multi-agent protocol guidelines at .agents/MULTI_AGENT_RULES.md to understand the design rules.
|
|
2. Run 'git status' and 'git diff' to analyze the current modifications and active work in the repository.
|
|
3. Read .mam/agent-sessions.yaml to see other running agent sessions, their roles, and confirm your own assigned role: $ROLE.
|
|
4. Once you have fully comprehended the project state, publish a completed event (using publish_event.py) with the detail 'Onboarding complete; aligned with role $ROLE'."
|
|
fi
|
|
|
|
# agent-sessions.yaml 에 append
|
|
DELEGATE_JOB_ID=""
|
|
if [ -n "$LAST_VISIBLE_STATUS_OVERRIDE" ]; then
|
|
SUBMIT_JOB_PROMPT=""
|
|
fi
|
|
if [ -n "$SUBMIT_JOB_PROMPT" ]; then
|
|
delegate_agent="${MAM_DELEGATE_AGENT_KEY:-}"
|
|
if [ -z "$delegate_agent" ]; then
|
|
case "$AGENT" in
|
|
claude) delegate_agent="claude-code" ;;
|
|
hermes) delegate_agent="hermes-agent" ;;
|
|
agy) delegate_agent="antigravity-cli" ;;
|
|
grok) delegate_agent="grok-build" ;;
|
|
opencode) delegate_agent="opencode-cli" ;;
|
|
codex) delegate_agent="codex-cli" ;;
|
|
*) echo "ERROR: cannot resolve delegate agent key for '$AGENT'" >&2; exit 2 ;;
|
|
esac
|
|
fi
|
|
agent_session="herdr:$SESSION_NAME"
|
|
DELEGATE_JOB_ID=$(delegate_submit_job "$SUBMIT_JOB_PROMPT" "$delegate_agent" "$agent_session")
|
|
echo "Submitted delegated job: $DELEGATE_JOB_ID"
|
|
DELEGATE_JOB_TOKEN=""
|
|
DELEGATE_JOB_ATTEMPT=""
|
|
DELEGATE_JOB_ACK_S=""
|
|
if [ -n "$DELEGATE_JOB_ID" ]; then
|
|
local_info=$(python3 -c "
|
|
import sys, json, os
|
|
jid = '$DELEGATE_JOB_ID'
|
|
reg_dir = os.environ.get('MAM_JOBS_DIR') or (os.path.join(os.environ['WORKSPACE_ROOT'], '.mam', 'jobs') if os.environ.get('WORKSPACE_ROOT') else '.mam/jobs')
|
|
p = os.path.join(reg_dir, f'{jid}.json')
|
|
if os.path.exists(p):
|
|
try:
|
|
with open(p) as f:
|
|
d = json.load(f)
|
|
tok = d.get('dispatch_token', '')
|
|
att = d.get('attempt_id', d.get('iteration', 1))
|
|
ack = d.get('ack_timeout_sec', 180)
|
|
print(f'{tok} {att} {ack}')
|
|
except Exception:
|
|
pass
|
|
" 2>/dev/null || true)
|
|
if [ -n "$local_info" ]; then
|
|
DELEGATE_JOB_TOKEN=$(echo "$local_info" | awk '{print $1}')
|
|
DELEGATE_JOB_ATTEMPT=$(echo "$local_info" | awk '{print $2}')
|
|
DELEGATE_JOB_ACK_S=$(echo "$local_info" | awk '{print $3}')
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ ! -f "$AGENT_SESSIONS_YAML" ]; then
|
|
mkdir -p "$(dirname "$AGENT_SESSIONS_YAML")"
|
|
echo "herdr_sessions: []" > "$AGENT_SESSIONS_YAML"
|
|
fi
|
|
|
|
# atomic_dump_yaml: flock + temp+rename + .bak + schema validate (P0-B).
|
|
# 모든 값은 환경변수로 전달 — heredoc interpolation 없음 (P1-B).
|
|
# 자식 pid 는 bash 에서 pgrep 으로 미리 구함 (P2: 도구명 필터).
|
|
CHILD_PID=0
|
|
if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ]; } && [ -n "$PANE_PID" ]; then
|
|
CHILD_PID=$(pgrep -P "$PANE_PID" -x "$AGENT" 2>/dev/null | head -1 || true)
|
|
CHILD_PID="${CHILD_PID:-0}"
|
|
fi
|
|
|
|
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
|
SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
|
HERDR_EPOCH="$HERDR_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
|
|
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
|
|
HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-default}" \
|
|
MAM_WS_LABEL="$MAM_WS_LABEL" \
|
|
SESSION_UUID="$SESSION_UUID" \
|
|
LAST_VISIBLE_STATUS_OVERRIDE="${LAST_VISIBLE_STATUS_OVERRIDE:-}" \
|
|
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" CONTROL_PLANE_JSON="$CP_JSON" <<'PYEOF'
|
|
name = os.environ['SESSION_NAME']
|
|
agent = os.environ['AGENT']
|
|
role = os.environ['ROLE']
|
|
pid = os.environ.get('PANE_PID', '')
|
|
epoch = os.environ.get('HERDR_EPOCH', '')
|
|
server_name = os.environ.get('HERDR_SESSION_NAME', 'default')
|
|
server_opt = f"-L {server_name} " if server_name and server_name != 'default' else ""
|
|
|
|
sessions = d.setdefault('herdr_sessions', [])
|
|
|
|
# P0-D: 같은 이름 엔트리가 status=running 이면만 거부. terminated/archived 는
|
|
# 재사용 가능 — 낡은 엔트리를 제거하고 새로 append (create -> delete -> create).
|
|
running_same = [s for s in sessions if s.get('name') == name and s.get('status') == 'running']
|
|
# Issue #50 follow-up: a reconcile pass (another create's end-of-run `--once`,
|
|
# the subscriber's cycle) can adopt the pane this create started moments ago,
|
|
# before this row lands. A running row of this name that the monitor
|
|
# registered, with no job, for a pane created after this create began, is
|
|
# this create's own pane - herdr keeps live names unique per server and the
|
|
# preflight refused an older session of this name - so it is replaced, not
|
|
# refused as a second live session.
|
|
def _adopted_own(s):
|
|
return (s.get('last_visible_note') == 'auto-registered by monitor' and not s.get('delegate_job_id')
|
|
and epoch.isdigit() and int(s.get('herdr_session_epoch') or 0) >= int(epoch) - 1)
|
|
if running_same and all(_adopted_own(s) for s in running_same):
|
|
print(f"NOTE: the monitor registered {name} while it was being created; that row is replaced by this one", flush=True)
|
|
running_same = []
|
|
old_roles.pop(name, None) # its role was read off the name; --role is the fact (atomic_yaml's role guard)
|
|
if running_same:
|
|
print(f"ERROR: {name} already running in agent-sessions.yaml", flush=True)
|
|
raise SystemExit(4)
|
|
sessions[:] = [s for s in sessions if s.get('name') != name]
|
|
|
|
entry = {
|
|
'name': name,
|
|
'status': 'running',
|
|
'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,
|
|
'herdr_workspace': os.environ.get('MAM_WS_LABEL', ''),
|
|
'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None,
|
|
'pane': {
|
|
'index': 0,
|
|
'pid': int(pid) if pid.isdigit() else 0,
|
|
'cmd': agent,
|
|
'cmd_full': os.environ['CMD_FULL'],
|
|
'cwd': os.environ['PANE_CWD'],
|
|
},
|
|
'start_command': os.environ['START_CMD'],
|
|
'attach_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
|
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
|
|
}
|
|
|
|
if agent == 'claude':
|
|
entry['tui'] = {
|
|
'model': '(unknown — capture after first message)',
|
|
'provider': 'anthropic',
|
|
'plan': '(unknown)',
|
|
'account': '(unknown — read from claude auth status)',
|
|
'version': '(unknown — read from TUI)',
|
|
}
|
|
assigned = os.environ.get('SESSION_UUID', '') or None
|
|
entry['claude_session_id_own'] = assigned
|
|
entry['session_id_source'] = 'assigned' if assigned else 'pending-discovery'
|
|
entry['session_id_verified'] = False
|
|
entry['last_visible_status'] = "assigned (awaiting first message)" if assigned else "unverified"
|
|
elif agent == 'agy':
|
|
cp = os.environ.get('CHILD_PID', '0')
|
|
entry['child_pid'] = int(cp) if cp.isdigit() else 0
|
|
entry['agy_conversation_id_own'] = None
|
|
entry['mcp_attachments'] = [
|
|
{
|
|
'name': 'stitch',
|
|
'transport': 'mcp-remote',
|
|
'endpoint': 'https://stitch.googleapis.com/mcp'
|
|
}
|
|
]
|
|
entry['last_visible_status'] = "unverified"
|
|
elif agent == 'hermes':
|
|
cp = os.environ.get('CHILD_PID', '0')
|
|
entry['child_pid'] = int(cp) if cp.isdigit() else 0
|
|
entry['hermes_conversation_id_own'] = None
|
|
entry['last_visible_status'] = "unverified"
|
|
elif agent == 'grok':
|
|
assigned = os.environ.get('SESSION_UUID', '') or None
|
|
entry['grok_session_id_own'] = assigned
|
|
entry['session_id_source'] = 'assigned' if assigned else 'pending-discovery'
|
|
entry['session_id_verified'] = False
|
|
entry['last_visible_status'] = "assigned (awaiting first message)" if assigned else "unverified"
|
|
elif agent == 'opencode':
|
|
cp = os.environ.get('CHILD_PID', '0')
|
|
entry['child_pid'] = int(cp) if cp.isdigit() else 0
|
|
entry['opencode_session_id_own'] = None
|
|
entry['session_id_source'] = 'pending-discovery'
|
|
entry['session_id_verified'] = False
|
|
entry['last_visible_status'] = "unverified"
|
|
elif agent == 'codex':
|
|
cp = os.environ.get('CHILD_PID', '0')
|
|
entry['child_pid'] = int(cp) if cp.isdigit() else 0
|
|
entry['codex_session_id_own'] = None
|
|
entry['session_id_source'] = 'pending-discovery'
|
|
entry['session_id_verified'] = False
|
|
entry['last_visible_status'] = "unverified"
|
|
|
|
last_vis_override = os.environ.get('LAST_VISIBLE_STATUS_OVERRIDE', '')
|
|
if last_vis_override:
|
|
entry['last_visible_status'] = last_vis_override
|
|
|
|
# Issue #21: the control-plane binding this row was created against.
|
|
_cp_raw = os.environ.get('CONTROL_PLANE_JSON', '')
|
|
if _cp_raw:
|
|
try:
|
|
import json as _json
|
|
_cp = _json.loads(_cp_raw)
|
|
entry['control_plane'] = {k: _cp[k] for k in ('workspace_root', 'agent_sessions_yaml', 'mam_jobs_dir', 'delegate_job_logs_dir')}
|
|
except Exception:
|
|
pass
|
|
sessions.append(entry)
|
|
|
|
snap = d.setdefault('snapshot', {})
|
|
snap['taken_at'] = os.environ['NOW_ISO']
|
|
snap['cwd'] = os.environ['PANE_CWD']
|
|
print(f"appended: {name}", flush=True)
|
|
PYEOF
|
|
ROW_APPENDED=1
|
|
|
|
echo
|
|
echo "=== created ==="
|
|
echo "herdr session: $SESSION_NAME (pane pid $PANE_PID, cmd $PANE_CMD, cwd $PANE_CWD)"
|
|
if [ -n "$DELEGATE_JOB_ID" ]; then
|
|
echo "delegate job: $DELEGATE_JOB_ID"
|
|
|
|
# The publish command the agent will run: the same shape the delegate-job
|
|
# wrapper hands its agents, from the one builder in lib.sh. The dispatch
|
|
# token and attempt id are what make the agent's own `started` name THIS
|
|
# dispatch (trial-2 finding: without them the token-checking ack below could
|
|
# never be satisfied by a real agent, and every onboarding sat out 180 s).
|
|
pub="$(delegate_publish_cmd "$DELEGATE_JOB_ID" "$DELEGATE_JOB_TOKEN" "$DELEGATE_JOB_ATTEMPT")"
|
|
# Issue #36: the task goes into a brief in the job's directory and the pane
|
|
# gets only its path, as the delegate-job wrapper does. The injected text is
|
|
# one argv string to herdr (capped at 131,072 bytes), so the whole task
|
|
# plus this preamble could not reach the agent.
|
|
task_reg_dir="${MAM_JOBS_DIR:-${WORKSPACE_ROOT:-$PWD}/.mam/jobs}"
|
|
[[ "$task_reg_dir" == /* ]] || task_reg_dir="$PWD/$task_reg_dir"
|
|
task_brief="$task_reg_dir/$DELEGATE_JOB_ID/brief.md"
|
|
if ! { mkdir -p "${task_brief%/*}" && printf '%s' "$SUBMIT_JOB_PROMPT" > "$task_brief"; }; then
|
|
echo "ERROR: cannot write the task brief $task_brief" >&2
|
|
delegate_publish_event "$DELEGATE_JOB_ID" error "task brief could not be written" "$DELEGATE_JOB_TOKEN" "$DELEGATE_JOB_ATTEMPT"
|
|
JOB_TERMINALIZED=1
|
|
exit 1
|
|
fi
|
|
instructions="Your job_id is \"$DELEGATE_JOB_ID\" (the one just registered for THIS delegation).
|
|
|
|
On start run: $pub --event started.
|
|
On progress (optional): $pub --event progress --detail '<short status>'.
|
|
On success run: $pub --event completed --detail '<one-line summary>'.
|
|
On failure run: $pub --event error --detail '<one-line reason>'.
|
|
|
|
Task: read the complete task in $task_brief and follow it.
|
|
If you delegate any part of it with multi-agent-mux-delegate-job submit, add --parent-job $DELEGATE_JOB_ID so the orchestrator waits for it."
|
|
|
|
# Inject instructions into the herdr pane
|
|
rc=0
|
|
inject_instructions "$SESSION_NAME" "$instructions" "$DELEGATE_JOB_ID" || rc=$?
|
|
# ISSUE-23: rc 4 is "Enter was not seen to be accepted within the send_keys
|
|
# window", which is where a TUI that is still finishing its start-up lands.
|
|
# It is a warning now; the agent's own `started` decides. rc 1/2/3 are still
|
|
# failures: they say the pane never quiesced, a dialog was blocking, or the
|
|
# text never appeared - none of which a later `started` can repair.
|
|
if [ "$rc" -ne 0 ] && [ "$rc" -ne 4 ]; then
|
|
delegate_publish_event "$DELEGATE_JOB_ID" error "instruction injection failed (prompt-lock, rc=$rc)" "$DELEGATE_JOB_TOKEN" "$DELEGATE_JOB_ATTEMPT"
|
|
JOB_TERMINALIZED=1
|
|
exit 1
|
|
fi
|
|
if [ "$rc" -eq 4 ]; then
|
|
echo "WARNING: Enter was not observed to be accepted for '$SESSION_NAME' (rc=4); waiting for the agent's own started event instead." >&2
|
|
fi
|
|
|
|
# ISSUE-23: this script no longer publishes `started` on the agent's behalf.
|
|
# It used to, which made the registry show a started job even when the agent
|
|
# never saw the prompt, and left any started-based verdict meaningless.
|
|
#
|
|
# How long to wait for the handshake: MAM_ONBOARD_ACK_S, else the wrapper's
|
|
# MAM_INJECT_ACK_GRACE_S, else the job's own ack timeout. The create-specific
|
|
# variable comes first so this wait can be shortened - in a sandbox with no
|
|
# real agent, for instance - without also retuning the delegate-job
|
|
# wrapper's injection ack, which is a different wait with its own tests.
|
|
ONBOARD_ACK_S="${MAM_ONBOARD_ACK_S:-${MAM_INJECT_ACK_GRACE_S:-${DELEGATE_JOB_ACK_S:-180}}}"
|
|
if ! delegate_wait_started "$DELEGATE_JOB_ID" "$ONBOARD_ACK_S" "$DELEGATE_JOB_TOKEN"; then
|
|
# The job failed, the session did not: the pane is up and the row is valid,
|
|
# only the handshake never landed. Killing a live agent over a slow or
|
|
# silent onboarding would destroy more than it protects, and it would
|
|
# contradict this script's own rule for the same situation one step
|
|
# earlier - a TUI that never signalled ready is preserved for diagnosis,
|
|
# not rolled back. So: mark the job, warn loudly, keep the session.
|
|
# MULTI_AGENT_RULES §6 has the GM gate on this job's `completed` anyway,
|
|
# so an unacknowledged onboarding already blocks real work.
|
|
# A job that is already terminal is not marked again: `error` over a
|
|
# `completed` is refused by the registry anyway (trial-2 log: "conflicting
|
|
# error after terminal completed"), and a job the agent has finished is
|
|
# not an undelivered one - only the evidence was missing.
|
|
ONBOARD_JOB_ST="$(delegate_job_status "$DELEGATE_JOB_ID")"
|
|
case "$ONBOARD_JOB_ST" in
|
|
completed|error|cancelled)
|
|
echo "WARNING: no started event naming this dispatch for '$SESSION_NAME' within ${ONBOARD_ACK_S}s, but job $DELEGATE_JOB_ID is already '$ONBOARD_JOB_ST'; leaving it as it is." >&2
|
|
;;
|
|
*)
|
|
delegate_publish_event "$DELEGATE_JOB_ID" error "onboarding delivery unconfirmed: no started within ${ONBOARD_ACK_S}s" "$DELEGATE_JOB_TOKEN" "$DELEGATE_JOB_ATTEMPT"
|
|
echo "WARNING: '$SESSION_NAME' did not acknowledge the onboarding prompt within ${ONBOARD_ACK_S}s." >&2
|
|
echo " The session is preserved and job $DELEGATE_JOB_ID is marked error; re-send the onboarding with multi-agent-mux-delegate-job submit once the agent is up." >&2
|
|
;;
|
|
esac
|
|
JOB_TERMINALIZED=1
|
|
fi
|
|
# Issue #50: the handshake only said the agent saw the prompt. Wait for the
|
|
# onboarding itself to end; its result is this script's exit code, so the
|
|
# orchestrator learns it from create (MULTI_AGENT_RULES §6) instead of
|
|
# guessing from herdr's agent_status. A job marked `error` just above is
|
|
# terminal and returns at once.
|
|
ONBOARD_WAIT_S=$(mam_int_knob MAM_ONBOARD_WAIT_S 600 1)
|
|
ONBOARD_T0=$(date +%s)
|
|
ONBOARD_STATUS="$(delegate_wait_terminal "$DELEGATE_JOB_ID" "$ONBOARD_WAIT_S")"
|
|
ONBOARD_STATUS="${ONBOARD_STATUS:-missing}"
|
|
# Trigger immediate priority reconcile cycle asynchronously
|
|
# Issue #21 phase 2 (A8): the control root's reconcile/watchdog, never the
|
|
# (unreviewed) copy in a worktree --workspace; identical paths by default.
|
|
(bash "$SKILL_DIR/multi-agent-mux-monitor/scripts/reconcile.sh" --once >/dev/null 2>&1 &) || true
|
|
WD_PID=$(start_watchdog "$DELEGATE_JOB_ID" "$WORKSPACE_ROOT")
|
|
echo "watchdog PID: $WD_PID"
|
|
case "$ONBOARD_STATUS" in
|
|
completed)
|
|
echo "onboarding completed: job $DELEGATE_JOB_ID ($(( $(date +%s) - ONBOARD_T0 ))s)"
|
|
;;
|
|
*)
|
|
# The session is kept, as for a missing handshake above: the row says
|
|
# what happened and the exit code tells the caller. The trap is cleared
|
|
# first - a non-zero exit must not roll a live session back.
|
|
_mark_row_note "onboarding-$ONBOARD_STATUS"
|
|
echo "ERROR: onboarding $ONBOARD_STATUS for '$SESSION_NAME': job $DELEGATE_JOB_ID; the session is kept" >&2
|
|
trap - EXIT
|
|
case "$(delegate_onboard_exit "$ONBOARD_STATUS")" in
|
|
44) exit 44 ;;
|
|
*) exit 45 ;;
|
|
esac
|
|
;;
|
|
esac
|
|
fi
|
|
# Clear cleanup trap as registration was fully successful
|
|
trap - EXIT
|
|
echo "agent-sessions.yaml updated"
|
|
echo
|
|
echo "Attach: herdr session attach $SESSION_NAME"
|
|
echo "Delete: use multi-agent-mux-stop skill"
|
|
echo "Resume: use multi-agent-mux-resume skill (after first message creates a session id)"
|