diff --git a/docs-ai/063-agent-workflows/000-plan.md b/docs-ai/063-agent-workflows/000-plan.md index 731386d0..2d2e21e3 100644 --- a/docs-ai/063-agent-workflows/000-plan.md +++ b/docs-ai/063-agent-workflows/000-plan.md @@ -589,3 +589,4 @@ attaches hooks through A2's launch boundary. - 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/004-pane-identity-env.md b/docs-ai/063-agent-workflows/004-pane-identity-env.md new file mode 100644 index 00000000..09650c12 --- /dev/null +++ b/docs-ai/063-agent-workflows/004-pane-identity-env.md @@ -0,0 +1,29 @@ +# 063.004 — Pane Identity Environment Variable (A1b) + +## Context + +The A1 review (PR #710) showed that the `prowl-cli` skill taught agents to find themselves with `select(.pane.focused == true)` — the very focus heuristic `create pane` was built to avoid. Prowl already injects `PROWL_WORKTREE_PATH` / `PROWL_ROOT_PATH` into every pane; the pane's own UUID was the one missing fact. The review weighed a caller-pane default for `create pane` against a per-pane environment variable and chose the variable: it is explicit (an unset shell variable fails with `INVALID_ARGUMENT` instead of silently splitting the caller), it serves every command (`send`, `read`, `close`, `create pane`) and the skill's self-guard, and it needs no contract change. + +## Change + +- `GhosttySurfaceView` generates its UUID before building the surface environment and adds `PROWL_PANE_ID=` to every surface (tabs, splits, restored layouts, profile launches); the merged environment is exposed as `launchEnvironment`. A caller-supplied `PROWL_PANE_ID` is overwritten, and profile overrides already reject the `PROWL_` prefix. +- `docs/components/cli.md` gains "Identity: which pane am I?" (the variable, how to resolve the own tab/worktree from it, the inherited-not-verified caveat) and drops the focused-pane self snippet. +- `skills/prowl-cli/SKILL.md` rewritten around a "Who You Are" section and trimmed: stale focused-self guidance removed, duplicated pitfalls merged, the detector-fixture capture recipe reduced to a pointer at `docs/components/agent-detection.md`, command/field claims re-verified against the current CLI and schema (375 → ~170 lines). + +## Decisions + +- One variable only. Tab and worktree identity are derivable from `prowl list --json` by `pane.id`; `PROWL_WORKTREE_PATH` already exists. +- Convenience identity, not attribution. The value is inherited and forgeable; `handoff` (and later `workflow done` / `agents signal`) keep resolving the calling pane from process ancestry. +- `create pane` keeps its explicit anchor (no caller-pane default); the recipe is `prowl create pane "$PROWL_PANE_ID" --direction right`. + +## Verification + +- `GhosttySurfaceViewTests/launchEnvironmentCarriesThePaneIdentity` (own UUID wins over a forged value, siblings differ); focused suites `GhosttySurfaceViewTests` / `WorktreeEnvironmentTests` / `AgentProfileTests`: 40 passed. +- Live Debug instance on an isolated socket: a new tab's `$PROWL_PANE_ID` equals its `pane.id`; a split created from it reports its own UUID, not the anchor's; from inside the split the skill recipe resolved the correct tab and worktree and `create pane "$PROWL_PANE_ID" --direction down` anchored on itself; `agents read --result-only --json` fails with `INVALID_ARGUMENT` as the skill states. +- `make check`, `make build-app`. + +## Refs + +- Slice: 063-A1b (release plan R1) +- Branch: `feat/cli-pane-identity-env` +- Follows: [003-cli-create-pane.md](003-cli-create-pane.md) diff --git a/docs/components/cli.md b/docs/components/cli.md index a5c5c497..1caa27ae 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -70,6 +70,32 @@ across an app restart. > resolve a concrete UUID `pane.id` from `prowl list --json`; for an interactive > same-session handoff, copy the `pN` handle from text `prowl list`. +### Identity: which pane am I? + +Every pane's shell starts with these environment variables, inherited by every +process launched inside it (agents, their tools, scripts): + +- `PROWL_PANE_ID` — the pane's own UUID, the same value as `pane.id` in `prowl list + --json`. Use it directly as a selector (`--pane "$PROWL_PANE_ID"`), as the anchor for + `create pane`, and as the guard that keeps automation from acting on itself. +- `PROWL_WORKTREE_PATH`, `PROWL_ROOT_PATH` — the worktree directory and repository root + (see [custom-actions](custom-actions.md)). + +Resolve your own tab and worktree from it: + +```bash +me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" +printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' +``` + +The variable is inherited, not verified: a process that scrubbed its environment +(`sudo`, `ssh`, containers) will not have it, and a tmux/screen session attached from a +different pane reports the pane its server started in. When it is unset or does not +match any `pane.id`, fall back to `prowl list --json` and choose by `pane.cwd` — never +assume the *focused* pane is you. Prowl itself never trusts the variable for +attribution; commands that need the calling pane (`handoff`) resolve it from the +caller's process ancestry. + ## Commands ### `prowl list` @@ -107,9 +133,10 @@ Claude running a background **workflow**); otherwise **idle**. See the good for coordination but lags a screen by ~2–3 s and can flip to idle **before** a TUI finishes painting — confirm with `read --wait-stable`. -Find your own pane (to avoid operating on yourself): +Your own pane is `$PROWL_PANE_ID` (see [Identity](#identity-which-pane-am-i)); compare +against it before sending input anywhere: ```bash -self_pane="$(prowl list --json | jq -r '.data.items[] | select(.pane.focused==true) | .pane.id')" +test "$pane" != "$PROWL_PANE_ID" ``` ### `prowl agents` @@ -437,18 +464,17 @@ artifacts and terminal excerpts do not appear in `git status`. ## Safety & self-targeting -- If your shell runs **inside a Prowl pane**, the focused pane is probably *you*. - Identify and avoid it (the `self_pane` snippet above) so you don't `key enter` - into your own session. +- If your shell runs **inside a Prowl pane**, `$PROWL_PANE_ID` is *you*. Compare + every target against it so you don't `key enter` into your own session; the + focused pane is not a reliable stand-in (`open` and `focus` move it). - Close commands require explicit targets and may prompt for GUI confirmation on protected work; `--force` bypasses the prompt. ## A complete loop (run, read, clean up) ```bash -self_pane="$(prowl list --json | jq -r '.data.items[]|select(.pane.focused==true)|.pane.id')" pane="$(prowl create tab MyApp --json | jq -r '.data.target.pane.id')" -test "$pane" != "$self_pane" +test "$pane" != "$PROWL_PANE_ID" prowl send --pane "$pane" 'swift build' --capture --timeout 300 --json prowl read --pane "$pane" --last 100 --wait-stable --json prowl close "$pane" --json @@ -458,6 +484,8 @@ prowl close "$pane" --json - Resolve a UUID `pane.id` or current text `pN` before `read`/`send`/`key`/ `focus`/close — never trust tab titles. +- You are `$PROWL_PANE_ID`, not "the focused pane"; look up your tab/worktree + from it when you need them. - Use `prowl agents --json` for discovery, then `prowl agents read ` for a supported agent's status, blocker, and trustworthy result state; use `prowl list --json` when you need all panes, including ordinary shells. diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index c1937122..2146ca64 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -6,188 +6,108 @@ description: >- # Prowl CLI -Use `prowl` only when the task is to inspect or control the running Prowl GUI app: read panes, check sibling agents, focus a pane, open a repo/path in Prowl, send text, or send keys. Do not use it merely because the current shell is inside the Prowl repo. +Use `prowl` only when the task is to inspect or control the running Prowl GUI app: read panes, check sibling agents, focus a pane, open a repo/path in Prowl, send text, send keys, or create/close panes and tabs. Do not use it merely because the current shell is inside the Prowl repo. The authoritative per-command reference is `docs/components/cli.md`. -## Safe Default Workflow - -For automation, always resolve a concrete pane UUID before `read`, `send`, `key`, `focus`, or destructive close commands. Text `list` and `agents` also expose current-process `pN` / `tN` handles for concise same-session targeting: `read p7`, `send p7 '…'`, `key p7 enter`, and `close t6` are valid. UUIDs remain the only cross-process identity. +## Who You Are -```bash -prowl list --json -``` +Every Prowl pane exports its own identity to the processes inside it: -Pick the target by `pane.id`, `tab.id`, `worktree.id`, `worktree.path`, `pane.cwd`, and `pane.focused`. Do not trust tab titles: they are free-form and can lag or lie. +- `PROWL_PANE_ID` — this pane's UUID, identical to `pane.id` in `prowl list --json`. +- `PROWL_WORKTREE_PATH`, `PROWL_ROOT_PATH` — this pane's worktree directory and repository root. -When you specifically need active agent status, prefer the agent roster: +Use `$PROWL_PANE_ID` as your own selector and as the guard against operating on yourself; resolve your tab and worktree from it when you need them: ```bash -prowl agents --json +me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" +printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' +test "$pane" != "$PROWL_PANE_ID" # before sending anything to $pane ``` -`prowl agents` lists detected agent panes only. It is the right starting point for "which agents are blocked/working/done?", while `prowl list` remains the all-pane inventory, including ordinary shells. For a currently active Codex or Claude Code agent, follow it with an immediate semantic snapshot: - -```bash -prowl agents read p7 --json -# Or use the canonical UUID from .data.agents[].pane.id. -``` +The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. If it is unset or matches no `pane.id`, pick yourself from `prowl list --json` by `pane.cwd`. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. -`agents read` has no focus fallback, timeout, or wait mode. Its JSON always contains the current -`.data.agent.status` and independent `.data.result.state`; do not assume `idle`/`done` means a -trusted result exists unless `result.state == "complete"`. - -If your session was launched from a Prowl pane, the focused pane is often you. Treat focused pane IDs as something to identify and avoid unless you intentionally want to operate on yourself. - -```bash -self_pane="$(prowl list --json | jq -r '.data.items[] | select(.pane.focused == true) | .pane.id')" -``` +## Safe Default Workflow -Use explicit `--pane`: +Resolve a concrete pane before `read`, `send`, `key`, `focus`, or `close`, and pass it explicitly: ```bash +prowl list --json # every pane: worktree → tab → pane, plus worktree task.status +prowl agents --json # detected agent panes only: status working|blocked|idle|done prowl read --pane "$pane" --last 80 --wait-stable --json prowl send --pane "$pane" 'printf "PWD:%s\n" "$PWD"' --capture --timeout 30 --json -prowl focus --pane "$pane" --json prowl key --pane "$pane" enter --json +prowl focus --pane "$pane" --json ``` +Pick targets by `pane.id`, `tab.id`, `worktree.id`/`name`/`path`, and `pane.cwd`. Never trust tab titles: they are free-form and can lag or lie. Text `prowl list` / `prowl agents` also print short handles (`p7`, `t6`) that work in any target position for the life of the app process (`read p7`, `close t6`); UUIDs are the only identity that survives an app restart. + +For a currently active Codex or Claude Code agent, `prowl agents read p7 --json` returns an immediate semantic snapshot: `.data.agent.status`, `.data.blocker.text` when blocked, and `.data.result` — a result is trustworthy only when `.data.result.state == "complete"`. + ## Common Recipes -Create a fresh tab for a listed worktree, then verify it is not yourself: +Open a split beside yourself (or any positively identified anchor) and capture the new pane: ```bash -project="/path/to/project" -worktree="$(prowl list --json | jq -r --arg project "$project" ' - .data.items[] - | select((.worktree.path | rtrimstr("/")) == ($project | rtrimstr("/"))) - | .worktree.id -' | head -n 1)" -pane="$(prowl create tab "$worktree" --json | jq -r '.data.target.pane.id')" -test "$pane" != "$self_pane" +pane="$(prowl create pane "$PROWL_PANE_ID" --direction right --json | jq -r '.data.target.pane.id')" ``` -Prefer a `worktree.id` or `worktree.name` returned by `prowl list` over a hand-typed path; list preserves normalization such as trailing slashes. Use `--path` only for the new tab's working directory inside the selected worktree. +Directions are `right`, `left`, `up`, `down`; the anchor must be a pane UUID or current `pN`. The new pane inherits the anchor's working directory, becomes focused, and Prowl selects its worktree and tab (as `create tab` does). Run input afterwards with an explicit `prowl send --pane "$pane" …`. -Create a sibling split from a positively identified anchor pane and capture the new UUID: +Create a fresh tab in a listed worktree: ```bash -pane="$(prowl create pane "$anchor" --direction right --json | jq -r '.data.target.pane.id')" -test "$pane" != "$anchor" +pane="$(prowl create tab "$worktree" --json | jq -r '.data.target.pane.id')" ``` -The anchor must be a pane UUID or current `pN` handle. Directions are `right`, `left`, `up`, -and `down`. Creation inherits the anchor's working directory and returns the exact new pane; -run input afterward with an explicit `prowl send --pane "$pane" …`. - -`prowl open /path` opens or focuses a matching project/path and may create a tab when needed. It is not guaranteed to create a new pane. Use `prowl create tab` or `prowl create pane` for deterministic new terminal sessions. +Prefer a `worktree.id` or `worktree.name` from `prowl list --json` over a hand-typed path; `--path` only sets the new tab's working directory inside that worktree. `prowl open /path` is navigation — it may reuse an existing pane — so use `create tab`/`create pane` when you need a guaranteed new shell. -Run a command and capture its result: +Run a command and capture its output and exit code: ```bash -prowl send --pane "$pane" 'git status --short' --capture --timeout 30 --json +out="$(prowl send --pane "$pane" 'git status --short' --capture --timeout 30 --json)" +printf '%s\n' "$out" | jq -r '.data.capture.text, .data.wait.exit_code' ``` -Deliver input without waiting: +Deliver input without waiting, or pre-fill and submit later: ```bash prowl send --pane "$pane" 'long-running command' --no-wait --json +prowl send --pane "$pane" 'echo ready' --no-enter --no-wait --json && prowl key --pane "$pane" enter --json ``` -Pre-fill text, then submit it later: - -```bash -prowl send --pane "$pane" 'echo ready' --no-enter --no-wait --json -prowl key --pane "$pane" enter --json -``` - -Use this pattern only for a pane you have positively identified. If `$pane` is your own pane, `key enter` submits text into your current session. - Send multiline input from stdin: ```bash printf '%s\n' 'echo first' 'echo second' | prowl send --pane "$pane" --capture --timeout 30 --json ``` -Close a temporary tab/pane when done: +Close what you created: ```bash prowl close "$pane" --json -prowl close --tab "$tab" --json +prowl close --tab "$tab" --force --json # --force skips the GUI confirmation for protected work ``` -`close` requires an explicit pane or tab target and intentionally has no focus or worktree fallback. For automation-created tabs, prefer the `tab.id` or `pane.id` returned by `create tab`. If the target has protected agent work or a long-running command, Prowl may ask for GUI confirmation. Use `--force` only after you have positively identified the target: - -```bash -prowl close "$pane" --force --json -``` +`close` requires an explicit pane or tab and has no focus or worktree fallback. ## Parsing JSON Output -Do not guess field names. Every `--json` response is `{ "ok", "command", "schema_version", "data": {...} }`, and the terminal text lives at `.data.text` — not `.content`, `.output`, or `.stdout`. Always parse with `jq` against the fields below; the authoritative, per-command field reference lives in `docs/components/cli.md`. - -When JSON is stored in a shell variable, use `printf '%s\n' "$json" | jq ...`. Do not use `echo "$json" | jq`: zsh can interpret JSON escape sequences such as `\u001B` and turn them back into raw control characters. - -```bash -# read: rendered terminal text is .data.text -prowl read --pane "$pane" --last 80 --wait-stable --json | jq -r '.data.text' +Every `--json` response is `{ "ok", "command", "schema_version", "data": {...} }`; failures are `{ "ok": false, "error": { "code", "message" } }`. Parser errors (bad flags) print plain text even with `--json`, so check the exit code before piping into `jq`. When JSON sits in a shell variable, use `printf '%s\n' "$json" | jq …` — zsh `echo` can turn `\u001B` escapes back into control characters. Pass shell values into `jq` with `--arg`. -# send --capture: captured output is .data.capture.text; exit code is .data.wait.exit_code -out="$(prowl send --pane "$pane" 'git status --short' --capture --timeout 30 --json)" -printf '%s\n' "$out" | jq -r '.data.capture.text' -printf '%s\n' "$out" | jq -r '.data.wait.exit_code' - -# create tab / open: new pane and tab ids -created="$(prowl create tab "$worktree" --json)" -printf '%s\n' "$created" | jq -r '.data.target.pane.id' -printf '%s\n' "$created" | jq -r '.data.target.tab.id' - -# create pane: resolved anchor, direction, and new pane id -split="$(prowl create pane "$anchor" --direction right --json)" -printf '%s\n' "$split" | jq -r '.data.anchor.pane.id' -printf '%s\n' "$split" | jq -r '.data.direction' -printf '%s\n' "$split" | jq -r '.data.target.pane.id' - -# list / agents: ids and status -prowl list --json | jq -r '.data.items[].pane.id' -prowl list --json | jq -r '.data.items[] | select(.pane.focused) | .pane.id' -prowl agents --json | jq -r '.data.agents[] | "\(.status)\t\(.pane.id)"' - -# guard before trusting data: bail if ok is not true -prowl list --json | jq -e '.ok == true' >/dev/null || echo "command failed" -``` +Key fields by command: -Key fields by command (see `docs/components/cli.md` for the full contract): +- `list` → `.data.items[]` with `.worktree.{id,name,path,root_path,kind}`, `.tab.{id,title,selected}`, `.pane.{id,title,cwd,focused,agent}`, `.task.status` (`running`|`idle`|null). +- `agents` → `.data.agents[]` with `.status`, `.raw_state`, `.detection_reason`, `.type`, `.name`, `.pane.{id,focused,cwd}`, `.tab`, `.worktree`, `.project.{name,branch,path}`. +- `agents read` → `.data.agent`, `.data.blocker.text`, `.data.result.{state,text}` — `pending`, `unavailable`, `missing`, `incomplete`, `too_large` carry no partial text. +- `read` → `.data.text`, `.data.line_count`, `.data.truncated`, `.data.mode`, `.data.source`; `.data.stabilized` / `.data.waited_ms` with `--wait-stable`. +- `send` → `.data.input`, `.data.wait.{exit_code,duration_ms}` when waiting, `.data.capture.{text,line_count,truncated}` with `--capture`. +- `create tab` / `open` → `.data.target.{pane,tab,worktree}`; `create pane` → `.data.anchor`, `.data.direction`, `.data.target`. -- `read` → `.data.text`, `.data.line_count`, `.data.truncated`, `.data.mode` (`snapshot`|`last`), `.data.source` (`screen`|`scrollback`|`mixed`|`detection`), plus `.data.stabilized` / `.data.waited_ms` / `.data.samples` when `--wait-stable`. -- `send` → `.data.input` (source/characters/bytes/trailing_enter_sent); `.data.wait.exit_code` and `.data.wait.duration_ms` when waiting; `.data.capture.text` / `.data.capture.line_count` / `.data.capture.truncated` when `--capture`. -- `list` / `agents` → `.data.items[]` / `.data.agents[]`, each with `.pane.id`, `.tab.id`, and `.worktree.{id,name,path}`. Agent entries also include `.status`, `.raw_state`, and optional `.detection_reason`; list entries include `.task.status`. -- `agents read` → `.data.agent` (current status/reason), `.data.blocker.text` when blocked, and `.data.result`. Only `.data.result.state == "complete"` carries `.data.result.text`; `pending`, `unavailable`, `missing`, `incomplete`, and `too_large` deliberately carry no partial text. -- `create tab` / `open` → `.data.target.{pane,tab,worktree}`. -- `create pane` → `.data.anchor`, `.data.direction`, and the created `.data.target.{pane,tab,worktree}`. +Terminal text is `.data.text` (read) and `.data.capture.text` (send) — never `.content`, `.output`, or `.stdout`. ## Reading Agent Output -For an active Codex or Claude Code pane, prefer `agents read` over terminal scraping: - -```bash -snapshot="$(prowl agents read p7 --json)" -printf '%s\n' "$snapshot" | jq -e '.ok == true and .data.agent.status == "blocked"' >/dev/null -printf '%s\n' "$snapshot" | jq -r '.data.blocker.text // empty' -``` - -The command is an immediate snapshot. A blocker is raw current TUI interaction text, so inspect it -before using `prowl key --pane p7 ...` or `prowl send --pane p7 ...`; read and write are not atomic. -Use `--result-only` only for a strict pipeline that needs exact raw result bytes and should fail on -anything except a complete, exact/high-attributed result. It cannot combine with `--json`. - -`task.status` is useful for coordination but is not enough to prove the screen finished rendering. `idle` can arrive before a TUI has painted its final response. - -Prefer `read --wait-stable` for screen snapshots: - -```bash -prowl read --pane "$pane" --last 200 --wait-stable --json -``` - -Most of the time `--wait-stable` alone is enough — it blocks until the screen stops changing. Only poll `task.status` first when you specifically need to wait for an agent to go from `working` back to `idle` (status flips before the TUI finishes painting, so still finish with `--wait-stable`). When polling in zsh, do not name the variable `status` — it is readonly there: +- 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`. +- For everything else, `prowl read --pane "$pane" --last 200 --wait-stable --json` blocks until the screen stops changing. `task.status` flips to `idle` before a TUI finishes painting, so poll it only to wait for a `working` agent, then still read with `--wait-stable`: ```bash for i in 1 2 3 4 5 6; do @@ -195,154 +115,38 @@ for i in 1 2 3 4 5 6; do [ "$task_state" = idle ] && break sleep 1 done -prowl read --pane "$pane" --last 200 --wait-stable --json -``` - -The default read source follows the viewport. Only when diagnosing agent-state detection or collecting a sanitized detector fixture, request the exact active-screen detector input and verify the returned source before trusting it. An older running app that does not honor `detection` fails with `READ_FAILED`; update or restart Prowl rather than accepting viewport text. - -Run the capture from the Prowl source checkout so its canonical private staging path is covered by `.gitignore`: - -```bash -repo_root="$(git rev-parse --show-toplevel)" -test -f "$repo_root/supacode.xcodeproj/project.pbxproj" -staging="$repo_root/.local/agent-screen-captures" -mkdir -p "$staging" -capture="$(prowl read --pane "$pane" --source detection --json)" -printf '%s\n' "$capture" | jq -e '.data.source == "detection"' >/dev/null -printf '%s\n' "$capture" | jq -j '.data.text' > "$staging/raw-capture.txt" ``` -Omit `--last` for detector captures. A scrolled pane's detection source can differ from its viewport. Treat the raw capture as private and redact it before committing any fixture. +- Rendered screens can truncate or fold content. When you need an agent's complete output, have the command write a file (`… > /tmp/out.txt`) and read that; shell redirection avoids the agent's own sandbox prompts. +- `read` returning fewer lines than `--last` with `truncated: false` means the pane simply has less history — do not retry. `--source detection` returns the exact detector input instead of the viewport; it exists for diagnosing agent-state detection (see `docs/components/agent-detection.md`), not for everyday reading. -When you need complete output from an agent, prefer writing or redirecting to a file over reading rendered TUI output. Screen capture can be truncated or miss folded content. +## Targeting & Arguments -For non-interactive agent CLIs, redirect stdout from the shell instead of asking the agent's tool layer to write outside its sandbox: - -```bash -prowl send --pane "$pane" \ - 'opencode run "Reply exactly: PROWL_OK" > /tmp/prowl-agent-out.txt' \ - --capture --timeout 120 --json -cat /tmp/prowl-agent-out.txt -``` - -Asking `opencode` or another agent to create `/tmp/...` itself may trigger permission prompts and fail. Shell redirection is usually simpler and more deterministic. - -## Targeting Shortcuts - -Find by worktree path: - -```bash -prowl list --json | jq -r ' - .data.items[] - | select(.worktree.path | rtrimstr("/") | endswith("/Prowl")) - | .pane.id -' -``` - -Find focused pane, usually to exclude it: - -```bash -prowl list --json | jq -r '.data.items[] | select(.pane.focused == true) | .pane.id' -``` - -Human scan: - -```bash -prowl list --no-color -``` - -Find active agents, prioritizing prompts that need attention: - -```bash -prowl agents --no-color -``` - -Get the first blocked agent pane and inspect it: - -```bash -pane="$(prowl agents --json | jq -r ' - .data.agents[] - | select(.status == "blocked") - | .pane.id -' | head -n 1)" -prowl agents read "$pane" --json -``` - -When no agent is blocked, use the same pattern with `working`, `done`, or `idle` depending on the task. The JSON payload also includes `.project.name`, `.project.branch`, `.worktree.path`, `.tab.title`, and `.pane.focused`, so automation can filter by human project label while still targeting the concrete pane. - -`-t/--target` and positional generic targets resolve `pN` as a pane, `tN` as a tab, then UUIDs and worktree references. A stale prefixed handle fails rather than falling back to a worktree of the same name. Explicit UUID `--pane` remains safest for automation. - -## Argument Rules - -`send` and `key` positional arguments are count-sensitive: - -| command | 0 args | 1 arg | 2 args | -|---|---|---|---| -| `send` | text from stdin | text to focused pane | ` ` | -| `key` | error | token to focused pane | ` ` | - -Avoid positional targeting in automation. The focused pane changes after `open` and `focus`. - -Important combinations: - -- `send --capture` waits for completion and sends a trailing Enter. It cannot combine with `--no-wait` or `--no-enter`. -- `send --no-enter` only pre-fills text. Use `key enter` to submit later. -- `key --repeat <1-100>` repeats a token, for example `prowl key --pane "$pane" down --repeat 10`. -- Do not mix stdin input with a positional text argument. - -## Quoting - -Use outer single quotes when variables should expand in the target pane: - -```bash -prowl send --pane "$pane" 'printf "PWD:%s\n" "$PWD"' --capture --timeout 30 --json -``` - -Avoid outer double quotes around payloads containing `$PWD`, `$VAR`, backticks, or command substitutions unless local expansion is intended. +- Selectors are mutually exclusive: `--pane `, `--tab `, `--worktree `, or `-t/--target` (auto: `pN`, `tN`, then UUID, then worktree). A stale handle fails rather than falling back to a same-named worktree. +- `send` and `key` positionals are count-sensitive: `send 'text'` and `key enter` go to the *focused* pane, `send p7 'text'` / `key p7 enter` to `p7`, and stdin replaces the text argument. Avoid positional targeting in automation. +- `send --capture` waits for completion and sends Enter; it cannot combine with `--no-wait` or `--no-enter`. `--capture` needs shell integration (OSC 133) on the target pane. +- `key --repeat <1-100>` repeats a token, e.g. `prowl key --pane "$pane" down --repeat 10`. +- Quote payloads with outer single quotes when variables should expand in the *target* pane: `prowl send --pane "$pane" 'printf "PWD:%s\n" "$PWD"'`. +- In zsh, never name a variable `status` — it is readonly. ## Pitfalls -- Never target by tab title alone; use `pane.id` plus path/cwd. -- Never omit `--pane` for `send`, `key`, `read`, or `focus` in automation. -- Use `prowl agents --json` for discovery and `prowl agents read --json` for a supported agent's current semantic snapshot; use `prowl list --json` for all panes and worktree-level `task.status`. -- `open /path` is a project/path navigation command. It may refocus an existing pane and is not a deterministic create command. -- Use `create tab` or `create pane` when automation needs a fresh shell, and capture the returned `pane.id` before sending input. -- Focused pane is not stable; `open` and `focus` change it. -- `read --wait-stable` sees rendered screen only. It cannot recover content folded by a TUI. -- `read` returning fewer lines than `--last` requested is normally `truncated: false` — the pane simply has less history and you already have it all, so do not retry for more. `truncated: true` flags a possibly-incomplete result (the full scrollback could not be read). +- `open /path` may refocus an existing pane; it is not a create command. +- Focus is not stable and is not you: `open` and `focus` change it, and the user clicks around. - `send --capture` captures a screen diff; multiline input may include command echo. -- Do not guess JSON field names. Terminal text is `.data.text` (read) and `.data.capture.text` (`send --capture`); see "Parsing JSON Output" and `docs/components/cli.md`. -- `prowl list --json | jq ...` snippets should pass shell values with `--arg`. -- In zsh, do not name variables `status`; it is readonly. -- Parser errors are not JSON even if `--json` is present, because parsing happens before command execution. -- The CLI talks to one socket owner by default. If two Prowl app instances are running, the default `prowl` command reaches whichever app owns the standard socket. For a manually launched dev instance, start the app and every CLI command with the same `PROWL_CLI_SOCKET=/tmp/name.sock`. -- Sandboxed agents must be allowed to connect to the Unix socket. `PROWL_CLI_SOCKET` is a workaround only when both the app and every CLI command use the same sandbox-accessible path. -- A newer CLI command sent to an older app can fail at transport level. If `prowl agents` returns `TRANSPORT_FAILED`, confirm the running app instance was built with the command. -- `cmd-w` can close a temporary tab, but double-check the pane first. +- The CLI talks to one socket owner. With two Prowl instances running, the default `prowl` reaches whichever owns the standard socket; a manually launched dev instance and every CLI call must share the same `PROWL_CLI_SOCKET=/tmp/name.sock`. Sandboxed agents must be allowed to connect to that Unix socket. +- A newer CLI talking to an older app can fail at transport level (`TRANSPORT_FAILED`) — confirm the running app was built with the command. ## Error Handling -In `--json` mode, command-level failures look like: - -```json -{ "ok": false, "error": { "code": "INVALID_ARGUMENT", "message": "..." } } -``` - -Common codes and recovery: - -- `APP_NOT_RUNNING`: Prowl is not reachable, or the socket is missing/stale. Ask before restarting the app. -- `SOCKET_PERMISSION_DENIED`: the socket exists but the sandbox or filesystem permissions blocked `connect()`. Report this as a permission/sandbox problem, not as an app-liveness problem. -- `TRANSPORT_FAILED`: the socket connection broke or the socket path is invalid (for example `ENOTSOCK` or a too-long `PROWL_CLI_SOCKET`). Recheck which Prowl instance owns the socket. -- `TARGET_NOT_FOUND` / `TARGET_NOT_UNIQUE`: run `prowl list --json` again and choose an explicit pane UUID, or refresh text `prowl list` and use its current `pN` handle. -- `EMPTY_INPUT`: `send` got neither argv text nor stdin. -- `NO_ACTIVE_PANE`: no pane resolved for positional (focused-pane) targeting; pass an explicit `--pane`. -- `INVALID_ARGUMENT`: illegal flag or flag combination, such as `--capture --no-wait`. -- `UNSUPPORTED_KEY` / `INVALID_REPEAT`: check `prowl key --help`. -- `CAPTURE_UNSUPPORTED`: `--capture` needs shell integration (OSC 133) on the target pane. Drop `--capture` and `read --wait-stable` instead, or redirect the command's output to a file (see "Reading Agent Output"). -- `WAIT_TIMEOUT`: command did not finish in time; retry with `--no-wait`, or raise `--timeout`. -- `PATH_NOT_FOUND` / `PATH_NOT_DIRECTORY` / `PATH_NOT_ALLOWED`: fix the path passed to `open`. - -Always check the exit code before piping output into `jq`; parser-level errors print plaintext usage to stderr. +- `APP_NOT_RUNNING`: Prowl is not reachable or the socket is stale — ask before restarting the app. +- `SOCKET_PERMISSION_DENIED`: the sandbox or filesystem blocked `connect()`; report a permission problem, not an app-liveness problem. +- `TRANSPORT_FAILED`: the connection broke or the socket path is invalid (`ENOTSOCK`, too-long `PROWL_CLI_SOCKET`). +- `TARGET_NOT_FOUND` / `TARGET_NOT_UNIQUE`: re-run `prowl list --json` and pass an explicit UUID or a current `pN`. +- `NO_ACTIVE_PANE`: focused-pane targeting found nothing — pass `--pane`. `SOURCE_REQUIRED`: `handoff` was run outside a Prowl pane without a selector. +- `EMPTY_INPUT`, `INVALID_ARGUMENT`, `UNSUPPORTED_KEY`, `INVALID_REPEAT`: fix the arguments (`prowl --help`). +- `CAPTURE_UNSUPPORTED`: drop `--capture` and use `read --wait-stable` or file redirection. `WAIT_TIMEOUT`: raise `--timeout` or use `--no-wait`. +- `PATH_NOT_FOUND` / `PATH_NOT_DIRECTORY` / `PATH_NOT_ALLOWED`: fix the path given to `open` or `create tab --path`. ## Handing Off Your Task @@ -355,21 +159,13 @@ prowl handoff to codex --brief - <<'EOF' … ## Current State … -## What Has Been Done -… -## Open Questions -… -## Risks / Watch Out -… ## Next Steps … -## Suggested Prompt For Next Agent -… EOF ``` -Write the briefing from your current working knowledge — required sections are `## Objective`, `## Current State`, and `## Next Steps`. The receiver launches in a background tab of the same worktree; your own session stays open. `prowl handoff save --brief -` writes the same briefing as a checkpoint without launching anyone. Use `--no-brief` only for an intentional context-only handoff, and an explicit `--pane` to hand off a pane other than your own. Details: `docs/components/handoff.md`. +Required sections are `## Objective`, `## Current State`, and `## Next Steps`; optional ones are `## What Has Been Done`, `## Open Questions`, `## Risks / Watch Out`, and `## Suggested Prompt For Next Agent`. The receiver launches in a background tab of the same worktree; your session stays open. `prowl handoff save --brief -` checkpoints the same briefing without launching anyone; `--no-brief` is for an intentional context-only handoff; `--pane` hands off a pane other than your own. Details: `docs/components/handoff.md`. ## Command Set -Current commands: `list`, `agents`, `agents read`, `read`, `send`, `key`, `focus`, `create tab`, `create pane`, `close`, `handoff to`, `handoff save`, and `open` (default). There is no CLI `quit`; close temporary tabs or panes with an explicit `close` target. `tab create`, `tab close`, and `pane close` remain deprecated aliases for one release. +`list`, `agents`, `agents read`, `read`, `send`, `key`, `focus`, `create tab`, `create pane`, `close`, `handoff to`, `handoff save`, and `open` (default). There is no CLI `quit`; close temporary tabs or panes with an explicit `close`. `tab create`, `tab close`, and `pane close` remain deprecated aliases for one release. diff --git a/supacode/Domain/AgentProfile/AgentProfileLaunchPlan.swift b/supacode/Domain/AgentProfile/AgentProfileLaunchPlan.swift index 19f52495..dafd7d32 100644 --- a/supacode/Domain/AgentProfile/AgentProfileLaunchPlan.swift +++ b/supacode/Domain/AgentProfile/AgentProfileLaunchPlan.swift @@ -74,7 +74,7 @@ nonisolated enum AgentProfileEnvironmentPolicy { } /// Account-home variables come from the adapters, not a hardcoded list; the - /// `PROWL_` prefix protects the worktree/root facts Prowl injects itself. + /// `PROWL_` prefix protects the worktree/root/pane facts Prowl injects itself. /// Letting an override set `CODEX_HOME` on an unbound profile would bypass /// home provisioning, deletion protection, and rooted session detection — /// custom homes are a separate capability, not an env-table backdoor. diff --git a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift index 4f95b2ee..a7c82d36 100644 --- a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift +++ b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift @@ -114,8 +114,14 @@ final class GhosttySurfaceView: NSView, Identifiable { } } + /// Environment variable that carries the pane's own UUID into every process + /// started inside it, so an agent can address itself (`--pane "$PROWL_PANE_ID"`) + /// without guessing from focus. Convenience identity only: the value is + /// inherited and forgeable, so trusted attribution stays on caller-PID resolution. + static let paneIdentityEnvironmentKey = "PROWL_PANE_ID" + let runtime: GhosttyRuntime - let id = UUID() + let id: UUID private var debugID: String { String(id.uuidString.prefix(8)) } @@ -124,6 +130,8 @@ final class GhosttySurfaceView: NSView, Identifiable { } let bridge: GhosttySurfaceBridge let launchWorkingDirectory: URL? + /// Environment handed to the surface's shell: the caller's variables plus the pane identity. + let launchEnvironment: [String: String] private(set) var surface: ghostty_surface_t? private var surfaceRef: GhosttyRuntime.SurfaceReference? private let workingDirectoryCString: UnsafeMutablePointer? @@ -261,6 +269,8 @@ final class GhosttySurfaceView: NSView, Identifiable { environment: [String: String] = [:], skipsSurfaceCreationForTesting: Bool = false ) { + let id = UUID() + self.id = id self.runtime = runtime self.bridge = GhosttySurfaceBridge() self.fontSize = fontSize ?? 0 @@ -281,7 +291,10 @@ final class GhosttySurfaceView: NSView, Identifiable { } else { initialInputCString = nil } - let sortedEnv = environment.sorted { $0.key < $1.key } + var launchEnvironment = environment + launchEnvironment[Self.paneIdentityEnvironmentKey] = id.uuidString + self.launchEnvironment = launchEnvironment + let sortedEnv = launchEnvironment.sorted { $0.key < $1.key } var allocatedStrings: [UnsafeMutablePointer] = [] allocatedStrings.reserveCapacity(sortedEnv.count * 2) for (key, value) in sortedEnv { diff --git a/supacodeTests/GhosttySurfaceViewTests.swift b/supacodeTests/GhosttySurfaceViewTests.swift index 50469f1f..f7e03a14 100644 --- a/supacodeTests/GhosttySurfaceViewTests.swift +++ b/supacodeTests/GhosttySurfaceViewTests.swift @@ -180,6 +180,28 @@ struct GhosttySurfaceViewTests { #expect(!duplicateApply) } + @Test func launchEnvironmentCarriesThePaneIdentity() { + let runtime = GhosttyRuntime() + let surfaceView = GhosttySurfaceView( + runtime: runtime, + workingDirectory: nil, + context: GHOSTTY_SURFACE_CONTEXT_SPLIT, + environment: ["PROWL_WORKTREE_PATH": "/repo/wt", "PROWL_PANE_ID": "forged"], + skipsSurfaceCreationForTesting: true + ) + let sibling = GhosttySurfaceView( + runtime: runtime, + workingDirectory: nil, + context: GHOSTTY_SURFACE_CONTEXT_SPLIT, + skipsSurfaceCreationForTesting: true + ) + + #expect(surfaceView.launchEnvironment["PROWL_PANE_ID"] == surfaceView.id.uuidString) + #expect(surfaceView.launchEnvironment["PROWL_WORKTREE_PATH"] == "/repo/wt") + #expect(sibling.launchEnvironment["PROWL_PANE_ID"] == sibling.id.uuidString) + #expect(sibling.id != surfaceView.id) + } + @Test func occlusionDoesNotApplyUntilViewHasSuperviewAndWindow() async { let runtime = GhosttyRuntime() let surfaceView = GhosttySurfaceView( -- 2.51.2 From 94f6138bbbcf0eeea29b7a946a960b3b4975c1d6 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 22 Aug 2026 16:28:13 +0900 Subject: [PATCH 2/8] docs(skill): point the prowl-cli skill at the bundled manual The skill is installed outside the Prowl checkout, so relative docs/components paths do not resolve there. Locate the docs folder from the installed CLI symlink (Prowl.app/Contents/Resources/docs) and refer to the per-feature manuals relative to it. Claude-Session: https://claude.ai/code/session_01YUytym5xEn5FKMhWjSkNkm --- skills/prowl-cli/SKILL.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index 2146ca64..ee61b848 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -6,7 +6,16 @@ description: >- # Prowl CLI -Use `prowl` only when the task is to inspect or control the running Prowl GUI app: read panes, check sibling agents, focus a pane, open a repo/path in Prowl, send text, send keys, or create/close panes and tabs. Do not use it merely because the current shell is inside the Prowl repo. The authoritative per-command reference is `docs/components/cli.md`. +Use `prowl` only when the task is to inspect or control the running Prowl GUI app: read panes, check sibling agents, focus a pane, open a repo/path in Prowl, send text, send keys, or create/close panes and tabs. Do not use it merely because the current shell is inside the Prowl repo. + +The authoritative per-command reference is Prowl's manual, `components/cli.md` under the docs folder. That folder is `docs/` in a Prowl source checkout; otherwise it ships inside the app bundle, which you can locate from the installed CLI (normally `/Applications/Prowl.app/Contents/Resources/docs`): + +```bash +prowl_docs="$(dirname "$(dirname "$(readlink -f "$(command -v prowl)")")")/docs" +ls "$prowl_docs/components/" # cli.md, agent-detection.md, handoff.md, … +``` + +Other `docs/components/*.md` references below live in that same folder. ## Who You Are @@ -118,7 +127,7 @@ done ``` - Rendered screens can truncate or fold content. When you need an agent's complete output, have the command write a file (`… > /tmp/out.txt`) and read that; shell redirection avoids the agent's own sandbox prompts. -- `read` returning fewer lines than `--last` with `truncated: false` means the pane simply has less history — do not retry. `--source detection` returns the exact detector input instead of the viewport; it exists for diagnosing agent-state detection (see `docs/components/agent-detection.md`), not for everyday reading. +- `read` returning fewer lines than `--last` with `truncated: false` means the pane simply has less history — do not retry. `--source detection` returns the exact detector input instead of the viewport; it exists for diagnosing agent-state detection (see `components/agent-detection.md` in the docs folder), not for everyday reading. ## Targeting & Arguments @@ -164,7 +173,7 @@ prowl handoff to codex --brief - <<'EOF' EOF ``` -Required sections are `## Objective`, `## Current State`, and `## Next Steps`; optional ones are `## What Has Been Done`, `## Open Questions`, `## Risks / Watch Out`, and `## Suggested Prompt For Next Agent`. The receiver launches in a background tab of the same worktree; your session stays open. `prowl handoff save --brief -` checkpoints the same briefing without launching anyone; `--no-brief` is for an intentional context-only handoff; `--pane` hands off a pane other than your own. Details: `docs/components/handoff.md`. +Required sections are `## Objective`, `## Current State`, and `## Next Steps`; optional ones are `## What Has Been Done`, `## Open Questions`, `## Risks / Watch Out`, and `## Suggested Prompt For Next Agent`. The receiver launches in a background tab of the same worktree; your session stays open. `prowl handoff save --brief -` checkpoints the same briefing without launching anyone; `--no-brief` is for an intentional context-only handoff; `--pane` hands off a pane other than your own. Details: `components/handoff.md` in the docs folder. ## Command Set -- 2.51.2 From 54832abc908f7fec1606a9b03809426f9aa232b1 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 22 Aug 2026 16:38:02 +0900 Subject: [PATCH 3/8] docs(cli): fail loudly when PROWL_PANE_ID matches no pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A silent empty result hides the two real causes — the variable is unset in this shell, or prowl reached a different Prowl instance than the one hosting the pane — so the lookup recipe now says so on stderr. Claude-Session: https://claude.ai/code/session_01YUytym5xEn5FKMhWjSkNkm --- docs/components/cli.md | 10 +++++----- skills/prowl-cli/SKILL.md | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/components/cli.md b/docs/components/cli.md index 1caa27ae..b74f9f21 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -84,14 +84,14 @@ process launched inside it (agents, their tools, scripts): Resolve your own tab and worktree from it: ```bash -me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" -printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' -``` +The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). If it is unset or matches nothing, pick yourself from `prowl list --json` by `pane.cwd`.``` The variable is inherited, not verified: a process that scrubbed its environment (`sudo`, `ssh`, containers) will not have it, and a tmux/screen session attached from a -different pane reports the pane its server started in. When it is unset or does not -match any `pane.id`, fall back to `prowl list --json` and choose by `pane.cwd` — never +different pane reports the pane its server started in. A value that matches no +`pane.id` usually means `prowl` reached a different Prowl instance than the one hosting +your pane (see [Transport & app launch](#transport--app-launch)). When it is unset or +matches nothing, fall back to `prowl list --json` and choose by `pane.cwd` — never assume the *focused* pane is you. Prowl itself never trusts the variable for attribution; commands that need the calling pane (`handoff`) resolve it from the caller's process ancestry. diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index ee61b848..7a0033da 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -28,11 +28,12 @@ Use `$PROWL_PANE_ID` as your own selector and as the guard against operating on ```bash me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" +test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; exit 1; } printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' test "$pane" != "$PROWL_PANE_ID" # before sending anything to $pane ``` -The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. If it is unset or matches no `pane.id`, pick yourself from `prowl list --json` by `pane.cwd`. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. +The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). If it is unset or matches nothing, pick yourself from `prowl list --json` by `pane.cwd`. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. ## Safe Default Workflow -- 2.51.2 From 400eb38f327785510f659f3e04e607e8f066713b Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 22 Aug 2026 16:40:44 +0900 Subject: [PATCH 4/8] docs(cli): restore the identity lookup snippet The previous commit replaced the code block with prose by mistake. Claude-Session: https://claude.ai/code/session_01YUytym5xEn5FKMhWjSkNkm --- docs/components/cli.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/components/cli.md b/docs/components/cli.md index b74f9f21..7ecd4d61 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -84,7 +84,10 @@ process launched inside it (agents, their tools, scripts): Resolve your own tab and worktree from it: ```bash -The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). If it is unset or matches nothing, pick yourself from `prowl list --json` by `pane.cwd`.``` +me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" +test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; exit 1; } +printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' +``` The variable is inherited, not verified: a process that scrubbed its environment (`sudo`, `ssh`, containers) will not have it, and a tmux/screen session attached from a -- 2.51.2 From 3007a4236e30c7e9a91509d2d8b779783e7f5187 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 22 Aug 2026 16:43:49 +0900 Subject: [PATCH 5/8] docs(cli): keep the identity guard from exiting an interactive shell `exit 1` closes the pane when the snippet is pasted into a terminal; `false` still fails the status (and stops a set -e script) without ending the shell. Claude-Session: https://claude.ai/code/session_01YUytym5xEn5FKMhWjSkNkm --- docs/components/cli.md | 2 +- skills/prowl-cli/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/components/cli.md b/docs/components/cli.md index 7ecd4d61..b6aafa60 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -85,7 +85,7 @@ Resolve your own tab and worktree from it: ```bash me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" -test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; exit 1; } +test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; false; } printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' ``` diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index 7a0033da..a5c0670f 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -28,7 +28,7 @@ Use `$PROWL_PANE_ID` as your own selector and as the guard against operating on ```bash me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" -test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; exit 1; } +test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; false; } printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' test "$pane" != "$PROWL_PANE_ID" # before sending anything to $pane ``` -- 2.51.2 From b502fe39b3844bd44857ab31bf1b83c9b204f0bb Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 22 Aug 2026 17:34:41 +0900 Subject: [PATCH 6/8] docs(cli): tighten identity guidance after review Pane UUIDs do not survive an app restart (only restored tab UUIDs do), so say so instead of calling UUIDs restart-stable. Make the self lookup fail closed: dependent commands live in the success branch, the self-guard fails when PROWL_PANE_ID is unset, and pane.cwd may stand in only for a unique match. Note that handoff's caller-pane resolution needs process ancestry (tmux/detached wrappers must pass --pane), that a Debug app needs its own CLI build as well as its own socket, and that error envelopes carry command and schema_version. Claude-Session: https://claude.ai/code/session_01YUytym5xEn5FKMhWjSkNkm --- docs/components/cli.md | 36 ++++++++++++++++++++++++------------ skills/prowl-cli/SKILL.md | 19 +++++++++++-------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/docs/components/cli.md b/docs/components/cli.md index b6aafa60..80a5e317 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -35,7 +35,7 @@ overrides it for both processes). - `--no-color` — disable colored text output (implied by `--json`). Success envelope: `{ "ok": true, "command": "...", "schema_version": "...", "data": {...} }`. -Error envelope: `{ "ok": false, "command": "...", "error": { "code": "...", "message": "..." } }`. +Error envelope: `{ "ok": false, "command": "...", "schema_version": "...", "error": { "code": "...", "message": "..." } }`. Exit code is 0 on success, non-zero on failure. **Parser errors print plain text** (not JSON) even with `--json`, because parsing happens before execution — always check the exit code before piping to `jq`. @@ -63,8 +63,9 @@ Text `list` and `agents` output exposes short, type-prefixed handles such as monotonic, and are never reused after a tab or pane closes. They work in every generic target position (`read p7`, `focus t6`, `send p7 '…'`); bare numbers remain worktree references there. A stale prefixed handle fails rather than falling back to -a same-named worktree. JSON keeps canonical UUIDs in `id`; do not cache handles -across an app restart. +a same-named worktree. JSON keeps canonical UUIDs in `id`. Neither handles nor pane +UUIDs survive an app restart (restored tabs keep their tab UUID; restored panes are new +surfaces with new UUIDs) — re-run `prowl list` instead of caching either. > **Never target by tab title.** Titles are free-form and can lie. For scripts, > resolve a concrete UUID `pane.id` from `prowl list --json`; for an interactive @@ -85,16 +86,21 @@ Resolve your own tab and worktree from it: ```bash me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" -test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; false; } -printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' +if [ -z "$me" ]; then + echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl reached another Prowl instance; stop, do not guess" >&2 +else + printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' +fi ``` The variable is inherited, not verified: a process that scrubbed its environment (`sudo`, `ssh`, containers) will not have it, and a tmux/screen session attached from a different pane reports the pane its server started in. A value that matches no `pane.id` usually means `prowl` reached a different Prowl instance than the one hosting -your pane (see [Transport & app launch](#transport--app-launch)). When it is unset or -matches nothing, fall back to `prowl list --json` and choose by `pane.cwd` — never +your pane (see [Transport & app launch](#transport--app-launch)). Keep every step that +depends on knowing yourself inside the success branch. When it is unset or matches +nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes +usually share one cwd — and may stand in for you only when the match is unique; never assume the *focused* pane is you. Prowl itself never trusts the variable for attribution; commands that need the calling pane (`handoff`) resolve it from the caller's process ancestry. @@ -137,9 +143,9 @@ good for coordination but lags a screen by ~2–3 s and can flip to idle **befor TUI finishes painting — confirm with `read --wait-stable`. Your own pane is `$PROWL_PANE_ID` (see [Identity](#identity-which-pane-am-i)); compare -against it before sending input anywhere: +against it before sending input anywhere — the check fails closed when the id is unset: ```bash -test "$pane" != "$PROWL_PANE_ID" +[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] ``` ### `prowl agents` @@ -369,8 +375,10 @@ prowl handoff save [target] [--brief -|--no-brief] [--note "…"] `--worktree `, or the positional target) wins; otherwise the source is **the calling pane** — Prowl maps the `prowl` process's ancestry to the pane whose shell spawned it, so an agent running the command hands off *itself* -regardless of UI focus. Outside any Prowl pane with no selector the command -errors with `SOURCE_REQUIRED`; the focused pane is never guessed. +regardless of UI focus. Outside any Prowl pane — or when the ancestry does not +reach the pane's shell, as under tmux/screen or a detached wrapper — a call with no +selector errors with `SOURCE_REQUIRED` (pass `--pane "$PROWL_PANE_ID"` once the +identity check matched); the focused pane is never guessed. **Briefing.** `--brief -` reads an inline agent-authored briefing from stdin (heredoc). Every handoff must provide it or use `--no-brief` as the explicit @@ -437,6 +445,10 @@ artifacts and terminal excerpts do not appear in `git status`. `$TMPDIR/prowl-cli.sock`. - If the app isn't running, the CLI launches it (`open -a Prowl`) and waits up to ~15s for the socket — except when `PROWL_CLI_SOCKET` is set. +- A separately launched (Debug) app needs both its own `PROWL_CLI_SOCKET` and the CLI + built with it (`./.build/debug/prowl` from that checkout, or + `Prowl Debug.app/Contents/Resources/prowl-cli/prowl`); the installed `prowl` may + report the same version yet lack newer commands. - Sandboxed agents must be allowed to connect to the Unix socket. If the CLI reports `SOCKET_PERMISSION_DENIED`, allowlist the socket path in the agent sandbox, run `prowl` outside that sandbox, or start both the app and CLI with @@ -477,7 +489,7 @@ artifacts and terminal excerpts do not appear in `git status`. ```bash pane="$(prowl create tab MyApp --json | jq -r '.data.target.pane.id')" -test "$pane" != "$PROWL_PANE_ID" +[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] prowl send --pane "$pane" 'swift build' --capture --timeout 300 --json prowl read --pane "$pane" --last 100 --wait-stable --json prowl close "$pane" --json diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index a5c0670f..aafd5d7b 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -28,12 +28,15 @@ Use `$PROWL_PANE_ID` as your own selector and as the guard against operating on ```bash me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" -test -n "$me" || { echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl is talking to another Prowl instance" >&2; false; } -printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' -test "$pane" != "$PROWL_PANE_ID" # before sending anything to $pane +if [ -z "$me" ]; then + echo "no pane matches PROWL_PANE_ID=[$PROWL_PANE_ID] — unset, or prowl reached another Prowl instance; stop, do not guess" >&2 +else + printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' +fi +[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] # self-guard before touching $pane; fails closed when the id is unset ``` -The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). If it is unset or matches nothing, pick yourself from `prowl list --json` by `pane.cwd`. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. +The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). Keep everything that depends on knowing yourself inside the success branch (or chained with `&&`). If it is unset or matches nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes usually share one cwd — and may stand in for you only when the match is unique. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. ## Safe Default Workflow @@ -48,7 +51,7 @@ prowl key --pane "$pane" enter --json prowl focus --pane "$pane" --json ``` -Pick targets by `pane.id`, `tab.id`, `worktree.id`/`name`/`path`, and `pane.cwd`. Never trust tab titles: they are free-form and can lag or lie. Text `prowl list` / `prowl agents` also print short handles (`p7`, `t6`) that work in any target position for the life of the app process (`read p7`, `close t6`); UUIDs are the only identity that survives an app restart. +Pick targets by `pane.id`, `tab.id`, `worktree.id`/`name`/`path`, and `pane.cwd`. Never trust tab titles: they are free-form and can lag or lie. Text `prowl list` / `prowl agents` also print short handles (`p7`, `t6`) that work in any target position for the life of the app process (`read p7`, `close t6`). UUIDs are the canonical identity of a *live* pane or tab, not a durable one: after an app restart restored tabs keep their tab UUID but panes are new surfaces with new UUIDs — never cache handles or pane UUIDs across a restart; re-run `prowl list`. For a currently active Codex or Claude Code agent, `prowl agents read p7 --json` returns an immediate semantic snapshot: `.data.agent.status`, `.data.blocker.text` when blocked, and `.data.result` — a result is trustworthy only when `.data.result.state == "complete"`. @@ -101,7 +104,7 @@ prowl close --tab "$tab" --force --json # --force skips the GUI confirmation f ## Parsing JSON Output -Every `--json` response is `{ "ok", "command", "schema_version", "data": {...} }`; failures are `{ "ok": false, "error": { "code", "message" } }`. Parser errors (bad flags) print plain text even with `--json`, so check the exit code before piping into `jq`. When JSON sits in a shell variable, use `printf '%s\n' "$json" | jq …` — zsh `echo` can turn `\u001B` escapes back into control characters. Pass shell values into `jq` with `--arg`. +Every `--json` response is `{ "ok", "command", "schema_version", "data": {...} }`; failures are `{ "ok": false, "command", "schema_version", "error": { "code", "message" } }`. Parser errors (bad flags) print plain text even with `--json`, so check the exit code before piping into `jq`. When JSON sits in a shell variable, use `printf '%s\n' "$json" | jq …` — zsh `echo` can turn `\u001B` escapes back into control characters. Pass shell values into `jq` with `--arg`. Key fields by command: @@ -144,7 +147,7 @@ done - `open /path` may refocus an existing pane; it is not a create command. - Focus is not stable and is not you: `open` and `focus` change it, and the user clicks around. - `send --capture` captures a screen diff; multiline input may include command echo. -- The CLI talks to one socket owner. With two Prowl instances running, the default `prowl` reaches whichever owns the standard socket; a manually launched dev instance and every CLI call must share the same `PROWL_CLI_SOCKET=/tmp/name.sock`. Sandboxed agents must be allowed to connect to that Unix socket. +- The CLI talks to one socket owner. With two Prowl instances running, the default `prowl` reaches whichever owns the standard socket; a manually launched dev instance and every CLI call must share the same `PROWL_CLI_SOCKET=/tmp/name.sock` *and* the CLI built with that app (`./.build/debug/prowl` from the same checkout, or `Prowl Debug.app/Contents/Resources/prowl-cli/prowl`) — the version string does not reveal a mismatch, a missing command does. Sandboxed agents must be allowed to connect to that Unix socket. - A newer CLI talking to an older app can fail at transport level (`TRANSPORT_FAILED`) — confirm the running app was built with the command. ## Error Handling @@ -160,7 +163,7 @@ done ## Handing Off Your Task -`prowl handoff to --brief -` hands your task to another agent. Run it from your own pane (the calling pane is the source — no selector needed) and pipe your briefing on stdin: +`prowl handoff to --brief -` hands your task to another agent. Run it from your own pane (the calling pane is the source — no selector needed) and pipe your briefing on stdin. Prowl finds the calling pane through process ancestry, so a direct shell or agent child works; under tmux/screen or a detached wrapper that resolution fails with `SOURCE_REQUIRED` — then pass `--pane "$PROWL_PANE_ID"` explicitly, after the identity check above matched. ```bash prowl handoff to codex --brief - <<'EOF' -- 2.51.2 From ead8c08b54240fd603bccda50f6dc1bd7762aa2e Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 22 Aug 2026 17:49:47 +0900 Subject: [PATCH 7/8] docs(cli): gate actions on the self-check and distrust PROWL_PANE_ID under tmux A bare predicate line does not stop an interactive shell, so the examples now put every dependent command in the else branch. A pane id that appears in prowl list proves the pane exists, not that the caller runs in it: under tmux/screen or a detached wrapper the inherited value names the pane the server started in, so the guidance says to identify the pane by other means there instead of passing PROWL_PANE_ID. Claude-Session: https://claude.ai/code/session_01YUytym5xEn5FKMhWjSkNkm --- docs/components/cli.md | 32 ++++++++++++++++++++------------ skills/prowl-cli/SKILL.md | 15 ++++++++++++--- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/components/cli.md b/docs/components/cli.md index 80a5e317..319baabe 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -97,9 +97,13 @@ The variable is inherited, not verified: a process that scrubbed its environment (`sudo`, `ssh`, containers) will not have it, and a tmux/screen session attached from a different pane reports the pane its server started in. A value that matches no `pane.id` usually means `prowl` reached a different Prowl instance than the one hosting -your pane (see [Transport & app launch](#transport--app-launch)). Keep every step that -depends on knowing yourself inside the success branch. When it is unset or matches -nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes +your pane (see [Transport & app launch](#transport--app-launch)). A match proves the pane +exists, not that you run in it: trust the value only when your process is a direct child +of the pane's shell — under tmux/screen or a detached wrapper it names the pane the +server started in, so identify your pane by other means (`prowl agents --json` for the +pane hosting your agent session, a unique `pane.cwd`) and pass it explicitly. Keep every +step that depends on knowing yourself inside the success branch. When it is unset or +matches nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes usually share one cwd — and may stand in for you only when the match is unique; never assume the *focused* pane is you. Prowl itself never trusts the variable for attribution; commands that need the calling pane (`handoff`) resolve it from the @@ -142,10 +146,10 @@ Claude running a background **workflow**); otherwise **idle**. See the good for coordination but lags a screen by ~2–3 s and can flip to idle **before** a TUI finishes painting — confirm with `read --wait-stable`. -Your own pane is `$PROWL_PANE_ID` (see [Identity](#identity-which-pane-am-i)); compare -against it before sending input anywhere — the check fails closed when the id is unset: +Your own pane is `$PROWL_PANE_ID` (see [Identity](#identity-which-pane-am-i)); gate +every action on a target behind the check, which fails closed when the id is unset: ```bash -[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] +[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] && prowl send --pane "$pane" '…' --json ``` ### `prowl agents` @@ -377,8 +381,9 @@ prowl handoff save [target] [--brief -|--no-brief] [--note "…"] whose shell spawned it, so an agent running the command hands off *itself* regardless of UI focus. Outside any Prowl pane — or when the ancestry does not reach the pane's shell, as under tmux/screen or a detached wrapper — a call with no -selector errors with `SOURCE_REQUIRED` (pass `--pane "$PROWL_PANE_ID"` once the -identity check matched); the focused pane is never guessed. +selector errors with `SOURCE_REQUIRED`; in those same setups `$PROWL_PANE_ID` is not a +trustworthy stand-in (it names the pane the server started in), so determine the pane +by other means and pass it with `--pane` explicitly. The focused pane is never guessed. **Briefing.** `--brief -` reads an inline agent-authored briefing from stdin (heredoc). Every handoff must provide it or use `--no-brief` as the explicit @@ -489,10 +494,13 @@ artifacts and terminal excerpts do not appear in `git status`. ```bash pane="$(prowl create tab MyApp --json | jq -r '.data.target.pane.id')" -[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] -prowl send --pane "$pane" 'swift build' --capture --timeout 300 --json -prowl read --pane "$pane" --last 100 --wait-stable --json -prowl close "$pane" --json +if [ -z "$PROWL_PANE_ID" ] || [ "$pane" = "$PROWL_PANE_ID" ]; then + echo "refusing: no verified pane of my own, or \$pane is me" >&2 +else + prowl send --pane "$pane" 'swift build' --capture --timeout 300 --json + prowl read --pane "$pane" --last 100 --wait-stable --json + prowl close "$pane" --json +fi ``` ## Gotchas for agents (quick list) diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index aafd5d7b..d3a18a65 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -33,10 +33,19 @@ if [ -z "$me" ]; then else printf '%s\n' "$me" | jq -r '.tab.id, .worktree.id, .worktree.name, .worktree.path' fi -[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] # self-guard before touching $pane; fails closed when the id is unset ``` -The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). Keep everything that depends on knowing yourself inside the success branch (or chained with `&&`). If it is unset or matches nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes usually share one cwd — and may stand in for you only when the match is unique. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. +Gate every action on a target behind the same check — a bare predicate line does not stop an interactive shell: + +```bash +if [ -z "$PROWL_PANE_ID" ] || [ "$pane" = "$PROWL_PANE_ID" ]; then + echo "refusing: no verified pane of my own, or \$pane is me" >&2 +else + prowl send --pane "$pane" 'git status --short' --capture --timeout 30 --json +fi +``` + +The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). A match only proves that pane exists, not that you are running in it: trust the value only when your process is a direct child of the pane's shell (no tmux/screen server or detached wrapper in between); under tmux/screen or a detached wrapper, identify your pane by other means — `prowl agents --json` for the pane hosting your own agent session, or a unique `pane.cwd` — and pass it explicitly. If it is unset or matches nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes usually share one cwd — and may stand in for you only when the match is unique. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. ## Safe Default Workflow @@ -163,7 +172,7 @@ done ## Handing Off Your Task -`prowl handoff to --brief -` hands your task to another agent. Run it from your own pane (the calling pane is the source — no selector needed) and pipe your briefing on stdin. Prowl finds the calling pane through process ancestry, so a direct shell or agent child works; under tmux/screen or a detached wrapper that resolution fails with `SOURCE_REQUIRED` — then pass `--pane "$PROWL_PANE_ID"` explicitly, after the identity check above matched. +`prowl handoff to --brief -` hands your task to another agent. Run it from your own pane (the calling pane is the source — no selector needed) and pipe your briefing on stdin. Prowl finds the calling pane through process ancestry, so a direct shell or agent child works; under tmux/screen or a detached wrapper that resolution fails with `SOURCE_REQUIRED`, and in exactly those setups `$PROWL_PANE_ID` is not trustworthy either (it names the pane the tmux server started in, which may still exist) — identify your pane by other means (`prowl agents --json`, a unique `pane.cwd`) and pass it with `--pane` explicitly. ```bash prowl handoff to codex --brief - <<'EOF' -- 2.51.2 From cfa16d56ca9eeacb01f09d1eba95fc5e3b9bb3e1 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 22 Aug 2026 18:05:20 +0900 Subject: [PATCH 8/8] docs(cli): gate actions on the identity lookup result, not the bare variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stale PROWL_PANE_ID that names a pane in another instance is non-empty and differs from the target, so a guard on the variable alone still ran the side effect. The action guards now require the lookup result (me) and the complete loop computes it first. Provenance wording says the process ancestry must reach the pane's shell — agents and their tool shells are descendants, not immediate children. Claude-Session: https://claude.ai/code/session_01YUytym5xEn5FKMhWjSkNkm --- docs/components/cli.md | 14 ++++++++------ skills/prowl-cli/SKILL.md | 10 +++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/components/cli.md b/docs/components/cli.md index 319baabe..5f33a795 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -98,8 +98,8 @@ The variable is inherited, not verified: a process that scrubbed its environment different pane reports the pane its server started in. A value that matches no `pane.id` usually means `prowl` reached a different Prowl instance than the one hosting your pane (see [Transport & app launch](#transport--app-launch)). A match proves the pane -exists, not that you run in it: trust the value only when your process is a direct child -of the pane's shell — under tmux/screen or a detached wrapper it names the pane the +exists, not that you run in it: trust the value only when your process ancestry reaches +the pane's shell — under tmux/screen or a detached wrapper it names the pane the server started in, so identify your pane by other means (`prowl agents --json` for the pane hosting your agent session, a unique `pane.cwd`) and pass it explicitly. Keep every step that depends on knowing yourself inside the success branch. When it is unset or @@ -147,9 +147,10 @@ good for coordination but lags a screen by ~2–3 s and can flip to idle **befor TUI finishes painting — confirm with `read --wait-stable`. Your own pane is `$PROWL_PANE_ID` (see [Identity](#identity-which-pane-am-i)); gate -every action on a target behind the check, which fails closed when the id is unset: +every action on a target behind the identity lookup result `me` from that section — +not the bare variable, which could be stale — so the check fails closed: ```bash -[ -n "$PROWL_PANE_ID" ] && [ "$pane" != "$PROWL_PANE_ID" ] && prowl send --pane "$pane" '…' --json +[ -n "$me" ] && [ "$pane" != "$PROWL_PANE_ID" ] && prowl send --pane "$pane" '…' --json ``` ### `prowl agents` @@ -493,9 +494,10 @@ artifacts and terminal excerpts do not appear in `git status`. ## A complete loop (run, read, clean up) ```bash +me="$(prowl list --json | jq -c --arg p "$PROWL_PANE_ID" '.data.items[] | select(.pane.id == $p)')" pane="$(prowl create tab MyApp --json | jq -r '.data.target.pane.id')" -if [ -z "$PROWL_PANE_ID" ] || [ "$pane" = "$PROWL_PANE_ID" ]; then - echo "refusing: no verified pane of my own, or \$pane is me" >&2 +if [ -z "$me" ] || [ "$pane" = "$PROWL_PANE_ID" ]; then + echo "refusing: self identity is unverified, or \$pane is me" >&2 else prowl send --pane "$pane" 'swift build' --capture --timeout 300 --json prowl read --pane "$pane" --last 100 --wait-stable --json diff --git a/skills/prowl-cli/SKILL.md b/skills/prowl-cli/SKILL.md index d3a18a65..5a100025 100644 --- a/skills/prowl-cli/SKILL.md +++ b/skills/prowl-cli/SKILL.md @@ -35,17 +35,17 @@ else fi ``` -Gate every action on a target behind the same check — a bare predicate line does not stop an interactive shell: +Gate every action on a target behind that lookup result (`$me`), never behind the bare variable — a stale id that points at another instance would otherwise pass — and keep the dependent commands inside the branch, because a bare predicate line does not stop an interactive shell: ```bash -if [ -z "$PROWL_PANE_ID" ] || [ "$pane" = "$PROWL_PANE_ID" ]; then - echo "refusing: no verified pane of my own, or \$pane is me" >&2 +if [ -z "$me" ] || [ "$pane" = "$PROWL_PANE_ID" ]; then + echo "refusing: self identity is unverified, or \$pane is me" >&2 else prowl send --pane "$pane" 'git status --short' --capture --timeout 30 --json fi ``` -The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). A match only proves that pane exists, not that you are running in it: trust the value only when your process is a direct child of the pane's shell (no tmux/screen server or detached wrapper in between); under tmux/screen or a detached wrapper, identify your pane by other means — `prowl agents --json` for the pane hosting your own agent session, or a unique `pane.cwd` — and pass it explicitly. If it is unset or matches nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes usually share one cwd — and may stand in for you only when the match is unique. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. +The variable is inherited, not verified: it is missing after `sudo`/`ssh`/containers and can name the wrong pane inside a tmux/screen session attached from elsewhere. A set value that matches no `pane.id` usually means `prowl` is talking to a different Prowl instance than the one hosting your pane (two apps running; see `PROWL_CLI_SOCKET` under Pitfalls). A match only proves that pane exists, not that you are running in it: trust the value only when your process ancestry reaches the pane's shell (no tmux/screen server or detached wrapper in between); under tmux/screen or a detached wrapper, identify your pane by other means — `prowl agents --json` for the pane hosting your own agent session, or a unique `pane.cwd` — and pass it explicitly. If it is unset or matches nothing, stop rather than guess: `pane.cwd` only narrows the candidates — several panes usually share one cwd — and may stand in for you only when the match is unique. Never assume the focused pane is you — `open` and `focus` move focus, and the user may be looking anywhere. ## Safe Default Workflow @@ -172,7 +172,7 @@ done ## Handing Off Your Task -`prowl handoff to --brief -` hands your task to another agent. Run it from your own pane (the calling pane is the source — no selector needed) and pipe your briefing on stdin. Prowl finds the calling pane through process ancestry, so a direct shell or agent child works; under tmux/screen or a detached wrapper that resolution fails with `SOURCE_REQUIRED`, and in exactly those setups `$PROWL_PANE_ID` is not trustworthy either (it names the pane the tmux server started in, which may still exist) — identify your pane by other means (`prowl agents --json`, a unique `pane.cwd`) and pass it with `--pane` explicitly. +`prowl handoff to --brief -` hands your task to another agent. Run it from your own pane (the calling pane is the source — no selector needed) and pipe your briefing on stdin. Prowl finds the calling pane through process ancestry, so any descendant of the pane's shell (an agent, its tool shell) works; under tmux/screen or a detached wrapper that resolution fails with `SOURCE_REQUIRED`, and in exactly those setups `$PROWL_PANE_ID` is not trustworthy either (it names the pane the tmux server started in, which may still exist) — identify your pane by other means (`prowl agents --json`, a unique `pane.cwd`) and pass it with `--pane` explicitly. ```bash prowl handoff to codex --brief - <<'EOF'