fix: reject agent prompts to blocked agents - #197
Merged
brchue-ux merged 1 commit intoAug 23, 2026
Conversation
Adopt upstream 9351b05 (herdrdev#2790). agent.prompt now returns an agent_blocked error instead of delivering text when the resolved agent's terminal is in the Blocked state, so an automated prompt can no longer answer an approval or question dialog by accident. The rejection happens before any terminal write, and before --wait starts its lifecycle wait. The fork's rewritten --wait wording in docs/next is preserved with the new blocked clause folded in, and skills/herdr/SKILL.md is left on its released text per the docs policy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adopts upstream
9351b058("fix: reject prompts to blocked agents", upstream herdrdev#2790) ontofork/master.What changes
agent.prompt(and thereforeherdr agent prompt, with or without--wait) now hard-errors withagent_blockedwhen the resolved agent's terminal is inAgentState::Blocked— an approval orquestion dialog — instead of typing the text into that dialog and pressing Enter.
The check sits in
src/app/api/agents.rs::handle_agent_prompt, immediately after the terminal isresolved and before any
try_send_bytes/send_bytes_after, so nothing is written and nodelayed Enter is scheduled.
terminal.stateis the same fieldpane_agent_status()maps to theAPI's
agent_status, so the rejection is exactly the state the API already reports asblocked.--waitinherits this for free:src/api/wait.rs::prompt_agentdispatches the prompt first andreturns the response verbatim when
agent_from_responsefails, so a blocked target returnsagent_blockedwithout ever entering the lifecycle wait.agent send-keysis untouched — it stays the deliberate way to answer a dialog.Reconciliation with this fork
Three conflicts, all resolved by hand:
src/app/api/agents.rs(tests) — the incoming side carried upstream's new test plusagent_prompt_focuses_copilot_before_submitting, which this fork has never had. Only the newtest was kept, and the
#[tokio::test]attribute belonging to the following test was restored.docs/next/CHANGELOG.md— the fork's### Fixedlist is its own; only the one relevantupstream line was taken, inserted at the top of the list per this fork's convention.
docs/next/website/src/content/docs/agent-automation.mdx— the fork had reworded theagent prompt --waitparagraph. The fork's wording is preserved with upstream's new"unless the agent is already
blocked" clause folded in.skills/herdr/SKILL.mdauto-merged and was reverted: perCLAUDE.mdit tracks the lateststable release and must not change in feature work. It also dragged in unrelated upstream
--timeoutwording. Theja/zh-cndoc mirrors andsrc/cli/spec.rs'safter_helpauto-mergedcleanly and were kept.
Verification
The error path is real end-to-end, not swallowed. Both the unit test and the CLI integration test
were run as a negative control with the guard deleted, and both fail:
app::api::agents::tests::agent_prompt_rejects_blocked_agent_without_writingerrorfield at all (it was a successagent_prompted)cli cases::agents::agent_start_command_workstests/cli/agents.rs:184, CLI exit codeSome(0)instead ofSome(1)The CLI test drives a real server and a real agent pane: it reports the pane
blocked, confirmsagent getshowsagent_status: "blocked", then runsagent prompt main "must not be submitted" --wait --timeout 2000and asserts exit code 1, stderr JSONerror.code == "agent_blocked", and— after a 400 ms settle — that the fake agent's captured-prompts file is byte-identical to before.
So the rejection is a real non-zero exit with a structured error, and no text reached the pane.
just checkis green on the committed tree (fmt + clippy, fullcargo nextestsuite,integration-assets, plugin-marketplace, Windows-target lint, and the 134 maintenance script tests).
Residual risk: firstmate's own fleet-driver scripts
I read (read-only, changed nothing) the sibling firstmate home's
bin/for input paths that do notcheck for
Blockedfirst. Findings, offered as a note rather than a fix — firstmate's scripts areout of scope for this task and live outside this worktree:
herdr agent promptat all. Every pane input goes throughpane run,pane send-text, orpane send-keys(bin/backends/herdr.sh:1686-1719), whichthis change does not gate. So adopting this cannot break the fleet drivers — but it also means
the fleet gets none of its protection.
fm_backend_herdr_send_text_submittypes before it looks. It callsfm_backend_herdr_send_literal(→pane send-text) first, and only then readsagent getas asubmit-confirmation baseline. In that classifier (
fm_backend_herdr_classify_submit_agent_status)blockedmaps tobusy, which is not a refusal — it only selects the composer-readconfirmation branch. Text has already been delivered by that point.
bin/fm-send.shdeliberately has no pre-send state preflight — there is an explicit commentsaying not to add one ("Do not add a separate passive liveness preflight here").
blocked.bin/fm-supervise-daemon.sh'stry_injectgates onpane_is_busy, which for herdr resolves tofm_backend_herdr_classify_agent_status, and that function mapsblocked -> idleon purpose(its comment: a blocked agent is "stuck waiting on the human, not grinding"). So a Blocked pane
is not considered busy, and the only thing between an escalation digest and a confirmation
dialog is the composer-emptiness guard, which is an incidental defence, not a state check.
None of this is affected by this PR, and none of these paths would have been changed by upstream's
fix even if the fleet had adopted it, because they are pane-level rather than agent-level input.
Worth a separate decision on whether
fm_backend_herdr_classify_agent_status'sblocked -> idlemapping should stay shared between the watcher and the injector.