You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while validating #727, and not filed anywhere. Validated against main @ cf5d5b99; every cite below was resolved by symbol.
The mechanism
A nudge is delivered as keystrokes plus Enter, not as a message on a side channel. GenericAdapter.send_text (src/bmad_loop/adapters/generic.py:1083-1084) forwards to the multiplexer, and BaseTmuxBackend.send_text (src/bmad_loop/adapters/tmux_base.py:358-361) is:
defsend_text(self, window_id: str, text: str) ->None:
self._tmux("send-keys", "-t", window_id, "-l", text)
time.sleep(0.3) # let the TUI ingest the paste before submittingself._tmux("send-keys", "-t", window_id, "Enter")
PsmuxMultiplexer does not override it, so both tmux-transport backends behave this way. (OpencodeHttpAdapter.send_text, adapters/opencode_http.py:687-697, posts over HTTP instead and is not affected.)
That is correct and necessary when the pane holds an agent waiting for input. It is not correct when the pane holds something else.
Why a permission prompt is exactly the state that trips it
The wake nudge fires on silence, and a session parked on a permission prompt is silent by construction — it has rendered its confirmation screen and is waiting, so the pane log stops growing.
STALL_NUDGE_TEXT (generic.py:133-141) is sent at generic.py:903, inside the branch at generic.py:891-911, when the pane log has not grown for _stall_grace_s.
_stall_grace_s / _stall_nudges come from _configure_dev_knobs (generic.py:1289-1299): limits.dev_stall_grace_s (default 600 s) and limits.dev_stall_nudges (default 2 per silence), bounded per session by limits.dev_stall_nudges_cap (default 6) — policy.py:113-131.
The grace re-arms on pane growth (generic.py:886-890), which is the guard that keeps a working session from being nudged. A parked prompt produces no growth, so the grace expires and the nudge is typed into the prompt.
So the nudge is not merely able to land on a prompt; the prompt's own stillness is what summons it.
What lands
send-keys -l types the nudge text literally into whatever has focus, then Enter submits. All four nudge strings begin with the word "You" — NUDGE_TEXT (generic.py:123), STALL_NUDGE_TEXT (:133), BUDGET_NUDGE_TEXT (:145), CONTRACT_NUDGE_TEXT (:161) — so the first character reaching a confirmation prompt is Y, followed eventually by Enter.
What that actually does is CLI- and prompt-specific and I am not going to claim a single outcome across five profiles: a y/n prompt reads it as an accept, a numbered menu may ignore the letters and have Enter take the highlighted default, and a text-input prompt may swallow the whole sentence. All three are bad, and none of them is a decision bmad-loop intended to make. An unattended loop answering a permission prompt on the operator's behalf, by accident, is the concerning shape regardless of which branch it takes.
Nothing in the orchestrator knows prompts exist
A sweep of src/bmad_loop/**/*.py (run in Python, not shell grep) for any permission-prompt concept returns nothing beyond the bypass_args template comments in policy.py/runsetup.py and the unrelated per-story-spec-approval gate mode. There is no detection, no guard, and no path that treats "a prompt is on screen" differently from "the agent is thinking".
Reachability is not hypothetical
#727 captured a claude session sitting on a confirmation screen with tokens: 0 and a ~1,930-byte pane log containing only the menu. That is precisely the input state described above. Two ways in that are already known: an operator whose bypass flags were dropped (#740 — adapter.extra_args replacing rather than extending bypass_args), and a CLI that prompts for something the bypass flags do not cover.
What is being asked for
Not removal of the nudge. It is the re-invocation bmad-loop otherwise lacks, and the comment at generic.py:128-132 explains why it has to exist — a turn ended to await a slow background process would otherwise wait forever.
The narrow ask is that the nudge stop being able to answer a question. Some directions, in rough order of cheapness:
Do not send the trailing Enter when the pane's last screen looks like a prompt rather than an idle agent. The nudge text still lands, visible to a human attaching later, but nothing is submitted.
Two channels are explicitly not the answer, both settled on #727:
env_fault_patterns — that channel means the CLI never reached the API and it pauses the whole run; the claude profile's own comment (src/bmad_loop/data/profiles/claude.toml:26-32) sets a hard evidentiary bar for adding a pattern.
Found while validating #727, and not filed anywhere. Validated against
main@cf5d5b99; every cite below was resolved by symbol.The mechanism
A nudge is delivered as keystrokes plus Enter, not as a message on a side channel.
GenericAdapter.send_text(src/bmad_loop/adapters/generic.py:1083-1084) forwards to the multiplexer, andBaseTmuxBackend.send_text(src/bmad_loop/adapters/tmux_base.py:358-361) is:PsmuxMultiplexerdoes not override it, so both tmux-transport backends behave this way. (OpencodeHttpAdapter.send_text,adapters/opencode_http.py:687-697, posts over HTTP instead and is not affected.)That is correct and necessary when the pane holds an agent waiting for input. It is not correct when the pane holds something else.
Why a permission prompt is exactly the state that trips it
The wake nudge fires on silence, and a session parked on a permission prompt is silent by construction — it has rendered its confirmation screen and is waiting, so the pane log stops growing.
STALL_NUDGE_TEXT(generic.py:133-141) is sent atgeneric.py:903, inside the branch atgeneric.py:891-911, when the pane log has not grown for_stall_grace_s._stall_grace_s/_stall_nudgescome from_configure_dev_knobs(generic.py:1289-1299):limits.dev_stall_grace_s(default 600 s) andlimits.dev_stall_nudges(default 2 per silence), bounded per session bylimits.dev_stall_nudges_cap(default 6) —policy.py:113-131.generic.py:886-890), which is the guard that keeps a working session from being nudged. A parked prompt produces no growth, so the grace expires and the nudge is typed into the prompt.So the nudge is not merely able to land on a prompt; the prompt's own stillness is what summons it.
What lands
send-keys -ltypes the nudge text literally into whatever has focus, then Enter submits. All four nudge strings begin with the word "You" —NUDGE_TEXT(generic.py:123),STALL_NUDGE_TEXT(:133),BUDGET_NUDGE_TEXT(:145),CONTRACT_NUDGE_TEXT(:161) — so the first character reaching a confirmation prompt isY, followed eventually by Enter.What that actually does is CLI- and prompt-specific and I am not going to claim a single outcome across five profiles: a y/n prompt reads it as an accept, a numbered menu may ignore the letters and have Enter take the highlighted default, and a text-input prompt may swallow the whole sentence. All three are bad, and none of them is a decision bmad-loop intended to make. An unattended loop answering a permission prompt on the operator's behalf, by accident, is the concerning shape regardless of which branch it takes.
Nothing in the orchestrator knows prompts exist
A sweep of
src/bmad_loop/**/*.py(run in Python, not shell grep) for any permission-prompt concept returns nothing beyond thebypass_argstemplate comments inpolicy.py/runsetup.pyand the unrelatedper-story-spec-approvalgate mode. There is no detection, no guard, and no path that treats "a prompt is on screen" differently from "the agent is thinking".Reachability is not hypothetical
#727 captured a
claudesession sitting on a confirmation screen withtokens: 0and a ~1,930-byte pane log containing only the menu. That is precisely the input state described above. Two ways in that are already known: an operator whose bypass flags were dropped (#740 —adapter.extra_argsreplacing rather than extendingbypass_args), and a CLI that prompts for something the bypass flags do not cover.What is being asked for
Not removal of the nudge. It is the re-invocation bmad-loop otherwise lacks, and the comment at
generic.py:128-132explains why it has to exist — a turn ended to await a slow background process would otherwise wait forever.The narrow ask is that the nudge stop being able to answer a question. Some directions, in rough order of cheapness:
crashed, andretryburns the last attempt #727's suggestion 3, which is the open design question there.crashed, andretryburns the last attempt #727's suggestion 2 (probe._probe_argv,probe.py:566, drives a content-free turn with the samebypass_argsa run would).Two channels are explicitly not the answer, both settled on #727:
env_fault_patterns— that channel means the CLI never reached the API and it pauses the whole run; theclaudeprofile's own comment (src/bmad_loop/data/profiles/claude.toml:26-32) sets a hard evidentiary bar for adding a pattern.stall_deadline/ the grace timings — untouched. session-end journaled 2h19 after session_timeout_min fires when the session is wedged inside a tool call #157's 59-minutedocker runis the counter-example to shortening them.