diff --git a/docs-ai/013-prowl-cli/contracts/agents-signal.md b/docs-ai/013-prowl-cli/contracts/agents-signal.md index 10a66712..7da4ee91 100644 --- a/docs-ai/013-prowl-cli/contracts/agents-signal.md +++ b/docs-ai/013-prowl-cli/contracts/agents-signal.md @@ -35,16 +35,19 @@ cannot upgrade source/confidence or satisfy a native-hook capability check. ## Bundled native-hook ingress The bundled CLI also contains a hidden `agents _hook` bridge for Prowl-managed Claude Code, -Codex, Copilot, Droid, and Qoder Profile launches. It is intentionally absent from help and -shell completion and is not a targetable public API. Codex appends one bounded JSON argv; -every other supported runtime writes its payload to bounded stdin. The bridge ignores unknown fields, never forwards `last_assistant_message`, stays +Codex, Copilot, Droid, Qoder, Pi, Oh My Pi, and OpenCode Profile launches. It is intentionally +absent from help and shell completion and is not a targetable public API. Codex appends one +bounded JSON argv; every other supported runtime writes its payload to bounded stdin (Pi, Oh My +Pi, and OpenCode through Prowl's bundled extensions, which forward the runtime's own event +names such as `agent_settled`, `session_switch`, or `session.idle` in the same envelope). The bridge ignores unknown fields, never forwards `last_assistant_message`, stays silent, uses a bounded socket attempt, and exits successfully when Prowl rejects or cannot receive evidence. The app accepts a hook only when an in-memory launch token, runtime/native event, normalized launch cwd, exact caller pane, and current or pending process generation all match. A valid -receipt uses `source: hook_claude`, `hook_codex`, `hook_copilot`, `hook_droid`, or -`hook_qodercli`; neither the token nor forwarding metadata appears in the response. Public `agents signal` cannot supply hook context and always remains +receipt uses `source: hook_claude`, `hook_codex`, `hook_copilot`, `hook_droid`, +`hook_qodercli`, `hook_pi`, `hook_omp`, or `hook_opencode`; neither the token nor forwarding +metadata appears in the response. Public `agents signal` cannot supply hook context and always remains `cooperative_cli`. Hook `turn-ended` is runtime evidence only. It never completes an assigned dispatch or diff --git a/docs-ai/064-agent-completion-signals/011-s3c-action.md b/docs-ai/064-agent-completion-signals/011-s3c-action.md new file mode 100644 index 00000000..400c624b --- /dev/null +++ b/docs-ai/064-agent-completion-signals/011-s3c-action.md @@ -0,0 +1,133 @@ +# 064.011 — S3c Pi/OMP/OpenCode Managed Hooks: Action + +## Status + +Implemented on `feat/agent-signal-hooks-s3c`. Plan: [010-s3c-plan.md](010-s3c-plan.md). With this +PR merged, S3 wave 1 — launch-scoped hooks for all eight tier-A runtimes — is complete. + +## Delivered behavior + +- `AgentNativeHookRuntime` gains `pi`, `omp`, and `opencode` (raw values equal to + `AgentProfileRuntime`); the public CLI sources are `hook_pi`, `hook_omp`, and `hook_opencode`. +- `AgentNativeHookDecoder.nativeEvents(for:)` is now the single source of every runtime's hook + table: the decoder validates against it and each adapter's `signalHooks` declares it, so a + registered event is always one the bridge can decode. The relayed runtimes decode through the + existing Claude-shaped path with their native event names: + - Pi: `session_start`, `agent_settled` (`turn-ended`), `session_shutdown`. + - Oh My Pi: `session_start` and `session_switch` (session-start), `session_stop` + (`turn-ended`), `tool_approval_requested` (`needs-input`), `session_shutdown`. + - OpenCode: `session.idle` (`turn-ended`), `permission.asked` and `question.asked` + (`needs-input`); no session-start, so it is non-announcing like Codex. +- Three bundled extensions in `Resources/agent-hooks/{pi,omp,opencode}/prowl-hooks.ts` (the + existing folder reference picks them up). Each is a dumb relay: it forwards only its listed + events to `prowl agents _hook ` with `hook_event_name`, `session_id`, `cwd`, + and an optional `reason`, resolves the CLI relative to itself, skips silently without a launch + token, and swallows every failure. The OpenCode plugin additionally remembers sub-agent + sessions from `session.created.info.parentID` and drops their events. +- Injection in `ManagedHookRendering.swift` / `AgentManagedHookPreparer.swift`: + - **Pi / Oh My Pi** — `ExtensionFlagHookRenderer` inserts `-e` / `--hook` with a carrier value + before the prompt positional; additive, so user extensions and `--no-extensions` are untouched. + Oh My Pi's `--cwd` (last wins) is registered as the launch directory. A missing bundled file + degrades before launch, because Pi refuses to start on a missing `-e` path. + - **OpenCode** — `OpenCodeHookPluginPreparer` appends the plugin's `file://` URL to the + `plugin` list of the effective `OPENCODE_CONFIG_CONTENT` (Profile override, else the login + shell via the new `ShellEnvironmentProbe`), preserving every other key. The merged JSON is + delivered launch-scoped through a new `environmentValues` carrier on + `AgentHookPreparedInvocation` → `PROWL_LAUNCH_HOOK_ENV_n` → `OPENCODE_CONFIG_CONTENT="$…"` + in `commandEnvironmentTokens`, appended after any Profile override so `env(1)` lets the merged + content win. Degrades on `--pure`, `OPENCODE_PURE` (set unless `0`/`false`; empty counts), + malformed content, an ambiguous project positional, or a probe that cannot run. `--auto` + removes `permission.asked` from that launch's registered table. + `OpenCodeLaunchDirectory` reads the TUI's `[project]` positional and `run --dir`. +- `ShellEnvironmentProbe` resolves several variables in one login-shell spawn, keeps "set but + empty" apart from "unset", and survives multi-line values; `DroidSettingsEnvironmentProbe` now + delegates to it. +- `OpenCodeSessionStore` filters `parent_id IS NULL`, so the detector never moves to a sub-agent + session (which, for a non-announcing runtime, would have retired the hook session permanently). +- Docs: `agent-detection.md`, `agent-profiles.md`, `cli.md`, the `agents-signal` contract, the + `prowl-cli` skill, `cli-output-schema.json`, and the research matrix rows. + +## Verification + +Repository gates on the branch: `make check` (35 script tests), `make build-cli`, +`make test-cli-integration` (97 tests), `make build-app`, focused `xcodebuild test` over the hook +suites, and `make test` (recorded below). + +Focused coverage lives in `supacodeTests/AgentS3cHookPayloadTests.swift` (tables, adapter/decoder +agreement, relayed envelope decoding, excluded events, raw-value round trips), +`AgentS3cHookRenderingTests.swift` (flag insertion and additivity, missing-resource degradation, +OMP `--cwd`, OpenCode content merge / `--pure` / `OPENCODE_PURE` / directory scan / override vs +shell precedence / `--auto` trimming, the real `/bin/sh` probe), `AgentProfileHookCarrierTests` +(environment carriers never reach the typed command; override ordering), `AgentObservationTests` +(OMP `session_switch` rotation with delayed-event rejection and idempotent sub-agent +`session_start`; OpenCode non-announcing verification and rotation), +`AgentSessionProfileTests` (sub-agent rows excluded), and `AgentsCommandParsingTests` (stdin relay +for the three runtimes). + +### Live acceptance + +Run in an isolated Debug instance (`CFFIXED_USER_HOME`, dedicated `PROWL_CLI_SOCKET`, scratch +repository, real agent credentials in the pane shell) with `caffeinate -d -u` keeping the display +awake — see "Display sleep" below. Each Profile: `create tab --profile` → answer any trust/setup +prompt → one prompt → `agents wait --until idle` → `agents --json`. + +| Check | Result | +| --- | --- | +| Pi 0.84.3 reaches `verified_live` with `source=hook_pi`; `turn-ended` resolves the wait, `binding=current` | PASS | +| Oh My Pi 18.0.6 reaches `verified_live` with `source=hook_omp`; `turn-ended` resolves the wait | PASS | +| OpenCode 1.18.23 reaches `verified_live` with `source=hook_opencode` from the first `session.idle` | PASS | +| OMP approval prompt: `agents wait --until blocked` resolves with `source=hook_omp` while the Approve/Deny dialog is on screen; Enter → `turn-ended` on the same channel | PASS | +| OpenCode question tool: `wait --until blocked` resolves with `source=hook_opencode` while the options dialog is on screen; Enter → `turn-ended` | PASS | +| Pi `/new`: channel rotates to the new session id and stays `verified_live` | PASS | +| OMP `/new` (`session_switch`): channel rotates to the new session id and stays `verified_live` | PASS | +| OpenCode sub-agent turn: the wait resolves (15 s, `hook_opencode`) only with the parent's `DONE` on screen | PASS | +| OMP sub-agent turn: the `task` approval reports `needs-input` on `hook_omp`; after approval the wait resolves (26 s) on the parent's `session_stop` with `DONE` on screen | PASS (after the sub-agent fix below) | +| Claude 2.1.245 / Codex 0.149.1 / Copilot 1.0.80 / Droid 0.203.0 / Qoder 1.1.29 regression on the same build: `verified_live`, `turn-ended`, `binding=current` | PASS ×5 | + +### Oh My Pi sub-agents have their own session ids + +The first OMP sub-agent run failed: `agents wait --until idle` never resolved and the app log +showed `managed hook rejected omp/session_stop reason=retired-session`. The plan had recorded +sub-agents as sharing the parent's session id — a misread of UUIDv7 ids whose time prefix +matches. In fact each in-process `task` sub-agent starts its own session (file nested inside the +parent's session directory and named after the agent, `_/PongResponder.jsonl`, +even `PongResponder.ExactPong.jsonl` one level deeper) and fires its own `session_start`, so the +relay announced a rotation, the store retired the main session, and the parent's real +`session_stop` was rejected. The extension now classifies a session by its file name: anything +other than `_.jsonl` is a sub-agent, whose `session_start` / +`session_shutdown` are dropped while its `tool_approval_requested` — which still blocks the user +— is forwarded under the main session id last announced. Pi carries the same guard. Re-verified +live: the approval reported `needs-input` on the main session and the wait resolved on the +parent's `session_stop`. + +### Display sleep is the CREATE_FAILED behind the "intermittent" Profile launches + +The first sweep failed every Profile launch of every runtime with `CREATE_FAILED` ("The terminal +surface for Agent Profile … could not be created") while plain `create tab` kept working — the +symptom S3b recorded as an intermittent obstruction. The unified log names the cause: +`CVDisplayLinkCreateWithCGDisplays error -6661 due to invalid display count (0)` followed by +`com.mitchellh.ghostty:embedded_window: error initializing surface err=error.OutOfMemory`. The +Mac's display had turned off (`pmset -g log`: "Display is turned off" at 18:59); the deferred +Ghostty surface (`armSurfaceCreation`) cannot initialize without an active display, so +`WorktreeTerminalState.launchAgentProfile` returns `.surfaceCreationFailed`. Waking the display +(`caffeinate -u`) made the same launch succeed immediately. The same cause explains the flaky +`deferredProfileAppliesFontSizeAdjustmentAfterSurfaceCreation()` test, which arms a real deferred +surface: the first full `make test` on this branch failed exactly that test at 19:58, ten seconds +after the display turned off again, and it passes with the display awake. Not an S3c defect and +not fixed here; it is a candidate issue (retry arming once a display is available, or fall back +to immediate creation for CLI-driven launches) and a runbook rule for every future live sweep +and full test run on this machine (`caffeinate -d -u`). + +## Open questions + +- Drift protection ([#726](https://github.com/onevcat/Prowl/issues/726)) can now be designed + against all eight runtimes. +- Display-sleep surface creation (above) deserves its own issue. +- Resuming a session that an earlier rotation retired stays outside the exact channel for + OpenCode (documented, honest heuristic fallback). + +## Refs + +- Slice: 064-S3c +- Branch: `feat/agent-signal-hooks-s3c` +- Follows: [009-s3b-action.md](009-s3b-action.md) diff --git a/docs/components/agent-detection.md b/docs/components/agent-detection.md index 901b2bbf..aed03872 100644 --- a/docs/components/agent-detection.md +++ b/docs/components/agent-detection.md @@ -155,8 +155,8 @@ lose lifecycle or signal evidence. ## Managed native completion signals Prowl Agent Profile launches of **Claude Code**, **Codex**, **GitHub Copilot**, **Droid**, -and **Qoder** also attach process-scoped native event bridges without writing user, -dedicated-home, or project configuration: +**Qoder**, **Pi**, **Oh My Pi**, and **OpenCode** also attach process-scoped native event +bridges without writing user, dedicated-home, or project configuration: - Claude `SessionStart` verifies launch coverage; `Stop` / `StopFailure` report `turn-ended`; `PermissionRequest` and supported elicitation notifications report @@ -169,6 +169,21 @@ dedicated-home, or project configuration: deliberately ignored: Copilot and Qoder both emit it while the permission service auto-approves a tool and no one is waiting, so it does not mean the agent is blocked. That also makes their `needs-input` arrive slightly later than Claude's. +- Pi, Oh My Pi, and OpenCode have no stdin hooks; Prowl ships a small extension for each that + relays the runtime's own event names through the same bridge. Pi reports `session_start`, + `agent_settled` (`turn-ended`; `agent_end` fires earlier and is ignored), and + `session_shutdown`; Pi has no permission system, so it never reports `needs-input`. Oh My + Pi reports `session_start` and `session_switch` (`/new`) as session starts, `session_stop` + as `turn-ended` (its `agent_end` fires once per in-process `task` sub-agent and is ignored), + `tool_approval_requested` as `needs-input` (the built-in approval prompt under + `--approval-mode always-ask`), and `session_shutdown`. Its in-process `task` sub-agents run + under their own session ids; the extension recognises them by their nested session file and + forwards only their approval prompts, attributed to the pane's session. OpenCode reports `session.idle` as + `turn-ended` and `permission.asked` / `question.asked` as `needs-input`, only for the + session the pane is talking to — sub-agent sessions are filtered out by their `parentID`. + OpenCode announces no session start: its session exists only after the first prompt and + `/new` / resume emit nothing, so, like Codex, the first `session.idle` verifies the channel + and an ordinary event with a new session id rotates it. Each runtime is enabled the way it supports per-launch injection, and none of them changes what the user already configured: Copilot loads an extra read-only plugin directory shipped @@ -177,11 +192,19 @@ never merged or rewritten), while Droid and Qoder receive a settings object that Prowl's handlers into whatever settings the Profile already passes. Because Droid accepts only a settings *path*, its merged copy is written to an owner-only file that is deleted with the pane. Qoder additionally cannot use flag-supplied hooks at all when `--setting-sources` -is set, so that launch runs unchanged with no exact coverage. +is set, so that launch runs unchanged with no exact coverage. Pi (`-e`) and Oh My Pi +(`--hook`) receive the bundled extension file through an additive flag inserted before the +prompt; a user's own extensions and `--no-extensions` keep working. OpenCode receives the +bundled plugin through a launch-scoped `OPENCODE_CONFIG_CONTENT` whose `plugin` list is +appended to whatever content the Profile or the user's shell already exports (config layers +concatenate plugin lists, so global and project plugins are unaffected). `--pure` or +`OPENCODE_PURE` disables every external plugin, so those launches run unchanged with one +warning; under `--auto`, OpenCode auto-replies `permission.asked` in the same millisecond, so +that event is not registered for such launches. Only an app-issued token plus exact caller-process ancestry and matching pane/runtime/cwd can -produce `hook_claude` / `hook_codex` / `hook_copilot` / `hook_droid` / `hook_qodercli` -evidence. The channel is not advertised as +produce `hook_claude` / `hook_codex` / `hook_copilot` / `hook_droid` / `hook_qodercli` / +`hook_pi` / `hook_omp` / `hook_opencode` evidence. The channel is not advertised as `verified_live` until a valid native event completes that end-to-end check. Early Claude `SessionStart` payloads wait for the first matching process generation instead of being lost. That first generation may attach after the detector's acquisition window; once attached, a @@ -196,7 +219,7 @@ or record preparation is uncertain, Prowl launches the original argv unchanged, exact coverage, and reports one non-blocking launch warning. Managed hooks apply only to Profile launches. Typing `claude`, `codex`, `copilot`, `droid`, -`qodercli`, or any other runtime manually keeps the existing +`qodercli`, `pi`, `omp`, `opencode`, or any other runtime manually keeps the existing cooperative/transcript/process/screen evidence. A hook `turn-ended` still does not prove assigned-task completion: dispatch receipts and workflow completion remain separate protocols. diff --git a/docs/components/agent-profiles.md b/docs/components/agent-profiles.md index f9d7cf34..16f9be0c 100644 --- a/docs/components/agent-profiles.md +++ b/docs/components/agent-profiles.md @@ -64,17 +64,25 @@ name (frozen at launch — later renames don't relabel live panes). The identity lives exactly as long as the launched agent: once it exits, any agent started manually in that pane shows its own name and runs with your default environment and account. -Claude Code and Codex Profile launches automatically prepare launch-scoped native signal -bridges. Prowl writes no hook configuration to runtime homes or repositories. Claude merges -an explicit final `--settings` JSON/file source in memory while preserving unknown fields and -existing hook arrays; Codex preserves an effective user notifier through a private transparent -dispatcher. Hook JSON, channel tokens, socket paths, and notifier argv ride in child-only -carriers rather than terminal input, shell history, preview values, logs, or durable Profile -state. A manual runtime started later in the same pane inherits none of this coverage. +Claude Code, Codex, GitHub Copilot, Droid, Qoder, Pi, Oh My Pi, and OpenCode Profile launches +automatically prepare launch-scoped native signal bridges. Prowl writes no hook configuration +to runtime homes or repositories. Claude merges an explicit final `--settings` JSON/file +source in memory while preserving unknown fields and existing hook arrays; Codex preserves an +effective user notifier through a private transparent dispatcher; Copilot, Pi, and Oh My Pi +load a read-only file shipped inside Prowl through an additive flag; Droid and Qoder receive a +merged settings object; OpenCode receives a launch-scoped `OPENCODE_CONFIG_CONTENT` whose +plugin list is appended to whatever the Profile or your shell already exports. Hook JSON, +channel tokens, socket paths, and notifier argv ride in child-only carriers rather than +terminal input, shell history, preview values, logs, or durable Profile state. A manual +runtime started later in the same pane inherits none of this coverage. See +[Agent detection](agent-detection.md#managed-native-completion-signals) for each runtime's +events and the cases that run unchanged. Preparation is bounded and occurs before a prompted dispatch is issued. If Prowl cannot -safely merge Claude settings, resolve Codex configuration, or preserve a notifier, the -Profile still launches with its original argv and no exact managed channel. Toolbar and +safely merge settings or content, resolve a configuration or shell environment, preserve a +notifier, or find its bundled resource — or the runtime's own flags disable hooks +(`--setting-sources`, `--pure`) — the Profile still launches with its original argv and no +exact managed channel. Toolbar and Command Palette show one non-blocking warning toast. CLI JSON adds one optional `warnings: [{code: "managed_hook_degraded", runtime, message}]`; text output writes the warning once to stderr. Receipt behavior is unchanged. diff --git a/docs/components/cli.md b/docs/components/cli.md index 4e840fcf..01cf294d 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -251,10 +251,11 @@ means explicit channel and caller-pane attribution, not verified business comple Claimed origin never upgrades trust. JSON uses `prowl.cli.agents.signal.v1`. Prowl's bundled CLI has a hidden, silent native-hook ingress for managed Claude Code, Codex, -Copilot, Droid, and Qoder Profile launches. It is not a user command and does not appear in -help/completion. Only an app-issued in-memory token plus exact caller ancestry, runtime, -native event, launch cwd, and process generation can produce -`source=hook_claude|hook_codex|hook_copilot|hook_droid|hook_qodercli` and a +Copilot, Droid, Qoder, Pi, Oh My Pi, and OpenCode Profile launches. It is not a user command +and does not appear in help/completion. Only an app-issued in-memory token plus exact caller +ancestry, runtime, native event, launch cwd, and process generation can produce +`source=hook_claude|hook_codex|hook_copilot|hook_droid|hook_qodercli|hook_pi|hook_omp|hook_opencode` +and a `verified_live` channel. Public `agents signal` cannot claim that provenance. Hook delivery is bounded and fail-open for the runtime; native `turn-ended` remains observation evidence, not dispatch or workflow completion. diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index 8c459b02..5fba4f9b 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -169,9 +169,9 @@ Terminal text is `.data.text` (read) and `.data.capture.text` (send) — never ` ## Reading Agent Output - For Codex/Claude Code, `prowl agents read` beats scraping: check `.data.agent.status`, inspect `.data.blocker.text` before answering a prompt with `send`/`key` (read and write are not atomic), and only trust `.data.result.text` when `state == "complete"`. `--result-only` prints the raw trusted result and fails otherwise; it cannot combine with `--json`. -- Prowl-launched Claude Code, Codex, Copilot, Droid, and Qoder Profiles may expose - `verified_live` channels with `source=hook_`; manually typing those runtimes does - not. A managed hook +- Prowl-launched Claude Code, Codex, Copilot, Droid, Qoder, Pi, Oh My Pi, and OpenCode + Profiles may expose `verified_live` channels with `source=hook_`; manually typing + those runtimes does not. A managed hook `turn-ended` proves only a runtime turn edge, never assigned-task completion. If Profile creation returns `managed_hook_degraded`, keep the successful pane but expect honest heuristic/cooperative fallback for that session.