diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index 8156922..709e879 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -101,17 +101,11 @@ case "$AGENT" in esac # 4. Spawn new herdr session + run agent with the saved id -case "$AGENT" in - claude) - START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" - eval "$START_CMD" - # auto-handle trust / bypass dialogs - handle_startup_dialogs "$SESSION_NAME" 20 - ;; - agy|hermes|grok|opencode|codex) - eval "herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" - ;; -esac +eval "herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\"" +# Issue #24: start-up dialogs (trust prompts, blocking notices) are answered for +# every agent from the adapter's signatures - the same call create makes. +# --no-auto-trust leaves a directory-trust dialog to a human (MAM_AUTO_TRUST=0). +handle_startup_dialogs "$SESSION_NAME" 20 "$AGENT" # 4. Update agent-sessions.yaml: status running, last_visible_status # (Also automatically publishes a `progress --detail "resumed"` event to the multi-agent-mux-delegate-job registry if a delegate_job_id exists) diff --git a/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh b/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh index c5a58ff..0776864 100755 --- a/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh +++ b/.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh @@ -13,6 +13,8 @@ Usage: $0 --workspace --agent --s Options: --herdr-session NAME specify isolated herdr session name (alias: --herdr-server) + --no-auto-trust do not answer a start-up directory-trust dialog (default: answer it; + other blocking notices are always answered) - same switch as create --dry-run Simulates resume flow (resolves binary, environment) without writing any updates to YAML or DB. Safe to execute inside active write transactions. EOF @@ -25,6 +27,10 @@ HERDR_SERVER_OPT="" HERDR_WORKSPACE_OPT="" DRY_RUN=0 +# Issue #24: a resumed CLI shows the same directory-trust dialog as a fresh +# one, so resume takes the same switch as create (MAM_AUTO_TRUST is what +# handle_startup_dialogs reads). +AUTO_TRUST=1 while [ $# -gt 0 ]; do case "$1" in @@ -33,6 +39,8 @@ while [ $# -gt 0 ]; do --session) SESSION_NAME="$2"; shift 2 ;; --herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;; --herdr-workspace) HERDR_WORKSPACE_OPT="$2"; shift 2 ;; + --auto-trust) AUTO_TRUST=1; shift ;; + --no-auto-trust) AUTO_TRUST=0; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "ERROR: unknown arg: $1" >&2; exit 2 ;; @@ -46,6 +54,7 @@ case "$AGENT" in *) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;; esac [ -n "$SESSION_NAME" ] || { echo "ERROR: --session required" >&2; exit 2; } +export MAM_AUTO_TRUST="$AUTO_TRUST" # Issue #21 phase 2 (A11): a session resumes only in the directory it ran in # (its conversation is keyed by that cwd; a class A agent would otherwise start @@ -226,10 +235,10 @@ fi # 4. Spawn new agent session (delegates to herdr translation shim) _herdr new-session -d -s "$SESSION_NAME" -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL" -if [ "$AGENT" = "claude" ]; then - # auto-handle trust / bypass dialogs - handle_startup_dialogs "$SESSION_NAME" 20 -fi +# Issue #24: every agent, the same call create makes. The signatures and accept +# keys come from the agent adapter; this used to run only for claude, so a +# resumed agy/grok/codex sat on its trust prompt. +handle_startup_dialogs "$SESSION_NAME" 20 "$AGENT" # Wait for TUI readiness or let it settle sleep 2 diff --git a/tests/test_issue24_resume_dialogs.py b/tests/test_issue24_resume_dialogs.py new file mode 100644 index 0000000..544006e --- /dev/null +++ b/tests/test_issue24_resume_dialogs.py @@ -0,0 +1,120 @@ +"""Issue #24, resume path: start-up dialogs are answered for every agent. + +resume_session.sh called handle_startup_dialogs only for claude and passed no +agent, so a resumed agy/grok/codex sat on its directory-trust prompt exactly as +the create path did before this branch. These fix the call contract resume +now shares with create. + +What is real: the shipped create/stop/resume scripts run end to end against +the mock herdr in a sandbox. What is not: the mock's pane always shows a +ready banner, so no test here drives a real trust dialog - the create-side +tests (test_issue23_24_startup_path.py, S4-S6) fix which keys each dialog +gets, and R4 shows the same shipped function is now reached from resume. +""" + +import json +import os +import subprocess + +import pytest + +SCRIPTS = ("multi-agent-mux-create/scripts/create_session.sh", + "multi-agent-mux-stop/scripts/stop_session.sh", + "multi-agent-mux-resume/scripts/resume_session.sh") + + +def _script(tmp_path, rel): + return str(tmp_path / ".agents" / "skills" / rel) + + +def _create_and_stop(tmp_path, agent, name): + ws = str(tmp_path / ("ws_" + name)) + os.makedirs(ws, exist_ok=True) + create, stop, _ = (_script(tmp_path, s) for s in SCRIPTS) + subprocess.run(["bash", create, "--workspace", ws, "--agent", agent, + "--role", "creator", "--session", name, "--no-onboard"], + check=True, cwd=str(tmp_path), capture_output=True, text=True) + subprocess.run(["bash", stop, "--session", name, "--agent", agent], + check=True, cwd=str(tmp_path), capture_output=True, text=True) + return ws + + +def _resume(tmp_path, agent, name, ws, extra=()): + _, _, resume = (_script(tmp_path, s) for s in SCRIPTS) + return subprocess.run(["bash", resume, "--workspace", ws, "--agent", agent, + "--session", name, *extra], + cwd=str(tmp_path), capture_output=True, text=True) + + +def _record_dialog_calls(tmp_path): + """Override handle_startup_dialogs in the SANDBOX copy of lib.sh (never the + repo's) so the call resume makes - session, timeout, agent, MAM_AUTO_TRUST + - is written down. Bash keeps the last definition, so appending wins.""" + log = tmp_path / "dialog_calls.log" + lib = tmp_path / ".agents" / "skills" / "lib.sh" + with open(lib, "a") as f: + f.write('\nhandle_startup_dialogs() {\n' + ' printf \'%%s\\n\' "sess=$1 timeout=${2:-} agent=${3:-} ' + 'auto_trust=${MAM_AUTO_TRUST:-}" >> "%s"\n' + '}\n' % log) + return log + + +def _calls(log): + return log.read_text().splitlines() if log.exists() else [] + + +@pytest.mark.parametrize("agent", ["agy", "codex"]) +def test_r1_resume_answers_dialogs_for_a_non_claude_agent_and_names_it( + mam_sandbox, mock_herdr, mock_agents, agent): + """R1: the gate is gone and the agent is passed - the two halves of the + create contract resume was missing.""" + tmp_path = mam_sandbox + name = "test-r1-" + agent + ws = _create_and_stop(tmp_path, agent, name) + log = _record_dialog_calls(tmp_path) # armed after create, so only resume is recorded + res = _resume(tmp_path, agent, name, ws) + assert res.returncode == 0, res.stdout + res.stderr + assert _calls(log) == ["sess=%s timeout=20 agent=%s auto_trust=1" % (name, agent)] + + +def test_r2_claude_keeps_its_dialog_handling(mam_sandbox, mock_herdr, mock_agents): + """R2: claude is still handled, now by the same call as everyone else.""" + tmp_path = mam_sandbox + ws = _create_and_stop(tmp_path, "claude", "test-r2-claude") + log = _record_dialog_calls(tmp_path) + res = _resume(tmp_path, "claude", "test-r2-claude", ws) + assert res.returncode == 0, res.stdout + res.stderr + assert _calls(log) == ["sess=test-r2-claude timeout=20 agent=claude auto_trust=1"] + + +def test_r3_no_auto_trust_reaches_the_dialog_handler(mam_sandbox, mock_herdr, mock_agents): + """R3: --no-auto-trust on resume sets MAM_AUTO_TRUST=0 for the handler, + the same switch with the same meaning as create's.""" + tmp_path = mam_sandbox + ws = _create_and_stop(tmp_path, "codex", "test-r3-codex") + log = _record_dialog_calls(tmp_path) + res = _resume(tmp_path, "codex", "test-r3-codex", ws, extra=("--no-auto-trust",)) + assert res.returncode == 0, res.stdout + res.stderr + assert _calls(log) == ["sess=test-r3-codex timeout=20 agent=codex auto_trust=0"] + + +def test_r4_the_shipped_handler_really_runs_for_a_non_claude_resume( + mam_sandbox, mock_herdr, mock_agents): + """R4 (no override): with the real handle_startup_dialogs, a codex resume + completes and the pane is read after the spawn - the handler was reached + and returned on the ready banner. Before this change no pane read happened + between new-session and the YAML update for a non-claude agent.""" + tmp_path = mam_sandbox + ws = _create_and_stop(tmp_path, "codex", "test-r4-codex") + before = len(json.load(open(mock_herdr)).get("calls", [])) + res = _resume(tmp_path, "codex", "test-r4-codex", ws) + assert res.returncode == 0, res.stdout + res.stderr + calls = json.load(open(mock_herdr)).get("calls", [])[before:] + + def _has(c, *verbs): # the mock records argv with a leading `--session ` + return any(tuple(c[i:i + len(verbs)]) == verbs for i in range(len(c))) + + spawn = next(i for i, c in enumerate(calls) if _has(c, "agent", "start")) + reads = [c for c in calls[spawn + 1:] if _has(c, "agent", "read") or _has(c, "pane", "read")] + assert reads, "no pane read after the spawn: handle_startup_dialogs never ran\n%s" % calls