native macOS codings agent orchestrator prowl.onev.cat
Prowl docs components agent-detection.md
33 kB

Agent Detection #

How Prowl knows there's an agent in a pane and whether it's Working, Blocked, Idle, or Done — and which agents it recognizes.

Keywords: agent detection, claude, codex, gemini, cursor, working, blocked, idle, done, status, process probe, screen heuristics, indicator, spinner

Related: active-agents · notifications · terminal

What it is #

Prowl continuously inspects each terminal pane to decide whether a coding agent is running and what state it's in. That signal drives the Active Agents panel, the per-tab activity indicator, Canvas cards lighting up, and notifications.

Devin CLI #

Devin 3000.11.3 uses a devin terminal process and a devin acp child. Prowl selects the ACP child for native session ownership and keeps the original terminal launcher identity. An open devin/cli/session_locks/<id>.lock descriptor identifies the session exactly; leftover lock files and unrelated transcripts are not scanned.

The live composer footer identifies Working while thinking, running tools, or streaming text. Directory trust, permission choices, and selection menus are Blocked. An empty composer after completion or cancellation is Idle. Retained dialog text and old working footers above a new composer are ignored.

Devin currently uses screen state and cooperative delivery, without a managed hook_devin channel or a native state provider. Diagnostic logs and the session store do not expose a complete live state contract; the native Stop hook fires before other hooks can prevent stopping. A workflow finishes only after its explicit prowl workflow deliver receipt. agents read has no semantic Devin result reader; use prowl read for terminal output and workflow delivery for complete artifacts.

Antigravity CLI #

Antigravity (agy, verified 1.3.2) keeps an attached conversation's ~/.gemini/antigravity-cli/presence/<id>.lock descriptor open for the life of the session, so that open file — not a lock file left behind after exit — identifies the session exactly. Its conversations/<id>.db and brain/<id>/.system_generated/logs/transcript.jsonl share the same id.

The detector reads the full active screen and anchors on the live composer: a terminal-wide ─ border, a column-0 > prompt row (wrapped input continues on indented rows), and a terminal-wide ─ bottom border. The built-in status row renders directly below that box — esc to cancel or esc to interrupt is Working while a turn runs, ? for shortcuts is Idle — padded away from the right-aligned model label (Gemini 3.8 Flash · high, Claude Sonnet 4.6 (Thinking), or nothing until the label resolves). The last composer on screen is the live one, and the row under its bottom border is the only state evidence: a stack_with_default status script renders verbatim below that row, so nothing below it counts, however it is shaped. A typed draft drops the signature from the status row (only the model label remains), and the slash-command autocomplete popup that opens under the composer when / is typed rewrites it to esc to cancel whatever the turn state; both are unknown, so the state from before the user started typing is retained. A screen without a composer, a composer caught without its status row, or a viewer such as /help or /model is unknown, never Idle.

Workspace-trust, tool-permission, and ask-user dialogs are Blocked: they replace the composer with option rows (a column-0 > marks the selection, within eight rows above the hint) and a ↑/↓ Navigate hint row, and a permission dialog keeps esc to cancel, so the dialog read runs before the status row and nothing below the hint can veto it. The autocomplete popup shares the hint shape but renders below a live composer box, which a dialog never does, so a hint under a composer is the popup. Agent responses render indented, so column-0 dialog chrome is the live dialog or the user's own echoed text; a verbatim quote at column 0 reads Blocked until it scrolls off the screen, a delay rather than a dispatch into a modal prompt. Answered dialogs leave only the chosen option echoed at column 0 (> Red), never the hint. A bare hint row with no selection — cropped chrome or residue — denies the composer evidence below it and reads unknown. Option shape alone is not dialog evidence: a slash command echoes as a column-0 > row with no rule above it and an indented result beneath, and an echoed prompt whose rule scrolled off the top of the screen looks the same, so a dialog whose hint copy the detector does not recognize falls through to the composer read and fails toward unknown, never Idle.

Terminal width is the longest ─-only column-0 row on screen. The echoed prompt's rule is narrower, agent responses render indented, and the dialog rule is not followed by a prompt row, so none of them can pose as the composer. The accepted residual is a user's own status script that draws a terminal-wide ─/>/─ box followed by a padded status signature under an idle or working composer; that output is the user's configuration, and the plain spoofs reviewed so far (? for shortcuts custom help, narrower boxes, dividers) fail toward unknown, while a live dialog stays Blocked whatever is stacked beneath it.

A pane that never shows a recognized screen — a headless agy --print run, a viewer overlay such as /diff, or a future layout change — reports unknown the whole time. Its process is still classified, but an unknown pane emits no roster entry: it is absent from Active Agents and prowl agents, agents wait --until idle|blocked times out on it, and agents wait --until exit treats it as gone. The state machine retains the last recognized state, so a working turn that only briefly drops its footer stays Working.

Antigravity currently uses screen state and cooperative delivery, without a managed hook channel or transcript reader.

Agents it recognizes #

Claude (Claude Code), Codex, Gemini, Cursor, Cline, OpenCode, GitHub Copilot, Kimi, Droid, Amp, Pi (pi), Oh My Pi (omp, oh-my-pi), Qoder CLI (qodercli), Qwen Code (qwen), Grok Build (grok), Devin (devin), and Antigravity (agy). Detection covers common wrappers (node, python, bun, bash, etc.) so agents launched indirectly are still found. Pi and Oh My Pi are independent detected agents. Pi recognizes its own minimal working/idle cues, including its built-in braille-prefixed Working... loader and a running pi-subagents background card after the parent turn settles; Oh My Pi owns its richer spinner and interactive Ask-prompt heuristics, plus its own session layout and icon. Grok Build also ships an agent symlink; Prowl only treats that name as Grok when the path points at a ~/.grok/ install (so Cursor's own agent entrypoint stays Cursor).

How detection works #

  1. Process probe. Prowl reads the pane's foreground process group and matches process names / argv against known agent executables, scoring argv[0] highest, then process name, then command-line tokens.

  2. Screen heuristics. Claude detection consumes the full active screen (bounded by the terminal height); Pi starts from the last ~32 non-blank lines so an expanded background-agent widget stays intact, and every other agent starts from the last ~24 as a guard against transcript history. The classifier then selects agent-specific live UI regions rather than treating every transcript line as current state. Structured confirmation/permission chrome is Blocked; status rows and spinners are Working. Claude working rows come from a live status block walked bottom-up from its prompt box by row shape — the spinner or ● status row, ⎿ attachments such as todo lists and tips, queued ❯ messages, and right-aligned chrome — stopping at the first transcript-shaped row, so a long todo list cannot push the live row out of view and a status row quoted inside a ⏺ block cannot read as live. Confirmation text is consulted only around a current numbered selection row such as ❯ 1. Yes; a bare input prompt cuts off the preceding transcript. Claude's internal scroll view keeps the composer visible while hiding live status. When Jump to bottom (click) ↓ or a counted new message(s) (click) ↓ control appears in the last non-blank row above that composer, Prowl treats the screen as a viewer and retains the last known state. The control can overlay text in the middle of a row. Return to the bottom to refresh screen-based status; completion while browsing history is not visible to this detector. Codex uses exact bottom-of-screen •/◦ Working (... esc to interrupt) and •/◦ Waiting for background terminal (... esc to interrupt) footer fallbacks. The footer can also sit up to three text rows above the composer, so └ detail rows such as a background command or a Tip: between them do not hide it. Braille-only starfield rows around the composer do not count toward that footer window; animation alone does not indicate Working. An empty Codex composer hint and status line remain Idle evidence with Astra's starfield background, so a workflow can send its first task before any turn has completed. The shortcut hint row that Codex 0.158 shows below the status line (← for agents · ? for shortcuts) does not change this. Draft text and attachments do not qualify as an empty composer. Its confirmation detector requires a numbered selected row such as › 1. Yes paired with a live bottom footer or an explicit Yes/No choice structure. It also recognizes the directory-trust (including Codex 0.158's Trust this folder? prompt), hook-review, and initial sign-in menus as Blocked from their complete selected-choice and footer structures. Ordinary prompt text and completed responses are not confirmation boundaries. Amp (verified 0.0.1791547250) reads only its bottom composer box. Any status in the box's bottom border — Connecting, Sending, Waiting, Thinking, Streaming (with or without a token counter), Running Tools, and whatever else the thread client reports behind its ∼/≈/≋ spinner — is Working; a bare border is Idle. Disconnected and Amp Is Redeploying keep the previous state. A dialog box directly above the composer (Approval Required with ‣-marked options, Tell Amp what to do differently) is Blocked even though the border still says Running Tools, and so is a ‣ option row inside the composer (Out of Credits). Quoted status or approval text elsewhere on screen is ignored, and a screen without a complete composer keeps the previous state. Older Amp builds cannot start threads any more, so their esc to cancel and approval-text cues are no longer recognized. Pi also treats its bottom ── <braille spinner> Working ── footer and the adjacent async subagent … · background header with a matching braille job row as Working. The compact subagents (N/M running), progressive Async agents · N agent(s) running, and multi-job Async agents · background layouts carry the same signal; completed, paused, and failed cards use static glyphs and remain idle. Oh My Pi's loader row is Working evidence when it leads with the theme's Esc glyph (󱊷, ⎋, or esc) followed by any label: the label is the model's own intent (Working…, Running requested command, 等待命令完成, …) and is not enumerated. The row counts in the bottom five non-blank rows and in the five rows directly above the live box composer, so a long queued draft does not hide it. While a turn runs, Oh My Pi's status line swaps its brand glyph for a braille spinner and turn timer (⠋ 9s); the box composer embeds that status in its ╭── header, which is Working evidence when the header belongs to the live composer — the last ╭ row followed only by │ input rows and a final ╰ row, which covers one-line, multiline, and IME-safe layouts. An older spinner frame above transcript text or above a newer idle composer is ignored. The band (fresh-install default), claude, and the other composer shapes start a status row with that spinner, which the leading-spinner rule reads; an idle composer shows the static brand glyph instead. Other agent families keep their own patterns (including Oh My Pi's Working… ⟦esc⟧ loader and the Esc-prefixed loader row above, braille frames, symbol cycles, Cursor's hexagons, Kimi's moon phases, etc.). Copilot recognizes the bottom Working … esc interrupt footer across its ∙ ∘ ○ ◎ ◉ animation frames, including an optional streaming-size field such as · 101 B or · 1.2 KB. Its live boxed numbered choices with enter to select · esc to cancel are Blocked, including folder trust; that picker takes precedence over the older esc to cancel working cue. Claude's live status row (● <label>… (<elapsed> · …)) accepts a multi-word label and a compound elapsed segment such as 28m 34s or 1h 4m 2s, so a turn keeps reporting Working after it passes a minute. For Claude, background agents end the main turn while they run, leaving ✻ Waiting for 1 background agent to finish above the input box — a spinner glyph with no ellipsis, which the spinner pattern alone rejects. Prowl reads that wait row as Working, along with the older background-workflow footer (3/5 agents done · 7m 29s · ↓ 288.5k tokens) below the box. The agent switcher block below the box (⏺ main plus one ◯ row per agent) is deliberately not read as activity: a subagent that returns control while it still awaits collection keeps its row with the elapsed frozen, so the rows cannot distinguish running work from finished work on a single frame.

  3. State decision. Every agent uses one state machine. Codex also supplies incremental JSONL lifecycle evidence from process-owned session logs, without installing hooks. Claude supplies process-scoped native state without hooks. Other agents keep their screen-only behavior. For Codex, an observed open parent turn or child task stays Working through quiet periods; current approval/question UI takes precedence as Blocked. Completion closes only its matching work. Initial historical records do not start new work.

    Multiple main sessions fall back to screen heuristics unless only one has open work or main-turn activity within the last two minutes. Known subagents are grouped with their parent. Child notifications and file mtimes do not refresh main activity. Missing/unreadable logs or unknown lineage also fall back to the screen. Attribution is heuristic: switching to an old quiet chat before entering a new prompt can temporarily retain the previous log candidate.

    A codex typed by hand in Codex 0.157+ attaches to a shared background daemon that owns every session log, so the TUI process holds none. Prowl then reads the daemon's logs for the thread this pane's Codex last submitted to, identified through the pane's Codex session log (see cli identity), plus that thread's subagents. Until the pane's Codex submits its first message, and for a moment after each new submit, detection falls back to the screen. A Codex launched from an Agent Profile or with --no-daemon owns its logs and is read directly. If a previously established daemon binding becomes unavailable, detection falls back to the screen but retains its log cursors. When the binding returns, it consumes pending events without replaying earlier turn starts.

    Claude reads the detected PID's native registry under its configured root (~/.claude/sessions by default). busy and shell mean Working, including assigned child and background shell work; waiting means Blocked, and idle means Idle. Process generation is checked before and after each read. /new and resume use the registry's current session. Two processes sharing a transcript retain independent states. Missing, partial, stale, or unsupported records fall back to the screen. Background daemon/remote sessions are not supported by this adapter. A manually relocated config root must be supplied through a Prowl launch profile; shell-only overrides can fall back to screen.

For diagnostics and sanitized regression captures, prowl read --source detection returns the exact active-screen buffer used by stage 2. It is explicitly requested because it can differ from the visible viewport when a pane is scrolled; the default prowl read behavior is unchanged.

prowl agents --json and prowl agents read --json use detection_reason for the final state decision and screen_reason for the screen rule. Codex can report log.openWork, log.turnEnded, or a screen.* fallback reason; raw_state remains the latest screen classification. A current blocker reports its screen-rule ID. Claude reports native.working, native.blocked, or native.idle; a fresh screen blocker can retain its screen-rule ID. Screen-only runtimes keep their existing rule identifiers. An ordinary profile miss reports fallback.noRuleMatched; unmigrated classifiers report legacy.detector. Reasons never include screen text. Screen fallback IDs are:

  • screen.logUnavailable: log authority is unavailable or suspended.
  • screen.ambiguousLogs: more than one log root is recently active or has open work.
  • screen.noLiveTurn: no root has current log authority.
  • screen.afterTurn: new Working screen evidence appeared after completion.
  • screen.retainedCompletion: an unchanged completed frame is still suppressed.

For idle waits and dispatch readiness, a current log.turnEnded or native.idle decision keeps the existing idle evidence and stabilization rules even when the screen reports fallback.noRuleMatched. Without current log authority, that unmatched screen provides no idle evidence. Dispatch still checks the input area before delivery.

Use status to decide whether intervention is needed. A blocked screen_reason with Idle status can be a stale prompt fenced by completion; do not send Enter based on the screen reason alone.

Native acquisition warnings report a bounded failure category and recovery, without registry content. They use the same per-category 30-second throttle.

Log acquisition warnings use the AgentDetection category. ProwlLogger writes them to stdout in Debug and the unified log in Release. They identify the PID, cursor count, and failure category (incompleteInventory, partialHeader, unknownLineage, or continuityLost). Warnings are limited to one per failure category per provider every 30 seconds; a successful read reports recovery once after any failure, including a throttled one. Continuity-loss warnings report the post-reset cursor count and a bounded error kind/code. These logs contain no transcript or screen content.

Detection tolerates several consecutive process-probe misses before declaring an agent gone. Screen-only state is deterministic: a recognized Working, Blocked, or Idle frame takes effect on the next active scan, without a time-based Working hold or generic screen-motion inference. Viewer overlays (Claude's transcript / history-search views) are an explicit exception: their chrome covers the live status area, so those frames keep the last trusted state instead of forcing Idle.

The state machine #

Raw states: working, blocked, idle, unknown.

Display states (what you see):

Display Derived from Meaning
Working raw working actively processing
Blocked raw blocked waiting for the user (a prompt)
Done raw idle + unseen just finished; you haven't looked yet
Idle raw idle + seen nothing running

A Done pane becomes Idle when it is actually viewed: its worktree and tab are selected, its pane is focused, and the Prowl window is key and visible. Keeping a pane selected while Prowl is inactive, hidden, or minimized does not mark its completion as read. Unknown window state is conservatively treated as not viewed. An acknowledged completion stays read when a pending session lookup finishes; a later unviewed completion still becomes Done. See Canvas for Canvas-specific behavior.

Cooperative signal bus #

Detection remains heuristic UI state. Separately, every live pane now has a terminal-owned multicast observation stream. A process inside the pane can report explicit runtime events:

prowl agents signal turn-ended --detail "Review complete"
prowl agents signal needs-input

Prowl attributes the socket caller through process ancestry, not focus or PROWL_PANE_ID. A signal can exist for an ordinary shell pane with no detected agent and does not create or overwrite a detected-agent entry; such a signal is recorded as unbound and never becomes wait or dispatch evidence. turn-ended means one interaction ended, not that an assigned task completed; only a matching agents dispatch-complete receipt can complete an exact dispatch.

Signal eligibility is generation-aware. Prowl binds evidence to the agent's launch process — the topmost member of the pane's foreground job above the detected process — by PID and process start time, plus its current session only when that attribution is exact or high confidence. Reading state from a forked engine child (Droid runs its TUI and a droid exec engine as two processes) therefore never counts as a replacement: hooks descend from both, and only a new launch changes the generation. A dispatch launch accepts its first process generation only when the process started within ten seconds of launch binding; a later-started process is a replacement even if the original runtime exited before detection. A medium-confidence session guess remains diagnostic and never rotates an evidence epoch. Evidence from a reused PID, a replaced session, a delayed child, or an unverifiable sessionless sender stays diagnostic and cannot advance a wait. prowl agents --json exposes current-epoch channels under each existing detected-agent row's signals; evidence-only shell panes are not added to the roster.

prowl agents wait <pane> --until idle|blocked|changed|exit consumes the eligible stream and reports its source and confidence. Exact/high evidence wins; the default auto policy may use a heuristic match only after two seconds of unchanged state. wait --dispatch is a separate exact receipt path and never treats screen state or turn-ended as task success.

Each observer receives an atomic current snapshot before live changes. Multiple observers do not compete with the app's existing single-consumer terminal event stream. Agent removal keeps the pane stream alive; pane closure emits surfaceClosed and finishes it. Bounded overflow is explicit so future waiters can re-subscribe and resnapshot rather than silently lose lifecycle or signal evidence.

Managed native completion signals #

Prowl Agent Profile launches of Claude Code, Codex, GitHub Copilot, Droid, Qoder, Pi, Oh My Pi, and OpenCode also attach process-scoped native event bridges without writing user, dedicated-home, or project configuration:

  • Claude SessionStart verifies launch coverage; Stop / StopFailure report turn-ended; PermissionRequest, Elicitation, and a Notification of type permission_prompt or elicitation_dialog report needs-input; SessionEnd reports session-end. The idle_prompt notification (Claude has sat at an empty composer for 60 s) is ignored: it means waiting, not needing a person, and accepting it would displace the turn-ended level that idle waits read and wake changed waits on idle panes.
  • Codex's native agent-turn-complete notifier reports turn-ended. Prowl never passes Codex's hook-trust bypass flag.
  • Copilot, Droid, and Qoder report SessionStart, Stop (plus Qoder's StopFailure), and SessionEnd the same way. Their needs-input comes only from a Notification whose type is permission_prompt or elicitation_dialog. Their PermissionRequest event is deliberately ignored: Copilot and Qoder both emit it while the permission service auto-approves a tool and no one is waiting, so it does not mean the agent is blocked. That also makes their needs-input arrive slightly later than Claude's.
  • Pi, Oh My Pi, and OpenCode have no stdin hooks; Prowl ships a small extension for each that relays the runtime's own event names through the same bridge. Pi reports session_start, agent_settled (turn-ended; agent_end fires earlier and is ignored), and session_shutdown; Pi has no permission system, so it never reports needs-input. Oh My Pi reports session_start and session_switch (/new) as session starts, session_stop as turn-ended (its agent_end fires once per in-process task sub-agent and is ignored), tool_approval_requested as needs-input (the built-in approval prompt under --approval-mode always-ask), and session_shutdown. Its in-process task sub-agents run under their own session ids with their session files nested inside the pane session's directory; the extension recognises them by that nesting and forwards only their approval prompts, attributed to the pane's session. OpenCode reports session.idle as turn-ended and permission.asked / question.asked as needs-input, only for the session the pane is talking to — sub-agent sessions are filtered out by their parentID. OpenCode announces no session start: its session exists only after the first prompt and /new / resume emit nothing, so, like Codex, the first session.idle verifies the channel and an ordinary event with a new session id rotates it.

Each runtime is enabled the way it supports per-launch injection, and none of them changes what the user already configured: Copilot loads an extra read-only plugin directory shipped inside Prowl (a user's own --plugin-dir and ~/.copilot/hooks/*.json keep working and are never merged or rewritten), while Droid and Qoder receive a settings object that merges Prowl's handlers into whatever settings the Profile already passes. Because Droid accepts only a settings path, its merged copy is written to an owner-only file that is deleted with the pane. Qoder additionally cannot use flag-supplied hooks at all when --setting-sources is set, so that launch runs unchanged with no exact coverage. Pi (-e) and Oh My Pi (--hook) receive the bundled extension file through an additive flag inserted before the prompt; a user's own extensions and --no-extensions keep working. OpenCode receives the bundled plugin through a launch-scoped OPENCODE_CONFIG_CONTENT whose plugin list is appended to whatever content the Profile or the user's shell already exports (config layers concatenate plugin lists, so global and project plugins are unaffected). --pure or OPENCODE_PURE disables every external plugin, so those launches run unchanged with one warning; under --auto, OpenCode auto-replies permission.asked in the same millisecond, so that event is not registered for such launches.

Only an app-issued token plus exact caller-process ancestry and matching pane/runtime/cwd can produce hook_claude / hook_codex / hook_copilot / hook_droid / hook_qodercli / hook_pi / hook_omp / hook_opencode evidence. The channel is not advertised as verified_live until a valid native event completes that end-to-end check. Early Claude SessionStart payloads wait for the first matching process generation instead of being lost. That first generation may attach after the detector's acquisition window; once attached, a replacement process, pane close, or launched-agent exit revokes coverage.

Codex exposes only one effective notifier. Before launch, Prowl asks Codex's own bounded app-server config/read protocol for the effective notifier, applies selected-profile and final CLI-override precedence, and ignores project-layer notify exactly as Codex does. An absent notifier (including a null configuration value) is valid and Prowl installs its own notifier without forwarding. An existing notifier is preserved through an owner-only ephemeral forwarding record and is exec'd with the original payload whether Prowl transport succeeds or fails. If resolution or record preparation is uncertain, Prowl launches the original argv unchanged, exposes no exact coverage, and reports one non-blocking launch warning.

Managed hooks apply only to Profile launches. Typing claude, codex, copilot, droid, qodercli, pi, omp, opencode, or any other runtime manually keeps the existing cooperative/transcript/process/screen evidence. A hook turn-ended still does not prove assigned-task completion: dispatch receipts and workflow completion remain separate protocols.

How often it runs #

  • No polling for cold panes that have not received recent input.
  • ~2 s for a short warm window after typing, paste, CLI input, or an initial command starts in the pane.
  • ~300 ms once an agent is detected, so Working/Blocked/Done stays responsive.

The heavier process probe is throttled (cached ≈ 0.75 s per process group unless something changes) so many panes don't add up to high CPU. Status indicators redraw on a coarse tick rather than every frame for the same reason.

The indicator #

In tabs and the Active Agents panel, a Working agent shows an animated spinner (the per-agent style detected on screen); Blocked is a distinct attention color; Idle/Done are static. The "working" animation style is also configurable in spirit — Prowl uses a bagua/trigram-style spinner in the agents list.

Worktree running indicator #

The sidebar worktree row spinner and prowl list's task.status report running whenever any pane in the worktree is busy. A pane is busy when:

  • a terminal command reports progress (OSC 9;4 / ConEmu-style, e.g. a long shell command), or
  • a detected agent is Working or Blocked — including Claude waiting on background agents, detected from the ✻ Waiting for … background agent … row even while the input box looks idle.

A Blocked agent is the exception to the spinner. Because it has stopped and is waiting on you, the sidebar row shows a red attention icon instead of the spinner — a spinner there would tell you to wait, which is backwards. The row still counts as running for prowl list's task.status, so the CLI contract is unchanged; use prowl agents to tell blocked from working. A worktree that is being created, archived, or deleted keeps its own spinner, which takes precedence over the agent indicator.

It's a single coarse running/idle bit (it can't distinguish background agents from a long command). For the agent's finer state use the Active Agents panel or prowl agents. Expect up to ~2 s before it lights on a warm pane; panes with a detected agent rescan about every 300 ms.

Settings #

Agent detection is on by default. Related toggles live in the Active Agents and Notifications settings (e.g. autoShowActiveAgentsPanel, showActiveAgentTabTitles).

Gotchas for agents #

  • Detection is heuristic and best-effort. A short-lived command between polls can be missed; an unusual prompt might read as the wrong state.
  • "Blocked" is the one that means a human is needed — it's typically a permission/confirmation prompt the agent is waiting on.
  • For deterministic assigned work, use the prompted Profile dispatch receipt and prowl agents wait --dispatch; generic state waits deliberately retain labelled heuristic fallback. prowl read --wait-stable remains useful screen evidence, but a stable screen is not task completion.