diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index c39b9e2..d3b5920 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -2204,7 +2204,7 @@ wait_for_tui_ready() { # handle it and do NOT declare ready on this iteration. tail_lines=$(printf '%s\n' "$content" | grep -v '^[[:space:]]*$' | tail -n 20) if printf '%s\n' "$tail_lines" | grep -E -q "$modal_pat" 2>/dev/null; then - handle_startup_dialogs "$sess" 1 || true + handle_startup_dialogs "$sess" 1 "$agent" || true sleep 1 continue fi @@ -2254,7 +2254,7 @@ wait_for_tui_ready() { fi # 4. Startup dialog & hint resolution if not ready - handle_startup_dialogs "$sess" 1 || true + handle_startup_dialogs "$sess" 1 "$agent" || true sleep 1 done echo "⚠️ TUI readiness check timed out for '$sess'." >&2 @@ -2702,29 +2702,113 @@ send_keys_safe() { return 4 } -# handle_startup_dialogs [timeout_sec=20] -# Post-start/resume dialog policy for claude: accept the trust / bypass -# dialogs ONLY when their signature is positively on screen; return as soon -# as the TUI banner is ready. Replaces the blind Enter/Down/Enter sequence. +# _mam_load_dialog_facts +# Populate MAM_STARTUP_DIALOGS for , once per agent per shell. Only that +# one fact is taken: `facts` is an eval contract (F8), and evaluating all of it +# here would clobber ready/placeholder tokens a caller deliberately set. +# An agent the adapters do not know keeps an empty list. +_mam_load_dialog_facts() { + local agent="$1" _f="" + if [ "${_MAM_DIALOGS_AGENT:-}" = "$agent" ]; then + return 0 + fi + _f="$("$(_delegate_py_bin)" -m lib_py.agents facts "$agent" 2>/dev/null | sed -n 's/^MAM_STARTUP_DIALOGS=//p')" || _f="" + if [ -n "$_f" ]; then + eval "MAM_STARTUP_DIALOGS=$_f" + else + MAM_STARTUP_DIALOGS='[]' + fi + _MAM_DIALOGS_AGENT="$agent" + return 0 +} + +# _mam_dialog_pick — first actionable dialog on this frame. +# stdout: "send|| ..." or "skip||", nothing when there is +# none to act on. rc 1 means "nothing to do" (matched-but-cooling counts). +_mam_dialog_pick() { + MAM_DLG_PANE="$1" MAM_DLG_JSON="$2" MAM_DLG_TRUST="$3" \ + MAM_DLG_STATE="$4" MAM_DLG_NOW="$5" MAM_DLG_COOLDOWN="$6" \ + python3 -c ' +import os, re, sys, json +pane = os.environ.get("MAM_DLG_PANE", "") +try: + dialogs = json.loads(os.environ.get("MAM_DLG_JSON") or "[]") +except Exception: + dialogs = [] +auto_trust = os.environ.get("MAM_DLG_TRUST", "1") != "0" +try: + now = int(os.environ.get("MAM_DLG_NOW") or 0) + cooldown = int(os.environ.get("MAM_DLG_COOLDOWN") or 5) +except ValueError: + sys.exit(1) +state = {} +for tok in (os.environ.get("MAM_DLG_STATE") or "").split(): + k, _, v = tok.partition(":") + try: + state[int(k)] = int(v) + except ValueError: + pass +for idx, entry in enumerate(dialogs): + try: + pattern, keys, kind = entry[0], entry[1], entry[2] + if not re.search(pattern, pane): + continue + except Exception: + continue + last = state.get(idx) + if last is not None and 0 <= now - last < cooldown: + sys.exit(1) + if kind == "trust" and not auto_trust: + print("skip|%d|" % idx) + sys.exit(0) + print("send|%d|%s" % (idx, " ".join(keys))) + sys.exit(0) +sys.exit(1) +' +} + +# handle_startup_dialogs [timeout_sec=20] [agent=claude] +# Answer the blocking modals an agent shows before its TUI is usable. The +# signatures and accept keys come from the agent adapter (MAM_STARTUP_DIALOGS), +# so this is agent-agnostic - issue #24: it used to be a claude-only ladder +# called only for claude, and agy/grok/codex sat on their trust prompt until +# the readiness wait timed out and onboarding was skipped. +# +# `--source visible` keeps showing an accepted dialog for a while (#24 +# comment), so a signature still on screen is NOT evidence that the keys were +# missed: re-sending for the same signature is gated by MAM_DIALOG_RESEND_S +# (default 5s) and acceptance is judged only by the ready tokens. +# +# MAM_AUTO_TRUST=0 leaves `trust` dialogs unanswered (warned once per +# cooldown); `notice` dialogs are always answered. handle_startup_dialogs() { - local sess="$1" timeout="${2:-20}" waited=0 pane + local sess="$1" timeout="${2:-20}" agent="${3:-claude}" + local waited=0 pane pick action rest idx keys key now + local cooldown="${MAM_DIALOG_RESEND_S:-5}" + local auto_trust="${MAM_AUTO_TRUST:-1}" + _mam_load_dialog_facts "$agent" + local dialogs="${MAM_STARTUP_DIALOGS:-[]}" + local ready_re="${MAM_STRONG_READY_TOKENS:-${MAM_READY_TOKENS:-}}" while [ "$waited" -lt "$timeout" ]; do pane=$(_pane_tail "$sess" 20) - if printf '%s\n' "$pane" | grep -Eq 'Do you trust the files|Yes, I trust this folder|Quick safety check'; then - _sks_herdr send-keys -t "$sess" Enter - elif printf '%s\n' "$pane" | grep -q 'Yes, try it'; then - # Fullscreen-renderer upsell modal (not the idle /tui tip). Enter would - # accept and restart the session without permission flags — reject. - _sks_herdr send-keys -t "$sess" Escape - elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then - _sks_herdr send-keys -t "$sess" Down - sleep 0.3 - _sks_herdr send-keys -t "$sess" Enter - elif printf '%s\n' "$pane" | grep -q 'Resuming the full session'; then - _sks_herdr send-keys -t "$sess" Enter - elif printf '%s\n' "$pane" | grep -q 'Press Enter to continue'; then - _sks_herdr send-keys -t "$sess" Enter - elif printf '%s\n' "$pane" | grep -Eq "${_MAM_READY_TOKENS_CLAUDE:-Anthropic|Assistant|Chat|Welcome}"; then + now=$(date +%s) + pick="" + pick=$(_mam_dialog_pick "$pane" "$dialogs" "$auto_trust" "${_MAM_DIALOG_SENT:-}" "$now" "$cooldown" 2>/dev/null || true) + if [ -n "$pick" ]; then + action="${pick%%|*}" + rest="${pick#*|}" + idx="${rest%%|*}" + keys="${rest#*|}" + if [ "$action" = "send" ]; then + for key in $keys; do + _sks_herdr send-keys -t "$sess" "$key" || true + sleep 0.3 + done + else + echo "handle_startup_dialogs: a trust dialog is open on '$sess' and --no-auto-trust was given; accept it manually" >&2 + fi + _MAM_DIALOG_SENT="${_MAM_DIALOG_SENT:-} $idx:$now" + elif [ -n "$ready_re" ] && printf '%s\n' "$pane" | grep -Eq "$ready_re" 2>/dev/null; then return 0 fi sleep 1 diff --git a/.agents/skills/lib_py/agents/__main__.py b/.agents/skills/lib_py/agents/__main__.py index d437b75..0796dfb 100644 --- a/.agents/skills/lib_py/agents/__main__.py +++ b/.agents/skills/lib_py/agents/__main__.py @@ -63,6 +63,7 @@ def main(): print(f"MAM_STRONG_READY_TOKENS={q(adapter.strong_ready_tokens)}") print(f"MAM_WEAK_READY_TOKENS={q(adapter.weak_ready_tokens)}") print(f"MAM_MODAL_TOKENS={q(adapter.modal_tokens or '')}") + print(f"MAM_STARTUP_DIALOGS={q(json.dumps(adapter.startup_dialogs))}") print(f"MAM_EXIT_KEY={q(adapter.exit_key)}") print(f"MAM_DELEGATE_AGENT_KEY={q(adapter.delegate_agent_key)}") diff --git a/.agents/skills/lib_py/agents/adapters/agy.py b/.agents/skills/lib_py/agents/adapters/agy.py index ba20214..948bc07 100644 --- a/.agents/skills/lib_py/agents/adapters/agy.py +++ b/.agents/skills/lib_py/agents/adapters/agy.py @@ -23,6 +23,15 @@ class AgyAgentAdapter(BaseAgentAdapter): def delegate_agent_key(self) -> str: return 'antigravity-cli' + @property + def startup_dialogs(self) -> list: + # #24: "Do you trust the contents of this project?" with + # "> Yes, I trust this folder" preselected - Enter accepts. + return [ + ('Do you trust the contents of this project|Yes, I trust this folder', + ['Enter'], 'trust'), + ] + @property def identity_cache_fields(self) -> tuple: return ('conversation_id', 'conversation_db', 'conversation_brain_dir') diff --git a/.agents/skills/lib_py/agents/adapters/claude.py b/.agents/skills/lib_py/agents/adapters/claude.py index 189334d..bf60fc7 100644 --- a/.agents/skills/lib_py/agents/adapters/claude.py +++ b/.agents/skills/lib_py/agents/adapters/claude.py @@ -44,6 +44,19 @@ class ClaudeAgentAdapter(BaseAgentAdapter): def modal_tokens(self) -> Optional[str]: return 'Try the new fullscreen renderer\\?' + @property + def startup_dialogs(self) -> list: + # The ladder handle_startup_dialogs used to hardcode, unchanged. + return [ + ('Do you trust the files|Yes, I trust this folder|Quick safety check', ['Enter'], 'trust'), + # Fullscreen-renderer upsell, not the idle /tui tip: Enter would + # accept and restart the session without the permission flags. + ('Yes, try it', ['Escape'], 'notice'), + ('Yes, proceed', ['Down', 'Enter'], 'trust'), + ('Resuming the full session', ['Enter'], 'notice'), + ('Press Enter to continue', ['Enter'], 'notice'), + ] + def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str: return f"{ctx.claude_dir}/{ctx.ws_key}/{uuid}.jsonl" diff --git a/.agents/skills/lib_py/agents/adapters/codex.py b/.agents/skills/lib_py/agents/adapters/codex.py index 1e7a5c7..8b688fc 100644 --- a/.agents/skills/lib_py/agents/adapters/codex.py +++ b/.agents/skills/lib_py/agents/adapters/codex.py @@ -24,6 +24,20 @@ class CodexAgentAdapter(BaseAgentAdapter): def delegate_agent_key(self) -> str: return 'codex-cli' + @property + def startup_dialogs(self) -> list: + # #24: the trust prompt lists "1. Yes, continue" preselected, so Enter + # accepts. Trust here also loads this project's local config, hooks and + # exec policy, which is why --no-auto-trust exists. + # + # The 0.155.1 update notice (observed 2026-09-22) blocks the session the + # same way; "2" is Skip, the choice that changes nothing. + return [ + ('Do you trust the contents of this directory|1\\. Yes, continue', + ['Enter'], 'trust'), + ('1\\. Update now|Skip until next version', ['2'], 'notice'), + ] + @property def identity_cache_fields(self) -> tuple: return ('session_id',) diff --git a/.agents/skills/lib_py/agents/adapters/grok.py b/.agents/skills/lib_py/agents/adapters/grok.py index 3f27860..055a4ab 100644 --- a/.agents/skills/lib_py/agents/adapters/grok.py +++ b/.agents/skills/lib_py/agents/adapters/grok.py @@ -26,6 +26,13 @@ class GrokAgentAdapter(BaseAgentAdapter): def delegate_agent_key(self) -> str: return 'grok-build' + @property + def startup_dialogs(self) -> list: + # #24: "Do you trust the contents of this directory?" answered with y. + return [ + ('Do you trust the contents of this directory', ['y'], 'trust'), + ] + @property def identity_cache_fields(self) -> tuple: return ('session_id', 'session_jsonl') diff --git a/.agents/skills/lib_py/agents/base.py b/.agents/skills/lib_py/agents/base.py index bd2221a..75d47ef 100644 --- a/.agents/skills/lib_py/agents/base.py +++ b/.agents/skills/lib_py/agents/base.py @@ -156,6 +156,21 @@ class BaseAgentAdapter: def modal_tokens(self) -> Optional[str]: return None + @property + def startup_dialogs(self) -> List[tuple]: + """Blocking modals this CLI can show before its TUI is usable, most + specific first: (pattern, keys, kind). + + `pattern` is an extended-regex signature matched against the pane tail, + `keys` are herdr send-keys arguments sent in order, and `kind` is + 'trust' (a directory-trust prompt, gated by --auto-trust) or 'notice' + (anything else that blocks the start, always answered). + + Issue #24: without this the start-up trust prompts of agy, grok and + codex burn the whole readiness timeout and onboarding is skipped. + """ + return [] + @property def strong_ready_tokens(self) -> str: return self.ready_tokens diff --git a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh index c8f8d81..de59d5f 100755 --- a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh +++ b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh @@ -43,6 +43,7 @@ Options: 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) @@ -60,6 +61,11 @@ 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. +AUTO_TRUST=1 while [ $# -gt 0 ]; do case "$1" in @@ -74,6 +80,8 @@ while [ $# -gt 0 ]; do --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 ;; @@ -81,6 +89,8 @@ while [ $# -gt 0 ]; do esac done +export MAM_AUTO_TRUST="$AUTO_TRUST" + if [ -n "$HERDR_SERVER_OPT" ]; then export HERDR_SESSION_NAME="$HERDR_SERVER_OPT" fi @@ -331,8 +341,11 @@ if [ "$TUI_READY_RC" -ne 0 ]; then fi fi -if [ "$AGENT" = "claude" ] && [ -z "$LAST_VISIBLE_STATUS_OVERRIDE" ]; then - handle_startup_dialogs "$SESSION_NAME" 15 +# 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 메타 캡처 diff --git a/tests/test_b19_headless_reconcile_fixes.py b/tests/test_b19_headless_reconcile_fixes.py index 7e618e4..4f925cd 100644 --- a/tests/test_b19_headless_reconcile_fixes.py +++ b/tests/test_b19_headless_reconcile_fixes.py @@ -282,13 +282,19 @@ if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; def test_mam_dialog_tokens_exclude_yes_try_it(): - """N-1 / F-1: token list must not contain Yes, try it; Escape branch stays.""" + """N-1 / F-1: token list must not contain Yes, try it; Escape branch stays. + + Issue #24 moved the per-agent ladder out of lib.sh into the agent adapter, + so the Escape branch is now claude's own `startup_dialogs` entry. The two + token-list assertions are unchanged; only where the branch lives moved. + """ content = open(_LIB_SH, encoding="utf-8").read() modal_line = next(l for l in content.splitlines() if l.startswith("_MAM_MODAL_TOKENS=")) hint_line = next(l for l in content.splitlines() if l.startswith("_MAM_HINT_TOKENS=")) assert "Yes, try it" not in modal_line assert "Yes, try it" not in hint_line - assert "grep -q 'Yes, try it'" in content + adapter = os.path.join(os.path.dirname(_LIB_SH), "lib_py", "agents", "adapters", "claude.py") + assert "('Yes, try it', ['Escape'], 'notice')" in open(adapter, encoding="utf-8").read() def test_wait_for_tui_ready_succeeds_on_fullscreen_tip(): diff --git a/tests/test_c1_tui_readiness.py b/tests/test_c1_tui_readiness.py index e91daba..96a14c7 100644 --- a/tests/test_c1_tui_readiness.py +++ b/tests/test_c1_tui_readiness.py @@ -55,7 +55,16 @@ if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; def test_c1b_hint_press_enter_sends_enter(): - """C-1b (B-3): 'Press Enter to continue' alone must return dialog CLOSED, and wait_for_tui_ready must send Enter exactly once per iteration (30 total across 30 iterations, not 60).""" + """C-1b (B-3): 'Press Enter to continue' alone must return dialog CLOSED, + and wait_for_tui_ready must never send Enter twice for one frame. + + Issue #24 replaced "once per iteration" with a time cooldown + (MAM_DIALOG_RESEND_S): a `--source visible` snapshot keeps showing an + accepted dialog for a while, so re-sending on every poll types into a TUI + that already moved on. This test stubs `sleep`, so the wall clock barely + advances and one cooldown window covers the whole run - hence 1, not 30. + The guard the case was written for (never 2 per iteration) is unchanged + and strictly stronger now.""" screen = """Some startup banner Press Enter to continue """ @@ -73,7 +82,7 @@ export MAM_STRONG_READY_TOKENS='NonExistentStrong' export MAM_WEAK_READY_TOKENS='' export MAM_INPUT_PLACEHOLDER='' rc=0 -wait_for_tui_ready dummy-sess custom || rc=$? +wait_for_tui_ready dummy-sess claude || rc=$? echo "RC=$rc" echo "KEYS_COUNT=$(grep -c "send-keys -t dummy-sess Enter" "$KEYS" || echo 0)" rm -f "$KEYS" @@ -81,7 +90,9 @@ rm -f "$KEYS" res = _run_lib_helpers(script) assert res.returncode == 0, res.stderr + res.stdout assert "DIALOG_CLOSED" in res.stdout - assert "KEYS_COUNT=30" in res.stdout + count = int(res.stdout.split("KEYS_COUNT=")[1].split()[0]) + assert count >= 1, "the dialog was never answered: " + res.stdout + assert count <= 30, "one frame must never be answered twice: " + res.stdout def test_c1c_transition_to_upsell_modal_sends_escape_not_enter(): diff --git a/tests/test_issue23_24_startup_path.py b/tests/test_issue23_24_startup_path.py index c5d8d3f..c73ec4c 100644 --- a/tests/test_issue23_24_startup_path.py +++ b/tests/test_issue23_24_startup_path.py @@ -302,3 +302,145 @@ def test_s13_the_loop_watcher_applies_the_same_rule(mam_sandbox): watcher.wait_once("agt", session="iso-x", run=_run) assert seen["env"] is not None and "HERDR_SESSION" not in ("",) + + +# -------------------------------------------------------------------------- +# item 2 — start-up dialogs come from the adapter (#24) +# +# MOCKED, and this is the limit of it: the pane text is a fixture string and +# send-keys is a shell stub, so these fix OUR contract - which screen signature +# gets which keys, once. They do NOT show that the keys drive a real agy, grok, +# codex or opencode TUI; no such TUI can be started in this environment. That +# check is a live herdr run after the loop. +# -------------------------------------------------------------------------- + +def _run_dialogs(tmp_path, agent, pane_text, timeout=1, env=None, prelude=""): + lib = tmp_path / ".agents" / "skills" / "lib.sh" + pane_file = tmp_path / "pane.txt" + pane_file.write_text(pane_text) + keys_log = tmp_path / "sent_keys.txt" + if keys_log.exists(): + keys_log.unlink() + script = ''' +source "%s" +_pane_tail() { cat "%s"; } +_sks_herdr() { printf '%%s\\n' "$*" >> "%s"; } +%s +handle_startup_dialogs test-sess %d %s +echo "rc=$?" +''' % (lib, pane_file, keys_log, prelude, timeout, agent) + run_env = dict(os.environ) + run_env.pop("MAM_AUTO_TRUST", None) + run_env.update(env or {}) + res = subprocess.run(["bash", "-c", script], capture_output=True, text=True, + cwd=str(tmp_path), env=run_env) + sent = keys_log.read_text().splitlines() if keys_log.exists() else [] + return res, sent + + +AGENT_DIALOGS = [ + ("agy", "Do you trust the contents of this project?\n > Yes, I trust this folder", + ["send-keys -t test-sess Enter"]), + ("grok", "Do you trust the contents of this directory?", + ["send-keys -t test-sess y"]), + ("codex", "Do you trust the contents of this directory?\n > 1. Yes, continue", + ["send-keys -t test-sess Enter"]), + ("codex", "Update available\n 1. Update now\n 2. Skip\n 3. Skip until next version", + ["send-keys -t test-sess 2"]), + ("claude", "Do you trust the files in this folder?\n Yes, I trust this folder", + ["send-keys -t test-sess Enter"]), + ("claude", "Try the new fullscreen renderer?\n Yes, try it", + ["send-keys -t test-sess Escape"]), +] + + +@pytest.mark.parametrize("agent,pane,expected", AGENT_DIALOGS) +def test_s4_each_agents_dialog_signature_gets_its_own_keys( + mam_sandbox, agent, pane, expected): + """S4 (stubbed pane + stubbed send-keys): the adapter's signature is + matched and exactly its keys are sent.""" + res, sent = _run_dialogs(mam_sandbox, agent, pane) + assert sent == expected, (res.stdout, res.stderr, sent) + + +def test_s4b_every_adapter_publishes_the_fact(mam_sandbox): + """S4b (real `lib_py.agents facts`): MAM_STARTUP_DIALOGS is part of the + facts contract for every agent, so the shell never has to know an agent + name. An agent with no known blocking modal publishes an empty list.""" + repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + skills = os.path.join(repo, ".agents", "skills") + seen = {} + for agent in ("claude", "agy", "grok", "codex", "opencode", "hermes"): + out = subprocess.run( + [os.path.join(repo, ".venv", "bin", "python"), "-m", "lib_py.agents", + "facts", agent], + capture_output=True, text=True, cwd=repo, + env=dict(os.environ, PYTHONPATH=skills)).stdout + line = [l for l in out.splitlines() if l.startswith("MAM_STARTUP_DIALOGS=")] + assert line, "no MAM_STARTUP_DIALOGS for %s" % agent + seen[agent] = json.loads(line[0].split("=", 1)[1].strip("'")) + for agent in ("claude", "agy", "grok", "codex"): + assert seen[agent], "%s must publish its start-up dialogs" % agent + for pattern, keys, kind in seen[agent]: + assert kind in ("trust", "notice"), (agent, kind) + assert keys, (agent, pattern) + assert seen["opencode"] == [] and seen["hermes"] == [] + + +def test_s5_an_accepted_dialog_still_on_screen_is_not_resent(mam_sandbox): + """S5 (stubbed pane + stubbed send-keys): #24's comment reports that + `--source visible` keeps showing the dialog for a while after it was + accepted, and _pane_capture hardcodes that source. The pane here never + changes, so a frame-count rule would send the keys again every poll; the + time cooldown must keep it at one.""" + res, sent = _run_dialogs( + mam_sandbox, "grok", "Do you trust the contents of this directory?", + timeout=3, env={"MAM_DIALOG_RESEND_S": "60"}) + assert sent == ["send-keys -t test-sess y"], (res.stdout, res.stderr, sent) + + +def test_s5b_the_cooldown_expiring_allows_one_more_try(mam_sandbox): + """S5b: the cooldown is a rate limit, not a one-shot - a dialog that really + did not take the keys is retried once the window passes.""" + res, sent = _run_dialogs( + mam_sandbox, "grok", "Do you trust the contents of this directory?", + timeout=4, env={"MAM_DIALOG_RESEND_S": "1"}) + assert len(sent) >= 2, (res.stdout, res.stderr, sent) + assert set(sent) == {"send-keys -t test-sess y"}, sent + + +def test_s5c_readiness_is_judged_by_the_ready_tokens_not_by_the_dialog_leaving( + mam_sandbox): + """S5c: a ready screen returns 0 without sending anything.""" + res, sent = _run_dialogs(mam_sandbox, "grok", "Grok ready\n>>> ", timeout=3) + assert sent == [], sent + assert "rc=0" in res.stdout, res.stdout + res.stderr + + +def test_s6_no_auto_trust_leaves_trust_dialogs_alone_but_answers_notices( + mam_sandbox): + """S6 (stubbed): --no-auto-trust. codex trust is left to a human with a + warning; the update notice still gets answered, since skipping it changes + nothing and it blocks the session just as hard.""" + res, sent = _run_dialogs( + mam_sandbox, "codex", "Do you trust the contents of this directory?\n > 1. Yes, continue", + timeout=1, env={"MAM_AUTO_TRUST": "0"}) + assert sent == [], sent + assert "no-auto-trust" in res.stderr, res.stderr + + res, sent = _run_dialogs( + mam_sandbox, "codex", "Update available\n 1. Update now\n 3. Skip until next version", + timeout=1, env={"MAM_AUTO_TRUST": "0"}) + assert sent == ["send-keys -t test-sess 2"], (res.stderr, sent) + + +def test_s6b_create_session_no_longer_gates_dialogs_on_claude(mam_sandbox): + """S6b (source read): the #24 root cause was the `AGENT = claude` gate at + the create_session call site, so the gate's absence is the regression.""" + repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + src = open(os.path.join(repo, ".agents", "skills", "multi-agent-mux-create", + "scripts", "create_session.sh")).read() + call = [l for l in src.splitlines() if "handle_startup_dialogs" in l] + assert call, "create_session no longer answers start-up dialogs at all" + assert all('"$AGENT"' in l for l in call), call + assert 'if [ "$AGENT" = "claude" ] && [ -z "$LAST_VISIBLE_STATUS_OVERRIDE" ]' not in src