From 935ed21e9cac656b16ddace0f1f3436d282d85b5 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 23 Aug 2026 11:29:09 +0900 Subject: [PATCH 1/3] docs: lock S2 dispatch wait design Co-authored-by: onevclaw --- docs-ai/063-agent-workflows/000-plan.md | 5 +- docs-ai/063-agent-workflows/release-plan.md | 16 +- .../064-agent-completion-signals/000-plan.md | 21 +- .../001-action.md | 6 +- .../002-s1-work-note.md | 4 + .../003-s2-dispatch-wait-design.md | 288 ++++++++++++++++++ 6 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md diff --git a/docs-ai/063-agent-workflows/000-plan.md b/docs-ai/063-agent-workflows/000-plan.md index ef53ed20..bc699940 100644 --- a/docs-ai/063-agent-workflows/000-plan.md +++ b/docs-ai/063-agent-workflows/000-plan.md @@ -2,7 +2,7 @@ | | | | --- | --- | -| **Status** | In progress — R1 foundations C0/A1/A1b implemented; A2 implemented in PR #714 | +| **Status** | In progress — R1 foundations C0/A1/A1b/A2 and 064-S1 merged; 064-S2 is next | | **Anchor date** | 2026-08-21 | | **Primary PRs** | R1 foundations: #709 (C0), #710 (A1), #713 (A1b), #714 (A2); B1–D3 TBD | | **Related** | [047 cross-agent-handoff](../047-cross-agent-handoff/000-plan.md), [049 agents-toolbar-entry](../049-agents-toolbar-entry/000-plan.md), [053 agent-profiles](../053-agent-profiles/000-plan.md), [055 agent-profile-runtimes](../055-agent-profile-runtimes/000-plan.md), [059 agent-transcript-snapshots](../059-agent-transcript-snapshots/000-plan.md), [060 cli-targeting-and-contract-governance](../060-prowl-cli-targeting-and-contract-governance/000-plan.md), [061 native-toolbar-controls](../061-native-toolbar-controls/toolbar-controls.md), [064 agent-completion-signals](../064-agent-completion-signals/000-plan.md) (signal bus, `agents signal` / `agents wait`), [#699 `prowl create pane`](https://github.com/onevcat/Prowl/issues/699), [PR #651 (direction reference, not merged)](https://github.com/onevcat/Prowl/pull/651), [DSL spec (living)](dsl-spec.md), [release plan (living)](release-plan.md), `docs/components/handoff.md`, `docs/components/agent-profiles.md`, `docs/components/cli.md` | @@ -593,6 +593,9 @@ attaches hooks through A2's launch boundary. ## Amendments +- Updated 2026-08-23: 064-S1 merged in #715 and the owner locked S2's paired dispatch and + evidence-wait contract, leaving S2 as the next R1 critical-path PR — see + [064.003](../064-agent-completion-signals/003-s2-dispatch-wait-design.md). - Updated 2026-08-22: Shipped C0 with the Agents sidebar group, Profiles page, and Command Line Tool page; Workflows remains deferred to D1 — see [002-settings-agents-group.md](002-settings-agents-group.md). - Updated 2026-08-22: Implemented A1 with the direct anchored split primitive and schema-governed `prowl create pane` command — see [003-cli-create-pane.md](003-cli-create-pane.md). - Updated 2026-08-22: Implemented A1b — `PROWL_PANE_ID` in every pane's environment, manual identity section, and the `prowl-cli` skill rewritten around it — see [004-pane-identity-env.md](004-pane-identity-env.md). diff --git a/docs-ai/063-agent-workflows/release-plan.md b/docs-ai/063-agent-workflows/release-plan.md index 2f79aa78..6a6fbda3 100644 --- a/docs-ai/063-agent-workflows/release-plan.md +++ b/docs-ai/063-agent-workflows/release-plan.md @@ -24,7 +24,7 @@ that 063-B3 consumes; 064-S3 attaches launch-scoped hooks through 063-A2's launc PRs merge to `main` one at a time (each keeps `main` shippable); engine PRs without a user-facing surface may merge before "their" release and stay dormant. Three releases: -### Current R1 status (2026-08-22) +### Current R1 status (2026-08-23) | Slice(s) | State | PR / next action | | --- | --- | --- | @@ -32,14 +32,14 @@ user-facing surface may merge before "their" release and stay dormant. Three rel | A1 | Merged | #710 | | A1b | Merged | #713 | | A2 | Merged | #714 | -| S1 | In progress | `feat/agent-completion-signal-bus`: bus, multicast observer, `agents signal` | -| S2 | Planned | Follows S1: atomic dispatch ID → completion receipt → `agents wait` path and honest heuristic fallback | +| S1 | Merged | #715: bus, multicast observer, `agents signal` | +| S2 | Design locked; next | [064.003](../064-agent-completion-signals/003-s2-dispatch-wait-design.md): paired dispatch receipt, strict ID wait, generic evidence wait | | S3 wave 1 | Planned | Follows A2 + S1: tier-A launch hooks | | 065-S0/K1 | Planned, parallel | Skill-target spike + bundled-skill registry | | 065-K2/K3 | Planned | Follow S0/K1 inside R1 | -A2 completes 063's R1 implementation work. The orchestration critical path now moves to -064-S1 → S2 → S3 wave 1; 065-S0/K1 may proceed independently in parallel. +A2 completes 063's R1 implementation work, and S1 is now on `main`. The orchestration +critical path is S2 → S3 wave 1; 065-S0/K1 may proceed independently in parallel. ### R1 — CLI orchestration primitives + completion signals @@ -52,7 +52,7 @@ A2 completes 063's R1 implementation work. The orchestration critical path now m | 2 | **A2** profile launch boundary + `create tab\|pane --profile

--prompt -` + `profiles list` | 063 | A1 | CLI launches a profile with a kickoff prompt and gets the pane back | | 2 | **S1** signal bus + `ObservedAgentState` multicast observer + `prowl agents signal` (`turn-ended`, needs-input/session/progress, bounded detail) | 064 | — | layer-0 signals for every runtime | | 2 | **065-K2** shared `SymlinkInstaller` + `prowl skills list\|install\|uninstall\|path` | 065 | 065-K1 | one command installs Prowl's skills into agent skill folders | -| 3 | **S2** atomic dispatch pairing (`create` dispatch ID, cooperative `dispatch-complete --detail`, bounded receipt retention, `agents wait --dispatch` with overflow resnapshot) + `source`/`confidence`, `--include-screen`, `agents` `signals`, and skill rubric | 064 | S1 | no hand-written polling or stale completion; heuristic results are labelled | +| 3 | **S2** prompted-profile dispatch pairing (`create` dispatch ID, required `dispatch-complete --outcome ... --summary`, 256-entry receipt retention, strict ID-only `agents wait --dispatch`) + generic evidence wait, `source`/`confidence`, `--include-screen`, live `agents.signals`, and skill rubric | 064 | S1 | no hand-written polling or stale completion; deterministic task receipts stay separate from labelled heuristics | | 3 | **065-K3** Agent Skills section on Settings › Command Line Tool | 065 | 065-K2 | GUI users install skills without a terminal | | 4 | **S3 wave 1** launch-scoped hooks for tier-A runtimes (Claude Code, Codex `notify`, Copilot, Droid, Qoder, Pi, OMP, OpenCode) + self-check | 064 | A2, S1 | `agents wait` is deterministic for Prowl-launched agents | @@ -107,6 +107,10 @@ R3+: V2 / S5 rest; delete HANDOFF_RETIRED stubs ## Change log +- 2026-08-23 — S1 merged in #715. Owner review then locked S2: prompted launches always + create a dispatch, completion has an immutable succeeded/failed summary receipt, strict + dispatch waits never accept heuristic completion, and generic waits retain honest auto + fallback. See [064.003](../064-agent-completion-signals/003-s2-dispatch-wait-design.md). - 2026-08-22 — S1 started on `feat/agent-completion-signal-bus`; owner review moved the complete dispatch-ID issuance/receipt/wait protocol into S2, renamed the runtime edge to `turn-ended`, retained bounded detail, and required explicit overflow resnapshot. diff --git a/docs-ai/064-agent-completion-signals/000-plan.md b/docs-ai/064-agent-completion-signals/000-plan.md index 1ecc0ab5..fdd68c0f 100644 --- a/docs-ai/064-agent-completion-signals/000-plan.md +++ b/docs-ai/064-agent-completion-signals/000-plan.md @@ -2,9 +2,9 @@ | | | | --- | --- | -| **Status** | In progress — S1 implementation on `feat/agent-completion-signal-bus` | +| **Status** | In progress — S1 merged in #715; S2 design locked for implementation | | **Anchor date** | 2026-08-22 | -| **Primary PRs** | S1 TBD | +| **Primary PRs** | #715 (S1); S2 TBD | | **Related** | [063 agent-workflows](../063-agent-workflows/000-plan.md) (consumer; defines the `ObservedAgentState` observer this entry feeds), [030 agent-status-detection](../030-agent-status-detection/000-plan.md), [045 native-agent-session-detection](../045-native-agent-session-detection/000-plan.md), [055 agent-profile-runtimes](../055-agent-profile-runtimes/000-plan.md), [059 agent-transcript-snapshots](../059-agent-transcript-snapshots/000-plan.md), [060 cli-targeting-and-contract-governance](../060-prowl-cli-targeting-and-contract-governance/000-plan.md), [#473](https://github.com/onevcat/Prowl/issues/473), [#676](https://github.com/onevcat/Prowl/issues/676), `docs/components/agent-detection.md`, `docs/components/cli.md` | ## Background @@ -119,9 +119,11 @@ prowl agents wait --until idle|blocked|changed|exit [--timeout 1…600] 059 result state — everything an orchestrating agent needs to judge a heuristic result in one call. - Prowl-dispatched work uses an opaque `dispatch_id`, not timestamps, to exclude stale - completion. S2 ships `create` issuance, `dispatch-complete --detail`, bounded receipt - retention, and `agents wait --dispatch` atomically. Receipts survive pane closure but not - app restart; surface generation is only the unpaired fallback. + completion. S2 ships `create` issuance, required `dispatch-complete --outcome + succeeded|failed --summary`, bounded receipt retention, and ID-only `agents wait + --dispatch` atomically. Receipts survive pane closure but not app restart; surface + generation is only the unpaired fallback. The finalized contract is + [003-s2-dispatch-wait-design.md](003-s2-dispatch-wait-design.md). - `removed` / `surfaceClosed` → `AGENT_GONE` (unless `--until exit`); timeout → `WAIT_TIMEOUT` with the last known status/source. The 600 s cap matches typical agent tool timeouts; the skill documents "re-arm on timeout". @@ -131,7 +133,8 @@ prowl agents wait --until idle|blocked|changed|exit [--timeout 1…600] When a Prowl-launched runtime declares a `sessionStart` hook, the launch boundary expects the corresponding signal within a grace window; if it never arrives the pane is marked `signals: none` (hooks did not load) instead of silently pretending. `prowl agents` -JSON gains `signals: {channels: [hook, transcript, osc], last: {...}}` per pane, and the +JSON gains `signals: {channels: [...], last: {...}}` per pane, where channels describe only +live observed or verified evidence rather than theoretical runtime support. The Active Agents panel shows a small "exact" badge for panes with a live deterministic channel. ### Judging heuristic results (skill, not code) @@ -166,7 +169,7 @@ interleaves with 063's slices, is owned by the shared living | Slice | Depends | Contents / expectation | | --- | --- | --- | | **S1** | — | Signal bus state + the `ObservedAgentState` multicast observer (snapshot / changed / removed / surfaceClosed / `.signal`; first specified in 063, delivered here so it ships first) + `prowl agents signal` for `turn-ended`, `needs-input`, session, and progress events (CLI four layers, bounded detail). Layer 0 works for every runtime immediately; 063-B3 later consumes the same observer. | -| **S2** | S1 | One atomic paired-dispatch path: `create --profile --prompt` returns `dispatch_id`; cooperative `dispatch-complete --detail`; bounded non-destructive in-memory receipts; `prowl agents wait --dispatch` with automatic overflow resnapshot; `agents` `signals` field; `--include-screen`; skill rubric. Route B usable; heuristic fallback honest. | +| **S2** | S1 | One atomic paired-dispatch path: every `create --profile --prompt` appends the completion protocol and returns `dispatch_id`; cooperative `dispatch-complete --outcome succeeded|failed --summary`; 256-entry non-destructive in-memory receipts; ID-only strict `prowl agents wait --dispatch`; generic `wait --until` with automatic overflow resnapshot and honest heuristic fallback; `agents` live `signals` field; `--include-screen`; skill rubric. Route B becomes usable without polling or stale completion. | | **S3 wave 1** | 063-A2, S1, research matrix | Launch-scoped hook injection (adapter `signalHooks`, self-check) for tier A of the research matrix (flag/env per launch, live-verified): Claude Code `--settings`, Codex `-c notify=[…]` (native `agent-turn-complete` maps to `turn-ended`; hook trust bypass is never passed), Copilot `--plugin-dir`, Droid `--settings`, Qoder `--settings`, Pi `-e`, OMP `--hook`, OpenCode `OPENCODE_CONFIG_CONTENT`. `agents wait` becomes deterministic for Prowl-launched agents on these runtimes. | | **S3 wave 2** | S3 wave 1, 053 dedicated homes | Tier B (`configDirOnly`: Gemini, Qwen, Grok, Cline, Kimi) for dedicated-home profiles only; tier C (Cursor, Amp: project files) is not attached. | | **S4** | S1 | Transcript file-watch and OSC producers — layer 2 without hooks. | @@ -232,6 +235,10 @@ opencode; partial for qodercli/qwen/amp; docs/bundle for the rest). Key conclusi ## Amendments +- Updated 2026-08-23 after S1 merged in #715: owner review locked S2's paired dispatch, + receipt lifecycle, exact-versus-heuristic wait policy, CLI outcomes, trust boundary, and + verification scope — see + [003-s2-dispatch-wait-design.md](003-s2-dispatch-wait-design.md). - Updated 2026-08-23 before merge: owner raised bounded signal `--detail` from 4 KiB to 32 KiB (32768 UTF-8 bytes). The larger bound remains well below the 32 MiB socket frame and macOS argument budget, accommodates useful completion summaries, and preserves the rule diff --git a/docs-ai/064-agent-completion-signals/001-action.md b/docs-ai/064-agent-completion-signals/001-action.md index 09abd7fc..6931a701 100644 --- a/docs-ai/064-agent-completion-signals/001-action.md +++ b/docs-ai/064-agent-completion-signals/001-action.md @@ -2,7 +2,7 @@ ## Status -Complete on `feat/agent-completion-signal-bus`; PR #715 is ready for merge. +Complete and merged in PR #715. ## Slice objective @@ -19,7 +19,9 @@ S1 does not add `agents wait`, launch-scoped runtime hooks, workflow completion, - Continue the 064 path before the 063 workflow runner. `prowl workflow done` remains the only command that completes a workflow step; agent signals are observation/control-plane evidence. - Rename the runtime edge from ambiguous `turn-complete` to `turn-ended`. A runtime hook can prove that a turn ended, not that an assigned task completed. -- Reserve `dispatch-complete` for S2's paired dispatch protocol. S2 must ship the entire path atomically: `create` returns a `dispatch_id`, the agent reports `dispatch-complete --detail`, a bounded in-memory receipt survives pane closure (but not app restart), and `agents wait --dispatch` consumes it without destructive read semantics. +- Reserve `dispatch-complete` for S2's paired dispatch protocol. S1 recorded the provisional + shape; the final owner-reviewed command and receipt contract is in + [003-s2-dispatch-wait-design.md](003-s2-dispatch-wait-design.md). - Keep bounded `--detail` in S1 so a cooperative producer can attach a short result/reason without forcing another CLI command. The owner raised its limit from 4 KiB to 32 KiB before merge: this remains small relative to the 32 MiB socket frame and 1 MiB macOS argument budget while accommodating useful completion summaries. Detail is caller-authored metadata, never logged, never raises confidence, and is not a replacement for large transcript/workflow output channels. - Public `--origin` is only a claimed origin. It cannot mark a channel as verified, raise confidence, or satisfy future hook self-checks. S3 may upgrade provenance only through a Prowl-configured launch-scoped capability; this is a correctness boundary, not a heavyweight security boundary. - Each observer has bounded buffering. State churn may be recovered by a newer snapshot. Signal/lifecycle loss is never silent: overflow terminates with an explicit internal error, and S2's waiter must re-subscribe/resnapshot before exposing a failure. diff --git a/docs-ai/064-agent-completion-signals/002-s1-work-note.md b/docs-ai/064-agent-completion-signals/002-s1-work-note.md index fac95ddd..ad08d660 100644 --- a/docs-ai/064-agent-completion-signals/002-s1-work-note.md +++ b/docs-ai/064-agent-completion-signals/002-s1-work-note.md @@ -45,6 +45,10 @@ This note tracks the authorized S1 execution. Durable design decisions belong in ## Deferred S2 contract (must not be lost) +This was the provisional S1 handoff. The owner-reviewed S2 command spelling, outcomes, +receipt lifecycle, and wait semantics are finalized in +[003-s2-dispatch-wait-design.md](003-s2-dispatch-wait-design.md). + S2 owns one atomic paired-dispatch slice: ```text diff --git a/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md b/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md new file mode 100644 index 00000000..730b862d --- /dev/null +++ b/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md @@ -0,0 +1,288 @@ +# 064.003 — S2 Paired Dispatch and Agent Wait Design + +## Context + +S1 merged in #715 and delivered the per-surface signal state, `ObservedAgentState` +multicast observer, lifecycle events, and cooperative `prowl agents signal` ingress. S2 is +the first consumer-facing slice: it must replace hand-written polling with an atomic, +stale-safe dispatch receipt while also making generic state waits honest about their +evidence. + +This amendment records the final owner review completed on 2026-08-23. It supersedes the +provisional S2 command spelling in `002-s1-work-note.md`; S1's shipped `agents signal +--detail` contract is unchanged. + +## Starting state and confirmed seams + +- `supacode/Features/Terminal/BusinessLogic/AgentObservationStore.swift` provides + snapshot-first, independently buffered multicast observation and retains only the latest + signal per live surface. It is the right state/signal wait source but cannot retain a + task receipt after surface closure. +- `supacode/CLIService/AgentSignalCommandHandler.swift` already attributes cooperative + events through the socket peer's process ancestry. Dispatch completion reuses that trust + boundary rather than focus or caller-provided pane identity. +- `supacode/Domain/AgentProfile/AgentProfileLaunchPlan.swift` distinguishes child command + environment from surface shell environment. Only the former is safe for a launch-scoped + dispatch id. +- `supacode/CLIService/LifecycleCommandHandler.swift` already returns typed profile-launch + metadata, but prompted launches have no task identity or receipt today. +- `supacode/CLIService/ReadCommandHandler.swift` owns stable viewport capture behavior that + can supply optional post-wait evidence without redefining completion. +- `supacode/CLIService/CLISocketServer.swift` allows an async handler to suspend while other + requests are accepted. S2 is the first long-lived CLI wait, so disconnect cancellation + must be propagated instead of leaving a subscription alive until timeout. +- `supacode/CLIService/Shared/CommandResponse.swift` currently carries only error code and + message. S2 needs an optional structured error-details field for receipts and last-known + observation; omitting that field preserves existing wire responses. +- `supacode/CLIService/Shared/AgentsCommandPayload.swift` exposes detected state but no + signal evidence. S2 adds live observed/verified signal visibility. + +Consequently, S2 needs a separate dispatch store and subscriber path while consuming the +S1 observer for status, signal, and lifecycle evidence. Reusing the existing signal record +as receipt storage would lose the result on pane closure and allow unrelated later signals +to overwrite it. + +## Scope + +S2 ships three connected surfaces in one PR: + +1. `create tab|pane --profile ... --prompt ...` becomes an atomic paired dispatch and + returns an opaque dispatch id. +2. The launched agent reports one immutable terminal outcome through + `prowl agents dispatch-complete`; `prowl agents wait --dispatch` consumes the resulting + non-destructive receipt. +3. Generic `prowl agents wait --until ...`, `--include-screen`, and the `agents` + `signals` field expose deterministic observations where available and labelled + heuristics otherwise. + +S2 does not install runtime hooks, watch transcript files, infer completion with an LLM, +persist receipts across app restarts, or change `prowl workflow done` semantics. Those +remain owned by S3/S4, the orchestrating skill, and 063 respectively. + +## Two planes, one observer context + +Signals and dispatch receipts are deliberately separate: + +| Plane | Answers | Scope | Storage | Consumer | +| --- | --- | --- | --- | --- | +| Signal observation | What just happened to this agent/runtime? | Surface | Latest in-memory observation; ends with the surface | `wait --until ...`, later watchdogs | +| Dispatch receipt | Did this exact assigned task reach a terminal outcome? | Opaque dispatch id | Bounded immutable in-memory receipt; survives surface closure | `wait --dispatch ` | + +`turn-ended` is a runtime edge, not task completion. `dispatch-complete` never fabricates or +maps to `turn-ended`; a normal run may record the dispatch receipt first and receive an +independent runtime `turn-ended` signal afterward. Conversely, a deterministic terminal +signal while a receipt remains pending is actionable evidence that the completion protocol +was not fulfilled, not permission to synthesize success. + +## Paired dispatch protocol + +Every prompted profile launch is a dispatch. S2 intentionally has no `--no-dispatch` path: + +```text +create --profile --prompt + -> mint pending dispatch + -> append the versioned Prowl completion instruction to the effective prompt + -> launch the runtime with child-only PROWL_DISPATCH_ID + -> return pane identity plus dispatch.id +``` + +Launch failure removes the pending record and returns the existing typed launch error. The +capacity check and id issuance happen before starting the runtime; binding the returned +surface and completing the create response remain one main-actor lifecycle transaction. + +An unprompted `create --profile` remains an interactive launch without a dispatch. A caller +that needs byte-for-byte prompt delivery can create an interactive pane and use the existing +`send` command. + +The id must be passed through the launch plan's child-process command environment, not the +surface shell environment. The latter outlives the launched runtime and could let a later, +manually started agent inherit a stale dispatch id. The effective prompt contains the +protocol command but never the id itself. + +The injected instruction tells the agent to choose one terminal outcome and make the +completion command its final tool action: + +```bash +prowl agents dispatch-complete \ + --outcome succeeded \ + --summary "Implemented the requested change; all tests pass." +``` + +or: + +```bash +prowl agents dispatch-complete \ + --outcome failed \ + --summary "The required SDK is unavailable on this deployment target." +``` + +`--outcome succeeded|failed` and a non-empty `--summary` are required. Summary is capped at +32 KiB of UTF-8 and is the concise result retained with the receipt, not a transcript or +artifact transport. S1 keeps optional `agents signal --detail`: signal detail is event +context or a reason, whereas dispatch summary is the required terminal delivery synopsis. + +The completion command accepts no public dispatch-id option. It reads the child-only +`PROWL_DISPATCH_ID`; the app independently resolves the socket peer's process ancestry and +requires the caller pane to match the dispatch-bound surface. Missing launch context fails +with `DISPATCH_CONTEXT_REQUIRED`; a mismatched caller fails with +`DISPATCH_SOURCE_MISMATCH`. + +## Receipt lifecycle and idempotency + +The terminal manager owns a separate dispatch store with a maximum of 256 records: + +- pending records are never evicted; +- creating a dispatch evicts the oldest terminal record first; +- if all 256 records are pending, creation fails before launch with + `DISPATCH_CAPACITY_EXCEEDED`; +- succeeded, failed, and gone receipts survive agent and pane closure; +- app restart clears the store, after which an old id returns `DISPATCH_NOT_FOUND`; +- there is no disk persistence or TTL in S2. + +Completion is first-write-wins. Retrying the same id with the same outcome and summary is +idempotent and returns the original receipt. A later completion with different content +returns `DISPATCH_ALREADY_COMPLETED` and cannot mutate the recorded outcome seen by existing +or future waiters. + +## Wait contracts + +### Exact dispatch wait + +Dispatch identity is sufficient; a pane argument would be redundant and would stop working +after surface closure: + +```bash +prowl agents wait --dispatch [--timeout 1...600] +``` + +Only the matching receipt can return task success. Idle state, screen content, and +`turn-ended` never substitute for it. The outcomes are: + +- succeeded receipt: successful command with the immutable summary; +- failed receipt: nonzero exit and structured `DISPATCH_FAILED`, including the receipt; +- exact/high `needs-input`: nonzero `DISPATCH_NEEDS_INPUT`, receipt remains pending; +- exact/high stable `turn-ended` without a receipt: nonzero `DISPATCH_INCOMPLETE`, receipt + remains pending; +- session end, agent removal, or surface closure before completion: `AGENT_GONE` backed by + a retained gone record; +- timeout: `WAIT_TIMEOUT` with the last observation and evidence. + +Before surfacing `DISPATCH_INCOMPLETE`, the waiter gives a 300 ms coalescing grace period for +independently delivered receipt and signal events. This is an event-ordering allowance, not +screen stabilization. `needs-input` and disappearance remain immediate. A completed receipt +returns immediately unless the caller explicitly requests stable screen evidence. + +The receipt read is non-destructive: any number of concurrent or later waiters observe the +same outcome. Socket-client disconnect or CLI cancellation must cancel the server-side wait +subscription rather than leave a waiter alive until the timeout cap. + +### Generic observation wait + +```bash +prowl agents wait --until idle|blocked|changed|exit \ + [--timeout 1...600] [--min-confidence exact|high|heuristic] \ + [--include-screen ] +``` + +The default confidence policy is `auto`: + +- when a live exact/high channel is verified, only deterministic evidence resolves the + requested condition; heuristic changes update diagnostics only; +- without such a channel, an already-stabilized screen/process observation may resolve the + wait with `confidence: heuristic`; +- `changed` requires a post-baseline normalized state or signal change and is never + satisfied by the initial snapshot; +- `exit` accepts removal or surface closure; disappearance is `AGENT_GONE` for other + requested conditions. + +An exit-zero heuristic result means only that the requested observable condition matched. +It never means the assigned task completed. The bundled `prowl-cli` skill teaches the +orchestrating agent to inspect `agents read`, optional screen evidence, and task context +before it decides to proceed, nudge, retry, or ask the owner. + +`--include-screen` is explicit and diagnostic. After the matching event it reuses the stable +read/capture boundary for a short bounded settle so trailing terminal rendering can arrive; +it never changes the receipt or confidence decision. Existing detector stabilization remains +the heuristic gate instead of adding a universal multi-second delay to exact signals. + +Observer overflow is never ignored. The waiter re-subscribes and evaluates a fresh snapshot; +if lost signal history prevents a safe conclusion, it surfaces a structured failure instead +of guessing. + +## Signal visibility + +`prowl agents --json` reports current evidence, not runtime marketing or theoretical +capability: + +- cooperative CLI is listed only after it has been observed on that pane; +- a future S3 hook is listed as verified only after launch injection and self-check succeed; +- the latest signal retains event, source, confidence, timestamp, and optional detail; +- screen/process state remains heuristic observation evidence and does not masquerade as a + deterministic signal channel; +- no observed or verified deterministic source means an empty `channels` array, enabling + the generic wait's honest auto fallback. + +## Error and response model + +The common CLI error envelope gains optional structured details so timeout, protocol, and +task failures can return their last observation or immutable receipt without encoding data +into error strings. Existing errors omit the field and remain wire-compatible. + +`create` adds a dispatch object alongside existing launch information. Wait success includes +the dispatch id, outcome, summary, target metadata, completion timestamp, waited duration, +and observation provenance. `DISPATCH_FAILED`, `DISPATCH_NEEDS_INPUT`, +`DISPATCH_INCOMPLETE`, `AGENT_GONE`, and `WAIT_TIMEOUT` use distinct nonzero errors. + +## Implementation boundaries + +The PR changes the existing launch planner and lifecycle handler under +`supacode/Domain/AgentProfile/` and `supacode/CLIService/`, adds the dispatch store beside +the observation domain under `supacode/Domain/AgentDetection/`, and adds governed wire, +parser, renderer, router, schema, and handler coverage through `ProwlCLI/`, +`supacode/CLIService/Shared/`, `ProwlCLITests/`, and `supacodeTests/`. + +The same PR updates the normative contracts under `docs-ai/013-prowl-cli/contracts/`, the +current CLI and agent-detection manuals under `docs/components/`, and the bundled +`prowl-cli` skill. S3 runtime adapter hook injection is explicitly excluded. + +## Verification plan + +- Dispatch store: issuance, binding, both outcomes, identical retry, conflicting retry, + two waiters, terminal eviction, all-pending capacity, pane closure, and app-lifetime reset. +- Launch: child-only id propagation, no surface-shell leakage, prompt protocol rendering, + atomic cleanup on launch failure, and unprompted launch parity. +- Completion ingress: missing context, caller ancestry mismatch, wrong pane, validation, + summary UTF-8 bounds, and immutable receipt behavior. +- Dispatch wait: already completed, delayed completion, failed outcome, needs input, + terminal signal grace, gone surface, timeout evidence, cancellation, and concurrency. +- Generic wait: initial snapshot, transition, post-baseline `changed`, exact/high gating, + auto heuristic fallback, stable screen evidence, overflow resnapshot, and target exit. +- Four CLI layers: parser, shared wire models, router/handler, text/JSON rendering, + executable schema, raw socket fixtures, and current manuals/skill. +- Required gates before PR: CLI build, smoke and integration tests, format/lint, app tests, + app build, and live prompted-profile checks for the paired route and heuristic fallback. + +## Owner decision record + +1. Every prompted profile launch appends the documented completion protocol. +2. Dispatch completion and runtime `turn-ended` remain separate facts and stores. +3. `wait --dispatch` is strict and never accepts an idle or heuristic substitute. +4. Exact/high stable signals may accelerate attention or failure transitions; heuristic + changes are evidence for the orchestrating agent only. +5. Dispatch uses required `summary`; S1 signal keeps optional `detail` because their content + roles differ. +6. Completion is first-write-wins with idempotent identical retries. +7. Terminal outcomes are explicitly `succeeded` or `failed`. +8. The store is memory-only, bounded to 256 records, and survives pane closure but not an + app restart. +9. Generic wait uses deterministic evidence when present and honest heuristic auto fallback + otherwise. +10. `wait --dispatch` is addressed only by dispatch id. +11. A failed receipt makes wait return nonzero `DISPATCH_FAILED` with structured receipt + details. +12. The prompted-profile dispatch path has no opt-out in S2. +13. Completion accepts only implicit launch context plus verified caller-pane ancestry. +14. `agents.signals` reports only live observed or verified channels. + +No product-level questions remain open for S2. Internal type names and small payload-layout +choices may be refined during RED/GREEN implementation without changing these contracts. -- 2.51.2 From f4a31c8a920414f467a273c6e0f97cb86c06f133 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 23 Aug 2026 12:02:49 +0900 Subject: [PATCH 2/3] docs: harden S2 contract after review Co-authored-by: onevclaw --- .../013-prowl-cli/contracts/agents-signal.md | 12 +- .../064-agent-completion-signals/000-plan.md | 34 ++- .../003-s2-dispatch-wait-design.md | 265 +++++++++++++++--- 3 files changed, 257 insertions(+), 54 deletions(-) diff --git a/docs-ai/013-prowl-cli/contracts/agents-signal.md b/docs-ai/013-prowl-cli/contracts/agents-signal.md index 57205e9b..1ac3152f 100644 --- a/docs-ai/013-prowl-cli/contracts/agents-signal.md +++ b/docs-ai/013-prowl-cli/contracts/agents-signal.md @@ -87,7 +87,11 @@ Optional fields are omitted rather than encoded as `null`. The executable schema ## Deferred paired completion `dispatch-complete` is deliberately not part of v1 S1. S2 ships one atomic paired path: -`create --profile --prompt` returns an opaque `dispatch_id`; the agent reports -`dispatch-complete --detail`; a bounded in-memory receipt survives pane closure but not app -restart; and `agents wait --dispatch` re-snapshots after observer overflow. Generic runtime -`turn-ended` never substitutes for that dispatch receipt or `workflow done`. +CLI `create tab|pane --profile --prompt` returns an opaque `dispatch_id`; the agent reports +`dispatch-complete --outcome succeeded|failed --summary ` from its +launch-scoped context; a bounded in-memory receipt survives pane closure but not app restart; +and ID-only `agents wait --dispatch` re-snapshots after observer overflow. Generic runtime +`turn-ended` never substitutes for that dispatch receipt or `workflow done`. This paragraph +is a forward reference, not a shipped command contract; the owner-reviewed S2 design is +[064.003](../../064-agent-completion-signals/003-s2-dispatch-wait-design.md), and normative +wait/completion contracts ship with the implementation. diff --git a/docs-ai/064-agent-completion-signals/000-plan.md b/docs-ai/064-agent-completion-signals/000-plan.md index fdd68c0f..165013fb 100644 --- a/docs-ai/064-agent-completion-signals/000-plan.md +++ b/docs-ai/064-agent-completion-signals/000-plan.md @@ -104,14 +104,18 @@ surfacing an error. Critical events are never silently discarded. ``` prowl agents wait --until idle|blocked|changed|exit [--timeout 1…600] - [--min-confidence exact|high|heuristic] [--include-screen ] [--json] + [--min-confidence auto|exact|high|heuristic] + [--include-screen <1…200>] [--json] +prowl agents wait --dispatch [--timeout 1…600] + [--include-screen <1…200>] [--json] ``` - Snapshot first: return immediately when the current state already satisfies `--until` at the required confidence. -- Default `--min-confidence auto`: if the pane has a deterministic channel (a live Prowl - hook, or an exact/high transcript attribution), only layer 0–2 events resolve the wait; - heuristic events merely update "last known". Without such a channel the wait resolves +- Default `--min-confidence auto`: a fresh exact/high event always matches; if the pane has + verified-live coverage for the requested condition (a self-checked Prowl hook or live + exact/high transcript watcher), only layer 0–2 events resolve the wait and heuristics + merely update "last known". Without such coverage the wait resolves heuristically once the state has been stable for `stable-for` (3 s hold + 2 s) and says so. - Response: `{status, raw_state, source, confidence, waited_ms, signals: […]}`; with @@ -124,9 +128,12 @@ prowl agents wait --until idle|blocked|changed|exit [--timeout 1…600] --dispatch` atomically. Receipts survive pane closure but not app restart; surface generation is only the unpaired fallback. The finalized contract is [003-s2-dispatch-wait-design.md](003-s2-dispatch-wait-design.md). -- `removed` / `surfaceClosed` → `AGENT_GONE` (unless `--until exit`); timeout → - `WAIT_TIMEOUT` with the last known status/source. The 600 s cap matches typical agent - tool timeouts; the skill documents "re-arm on timeout". +- Exact matching `session-end` / `surfaceClosed` → `AGENT_GONE` (unless generic + `--until exit`); detector `.removed` is heuristic and cannot terminalize a dispatch. + Timeout defaults to and is capped at 600 s, returning `WAIT_TIMEOUT` with the last known + status/source; the skill documents "re-arm on timeout". The normative transition table, + evidence epochs, channel registry, capture defaults, cancellation transport, and payload + shapes are frozen in [003-s2-dispatch-wait-design.md](003-s2-dispatch-wait-design.md). ### Self-check and visibility @@ -134,7 +141,7 @@ When a Prowl-launched runtime declares a `sessionStart` hook, the launch boundar the corresponding signal within a grace window; if it never arrives the pane is marked `signals: none` (hooks did not load) instead of silently pretending. `prowl agents` JSON gains `signals: {channels: [...], last: {...}}` per pane, where channels describe only -live observed or verified evidence rather than theoretical runtime support. The +current-epoch observed or verified-live evidence rather than theoretical runtime support. The Active Agents panel shows a small "exact" badge for panes with a live deterministic channel. ### Judging heuristic results (skill, not code) @@ -169,7 +176,7 @@ interleaves with 063's slices, is owned by the shared living | Slice | Depends | Contents / expectation | | --- | --- | --- | | **S1** | — | Signal bus state + the `ObservedAgentState` multicast observer (snapshot / changed / removed / surfaceClosed / `.signal`; first specified in 063, delivered here so it ships first) + `prowl agents signal` for `turn-ended`, `needs-input`, session, and progress events (CLI four layers, bounded detail). Layer 0 works for every runtime immediately; 063-B3 later consumes the same observer. | -| **S2** | S1 | One atomic paired-dispatch path: every `create --profile --prompt` appends the completion protocol and returns `dispatch_id`; cooperative `dispatch-complete --outcome succeeded|failed --summary`; 256-entry non-destructive in-memory receipts; ID-only strict `prowl agents wait --dispatch`; generic `wait --until` with automatic overflow resnapshot and honest heuristic fallback; `agents` live `signals` field; `--include-screen`; skill rubric. Route B becomes usable without polling or stale completion. | +| **S2** | S1 | One atomic paired-dispatch path: every CLI `create tab|pane --profile --prompt` appends the completion protocol and returns `dispatch_id`; cooperative `dispatch-complete --outcome succeeded|failed --summary`; 256-entry non-destructive in-memory receipts; ID-only strict `prowl agents wait --dispatch`; generic `wait --until` with automatic overflow resnapshot and honest heuristic fallback; `agents` current evidence field; `--include-screen`; skill rubric. Route B becomes usable without polling or stale completion. | | **S3 wave 1** | 063-A2, S1, research matrix | Launch-scoped hook injection (adapter `signalHooks`, self-check) for tier A of the research matrix (flag/env per launch, live-verified): Claude Code `--settings`, Codex `-c notify=[…]` (native `agent-turn-complete` maps to `turn-ended`; hook trust bypass is never passed), Copilot `--plugin-dir`, Droid `--settings`, Qoder `--settings`, Pi `-e`, OMP `--hook`, OpenCode `OPENCODE_CONFIG_CONTENT`. `agents wait` becomes deterministic for Prowl-launched agents on these runtimes. | | **S3 wave 2** | S3 wave 1, 053 dedicated homes | Tier B (`configDirOnly`: Gemini, Qwen, Grok, Cline, Kimi) for dedicated-home profiles only; tier C (Cursor, Amp: project files) is not attached. | | **S4** | S1 | Transcript file-watch and OSC producers — layer 2 without hooks. | @@ -228,13 +235,18 @@ opencode; partial for qodercli/qwen/amp; docs/bundle for the rest). Key conclusi - Whether hook subprocesses can always reach Prowl's socket from sandboxed runtimes (Codex sandbox, OpenCode/Pi/OMP plugin runtimes); `PROWL_CLI_SOCKET` and the bundled binary path must be passed through and verified per runtime in S3. -- Exact `stable-for` and self-check grace defaults (Claude's trust-dialog hold suggests a - generous, state-aware grace rather than a fixed few seconds). +- S3 hook self-check grace defaults (Claude's trust-dialog hold suggests a generous, + state-aware grace rather than a fixed few seconds). - Re-verification cadence: the matrix is versioned per row; S3 adapters need fixture tests that fail loudly when a CLI's hook syntax changes. ## Amendments +- Updated 2026-08-23 after PR #716 initial review: froze peer-EOF cancellation, generic wait + transition/freshness rules, per-source channel state, old-app fail-closed behavior, + receipt eviction linearization, exact include-screen behavior, and JSON/schema boundaries; + detector removal no longer terminalizes strict dispatch — see + [003-s2-dispatch-wait-design.md](003-s2-dispatch-wait-design.md). - Updated 2026-08-23 after S1 merged in #715: owner review locked S2's paired dispatch, receipt lifecycle, exact-versus-heuristic wait policy, CLI outcomes, trust boundary, and verification scope — see diff --git a/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md b/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md index 730b862d..bf000818 100644 --- a/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md +++ b/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md @@ -16,8 +16,9 @@ provisional S2 command spelling in `002-s1-work-note.md`; S1's shipped `agents s - `supacode/Features/Terminal/BusinessLogic/AgentObservationStore.swift` provides snapshot-first, independently buffered multicast observation and retains only the latest - signal per live surface. It is the right state/signal wait source but cannot retain a - task receipt after surface closure. + signal per live surface. It has no per-source channel registry or session/launch epoch. + It is the right state/signal wait source but cannot retain a task receipt after surface + closure or answer whether a deterministic channel is still live. - `supacode/CLIService/AgentSignalCommandHandler.swift` already attributes cooperative events through the socket peer's process ancestry. Dispatch completion reuses that trust boundary rather than focus or caller-provided pane identity. @@ -30,12 +31,17 @@ provisional S2 command spelling in `002-s1-work-note.md`; S1's shipped `agents s can supply optional post-wait evidence without redefining completion. - `supacode/CLIService/CLISocketServer.swift` allows an async handler to suspend while other requests are accepted. S2 is the first long-lived CLI wait, so disconnect cancellation - must be propagated instead of leaving a subscription alive until timeout. + must be propagated instead of leaving a subscription alive until timeout. The current + server does not monitor peer EOF after reading the request frame. - `supacode/CLIService/Shared/CommandResponse.swift` currently carries only error code and message. S2 needs an optional structured error-details field for receipts and last-known observation; omitting that field preserves existing wire responses. - `supacode/CLIService/Shared/AgentsCommandPayload.swift` exposes detected state but no signal evidence. S2 adds live observed/verified signal visibility. +- `supacode/Features/Terminal/Models/WorktreeTerminalState+AgentDetection.swift` removes an + agent entry when the foreground-job probe loses its stabilized identity. That event is + useful heuristic observation, but it is not proof that the launched dispatch runtime + exited and cannot directly create an immutable gone receipt. Consequently, S2 needs a separate dispatch store and subscriber path while consuming the S1 observer for status, signal, and lifecycle evidence. Reusing the existing signal record @@ -46,7 +52,7 @@ to overwrite it. S2 ships three connected surfaces in one PR: -1. `create tab|pane --profile ... --prompt ...` becomes an atomic paired dispatch and +1. CLI `create tab|pane --profile ... --prompt ...` becomes an atomic paired dispatch and returns an opaque dispatch id. 2. The launched agent reports one immutable terminal outcome through `prowl agents dispatch-complete`; `prowl agents wait --dispatch` consumes the resulting @@ -76,7 +82,8 @@ was not fulfilled, not permission to synthesize success. ## Paired dispatch protocol -Every prompted profile launch is a dispatch. S2 intentionally has no `--no-dispatch` path: +Every prompted profile launch through CLI `create tab|pane` is a dispatch. S2 intentionally +has no `--no-dispatch` path: ```text create --profile --prompt @@ -94,6 +101,12 @@ An unprompted `create --profile` remains an interactive launch without a dispatc that needs byte-for-byte prompt delivery can create an interactive pane and use the existing `send` command. +This rule is deliberately scoped to the CLI create request, not every internal consumer of +the shared profile-launch boundary. The lifecycle request passes an explicit dispatch +context into the shared launch seam. 063 workflow launches keep their separate +`prowl workflow done` activation protocol, and future prompt launchers must choose their own +completion contract rather than inheriting dispatch behavior accidentally. + The id must be passed through the launch plan's child-process command environment, not the surface shell environment. The latter outlives the launched runtime and could let a later, manually started agent inherit a stale dispatch id. The effective prompt contains the @@ -120,6 +133,9 @@ prowl agents dispatch-complete \ 32 KiB of UTF-8 and is the concise result retained with the receipt, not a transcript or artifact transport. S1 keeps optional `agents signal --detail`: signal detail is event context or a reason, whereas dispatch summary is the required terminal delivery synopsis. +An accepted completion command returns success for either outcome because it recorded the +receipt correctly; `outcome: failed` becomes nonzero only when a coordinator later consumes +that receipt through `wait --dispatch`. The completion command accepts no public dispatch-id option. It reads the child-only `PROWL_DISPATCH_ID`; the app independently resolves the socket peer's process ancestry and @@ -127,6 +143,28 @@ requires the caller pane to match the dispatch-bound surface. Missing launch con with `DISPATCH_CONTEXT_REQUIRED`; a mismatched caller fails with `DISPATCH_SOURCE_MISMATCH`. +### Create response and version skew + +The existing `prowl.cli.create.v1` response gains an optional sibling to `launch`: + +```json +{ + "dispatch": { + "id": "opaque-dispatch-id", + "state": "pending", + "created_at": "2026-08-23T02:00:00.000Z" + } +} +``` + +This is an additive v1 response property: unprompted and ordinary shell creates omit it. +The new CLI must nevertheless fail closed when its own request contained both `--profile` +and `--prompt`: a successful app response without a valid pending dispatch object becomes +`CREATE_FAILED` with the same inspect-and-close warning used by the existing launch +compatibility guard. This prevents a newer CLI talking to an older app from silently +launching untracked work. Parser, shared payload, executable-schema, and old-app response +fixtures pin the conditional check. + ## Receipt lifecycle and idempotency The terminal manager owns a separate dispatch store with a maximum of 256 records: @@ -144,6 +182,14 @@ idempotent and returns the original receipt. A later completion with different c returns `DISPATCH_ALREADY_COMPLETED` and cannot mutate the recorded outcome seen by existing or future waiters. +All store operations are main-actor serialized. Wait registration and initial lookup are +one operation. A terminal transition stores one immutable receipt snapshot and resumes all +currently registered waiters with their own copy before another dispatch can trigger +eviction. Eviction affects only future lookup; a waiter that already received or captured a +receipt is not invalidated and does not pin store capacity. “Later waiter” therefore means a +waiter that starts while the terminal receipt is still retained; after eviction it receives +`DISPATCH_NOT_FOUND`. + ## Wait contracts ### Exact dispatch wait @@ -152,9 +198,13 @@ Dispatch identity is sufficient; a pane argument would be redundant and would st after surface closure: ```bash -prowl agents wait --dispatch [--timeout 1...600] +prowl agents wait --dispatch [--timeout 1...600] \ + [--include-screen <1...200>] ``` +`--timeout` defaults to 600 seconds. It limits the time to reach a matching receipt or +terminal observation, not the optional post-match screen capture. + Only the matching receipt can return task success. Idle state, screen content, and `turn-ended` never substitute for it. The outcomes are: @@ -163,47 +213,97 @@ Only the matching receipt can return task success. Idle state, screen content, a - exact/high `needs-input`: nonzero `DISPATCH_NEEDS_INPUT`, receipt remains pending; - exact/high stable `turn-ended` without a receipt: nonzero `DISPATCH_INCOMPLETE`, receipt remains pending; -- session end, agent removal, or surface closure before completion: `AGENT_GONE` backed by - a retained gone record; +- exact/high `session-end` for the matching evidence epoch, or surface closure before + completion: `AGENT_GONE` backed by a retained gone record; - timeout: `WAIT_TIMEOUT` with the last observation and evidence. Before surfacing `DISPATCH_INCOMPLETE`, the waiter gives a 300 ms coalescing grace period for independently delivered receipt and signal events. This is an event-ordering allowance, not -screen stabilization. `needs-input` and disappearance remain immediate. A completed receipt +screen stabilization. `needs-input`, matching `session-end`, and surface closure remain +immediate. A detector `.removed` event is diagnostic only for a pending dispatch: it cannot +write an irreversible gone receipt or end a strict dispatch wait. A completed receipt returns immediately unless the caller explicitly requests stable screen evidence. The receipt read is non-destructive: any number of concurrent or later waiters observe the same outcome. Socket-client disconnect or CLI cancellation must cancel the server-side wait subscription rather than leave a waiter alive until the timeout cap. +### Connection cancellation + +S2 adds a request-scoped peer-disconnect monitor after the server has consumed the request +frame. A `DispatchSourceRead` on the client file descriptor runs off the main actor and uses +non-consuming `recv(..., MSG_PEEK | MSG_DONTWAIT)` to distinguish peer EOF from unexpected +extra input. EOF wins a single response-versus-disconnect race and cancels the route task; +unexpected post-frame input closes the protocol-violating connection; normal route +completion cancels the monitor before response writing. + +The route task carries ordinary Swift task cancellation into the wait handler. The handler +uses `withTaskCancellationHandler` to unregister both dispatch-store and observation-store +subscriptions promptly and returns no response after peer loss. An explicit protocol cancel +request is insufficient because SIGINT/SIGTERM/SIGKILL may prevent the client from sending +it, so S2 does not add one. + ### Generic observation wait ```bash prowl agents wait --until idle|blocked|changed|exit \ - [--timeout 1...600] [--min-confidence exact|high|heuristic] \ - [--include-screen ] + [--timeout 1...600] [--min-confidence auto|exact|high|heuristic] \ + [--include-screen <1...200>] ``` -The default confidence policy is `auto`: +`--timeout` defaults to 600 seconds. Confidence is an ordered minimum: `exact` accepts only +exact evidence, `high` accepts exact or high, and `heuristic` accepts all three levels. +`auto` is both the default and an explicit accepted token: -- when a live exact/high channel is verified, only deterministic evidence resolves the - requested condition; heuristic changes update diagnostics only; +- a fresh matching exact/high event always resolves the condition; +- when a verified-live channel covers the requested condition, heuristic changes update + diagnostics and invalidate stale terminal facts but cannot resolve the wait; - without such a channel, an already-stabilized screen/process observation may resolve the - wait with `confidence: heuristic`; + wait with `confidence: heuristic` after the existing 3-second working hold and an + additional 2 seconds of unchanged normalized state; - `changed` requires a post-baseline normalized state or signal change and is never satisfied by the initial snapshot; -- `exit` accepts removal or surface closure; disappearance is `AGENT_GONE` for other - requested conditions. +- `exit` accepts exact/high matching `session-end`, exact surface closure, or stabilized + detector removal only as heuristic fallback; disappearance is `AGENT_GONE` for other + requested conditions only when the evidence is exact/high. + +### Deterministic matching and freshness + +The waiter evaluates semantic evidence, not merely `latestSignal`: + +| `--until` | Exact/high match | Heuristic fallback | Initial snapshot | +| --- | --- | --- | --- | +| `idle` | active `turn-ended` in the current evidence epoch | stabilized detected `idle` or `done` | allowed while the evidence remains active | +| `blocked` | active `needs-input` in the current evidence epoch | stabilized detected `blocked` | allowed while the evidence remains active | +| `changed` | any qualifying signal with revision greater than the wait baseline | normalized detected-state change after baseline | never | +| `exit` | active matching `session-end`, or `surfaceClosed` after subscription | stabilized detector removal | allowed for active `session-end` only | + +Each surface has an evidence epoch minted for the CLI dispatch launch (or the first stable +agent detection on an unpaired surface). The dispatch's first detected runtime and first +`session-start` attach to that epoch; a later stable agent-identity replacement or different +session starts a new epoch. The first non-empty signal session id may bind the current epoch; +a later non-empty mismatched id is retained for diagnostics but cannot match a condition. +Signals without a session id bind to the current epoch at ingestion. + +`turn-ended` activates idle evidence and invalidates blocked evidence; `needs-input` does +the inverse. Later `progress`, `session-start`, or normalized working activity invalidates +both terminal facts without letting heuristic activity itself satisfy a deterministic wait. +`session-end` invalidates both and activates exit. Therefore an old terminal snapshot may +return immediately only when it is from the current epoch and no later activity revision has +invalidated it. The terminal signal itself needs no multi-second delay; the dispatch-only +300 ms grace handles receipt/signal delivery reordering. An exit-zero heuristic result means only that the requested observable condition matched. It never means the assigned task completed. The bundled `prowl-cli` skill teaches the orchestrating agent to inspect `agents read`, optional screen evidence, and task context before it decides to proceed, nudge, retry, or ask the owner. -`--include-screen` is explicit and diagnostic. After the matching event it reuses the stable -read/capture boundary for a short bounded settle so trailing terminal rendering can arrive; -it never changes the receipt or confidence decision. Existing detector stabilization remains -the heuristic gate instead of adding a universal multi-second delay to exact signals. +`--include-screen` applies to both wait modes and is explicit and diagnostic. It captures +1...200 recent detection-source lines, sampling every 200 ms until unchanged for 800 ms, +with a 2-second post-match capture cap. A capture timeout returns the last sample with +`stabilized: false`; an unavailable or already-closed surface returns screen status +`unavailable`. Neither case changes the primary wait outcome or exit code. Condition +`waited_ms` stops at the match; screen evidence reports its own `waited_ms`. Observer overflow is never ignored. The waiter re-subscribes and evaluates a fresh snapshot; if lost signal history prevents a safe conclusion, it surfaces a structured failure instead @@ -214,30 +314,92 @@ of guessing. `prowl agents --json` reports current evidence, not runtime marketing or theoretical capability: -- cooperative CLI is listed only after it has been observed on that pane; -- a future S3 hook is listed as verified only after launch injection and self-check succeed; +- cooperative CLI is listed as event-only `observed` after it has been seen in the current + epoch; this proves the event but not future delivery coverage; +- a future S3 hook is `verified_live` only after launch injection and self-check succeed; - the latest signal retains event, source, confidence, timestamp, and optional detail; - screen/process state remains heuristic observation evidence and does not masquerade as a deterministic signal channel; -- no observed or verified deterministic source means an empty `channels` array, enabling - the generic wait's honest auto fallback. +- no observed or verified deterministic source means an empty `channels` array. Auto + fallback depends specifically on `verified_live` coverage, not array emptiness. + +S2 extends the observation record with `EvidenceChannelState` keyed by normalized source +(`cooperative_cli`, hook runtime, transcript, process, or OSC), independently of +`latestSignal`. Each entry records `observed|verified_live`, confidence, covered event kinds, +evidence epoch, and last-seen revision/time. A new signal updates only its own channel; +therefore hook observed → cooperative signal cannot erase hook liveness or enable heuristic +success. Epoch change and surface close invalidate old channel state; future S3 self-check +failure removes `verified_live` status. Only `verified_live` coverage suppresses heuristic +auto fallback—an event-only cooperative observation does not promise the next event. ## Error and response model -The common CLI error envelope gains optional structured details so timeout, protocol, and -task failures can return their last observation or immutable receipt without encoding data -into error strings. Existing errors omit the field and remain wire-compatible. +The common CLI error envelope gains `details: RawJSON?` so timeout, protocol, and task +failures can return their last observation or immutable receipt without encoding data into +error strings. This is a governed change: `CommandError` coding, the common executable +`error` schema (currently closed to code/message), command-specific details schemas, text +rendering, and raw-socket fixtures change together. Existing errors omit the optional field +and remain wire-compatible. `create` adds a dispatch object alongside existing launch information. Wait success includes the dispatch id, outcome, summary, target metadata, completion timestamp, waited duration, and observation provenance. `DISPATCH_FAILED`, `DISPATCH_NEEDS_INPUT`, `DISPATCH_INCOMPLETE`, `AGENT_GONE`, and `WAIT_TIMEOUT` use distinct nonzero errors. +New commands use `prowl.cli.agents.dispatch-complete.v1` and +`prowl.cli.agents.wait.v1`; additive `dispatch` and `signals` fields remain in the existing +`prowl.cli.create.v1` and `prowl.cli.agents.v1` responses. The shared dispatch record is a +strict tagged union. A completed variant is: + +```json +{ + "id": "opaque-dispatch-id", + "state": "completed", + "outcome": "succeeded", + "summary": "Implemented and verified.", + "created_at": "2026-08-23T02:00:00.000Z", + "completed_at": "2026-08-23T02:03:00.000Z" +} +``` + +`pending` carries only `id/state/created_at`; `completed` additionally requires +`outcome/summary/completed_at`; `gone` additionally requires `gone_at` and +`gone_reason: session_end|surface_closed` and carries no outcome or summary. Every variant +uses `additionalProperties: false`. + +The success payloads are fixed as follows: + +- dispatch completion: `target` (the existing `#/$defs/target` shape), `receipt` (completed + record), and `replayed` (Boolean; true only for an identical retry); +- dispatch wait: `mode: dispatch`, `waited_ms`, `target`, `receipt` (completed record), + `signals`, and optional `screen`; +- generic wait: `mode: condition`, `condition`, `waited_ms`, `target`, `observation`, + `signals`, and optional `screen`. + +The generic observation requires `status`, `raw_state`, `source`, `confidence`, `at`, and +`revision`. A signal channel requires `source`, `state: observed|verified_live`, +`confidence`, `events`, and `last_seen_at`, with optional `session_id`; `signals.last` uses +the existing signal payload shape and is omitted when absent. + +A present screen object requires `status: captured|unavailable`, `requested_lines`, +`source: detection`, and `waited_ms`. Captured evidence additionally requires `text`, +`line_count`, and `stabilized`; unavailable evidence carries none of those fields. Optional +members are omitted rather than encoded as null. + +Error details are strict mode-specific objects. Dispatch errors carry `mode: dispatch`, +`waited_ms`, optional target, the current dispatch record when known, and optional last +observation/signals. Generic errors carry `mode: condition`, `condition`, `waited_ms`, +optional target, and optional last observation/signals. A failed receipt is +`DISPATCH_FAILED`, not a success payload. These shapes and their +`additionalProperties: false` schemas ship in the S2 normative command contracts before +handlers turn GREEN. + ## Implementation boundaries The PR changes the existing launch planner and lifecycle handler under -`supacode/Domain/AgentProfile/` and `supacode/CLIService/`, adds the dispatch store beside -the observation domain under `supacode/Domain/AgentDetection/`, and adds governed wire, +`supacode/Domain/AgentProfile/` and `supacode/CLIService/`, adds the dispatch store and +evidence-channel state beside the existing observation store under +`supacode/Features/Terminal/BusinessLogic/`, and adds governed wire, parser, renderer, router, schema, and handler coverage through `ProwlCLI/`, `supacode/CLIService/Shared/`, `ProwlCLITests/`, and `supacodeTests/`. @@ -248,15 +410,24 @@ current CLI and agent-detection manuals under `docs/components/`, and the bundle ## Verification plan - Dispatch store: issuance, binding, both outcomes, identical retry, conflicting retry, - two waiters, terminal eviction, all-pending capacity, pane closure, and app-lifetime reset. + two waiters, completion/lookup/eviction linearization, all-pending capacity, pane closure, + post-eviction not-found, and app-lifetime reset. - Launch: child-only id propagation, no surface-shell leakage, prompt protocol rendering, - atomic cleanup on launch failure, and unprompted launch parity. + atomic cleanup on launch failure, unprompted launch parity, and new-CLI/old-app fail-closed + behavior when a prompted response omits dispatch. - Completion ingress: missing context, caller ancestry mismatch, wrong pane, validation, summary UTF-8 bounds, and immutable receipt behavior. - Dispatch wait: already completed, delayed completion, failed outcome, needs input, - terminal signal grace, gone surface, timeout evidence, cancellation, and concurrency. + terminal signal grace, exact session/surface exit, detector-removal non-terminal behavior, + timeout evidence, cancellation, and concurrency. - Generic wait: initial snapshot, transition, post-baseline `changed`, exact/high gating, - auto heuristic fallback, stable screen evidence, overflow resnapshot, and target exit. + session/epoch freshness, confidence thresholds, auto heuristic fallback, stable screen + evidence, overflow resnapshot, and target exit. +- Channel registry: per-source preservation, epoch invalidation, observed versus + verified-live coverage, and hook observed → cooperative event → heuristic still blocked. +- Real transport cancellation: framed wait request followed by direct peer close, plus a + killed CLI process, returns observer/dispatch subscriber counts to baseline without + waiting for the 600-second timeout. - Four CLI layers: parser, shared wire models, router/handler, text/JSON rendering, executable schema, raw socket fixtures, and current manuals/skill. - Required gates before PR: CLI build, smoke and integration tests, format/lint, app tests, @@ -264,7 +435,8 @@ current CLI and agent-detection manuals under `docs/components/`, and the bundle ## Owner decision record -1. Every prompted profile launch appends the documented completion protocol. +1. Every prompted CLI `create tab|pane` profile launch appends the documented completion + protocol. 2. Dispatch completion and runtime `turn-ended` remain separate facts and stores. 3. `wait --dispatch` is strict and never accepts an idle or heuristic substitute. 4. Exact/high stable signals may accelerate attention or failure transitions; heuristic @@ -280,9 +452,24 @@ current CLI and agent-detection manuals under `docs/components/`, and the bundle 10. `wait --dispatch` is addressed only by dispatch id. 11. A failed receipt makes wait return nonzero `DISPATCH_FAILED` with structured receipt details. -12. The prompted-profile dispatch path has no opt-out in S2. +12. The prompted CLI create dispatch path has no opt-out in S2; other internal prompt + launchers do not inherit it implicitly. 13. Completion accepts only implicit launch context plus verified caller-pane ancestry. -14. `agents.signals` reports only live observed or verified channels. +14. `agents.signals` reports only current-epoch observed or verified-live channels. + +## External review disposition + +The 2026-08-23 PR #716 initial review correctly identified that the owner decisions were +closed but several implementation contracts were not. S2 adopts its cancellation, +deterministic matching/freshness, channel registry, exact-screen grammar, old-app fail-closed, +eviction linearization, schema, and scope findings. + +The remedies are intentionally narrower than two suggested alternatives: cancellation is +driven by actual peer EOF rather than an explicit cancel request that cannot survive process +death, and detector `.removed` never creates a dispatch gone receipt, avoiding a brittle +binding to a foreground-probe PID. Exact matching `session-end` or surface closure owns that +terminal transition. -No product-level questions remain open for S2. Internal type names and small payload-layout -choices may be refined during RED/GREEN implementation without changing these contracts. +No owner-level product choice remains open for S2. The review-frozen transition, transport, +compatibility, and payload rules above are implementation acceptance criteria; internal type +names may still change without weakening them. -- 2.51.2 From c00f53098dd9dc8a54a8ae436d876e8e4ef53643 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 23 Aug 2026 12:30:06 +0900 Subject: [PATCH 3/3] docs: close S2 lifecycle review gaps Co-authored-by: onevclaw --- .../003-s2-dispatch-wait-design.md | 218 +++++++++++++----- 1 file changed, 165 insertions(+), 53 deletions(-) diff --git a/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md b/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md index bf000818..ef931d91 100644 --- a/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md +++ b/docs-ai/064-agent-completion-signals/003-s2-dispatch-wait-design.md @@ -20,8 +20,10 @@ provisional S2 command spelling in `002-s1-work-note.md`; S1's shipped `agents s It is the right state/signal wait source but cannot retain a task receipt after surface closure or answer whether a deterministic channel is still live. - `supacode/CLIService/AgentSignalCommandHandler.swift` already attributes cooperative - events through the socket peer's process ancestry. Dispatch completion reuses that trust - boundary rather than focus or caller-provided pane identity. + events to a surface through the socket peer's process ancestry. The current resolver stops + at the pane shell and does not prove which agent-process generation emitted an event. S2 + must preserve the surface trust boundary while adding generation-aware evidence matching; + dispatch completion separately reuses the surface attribution plus launch context. - `supacode/Domain/AgentProfile/AgentProfileLaunchPlan.swift` distinguishes child command environment from surface shell environment. Only the former is safe for a launch-scoped dispatch id. @@ -95,7 +97,10 @@ create --profile --prompt Launch failure removes the pending record and returns the existing typed launch error. The capacity check and id issuance happen before starting the runtime; binding the returned -surface and completing the create response remain one main-actor lifecycle transaction. +surface, an immutable launch-time `TabTarget` snapshot, and the dispatch evidence epoch, +then completing the create response, remain one main-actor lifecycle transaction. The +private target snapshot outlives surface metadata and is the source for every later +completion or wait response; it is not duplicated inside the public receipt tagged union. An unprompted `create --profile` remains an interactive launch without a dispatch. A caller that needs byte-for-byte prompt delivery can create an interactive pane and use the existing @@ -173,14 +178,40 @@ The terminal manager owns a separate dispatch store with a maximum of 256 record - creating a dispatch evicts the oldest terminal record first; - if all 256 records are pending, creation fails before launch with `DISPATCH_CAPACITY_EXCEEDED`; -- succeeded, failed, and gone receipts survive agent and pane closure; +- succeeded, failed, gone, and explicitly abandoned records are terminal and survive agent + and pane closure; - app restart clears the store, after which an old id returns `DISPATCH_NOT_FOUND`; - there is no disk persistence or TTL in S2. Completion is first-write-wins. Retrying the same id with the same outcome and summary is idempotent and returns the original receipt. A later completion with different content returns `DISPATCH_ALREADY_COMPLETED` and cannot mutate the recorded outcome seen by existing -or future waiters. +or future waiters. A completion that loses a race to a committed gone or abandoned state +returns `DISPATCH_ALREADY_TERMINAL`. + +S2 provides an explicit recovery action for an assignment that will never complete: + +```bash +prowl agents dispatch-abandon --dispatch \ + --reason "The worker returned to a shell without reporting completion." +``` + +The exact opaque id and a non-empty reason of at most 32 KiB UTF-8 are required. The command +does not stop the runtime, close its pane, or claim task failure; it only records the +coordinator's decision to stop waiting. The resulting immutable `abandoned` variant carries +`abandoned_at` and `reason`, makes current and future waits fail with +`DISPATCH_ABANDONED`, and becomes eligible for ordinary terminal-record eviction. An +identical retry is idempotent; a different reason or an already completed/gone record returns +`DISPATCH_ALREADY_TERMINAL`. Abandonment and worker completion are main-actor serialized, +and whichever commits first wins. Because abandonment is an explicit local administrative +action rather than a worker report, it is addressed by id and does not require caller-pane +ancestry. The opaque id acts as the local capability, and the retained record supplies the +in-memory audit trail until eviction or app restart. + +This recovery path is intentionally manual. A timeout leaves the receipt pending; detector +removal, a quiet shell, elapsed time, and process-probe ambiguity cannot abandon it. If the +worker may still be running, the coordinator must inspect or stop that worker separately +before choosing to abandon the dispatch. All store operations are main-actor serialized. Wait registration and initial lookup are one operation. A terminal transition stores one immutable receipt snapshot and resumes all @@ -210,6 +241,7 @@ Only the matching receipt can return task success. Idle state, screen content, a - succeeded receipt: successful command with the immutable summary; - failed receipt: nonzero exit and structured `DISPATCH_FAILED`, including the receipt; +- abandoned record: nonzero `DISPATCH_ABANDONED`, including its reason; - exact/high `needs-input`: nonzero `DISPATCH_NEEDS_INPUT`, receipt remains pending; - exact/high stable `turn-ended` without a receipt: nonzero `DISPATCH_INCOMPLETE`, receipt remains pending; @@ -217,12 +249,22 @@ Only the matching receipt can return task success. Idle state, screen content, a completion: `AGENT_GONE` backed by a retained gone record; - timeout: `WAIT_TIMEOUT` with the last observation and evidence. -Before surfacing `DISPATCH_INCOMPLETE`, the waiter gives a 300 ms coalescing grace period for -independently delivered receipt and signal events. This is an event-ordering allowance, not -screen stabilization. `needs-input`, matching `session-end`, and surface closure remain -immediate. A detector `.removed` event is diagnostic only for a pending dispatch: it cannot -write an irreversible gone receipt or end a strict dispatch wait. A completed receipt -returns immediately unless the caller explicitly requests stable screen evidence. +`turn-ended`, matching `session-end`, and surface closure are terminal-adjacent evidence that +can race with `dispatch-complete` over independent socket or lifecycle paths. They therefore +enter one 300 ms coalescing window. During that window a matching completion has priority: +it commits the succeeded/failed receipt and cancels the candidate transition. If the window +expires first, `turn-ended` surfaces `DISPATCH_INCOMPLETE` while leaving the record pending; +matching `session-end` or surface closure commits an immutable gone record. A completion +arriving after a gone record commits is rejected by first-write-wins. `needs-input` remains +immediate because it is an attention state, not an irreversible terminal record. + +Surface closure reaches the dispatch store directly and unconditionally from +`WorktreeTerminalManager.onSurfaceClosed`, independently of whether +`AgentObservationStore` has a record or subscribers. The observation store still receives +its ordinary close publication, but it is not the dispatch lifecycle bridge. A detector +`.removed` event is diagnostic only for a pending dispatch: it cannot write an irreversible +gone receipt or end a strict dispatch wait. A completed receipt returns immediately unless +the caller explicitly requests stable screen evidence. The receipt read is non-destructive: any number of concurrent or later waiters observe the same outcome. Socket-client disconnect or CLI cancellation must cancel the server-side wait @@ -279,19 +321,38 @@ The waiter evaluates semantic evidence, not merely `latestSignal`: | `exit` | active matching `session-end`, or `surfaceClosed` after subscription | stabilized detector removal | allowed for active `session-end` only | Each surface has an evidence epoch minted for the CLI dispatch launch (or the first stable -agent detection on an unpaired surface). The dispatch's first detected runtime and first -`session-start` attach to that epoch; a later stable agent-identity replacement or different -session starts a new epoch. The first non-empty signal session id may bind the current epoch; -a later non-empty mismatched id is retained for diagnostics but cannot match a condition. -Signals without a session id bind to the current epoch at ingestion. - -`turn-ended` activates idle evidence and invalidates blocked evidence; `needs-input` does -the inverse. Later `progress`, `session-start`, or normalized working activity invalidates -both terminal facts without letting heuristic activity itself satisfy a deterministic wait. -`session-end` invalidates both and activates exit. Therefore an old terminal snapshot may -return immediately only when it is from the current epoch and no later activity revision has -invalidated it. The terminal signal itself needs no multi-second delay; the dispatch-only -300 ms grace handles receipt/signal delivery reordering. +agent detection on an unpaired surface). Detection records an `AgentProcessGeneration` as +the agent PID plus its process start time; a stable replacement mints a new epoch even when +the detected agent kind is unchanged. The first generation observed after dispatch launch +attaches to the launch epoch rather than minting another one. + +S2 extends caller resolution to retain the process ancestry walked before the pane shell. +Surface attribution is enough to accept and retain a cooperative signal, but not enough to +let that signal satisfy an epoch-sensitive wait. Such a signal is match-eligible only when +its caller ancestry contains the current `AgentProcessGeneration`. A supplied session id +must additionally match the independently resolved current `AgentSession.id` when one is +available. If no active generation can be proved, the signal remains diagnostic and cannot +resolve generic or dispatch wait. + +The first eligible non-empty session id may bind the current epoch. After an epoch is bound, +only an eligible `session-start` carrying a different non-empty id that also matches the +independently resolved current session may advance to a new session epoch. Every other +mismatched-id event is diagnostic only. A sessionless signal may bind to the current epoch +only when caller generation is proved and that process generation has not crossed a prior +session-id replacement; after such a replacement, sessionless events stay diagnostic until +a new process generation begins. These rules make consecutive `session-start A` / `B` +deterministic and prevent a delayed child of A from satisfying B's waits. They deliberately +prefer losing an optimization over accepting stale evidence. + +An eligible `turn-ended` activates idle evidence and invalidates blocked evidence; an +eligible `needs-input` does the inverse. Later `progress`, `session-start`, or normalized +working activity invalidates both terminal facts without letting heuristic activity itself +satisfy a deterministic wait. +An eligible `session-end` invalidates both and activates exit. Therefore an old terminal +snapshot may return immediately only when it is from the current epoch and no later activity +revision has invalidated it. Diagnostic/unbound signals may appear in error evidence but +never drive a transition. The terminal signal itself needs no multi-second stabilization; +the dispatch-only 300 ms coalescing window handles receipt/signal delivery reordering. An exit-zero heuristic result means only that the requested observable condition matched. It never means the assigned task completed. The bundled `prowl-cli` skill teaches the @@ -323,6 +384,14 @@ capability: - no observed or verified deterministic source means an empty `channels` array. Auto fallback depends specifically on `verified_live` coverage, not array emptiness. +The command continues to enumerate detected-agent rows only. S2 does not turn `agents` into +a live-surface or bare-shell listing. A signal received before detector row creation may be +retained internally, but it is exposed on a later row only if it was already bound to that +row's current evidence epoch; unbound or stale evidence is not retroactively promoted. +Evidence-only surfaces remain addressable through explicit pane targets where the command +supports them, and dispatch state remains addressable by dispatch id. This keeps the JSON +row semantics stable while making the visibility boundary explicit. + S2 extends the observation record with `EvidenceChannelState` keyed by normalized source (`cooperative_cli`, hook runtime, transcript, process, or OSC), independently of `latestSignal`. Each entry records `observed|verified_live`, confidence, covered event kinds, @@ -342,14 +411,16 @@ rendering, and raw-socket fixtures change together. Existing errors omit the opt and remain wire-compatible. `create` adds a dispatch object alongside existing launch information. Wait success includes -the dispatch id, outcome, summary, target metadata, completion timestamp, waited duration, -and observation provenance. `DISPATCH_FAILED`, `DISPATCH_NEEDS_INPUT`, -`DISPATCH_INCOMPLETE`, `AGENT_GONE`, and `WAIT_TIMEOUT` use distinct nonzero errors. +the dispatch id, outcome, summary, immutable launch target metadata, completion timestamp, +waited duration, and observation provenance. `DISPATCH_FAILED`, `DISPATCH_ABANDONED`, +`DISPATCH_NEEDS_INPUT`, `DISPATCH_INCOMPLETE`, `AGENT_GONE`, and `WAIT_TIMEOUT` use distinct +nonzero errors. -New commands use `prowl.cli.agents.dispatch-complete.v1` and -`prowl.cli.agents.wait.v1`; additive `dispatch` and `signals` fields remain in the existing -`prowl.cli.create.v1` and `prowl.cli.agents.v1` responses. The shared dispatch record is a -strict tagged union. A completed variant is: +New commands use `prowl.cli.agents.dispatch-complete.v1`, +`prowl.cli.agents.dispatch-abandon.v1`, and `prowl.cli.agents.wait.v1`; additive `dispatch` +and `signals` fields remain in the existing `prowl.cli.create.v1` and +`prowl.cli.agents.v1` responses. The shared dispatch record is a strict tagged union. A +completed variant is: ```json { @@ -364,13 +435,15 @@ strict tagged union. A completed variant is: `pending` carries only `id/state/created_at`; `completed` additionally requires `outcome/summary/completed_at`; `gone` additionally requires `gone_at` and -`gone_reason: session_end|surface_closed` and carries no outcome or summary. Every variant +`gone_reason: session_end|surface_closed`; `abandoned` additionally requires +`abandoned_at` and `reason`. Gone and abandoned carry no outcome or summary. Every variant uses `additionalProperties: false`. The success payloads are fixed as follows: - dispatch completion: `target` (the existing `#/$defs/target` shape), `receipt` (completed record), and `replayed` (Boolean; true only for an identical retry); +- dispatch abandonment: `target`, `record` (abandoned variant), and `replayed`; - dispatch wait: `mode: dispatch`, `waited_ms`, `target`, `receipt` (completed record), `signals`, and optional `screen`; - generic wait: `mode: condition`, `condition`, `waited_ms`, `target`, `observation`, @@ -379,20 +452,25 @@ The success payloads are fixed as follows: The generic observation requires `status`, `raw_state`, `source`, `confidence`, `at`, and `revision`. A signal channel requires `source`, `state: observed|verified_live`, `confidence`, `events`, and `last_seen_at`, with optional `session_id`; `signals.last` uses -the existing signal payload shape and is omitted when absent. +the existing signal payload shape and is omitted when absent. When `last` is present, the +signals object also requires `last_binding: current|unbound|stale`. Detected-agent rows expose +only `current`; wait error diagnostics may carry the other two values so a coordinator never +mistakes retained but ineligible evidence for a matching event. A present screen object requires `status: captured|unavailable`, `requested_lines`, `source: detection`, and `waited_ms`. Captured evidence additionally requires `text`, `line_count`, and `stabilized`; unavailable evidence carries none of those fields. Optional members are omitted rather than encoded as null. -Error details are strict mode-specific objects. Dispatch errors carry `mode: dispatch`, -`waited_ms`, optional target, the current dispatch record when known, and optional last -observation/signals. Generic errors carry `mode: condition`, `condition`, `waited_ms`, +Error details are strict mode-specific objects. Once `create` has returned a dispatch id, +the private binding always exists, so every known-dispatch error carries `mode: dispatch`, +`waited_ms`, the required immutable launch `target`, the current dispatch record, and +optional last observation/signals. Only lookup failures such as `DISPATCH_NOT_FOUND` lack a +binding and target. Generic errors carry `mode: condition`, `condition`, `waited_ms`, optional target, and optional last observation/signals. A failed receipt is -`DISPATCH_FAILED`, not a success payload. These shapes and their -`additionalProperties: false` schemas ship in the S2 normative command contracts before -handlers turn GREEN. +`DISPATCH_FAILED`, and an abandoned record is `DISPATCH_ABANDONED`; neither is a success +payload. These shapes and their `additionalProperties: false` schemas ship in the S2 +normative command contracts before handlers turn GREEN. ## Implementation boundaries @@ -403,26 +481,37 @@ evidence-channel state beside the existing observation store under parser, renderer, router, schema, and handler coverage through `ProwlCLI/`, `supacode/CLIService/Shared/`, `ProwlCLITests/`, and `supacodeTests/`. +`WorktreeTerminalManager` owns the direct lifecycle fan-out: surface close is delivered to +both observation and dispatch stores even when either store has no existing observation +record. Signal caller resolution also gains a generation-aware result backed by the pane's +current `PaneAgentState` process id and `ProcessDetection.processStartDate`; focus and +caller-claimed ids remain invalid trust sources. + The same PR updates the normative contracts under `docs-ai/013-prowl-cli/contracts/`, the current CLI and agent-detection manuals under `docs/components/`, and the bundled `prowl-cli` skill. S3 runtime adapter hook injection is explicitly excluded. ## Verification plan -- Dispatch store: issuance, binding, both outcomes, identical retry, conflicting retry, - two waiters, completion/lookup/eviction linearization, all-pending capacity, pane closure, - post-eviction not-found, and app-lifetime reset. +- Dispatch store: issuance, binding and immutable target snapshot, both outcomes, identical + retry, conflicting retry, two waiters, completion/lookup/eviction linearization, + all-pending capacity, explicit abandonment and replay, completion-versus-abandonment race, + pane closure, post-eviction not-found, and app-lifetime reset. - Launch: child-only id propagation, no surface-shell leakage, prompt protocol rendering, atomic cleanup on launch failure, unprompted launch parity, and new-CLI/old-app fail-closed behavior when a prompted response omits dispatch. - Completion ingress: missing context, caller ancestry mismatch, wrong pane, validation, summary UTF-8 bounds, and immutable receipt behavior. -- Dispatch wait: already completed, delayed completion, failed outcome, needs input, - terminal signal grace, exact session/surface exit, detector-removal non-terminal behavior, - timeout evidence, cancellation, and concurrency. +- Dispatch wait: already completed, delayed completion, failed and abandoned outcomes, + needs input, terminal signal grace, exact session/surface exit, detector-removal + non-terminal behavior, timeout evidence, cancellation, and concurrency. Deterministic + lifecycle coverage includes create -> no signal/detection/wait -> close -> first wait, + plus two-socket `session-end`/completion and surface-close/completion reverse ordering on + both sides of the 300 ms boundary. - Generic wait: initial snapshot, transition, post-baseline `changed`, exact/high gating, - session/epoch freshness, confidence thresholds, auto heuristic fallback, stable screen - evidence, overflow resnapshot, and target exit. + session/epoch freshness, delayed child A versus replacement B, consecutive + `session-start A`/`B`, sessionless evidence after replacement, confidence thresholds, auto + heuristic fallback, stable screen evidence, overflow resnapshot, and target exit. - Channel registry: per-source preservation, epoch invalidation, observed versus verified-live coverage, and hook observed → cooperative event → heuristic still blocked. - Real transport cancellation: framed wait request followed by direct peer close, plus a @@ -430,6 +519,10 @@ current CLI and agent-detection manuals under `docs/components/`, and the bundle waiting for the 600-second timeout. - Four CLI layers: parser, shared wire models, router/handler, text/JSON rendering, executable schema, raw socket fixtures, and current manuals/skill. +- Closed-surface payloads: succeeded, failed, gone, and abandoned records retain the exact + launch target snapshot after live terminal metadata is removed. +- Capacity recovery: 256 pending records reject another launch; explicitly abandoning one + creates an evictable terminal record and allows the next prompted create without restart. - Required gates before PR: CLI build, smoke and integration tests, format/lint, app tests, app build, and live prompted-profile checks for the paired route and heuristic fallback. @@ -446,7 +539,7 @@ current CLI and agent-detection manuals under `docs/components/`, and the bundle 6. Completion is first-write-wins with idempotent identical retries. 7. Terminal outcomes are explicitly `succeeded` or `failed`. 8. The store is memory-only, bounded to 256 records, and survives pane closure but not an - app restart. + app restart; pending is never evicted or expired automatically. 9. Generic wait uses deterministic evidence when present and honest heuristic auto fallback otherwise. 10. `wait --dispatch` is addressed only by dispatch id. @@ -455,7 +548,18 @@ current CLI and agent-detection manuals under `docs/components/`, and the bundle 12. The prompted CLI create dispatch path has no opt-out in S2; other internal prompt launchers do not inherit it implicitly. 13. Completion accepts only implicit launch context plus verified caller-pane ancestry. -14. `agents.signals` reports only current-epoch observed or verified-live channels. +14. `agents.signals` reports only current-epoch observed or verified-live channels on the + command's existing detected-agent rows; S2 does not enumerate evidence-only surfaces. +15. Surface attribution alone cannot make a signal epoch-sensitive. Sessionless cooperative + evidence must prove the current process generation and becomes diagnostic after a + same-generation session replacement. +16. `turn-ended`, matching `session-end`, and surface closure share a 300 ms receipt-priority + coalescing window; surface closure reaches the dispatch store directly, regardless of + observation-store state. +17. A coordinator may explicitly terminalize an unrecoverable pending assignment with + `dispatch-abandon --dispatch ... --reason ...`; this does not stop or fail the worker. +18. A private dispatch binding retains the immutable launch target snapshot for all later + completion, wait, gone, and abandonment responses. ## External review disposition @@ -464,11 +568,19 @@ closed but several implementation contracts were not. S2 adopts its cancellation deterministic matching/freshness, channel registry, exact-screen grammar, old-app fail-closed, eviction linearization, schema, and scope findings. -The remedies are intentionally narrower than two suggested alternatives: cancellation is +The follow-up review at `f4a31c8a` identified six remaining lifecycle and visibility gaps. +All six are accepted at the contract level: close now fans out directly to the dispatch +store; cooperative evidence gains process-generation proof and an unbound diagnostic state; +terminal evidence uses receipt-priority coalescing; `agents --json` explicitly retains its +detected-row boundary; private bindings retain launch targets; and pending capacity has an +explicit abandonment path. + +The remedies are intentionally narrower than several possible alternatives: cancellation is driven by actual peer EOF rather than an explicit cancel request that cannot survive process -death, and detector `.removed` never creates a dispatch gone receipt, avoiding a brittle -binding to a foreground-probe PID. Exact matching `session-end` or surface closure owns that -terminal transition. +death; `agents` does not begin enumerating bare shells; and detector `.removed` never creates +a dispatch gone receipt or auto-abandons pending work. Only a coalesced, matching +`session-end`, exact surface closure, or explicit coordinator abandonment owns those terminal +transitions. No owner-level product choice remains open for S2. The review-frozen transition, transport, compatibility, and payload rules above are implementation acceptance criteria; internal type -- 2.51.2