feat(issue-24): startup dialogs come from the agent adapter and are handled while waiting for the TUI

handle_startup_dialogs was a claude-only string ladder called only when
AGENT=claude, and wait_for_tui_ready does not treat a dialog screen as ready.
So agy, grok and codex sat on their directory-trust prompt for the full
readiness timeout, the session was recorded as tui-ready-timeout, and
onboarding injection was disabled (create_session.sh:313-314) - a human had to
accept each dialog by hand.

The signatures and accept keys move to the adapters as `startup_dialogs`
(pattern, keys, kind), published through the existing `facts` eval contract as
MAM_STARTUP_DIALOGS. claude's ladder moves over verbatim; agy takes Enter, grok
y, codex Enter, and codex's update notice takes 2 (Skip), which blocks a start
just as hard while changing nothing. kind is `trust` or `notice`:
--no-auto-trust leaves trust dialogs to a human and still answers notices,
since trusting a directory makes codex load its project-local config, hooks and
exec policy.

Re-sending is gated by time (MAM_DIALOG_RESEND_S, default 5s), not by frames.
#24's comment reports that `--source visible` keeps showing an accepted dialog
for a while, and _pane_capture hardcodes that source, so "still on screen" is
not evidence the keys were missed; acceptance is judged by the ready tokens
alone.

Two existing tests asserted the old shape and are updated, not weakened:
b19's "Escape branch stays" now looks for it in claude's adapter, and c1b's
"Enter once per iteration" becomes "never twice for one frame", which the
cooldown satisfies strictly more tightly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-22 04:22:09 +00:00
1 parent 3bcaa39ab0
commit 5ba04a27ca
11 files changed
+344 -29

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 메타 캡처