fix(#23, #24): 교차 서버 herdr 스코프와 에이전트 무관 시작 다이얼로그, 실제 에이전트가 충족하는 온보딩 전달 판정 #35

Merged
Godopu merged 15 commits from fix/issue-23-24 into main 2026-09-23 23:36:37 +00:00
11 changed files with 344 additions and 29 deletions
Showing only changes of commit 5ba04a27ca - Show all commits

No files matched your search

+106 -22
View File
@@ -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 <sess> [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 <agent>
# Populate MAM_STARTUP_DIALOGS for <agent>, 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|<idx>|<key> <key>..." or "skip|<idx>|", 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 <sess> [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
+1
View File
@@ -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)}")
@@ -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')
@@ -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"
@@ -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',)
@@ -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')
+15
View File
@@ -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
@@ -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 메타 캡처
+8 -2
View File
@@ -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():
+14 -3
View File
@@ -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():
+142
View File
@@ -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