Skip to content

fix: reject agent prompts to blocked agents - #197

Merged
brchue-ux merged 1 commit into
masterfrom
fm/herdr-upstream-blocked-agent-rejection-20260823-v2
Aug 23, 2026
Merged

fix: reject agent prompts to blocked agents#197
brchue-ux merged 1 commit into
masterfrom
fm/herdr-upstream-blocked-agent-rejection-20260823-v2

Conversation

@brchue-ux

Copy link
Copy Markdown
Owner

Adopts upstream 9351b058 ("fix: reject prompts to blocked agents", upstream herdrdev#2790) onto fork/master.

What changes

agent.prompt (and therefore herdr agent prompt, with or without --wait) now hard-errors with
agent_blocked when the resolved agent's terminal is in AgentState::Blocked — an approval or
question 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 is
resolved and before any try_send_bytes / send_bytes_after, so nothing is written and no
delayed Enter is scheduled. terminal.state is the same field pane_agent_status() maps to the
API's agent_status, so the rejection is exactly the state the API already reports as blocked.

--wait inherits this for free: src/api/wait.rs::prompt_agent dispatches the prompt first and
returns the response verbatim when agent_from_response fails, so a blocked target returns
agent_blocked without ever entering the lifecycle wait.

agent send-keys is 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 plus
    agent_prompt_focuses_copilot_before_submitting, which this fork has never had. Only the new
    test was kept, and the #[tokio::test] attribute belonging to the following test was restored.
  • docs/next/CHANGELOG.md — the fork's ### Fixed list is its own; only the one relevant
    upstream 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 the
    agent prompt --wait paragraph. The fork's wording is preserved with upstream's new
    "unless the agent is already blocked" clause folded in.

skills/herdr/SKILL.md auto-merged and was reverted: per CLAUDE.md it tracks the latest
stable release and must not change in feature work. It also dragged in unrelated upstream
--timeout wording. The ja/zh-cn doc mirrors and src/cli/spec.rs's after_help auto-merged
cleanly 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:

test with guard guard removed
app::api::agents::tests::agent_prompt_rejects_blocked_agent_without_writing PASS FAIL — response has no error field at all (it was a success agent_prompted)
cli cases::agents::agent_start_command_works PASS FAIL — tests/cli/agents.rs:184, CLI exit code Some(0) instead of Some(1)

The CLI test drives a real server and a real agent pane: it reports the pane blocked, confirms
agent get shows agent_status: "blocked", then runs agent prompt main "must not be submitted" --wait --timeout 2000 and asserts exit code 1, stderr JSON error.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 check is green on the committed tree (fmt + clippy, full cargo nextest suite,
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 not
check for Blocked first. Findings, offered as a note rather than a fix — firstmate's scripts are
out of scope for this task and live outside this worktree:

  1. No firstmate script uses herdr agent prompt at all. Every pane input goes through
    pane run, pane send-text, or pane send-keys (bin/backends/herdr.sh:1686-1719), which
    this change does not gate. So adopting this cannot break the fleet drivers — but it also means
    the fleet gets none of its protection.
  2. fm_backend_herdr_send_text_submit types before it looks. It calls
    fm_backend_herdr_send_literal (→ pane send-text) first, and only then reads agent get as a
    submit-confirmation baseline. In that classifier (fm_backend_herdr_classify_submit_agent_status)
    blocked maps to busy, which is not a refusal — it only selects the composer-read
    confirmation branch. Text has already been delivered by that point.
  3. bin/fm-send.sh deliberately has no pre-send state preflight — there is an explicit comment
    saying not to add one ("Do not add a separate passive liveness preflight here").
  4. The away-mode injector's busy-guard does not cover blocked.
    bin/fm-supervise-daemon.sh's try_inject gates on pane_is_busy, which for herdr resolves to
    fm_backend_herdr_classify_agent_status, and that function maps blocked -> idle on 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's blocked -> idle
mapping should stay shared between the watcher and the injector.

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.
@brchue-ux
brchue-ux merged commit baf1a98 into master Aug 23, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant