#!/usr/bin/env bash # Hand a prompt to whichever coding agent this machine has chosen. # # Every rung of the escalation ladder ends here: a crash toast, a failed shell # reload, a red health check, `panama diagnose`. They gather facts; this decides # which binary runs them and puts it in a terminal you can watch and interrupt. # # Two settings decide everything, and both are read at press time rather than at # start time, so choosing an agent in Settings takes effect on the next crash # without restarting anything: # # preferredAgent none | claude | codex ("none" is the default: silence) # agentAutoApprove true -> the agent starts in its own "don't stop to ask" # mode; false -> its normal prompting mode, untouched. # # "none" exits 0 without a word. It is not an error to have no agent; it is the # shipped state, and a rung that shouted about it would be a rung that gets # turned off. # # panama-agent open the agent on the repo # panama-agent --prompt "text" open it with something to work on # # Environment seams, for the contract and for a second checkout: # # PANAMA_PATH the repository; also the agent's working directory # PANAMA_AGENT_SETTINGS the settings file to read (default: the real one) # # --------------------------------------------------------------------------- # Adapted from Omarchy's bin/omarchy-agent (https://github.com/basecamp/omarchy) # # Copyright (c) David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # --------------------------------------------------------------------------- set -euo pipefail PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}" SETTINGS="${PANAMA_AGENT_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}" # A fixed window class rather than the per-binary default, so one Hyprland rule # can catch every agent window regardless of which agent is chosen. readonly WINDOW_CLASS="panama-agent" # THE INHERITED PATH IS NOT THE USER'S PATH. Every rung except `panama diagnose` # reaches this script from the Quickshell shell, which systemd starts with # neither PANAMA_PATH nor ~/.local/bin -- and ~/.local/bin is where both agents # install themselves. Trusting PATH here meant a perfectly well installed agent # reporting itself as missing, into the stderr of a detached process nobody will # ever read: the whole ladder failing silently, which is the exact failure it # exists to prevent. # # So the binary is resolved rather than named. PATH first, because a user who # put an agent somewhere else meant it; then the XDG user bin directory, which # is where the installers actually put them. resolve_agent() { local name="$1" found found="$(command -v "$name" 2>/dev/null)" && { printf '%s' "$found"; return 0; } [[ -x "$HOME/.local/bin/$name" ]] && { printf '%s' "$HOME/.local/bin/$name"; return 0; } return 1 } # The same repair, for the agent's own sake rather than this script's: an agent # launched from a notification click would otherwise run every shell command it # is asked to with a PATH unlike the one the user gets in a terminal. Applied # just before the spawn rather than here, so resolve_agent above is answering # the question the caller actually asked -- "can this be found from where I was # started" -- instead of one this script has already fixed for itself. repair_path() { case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) PATH="$PATH:$HOME/.local/bin" ;; esac export PATH } usage() { cat <<'EOF' Usage: panama-agent [--prompt "text"] Opens the agent named by preferredAgent in a terminal, in the Panama checkout. With no agent chosen, exits silently: choose one on Settings > System > Agents. EOF } prompt="" while (($#)); do case "$1" in --prompt) prompt="${2:?--prompt needs a value}" shift 2 ;; -h | --help) usage exit 0 ;; *) printf 'panama-agent: unexpected argument: %s\n' "$1" >&2 usage >&2 exit 1 ;; esac done # Same shape as panama-idle's reader: a missing file, a missing key and an # explicit null all mean "the default", because all three describe a machine # that has never been asked the question. read_setting() { local key="$1" fallback="$2" [[ -r "$SETTINGS" ]] || { printf '%s' "$fallback"; return; } command -v jq >/dev/null 2>&1 || { printf '%s' "$fallback"; return; } jq -r --arg k "$key" --arg d "$fallback" \ 'if has($k) and (.[$k] != null) then (.[$k] | tostring) else $d end' \ "$SETTINGS" 2>/dev/null || printf '%s' "$fallback" } agent="$(read_setting preferredAgent none)" # The shipped state. Nothing to launch, nothing to say. [[ -n "$agent" && "$agent" != "none" ]] || exit 0 auto_approve="$(read_setting agentAutoApprove true)" case "$agent" in claude | codex) ;; *) printf 'panama-agent: unsupported preferredAgent: %s\n' "$agent" >&2 printf 'Choose one on Settings > System > Agents.\n' >&2 exit 1 ;; esac # Resolved to a path before argv is built, so kitty is never asked to repeat a # PATH lookup this script has already done more carefully than kitty could. if ! agent_bin="$(resolve_agent "$agent")"; then printf 'panama-agent: %s is not installed.\n' "$agent" >&2 printf 'Looked on PATH and in %s.\n' "$HOME/.local/bin" >&2 printf 'Install it, or choose another agent on Settings > System > Agents.\n' >&2 exit 1 fi # The per-agent launch table. VERIFIED against the installed binaries' --help on # 2026-08-25 (claude 2.1.245, codex-cli 0.149.1) -- these flags are not the same # from release to release, so re-run --help before changing them. # # claude --permission-mode auto "auto" is one of acceptEdits/auto/ # bypassPermissions/manual/dontAsk/plan # codex --approve-for-me routes approvals through automatic review # inside the workspace-write sandbox # # With agentAutoApprove off, no mode flag is passed at all: the agent's own # configured default is a choice the user already made, and overriding it with # an explicit "prompt me" would be this script having an opinion it was told not # to have. declare -a argv=("$agent_bin") case "$agent" in claude) [[ "$auto_approve" == "true" ]] && argv+=(--permission-mode auto) ;; codex) [[ "$auto_approve" == "true" ]] && argv+=(--approve-for-me) ;; esac # One argv element, after the option terminator. Both CLIs take the prompt as a # trailing positional, and `--` is what stops a prompt beginning with a dash -- # or one that happens to read like a subcommand -- from being parsed as flags. [[ -n "$prompt" ]] && argv+=(-- "$prompt") # The checkout, not $HOME: the skills the prompts point at, the repository the # agent is being asked about, and .claude/settings.json's pre-approved read-only # diagnostics all live here. An agent started anywhere else finds none of them. cd "$PANAMA_PATH" repair_path # setsid so the agent outlives whatever spawned it -- a notification handler, a # crash watcher, a terminal that is about to close. exec setsid kitty \ --directory "$PANAMA_PATH" \ --class "$WINDOW_CLASS" \ -e "${argv[@]}"