diff --git a/docs-ai/013-prowl-cli/contracts/agents-wait.md b/docs-ai/013-prowl-cli/contracts/agents-wait.md index 7f97e54f..e8a02f00 100644 --- a/docs-ai/013-prowl-cli/contracts/agents-wait.md +++ b/docs-ai/013-prowl-cli/contracts/agents-wait.md @@ -4,28 +4,77 @@ Current versions: +- `prowl.cli.agents.dispatch.v1` - `prowl.cli.agents.dispatch-complete.v1` - `prowl.cli.agents.dispatch-abandon.v1` - `prowl.cli.agents.wait.v1` ## Exact dispatch completion -Every prompted `create tab|pane --profile … --prompt -` returns a pending dispatch. The -launched child receives its id through `PROWL_DISPATCH_ID`; callers cannot supply an id to -completion manually. +Every prompted `create tab|pane --profile … --prompt -` returns a pending dispatch, and +`agents dispatch` (below) returns one for an agent already running in a pane. Callers cannot +supply an id to completion manually. ```bash prowl agents dispatch-complete --outcome succeeded|failed --summary [--json] ``` `--summary` is required, control-free, and limited to 32768 UTF-8 bytes. The app resolves the -socket peer ancestry and requires the caller to belong to the dispatch-bound surface. -Completion is first-write-wins: an identical retry replays the receipt, while a conflicting -outcome or summary fails. `turn-ended` is observation only and never becomes success. +socket peer ancestry to the caller's pane and completes that pane's current pending record; +with no pending record it addresses the pane's most recently issued record so an identical +retry replays its receipt and a conflicting one fails. A caller outside any pane fails with +`DISPATCH_CONTEXT_REQUIRED`; a pane that never held a dispatch fails with +`DISPATCH_NOT_FOUND`. The launch child still receives `PROWL_DISPATCH_ID`, and the CLI +forwards it when present, but it is compatibility diagnostics only: a process launched with an +older id completes the pane's current record. Completion is first-write-wins. `turn-ended` is +observation only and never becomes success. Success returns `target`, completed `receipt`, and `replayed`. The immutable receipt contains `id`, `state=completed`, `outcome`, `summary`, `created_at`, and `completed_at`. +## Re-dispatch into an existing pane + +```bash +prowl agents dispatch --prompt - [--json] +``` + +The pane is resolved once to the immutable target snapshot. `--prompt` accepts only `-` and +reads piped UTF-8 stdin up to 256 KiB; an interactive terminal is rejected. CRLF is +normalized and trailing newlines are dropped by the CLI; the app rejects an empty prompt or +any control character other than newline and tab with `INVALID_ARGUMENT`, because the text is +delivered through the pane's input path (one bracketed paste — `ghostty_surface_text` goes +through Ghostty's paste encoder, so newlines survive as one message in Claude Code and Codex — +followed by Enter) where other control bytes are stripped or reinterpreted. + +Checks run in this order, before any text is typed: + +1. `DISPATCH_PENDING` when the pane already holds a pending record; `error.details.record` + carries it. The existing record is never overwritten — complete, abandon, or lose it first. +2. `AGENT_NOT_FOUND` when the pane hosts no detected agent (no appearance grace: the command + targets an agent that already exists); `AGENT_GONE` when the surface is closed. +3. `DISPATCH_TARGET_BUSY` unless the agent is idle by the arm-time rules of + `agents wait --until idle` under `auto`: an exact `turn-ended` level counts only with detector + corroboration (idle/done); a detector-only idle view counts after two seconds unchanged, and + only where the wait would fall back to the detector (no covering `verified_live` channel, or + one holding no terminal level). Idle by one source alone — a `turn-ended` the detector has + not corroborated yet (its working hold after a turn), or a detector view still stabilizing — + is not a refusal: the precondition polls every 200 ms for up to five seconds, as the wait + would, and refuses only when the budget expires. A working or blocked detector state without + such evidence, or a runtime `needs-input` level, refuses immediately. + `error.details.observation` and `signals` carry the evidence. + +Then one main-actor step issues the record and binds it to the pane's *current* evidence +epoch — never a new one, so the generation that will report the receipt is the one already +proven for the pane — and the rendered text (`[Prowl] ` + prompt + the versioned completion +protocol) is inserted and submitted. A delivery failure cancels the issuance. Success returns +`target` and the pending `dispatch` record with the `create` shape; the id never appears in the +typed text. + +Every later rule is unchanged: `agents wait --dispatch`, `dispatch-abandon`, +`DISPATCH_NEEDS_INPUT`, `DISPATCH_INCOMPLETE`, `AGENT_GONE`, capacity, and eviction apply to the +new record exactly as to a launch record. One pending record per surface is enforced by the +store on binding (`surfacePending`), so a launch and a re-dispatch can never race for one pane. + ## Explicit abandonment ```bash diff --git a/docs-ai/013-prowl-cli/contracts/agents.md b/docs-ai/013-prowl-cli/contracts/agents.md index 7bf85058..a2ed5e23 100644 --- a/docs-ai/013-prowl-cli/contracts/agents.md +++ b/docs-ai/013-prowl-cli/contracts/agents.md @@ -19,7 +19,8 @@ create roster rows. Text output additionally shows a current-process `pN` handle Use `prowl agents read ` for a semantic agent snapshot. A process inside a Prowl pane can report cooperative runtime events with `prowl agents signal`; these commands have separate [read](agents-read.md) and [signal](agents-signal.md) contracts. -Condition and exact-receipt waiting is specified by [agents-wait](agents-wait.md). +Condition and exact-receipt waiting, and re-dispatching a new task into an existing agent +pane with `prowl agents dispatch`, are specified by [agents-wait](agents-wait.md). The complete roster response schema is `#/$defs/agentsResponse` in [`schema-bundle.json`](../../../ProwlCLIContracts/Resources/cli-output-schema.json). diff --git a/docs-ai/013-prowl-cli/contracts/input.md b/docs-ai/013-prowl-cli/contracts/input.md index 55fde449..9cd70979 100644 --- a/docs-ai/013-prowl-cli/contracts/input.md +++ b/docs-ai/013-prowl-cli/contracts/input.md @@ -9,7 +9,7 @@ and the executable [schema bundle](schema.md). ```text prowl [path] prowl open [path] -prowl list | agents [read|signal] | profiles | skills | focus | read | send | key | handoff | create | close +prowl list | agents [read|signal|dispatch|dispatch-complete|dispatch-abandon|wait] | profiles | skills | focus | read | send | key | handoff | create | close ``` Bare path forms (`/`, `./`, `../`, `~/`, `file://`, `.`, `..`) enter `open`. @@ -85,10 +85,27 @@ Session/origin are at most 256 UTF-8 bytes and detail is at most 32768. All are and control-free when present. Parser and handler enforce the same shared validation. See [agents-signal.md](agents-signal.md). +## Agent dispatch grammar + +```bash +prowl agents dispatch --prompt - +prowl agents dispatch-complete --outcome --summary +prowl agents dispatch-abandon --dispatch --reason +prowl agents wait --dispatch [--timeout <1...600>] [--include-screen <1...200>] +prowl agents wait --until [--timeout <1...600>] + [--min-confidence ] [--include-screen <1...200>] +``` + +`agents dispatch` requires a pane-only target and `--prompt -`: the prompt is piped UTF-8 +stdin up to 256 KiB, CRLF-normalized, trailing newlines dropped, non-empty, and free of +control characters other than newline and tab. `dispatch-complete` takes no id; it forwards +a launch-scoped `PROWL_DISPATCH_ID` only when present. See [agents-wait.md](agents-wait.md). + ## Command-specific exceptions - `agents read ` is a pane-only semantic snapshot, no selectors or focus fallback. +- `agents dispatch ` is pane-only as well; it never falls back to focus. - `agents signal` accepts no selector. Its source is the caller pane resolved from the socket peer process ancestry, never UI focus or `PROWL_PANE_ID`. - `handoff` defaults to the calling pane, not UI focus. diff --git a/docs-ai/013-prowl-cli/contracts/schema.md b/docs-ai/013-prowl-cli/contracts/schema.md index fe766410..0300158c 100644 --- a/docs-ai/013-prowl-cli/contracts/schema.md +++ b/docs-ai/013-prowl-cli/contracts/schema.md @@ -16,6 +16,10 @@ The bundle has one versioned success-or-error response schema for every wire com | `agents` | `#/$defs/agentsResponse` | | `agents.read` | `#/$defs/agentsReadResponse` | | `agents.signal` | `#/$defs/agentsSignalResponse` | +| `agents.dispatch` | `#/$defs/agentsDispatchResponse` (errors may carry `#/$defs/agentsDispatchErrorDetails`) | +| `agents.dispatch-complete` | `#/$defs/agentsDispatchCompleteResponse` | +| `agents.dispatch-abandon` | `#/$defs/agentsDispatchAbandonResponse` | +| `agents.wait` | `#/$defs/agentsWaitResponse` (errors may carry `#/$defs/agentsWaitErrorDetails`) | | `profiles` | `#/$defs/profilesResponse` | | `skills` (local-only) | `#/$defs/skillsResponse` | | `focus` | `#/$defs/focusResponse` | diff --git a/docs-ai/064-agent-completion-signals/000-plan.md b/docs-ai/064-agent-completion-signals/000-plan.md index dc488be2..eb374ae5 100644 --- a/docs-ai/064-agent-completion-signals/000-plan.md +++ b/docs-ai/064-agent-completion-signals/000-plan.md @@ -246,6 +246,11 @@ opencode; partial for qodercli/qwen/amp; docs/bundle for the rest). Key conclusi ## Amendments +- Updated 2026-08-29 (#733 re-dispatch): `prowl agents dispatch --prompt -` creates a + new pending record for an agent already running in a pane (one pending record per surface, + idle precondition shared with `agents wait --until idle`, delivery measured as one bracketed + paste), and `dispatch-complete` resolves the record from the caller pane instead of + `PROWL_DISPATCH_ID` — see [014-re-dispatch.md](014-re-dispatch.md). - Updated 2026-08-29 (063 B1 kickoff): 063's `expect` activations are records in this entry's dispatch store (`launch` via the S2 prompted-launch path, `message` via #733's re-dispatch), and S5's watchdog part ships with 063-B2 instead of D2. #733 therefore lands before 063-B3. diff --git a/docs-ai/064-agent-completion-signals/014-re-dispatch.md b/docs-ai/064-agent-completion-signals/014-re-dispatch.md new file mode 100644 index 00000000..b576609c --- /dev/null +++ b/docs-ai/064-agent-completion-signals/014-re-dispatch.md @@ -0,0 +1,112 @@ +# 064.014 — Re-dispatch Into an Existing Pane: Plan and Action + +## Status + +Implemented from `feat/agent-redispatch` for [#733](https://github.com/onevcat/Prowl/issues/733) +(R2a in the shared [release plan](../063-agent-workflows/release-plan.md)). Amends S2 +([003](003-s2-dispatch-wait-design.md)) and the evidence rules of [012](012-cli-evidence-semantics.md) / +[013](013-idle-evidence-fallback.md); the 063 workflow runner consumes the store primitive +introduced here for `message` steps ([dsl-spec §5](../063-agent-workflows/dsl-spec.md)). + +## Trigger + +A coordinator running several review rounds against one reviewer had to choose between a +plain `send` (context kept, no receipt — "asked a question" looks like "finished") and a fresh +Profile launch per round (exact receipt, context lost, every round re-reads the diff and is +exposed to startup failures again). The #732 loop launched three Profiles for that reason; +063's `prowl.adversarial-review` wants one interactive reviewer that is re-dispatched until +its verdict is clean. + +Before this change a dispatch record existed only for a prompted Profile launch: the id rode +the child-only `PROWL_DISPATCH_ID`, `dispatch-complete` read it back from the environment, and +a pane therefore had at most one dispatch for its whole lifetime. + +## Decisions + +| # | Decision | Alternatives rejected | +| --- | --- | --- | +| D1 | `prowl agents dispatch --prompt -` creates a new pending record bound to the existing surface and delivers `[Prowl] ` + prompt + the same versioned completion protocol a launch appends, through the pane's input path (`insertCommittedText` + `submitLine`, the `send` path). The response has the `create` shape (`target`, `dispatch.{id,state,created_at}`). | A `--dispatch` flag on `send` (would make an ordinary send grow receipt semantics); a compact single-line protocol suffix (unnecessary once delivery was measured, and would diverge from the launch prompt agents already recognize). | +| D2 | Multi-line prompts are accepted. Measured: `ghostty_surface_text` routes through `Surface.completeClipboardPaste` → `input.paste.encode`, which wraps the text in bracketed paste (`\x1b[200~ … \x1b[201~`) whenever the TUI has mode 2004 on and otherwise rewrites `\n` as `\r`; a three-line `prowl send` reached both Claude Code 2.1.251 and Codex 0.149.1 as one message (each replied `RECEIVED 3 lines`). The CLI normalizes CRLF and drops trailing newlines; the app rejects any control character other than newline and tab (ESC and the C0 bytes the paste encoder strips to spaces) with `INVALID_ARGUMENT`. Cap 256 KiB, as for `create --prompt`. | Requiring a single line (the fallback the issue anticipated if delivery had split the text). | +| D3 | `dispatch-complete` resolves the record from the caller's process ancestry → pane → that pane's current pending record; with no pending record it addresses the pane's most recently issued record so an identical retry still replays and a conflicting one still fails. `PROWL_DISPATCH_ID` stays launch-scoped, the CLI forwards it when present, the app only logs it when it differs. `DispatchCompleteInput.dispatch_id` became optional (an old CLI still sends it; a new CLI without the variable no longer fails client-side with `DISPATCH_CONTEXT_REQUIRED`). | Requiring the id to match the current record (would break a launched worker after its first re-dispatch, contradicting the issue); a public `--dispatch` on completion (breaks the "no public id" trust model). | +| D4 | One pending record per surface, enforced in the store: `bind` throws `surfacePending` when the surface already holds a pending record (self-rebinding stays idempotent), `pendingSnapshot(surfaceID:)` answers the handler, and the manager checks before issuing so no slot is consumed. A second `dispatch` fails with `DISPATCH_PENDING` carrying the pending record; nothing is typed. | Checking only in the handler (the launch and re-dispatch paths would still be able to race for one pane); superseding the previous record (would silently abandon a running assignment). | +| D5 | The idle precondition reuses the wait handler's evidence rules instead of a new heuristic. The pure evaluation (`exactMatch`, `detectorReports`, `allowsHeuristic`, `heuristicMatches`, the two-second `HeuristicStabilizer`, `normalizedState`) moved from `AgentWaitCommandHandler` into `AgentConditionEvidence`, and `agents dispatch` evaluates the arm-time `--until idle` decision under `auto`: a `turn-ended` level counts only with detector corroboration (every level is pre-arm at dispatch time), a detector-only idle view must stay unchanged for two seconds and only where the wait would fall back to the detector. Idle by one source alone is not yet a refusal — the wait would keep polling — so the precondition polls for up to five seconds (`idleGraceMilliseconds`, covering the detector's three-second working hold after a turn and the two-second stabilization) and only then refuses; working or blocked without such evidence, or a runtime `needs-input` the screen does not show, is `DISPATCH_TARGET_BUSY` immediately, with the observation and signals in `error.details`. `AGENT_NOT_FOUND` has no appearance grace: the command targets an agent that already exists. | Blocking until idle with a `--timeout` (the issue asks for a refusal, and a coordinator that wants to wait has `agents wait --until idle`); accepting the detector's idle view without stabilization (a transient screen would let text merge into a running turn); refusing an uncorroborated `turn-ended` immediately (the first live run did: `--until idle` resolved on the fresh hook signal while the detector still held `working`, and the dispatch issued in the same second was refused — the recipe's two commands must work back to back). | +| D6 | The record binds to the pane's *current* evidence epoch (after reconciling the detector's process generation), never to a new one: the generation that will report the receipt is the one already proven for the pane, so its managed-hook and cooperative signals keep counting. A pane without an epoch record cannot be dispatched to (`DISPATCH_FAILED`). | Calling `beginDispatchEpoch` as the launch path does (its ten-second first-generation window would reject the long-running agent and silence its `verified_live` channel). | +| D7 | Wire additions: `Command.agentsDispatch(DispatchInput{pane,prompt})` → `agents.dispatch`, response `prowl.cli.agents.dispatch.v1` (`AgentDispatchCommandPayload`), governed `AgentDispatchErrorDetails{target,record?,observation?,signals?}` behind `agentsDispatchError`, and the new codes `DISPATCH_PENDING` / `DISPATCH_TARGET_BUSY`. Text mode renders the pending id and refusal evidence. | Reusing the condition-mode wait error details (their `mode`/`condition` fields would lie). | + +## Delivered behavior + +- `AgentDispatchStore`: `pendingSnapshot(surfaceID:)`, `complete(surfaceID:outcome:summary:)` + (pending → latest → `notFound`), and the `surfacePending` guard on `bind`. +- `WorktreeTerminalManager`: `issueAgentDispatch(boundTo:)` (issue + bind to the current epoch + in one main-actor step, rolling back the issuance on a bind failure), + `pendingAgentDispatchSnapshot(surfaceID:)`, `completeAgentDispatch(surfaceID:…)`; the four + copies of the evidence-epoch refresh collapsed into `refreshEvidenceEpoch(surfaceID:)`. +- `AgentConditionEvidence` (new) holds the shared condition rules; `AgentWaitCommandHandler` + keeps its behavior and tests through `typealias ConditionSnapshot = AgentConditionSnapshot`. +- `AgentDispatchCommandHandler` (new): validate → resolve pane → `DISPATCH_PENDING` → + `AGENT_GONE` / `AGENT_NOT_FOUND` → idle verdict (`DISPATCH_TARGET_BUSY`) → issue+bind + (`DISPATCH_CAPACITY_EXCEEDED`, `DISPATCH_PENDING` on a race, `DISPATCH_FAILED`) → deliver + (`DISPATCH_FAILED` cancels the issuance) → `target` + pending `dispatch`. + `AgentDispatchCompleteCommandHandler` now completes by caller surface. +- `AgentDispatchPrompt.renderInjected(userPrompt:)` = `[Prowl] ` + the launch rendering. +- CLI: `prowl agents dispatch --prompt -` (`AgentsDispatchPromptCommand.swift`), + `dispatch-complete` without a required environment id, text renderers, the executable + schema (`agentsDispatchResponse`, `agentsDispatchErrorDetails`), and the mock-socket + integration fixtures. +- Docs: `docs/components/cli.md`, the contract pages (`agents-wait.md`, `input.md`, + `schema.md`, `agents.md`), and the `prowl-cli` skill's "reuse one reviewer across rounds" + recipe with the `--until idle` step between rounds. + +## Verification + +- Store, handler, evidence, lifecycle, signal, and hook-carrier suites via `xcodebuild test`; + CLI parsing, wire-model, schema, renderer, and socket round-trip tests via SwiftPM; `make + check`, `make build-cli`, `make test-cli-unit`, `make test-cli-smoke`, + `make test-cli-integration`, `make build-app` (results in the PR). +- Delivery measurement (D2) against the baseline Debug build in an isolated instance + (`CFFIXED_USER_HOME` scratch home, `PROWL_CLI_SOCKET=/tmp/redispatch.sock`): unprompted + "Claude Code" and "Codex" Profiles, `printf '%s\n' | prowl send --pane … --no-wait`; + both TUIs echoed the three lines as one composer entry and answered `RECEIVED 3 lines`. +- Live end-to-end against the branch's Debug build in the same isolated instance, with the + "Claude Code" (2.1.251) and "Codex" (0.149.1) Profiles and the branch's CLI on the + agents' `PATH` (full transcript in the PR): + - `create tab --profile … --prompt -` → `wait --dispatch` succeeded (Claude Code 5 s; + Codex first returned `DISPATCH_INCOMPLETE` — see below — and the re-armed wait returned + the receipt). + - `wait --until idle` (exact `hook_claude` / `hook_codex`) → `agents dispatch --prompt -` + → pending record → a second `dispatch` in the same second refused `DISPATCH_PENDING` with + the record in `error.details` → `wait --dispatch` succeeded in 2.7 s / 4.0 s with + summaries that reference round 1 ("previous round was ROUND 1"), i.e. the same session + completed the new record although its process was launched with the round-1 + `PROWL_DISPATCH_ID`. The pane shows the `[Prowl] ` prefixed prompt and protocol as one + composer entry. + - A plain `send` that keeps the agent busy (`sleep 20`) → `dispatch` refused + `DISPATCH_TARGET_BUSY` (`observation.status: working`) for both runtimes → `wait --until + idle` resolved on the fresh hook `turn-ended` while the detector still held `working` → + the dispatch issued in the same second settled within the grace (record created 3 s after + the wait resolved) → round 3 asked for `--outcome failed` → `wait --dispatch` returned + `DISPATCH_FAILED` with the failed receipt, for both runtimes. + - `dispatch-complete` from the coordinator's own shell (outside any pane) → + `DISPATCH_CONTEXT_REQUIRED`; from a plain shell pane that never held a dispatch → + `DISPATCH_NOT_FOUND`. + +## Observed but not changed + +- Codex emitted a `turn-ended` before running the completion command on its first prompted + turn (the "usage limit reset available" notice preceded the tool call), so the first + `wait --dispatch` returned `DISPATCH_INCOMPLETE` and a re-armed wait returned the receipt + twelve seconds after issuance — the S2 incomplete → re-arm flow, unchanged. +- A re-dispatch into a manually launched agent (no `verified_live` channel) takes the + two-second detector stabilization every time; documented, and the exact path resolves at once + once the agent has reported `turn-ended` cooperatively. +- `agents dispatch` types into the pane but never focuses it, so a background reviewer stays + in the background; a person typing into that pane at the same moment would interleave, as + with `send`. +- Build hygiene: the first incremental Debug build of this branch on top of a `main` build + crashed (`EXC_BAD_ACCESS` in `swift_release` while a completed `send --capture` route task + released its `CommandEnvelope`). `CLISocketServer.o` had not been recompiled after `Command` + gained the `agentsDispatch` case, so its stale outlined destroy of `Command` released the + new `.send` payload with the old `.key` layout. A clean build (`xcodebuild clean` + + `make build-app`) does not reproduce it and the test bundle, compiled fresh, never did. + Reviewers building this branch incrementally on top of an older DerivedData should clean + first. diff --git a/docs/components/cli.md b/docs/components/cli.md index ebf79441..c3c5495d 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -4,7 +4,7 @@ > an agent) can list panes, read their screens, run commands and capture output, > send keystrokes, focus, and open/close tabs and panes programmatically. -**Keywords:** prowl cli, command line, prowl list, prowl agents, prowl agents read, prowl agents signal, prowl profiles list, prowl skills, skills install, agent skills, prowl read, prowl send, prowl key, prowl focus, prowl create, prowl close, prowl open, prowl handoff, pane id, agent, profile, automation, json, capture, socket +**Keywords:** prowl cli, command line, prowl list, prowl agents, prowl agents read, prowl agents signal, prowl agents dispatch, prowl agents wait, prowl profiles list, prowl skills, skills install, agent skills, prowl read, prowl send, prowl key, prowl focus, prowl create, prowl close, prowl open, prowl handoff, pane id, agent, profile, automation, json, capture, socket **Related:** [terminal](terminal.md) · [concepts](../concepts.md) · [active-agents](active-agents.md) · [agent-detection](agent-detection.md) · the bundled **`prowl-cli` skill** (`skills/prowl-cli/SKILL.md`) @@ -288,11 +288,46 @@ prowl agents dispatch-complete --outcome failed --summary "Blocked by an invalid ``` The required summary must be one non-empty line with no control characters and at most 32 KiB -of UTF-8. The command accepts no public dispatch id; outside a prompted launch it fails with -`DISPATCH_CONTEXT_REQUIRED`. Prowl reads the launch-scoped environment value and independently -verifies the socket caller's process ancestry against the immutable launch pane. Repeating -identical completion is safe; a conflicting retry is rejected. Unprompted Profile launches -remain interactive and do not create a dispatch. +of UTF-8. The command accepts no public dispatch id: Prowl resolves the socket caller's +process ancestry to its pane and completes *that pane's current pending dispatch*, so a +worker completes whatever it was most recently assigned even when it was launched with an +older `PROWL_DISPATCH_ID` (the variable is kept for the launch case as diagnostics only). +Outside any Prowl pane the command fails with `DISPATCH_CONTEXT_REQUIRED`; in a pane that +never held a dispatch it fails with `DISPATCH_NOT_FOUND`. Repeating an identical completion +replays the receipt; a conflicting retry is rejected. Unprompted Profile launches remain +interactive and do not create a dispatch. + +A pane keeps its agent between assignments. To hand a *new* task to an agent that is already +running — a reviewer that should keep its context across rounds — dispatch into the pane +instead of launching another Profile: + +```bash +prowl agents dispatch p7 --prompt - --json <<'EOF' +Round 2: re-review the diff against main. Report only findings not already fixed. +EOF +``` + +`--prompt -` reads the prompt from piped stdin (up to 256 KiB of UTF-8; newlines and tabs are +allowed, other control characters are rejected with `INVALID_ARGUMENT`). Prowl creates a new +pending `data.dispatch` bound to the pane and its current agent generation, then types the +prompt plus the same completion protocol a launch appends — as one bracketed paste followed by +Enter, prefixed with `[Prowl] ` so the origin is visible — through the pane's input path. +The response carries `data.target` and `data.dispatch.{id,state,created_at}` exactly like a +prompted `create`, and every wait, receipt, abandon, needs-input, incomplete, and gone rule +below applies to the new record unchanged. + +Preconditions are checked before anything is typed: the pane must host a detected agent +(`AGENT_NOT_FOUND` otherwise) that is idle by the same evidence rules as +`agents wait --until idle` — a `turn-ended` the detector corroborates resolves at once; a +`turn-ended` the screen has not caught up with yet (the detector holds `working` for a few +seconds after a turn) or a detector-only idle view that still needs its two seconds of +stability is given up to five seconds to settle; a working or blocked agent without such +evidence (including a runtime `needs-input` the screen does not show) is refused with +`DISPATCH_TARGET_BUSY` rather than having text merged into its running turn. One pending +dispatch per pane: while a record is pending, a second `dispatch` fails with +`DISPATCH_PENDING` and never overwrites it; complete, abandon, or lose the previous record +first. Because a receipt can precede Codex's own `turn-ended` by a second or two, wait for +`--until idle` between rounds before dispatching again. The coordinator waits by exact id: @@ -717,9 +752,11 @@ artifacts and terminal excerpts do not appear in `git status`. | `TARGET_NOT_UNIQUE` | Selector matched several — be more specific (use `--pane`). | | `PROFILE_NOT_FOUND` | No enabled Profile matches the UUID or exact name — re-run `profiles list`; disabled Profiles cannot launch. | | `PROFILE_NOT_UNIQUE` | Several enabled Profiles have the exact name — use the Profile UUID from `profiles list`. | -| `AGENT_NOT_FOUND` / `AGENT_UNSUPPORTED` | `agents read` target no longer hosts an agent, or it is not Codex/Claude Code; `agents wait --until …` saw no detected agent within its ten-second appearance grace. Re-run `agents`. | -| `DISPATCH_NOT_FOUND` | No dispatch record matches `--dispatch`; records are memory-only and reset on app restart. | -| `DISPATCH_CONTEXT_REQUIRED` | `dispatch-complete` ran outside a prompted Profile launch (no launch-scoped `PROWL_DISPATCH_ID`). | +| `AGENT_NOT_FOUND` / `AGENT_UNSUPPORTED` | `agents read` target no longer hosts an agent, or it is not Codex/Claude Code; `agents wait --until …` saw no detected agent within its ten-second appearance grace; `agents dispatch` targeted a pane with no detected agent. Re-run `agents`. | +| `DISPATCH_NOT_FOUND` | No dispatch record matches `--dispatch`, or `dispatch-complete` ran in a pane that never held one; records are memory-only and reset on app restart. | +| `DISPATCH_CONTEXT_REQUIRED` | `dispatch-complete` ran from a process outside any Prowl pane (tmux/detached wrapper, another terminal), so no pane could own the receipt. | +| `DISPATCH_PENDING` | `agents dispatch` refused: the pane already holds a pending dispatch (`.error.details.record`). Wait for it, or `dispatch-abandon` it, before dispatching again. | +| `DISPATCH_TARGET_BUSY` | `agents dispatch` refused: the pane's agent is working or blocked (`.error.details.observation`, `.signals`). Wait for `--until idle`, then retry. | | `DISPATCH_ALREADY_TERMINAL` | `dispatch-abandon` targeted a record that already completed, was abandoned, or is gone. | | `DISPATCH_FAILED` / `DISPATCH_ABANDONED` / `DISPATCH_NEEDS_INPUT` / `DISPATCH_INCOMPLETE` | `agents wait --dispatch` structured outcomes; `.error.details` retains the record, target, and evidence (see **Dispatch completion and waiting**). | | `SOURCE_REQUIRED` | A caller-owned command such as `agents signal` or selector-free `handoff` could not map the socket peer ancestry to a Prowl pane. Run it inside the source pane without tmux/detached wrappers, or use an explicit selector where that command permits one. | @@ -781,6 +818,8 @@ fi latency, and text typed into a runtime that is still starting can merge with the next message. - `--until idle|blocked` report the current state (a pre-existing signal needs the detector to agree); use `--until changed` to wait for the next turn edge. +- To give a running agent another task with a receipt, use `agents dispatch --prompt -` + once it is idle — not `send` (no receipt) and not a fresh Profile launch (loses its context). - `agents read` returns `pending` while the agent works or is blocked, even if a previous turn completed. - `--capture` needs shell integration; otherwise `read --wait-stable` or file diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index 6a89321e..8d9f9904 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -116,6 +116,40 @@ own `turn-ended`, so `prowl agents` / `agents read` right after a receipt can st `working` / `pending`; if the next action sends another prompt to the same pane, wait for an idle condition or read a stable screen first. +Reuse one reviewer across rounds instead of launching a fresh Profile per round: launch once, +then hand each later round to the same pane with `agents dispatch`, which keeps the reviewer's +context and still returns an exact receipt per round. + +```bash +launch="$(prowl create pane "$PROWL_PANE_ID" --direction right --profile Reviewer --prompt - --json <<'EOF' +Round 1: review the current branch against main. Write findings to /tmp/review-1.md. +EOF +)" +pane="$(printf '%s\n' "$launch" | jq -r '.data.target.pane.id')" +dispatch="$(printf '%s\n' "$launch" | jq -r '.data.dispatch.id')" +prowl agents wait --dispatch "$dispatch" --json | jq -r '.data.receipt.summary' +# … fix the findings, then: +prowl agents wait "$pane" --until idle --timeout 30 --json >/dev/null +round="$(prowl agents dispatch "$pane" --prompt - --json <<'EOF' +Round 2: re-review the diff against main. Report only findings not already fixed; write them to /tmp/review-2.md. +EOF +)" +dispatch="$(printf '%s\n' "$round" | jq -r '.data.dispatch.id')" +prowl agents wait --dispatch "$dispatch" --json | jq -r '.data.receipt.summary' +``` + +`agents dispatch` needs a pane whose detected agent is idle: it applies the same evidence +rules as `--until idle` (a corroborated `turn-ended` resolves at once; a detector-only idle +view must hold for two seconds) and refuses a working or blocked agent with +`DISPATCH_TARGET_BUSY` instead of merging text into the running turn — hence the `--until idle` +between rounds, which also covers Codex's late `turn-ended`. When that wait resolved on a fresh +runtime `turn-ended` a moment before the screen caught up, the dispatch waits up to five +seconds for the screen to agree before refusing, so the two commands work back to back. A pane +holds at most one pending dispatch: a second `dispatch` fails with `DISPATCH_PENDING` (the +record is in `.error.details.record`) until you wait for or `dispatch-abandon` the previous +one. The prompt is piped stdin (multi-line is fine; it arrives as one message), and the +reviewer completes with the usual `agents dispatch-complete` — from its own pane, no id needed. + Create a fresh tab in a listed worktree: ```bash @@ -163,6 +197,7 @@ Key fields by command: - `agents` → `.data.agents[]` with `.status`, `.raw_state`, `.detection_reason`, `.type`, `.name`, `.pane.{id,focused,cwd}`, `.tab`, `.worktree`, `.project.{name,branch,path}`. - `agents read` → `.data.agent`, `.data.blocker.text`, `.data.result.{state,text}` — `pending`, `unavailable`, `missing`, `incomplete`, `too_large` carry no partial text; `pending` is returned whenever the agent is working or blocked, even if an earlier turn completed. - `agents signal` → `.data.pane.{id,worktree_id}`, `.data.signal.{event,source,confidence,binding,at,session_id,detail,claimed_origin}`, optional `.data.warnings[]` (`code=signal_unbound`); optional fields are omitted. +- `agents dispatch` → `.data.target` and the new pending `.data.dispatch.{id,state,created_at}`; refusals carry `.error.details.{target,record,observation,signals}`. - `agents wait --dispatch` → `.data.receipt`, immutable `.data.target`, `.data.signals`, optional `.data.screen`; nonzero results retain the record and evidence under `.error.details`. - `agents wait --until …` → `.data.observation.{status,raw_state,source,confidence,at,revision}`, `.data.signals`, and optional `.data.screen`. `source`/`confidence` are the evidence that satisfied the condition; `status`/`raw_state` are what the screen detector saw at that moment, so `idle` satisfied by a `turn-ended` signal may still show `status: working`. - `read` → `.data.text`, `.data.line_count`, `.data.truncated`, `.data.mode`, `.data.source`; `.data.stabilized` / `.data.waited_ms` with `--wait-stable`. @@ -233,7 +268,8 @@ printf '%s\n' "$result" | jq '.data.observation, .data.screen' - `EMPTY_INPUT`, `INVALID_ARGUMENT`, `UNSUPPORTED_KEY`, `INVALID_REPEAT`: fix the arguments (`prowl --help`). - `CAPTURE_UNSUPPORTED`: drop `--capture` and use `read --wait-stable` or file redirection. - `WAIT_TIMEOUT`: inspect `.error.details`, then re-arm the wait if the task remains active. -- `AGENT_NOT_FOUND` (`agents wait`): no detected agent appeared in the pane within ten seconds — confirm the pane with `prowl agents --json` before re-arming. `DISPATCH_NOT_FOUND`: no such dispatch record (records reset on app restart). `DISPATCH_CONTEXT_REQUIRED`: `dispatch-complete` ran outside a prompted Profile launch. `DISPATCH_ALREADY_TERMINAL`: `dispatch-abandon` hit a record that already completed, was abandoned, or is gone. +- `AGENT_NOT_FOUND` (`agents wait`, `agents dispatch`): no detected agent in the pane (a wait tolerates ten seconds; a dispatch does not) — confirm the pane with `prowl agents --json`. `DISPATCH_NOT_FOUND`: no such dispatch record, or `dispatch-complete` ran in a pane that never held one (records reset on app restart). `DISPATCH_CONTEXT_REQUIRED`: `dispatch-complete` ran from a process outside any Prowl pane. `DISPATCH_ALREADY_TERMINAL`: `dispatch-abandon` hit a record that already completed, was abandoned, or is gone. +- `DISPATCH_PENDING` / `DISPATCH_TARGET_BUSY` (`agents dispatch`): the pane still holds a pending record (wait for it or abandon it), or its agent is working/blocked (wait `--until idle`, then retry). Nothing was typed into the pane. - `DISPATCH_FAILED` / `DISPATCH_ABANDONED`: the exact dispatch is terminal; inspect its retained record and immutable target in `.error.details`. - `AGENT_GONE`: inspect `.error.details.mode`. `dispatch` means the exact worker is terminal and retains a record; `condition` means the target pane closed. Without details on `agents signal`, the caller pane disappeared before recording. - `DISPATCH_NEEDS_INPUT` / `DISPATCH_INCOMPLETE`: the dispatch remains pending; use the intervention sequencing in **Reading Agent Output** before waiting again. @@ -259,4 +295,4 @@ Required sections are `## Objective`, `## Current State`, and `## Next Steps`; o ## Command Set -`list`, `agents`, `agents read`, `agents signal`, `agents dispatch-complete`, `agents dispatch-abandon`, `agents wait`, `profiles list`, `skills list|install|uninstall|path` (local-only), `read`, `send`, `key`, `focus`, `create tab`, `create pane`, `close`, `handoff to`, `handoff save`, and `open` (default). There is no CLI `quit`; close temporary tabs or panes with an explicit `close`. `tab create`, `tab close`, and `pane close` remain deprecated aliases for one release. +`list`, `agents`, `agents read`, `agents signal`, `agents dispatch`, `agents dispatch-complete`, `agents dispatch-abandon`, `agents wait`, `profiles list`, `skills list|install|uninstall|path` (local-only), `read`, `send`, `key`, `focus`, `create tab`, `create pane`, `close`, `handoff to`, `handoff save`, and `open` (default). There is no CLI `quit`; close temporary tabs or panes with an explicit `close`. `tab create`, `tab close`, and `pane close` remain deprecated aliases for one release.