No error is a dead end: crash, click, and your agent is already looking

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 12:50:09 -04:00
parent ada0faf1d1
commit cc7d91d09c
43 changed files with 4648 additions and 327 deletions
+81
View File
@@ -9,6 +9,7 @@
# sync Review, commit & push local changes to this repo
# edit Open the Panama repo in Neovim
# doctor Report what is actually running on this machine
# diagnose Hand this machine's health and recent errors to your agent
# test Run every contract under tests/ (--safe skips the hijacking ones)
# contracts Name the contracts that mention a given file
# upgrade Re-run the installer from anywhere, interview included
@@ -81,6 +82,9 @@ ${BOLD}Commands:${RESET}
${GREEN}edit${RESET} Open the Panama repo in Neovim.
${GREEN}doctor${RESET} Report what is actually running on this machine, rather
than what was installed. Takes --summary for one line per check.
${GREEN}diagnose${RESET} Hand the health summary, the recent journal errors and
whatever you say is wrong to your coding agent, in a terminal.
Needs an agent chosen on Settings System Agents.
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
a subset: 'panama test dock' runs the ones matching 'dock'.
--safe skips the ones that take over the live desktop; what
@@ -111,6 +115,8 @@ ${BOLD}Examples:${RESET}
$PROGRAM sync
$PROGRAM edit
$PROGRAM doctor --summary
$PROGRAM diagnose
$PROGRAM diagnose the bar disappears after unplugging the monitor
$PROGRAM test dock
$PROGRAM test --safe
$PROGRAM contracts config/dot/quickshell/services/Displays.qml
@@ -348,6 +354,80 @@ cmd_doctor() {
exec "$doctor" "$@"
}
# ----------------------------------------------------------------------------
# Command: diagnose
# ----------------------------------------------------------------------------
#
# The by-hand rung of the escalation ladder. Every other rung starts from an
# event -- a crash, a failed reload, a red check -- and this one starts from a
# person who can tell that something is wrong but not what.
#
# It gathers the two things anybody would be asked for first anyway (what the
# health check says, what the journal has been complaining about) and whatever
# words follow the command, then hands the lot to the configured agent. The free
# text is the valuable part: "the bar disappears after unplugging the monitor"
# is a symptom no collector reports, and it is the difference between an agent
# reading a health summary and an agent looking for something.
cmd_diagnose() {
local launcher="$PANAMA_DIR/bin/panama-agent"
if [[ ! -x "$launcher" ]]; then
err "The agent launcher is missing from $launcher"
exit 1
fi
local complaint="$*"
local health="(the health check did not run)"
local doctor="$PANAMA_DIR/config/dot/quickshell/scripts/panama-doctor"
if [[ -x "$doctor" ]]; then
health="$("$doctor" --summary 2>&1)" || true
fi
# Bounded twice, and not out of tidiness. journalctl counts entries, not
# lines, and thirty entries on this machine came to 2,430 lines and a quarter
# of a megabyte -- one multi-line traceback each. The prompt leaves as a
# single argv element, which the kernel caps at 128KB, so an unbounded excerpt
# turns this command into "Argument list too long" rather than a diagnosis.
local errors="(nothing at error level in this boot's user journal)"
if command -v journalctl >/dev/null 2>&1; then
local recent
recent="$(journalctl --user -b -p err -n 30 --no-pager --output=short 2>/dev/null \
| cut -c 1-300 | tail -80)" || true
[[ -n "${recent// }" ]] && errors="$recent"
fi
local complaint_section="Nothing in particular was reported; this was run to look around."
[[ -n "${complaint// }" ]] && complaint_section="$complaint"
local prompt
prompt="$(cat <<PROMPT
Something is wrong with this Panama machine and I would like to know what.
What I noticed:
$complaint_section
What panama doctor --summary says:
$health
The last error-level lines in this boot's user journal:
$errors
Panama is checked out at $PANAMA_DIR and every dotfile in ~/.config is a symlink
into it, so anything you find is a tracked file here rather than a copy. Start
by reading: work out what is actually broken and say so before changing
anything. If a check is red, 'panama doctor' with no arguments has the long form
of it. Root work goes through panama-sudo, which shows me your reason.
PROMPT
)"
# exec: from here on the agent's terminal is the process, and this shell has
# nothing left to do that the agent is not doing better.
exec "$launcher" --prompt "$prompt"
}
# ----------------------------------------------------------------------------
# The desktop-hijacking ledger
# ----------------------------------------------------------------------------
@@ -805,6 +885,7 @@ main() {
sync) shift; cmd_sync "$@" ;;
edit) shift; cmd_edit "$@" ;;
doctor) shift; cmd_doctor "$@" ;;
diagnose) shift; cmd_diagnose "$@" ;;
test) shift; cmd_test "$@" ;;
contracts) shift; cmd_contracts "$@" ;;
upgrade) shift; cmd_upgrade "$@" ;;
+195
View File
@@ -0,0 +1,195 @@
#!/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[@]}"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# "Something crashed" -> an agent already reading the core dump.
#
# Reached by clicking the crash notification panama-crash-watch sends, or run by
# hand against any PID in `coredumpctl list`. It gathers the four facts
# systemd-coredump recorded and points at the skill that says what to do with
# them; the method lives in the skill so it is edited in one place and works
# whichever agent is configured.
#
# panama-agent-crash <pid> [comm] [exe] [signal]
#
# The skill is named AND given as an absolute path. A harness with a skill
# mechanism follows the name; one without still has a file to read. That is the
# whole reason this ladder works for more than one agent.
#
# ---------------------------------------------------------------------------
# Adapted from Omarchy's bin/omarchy-agent-crash
# (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)}"
pid="${1:-}"
if [[ ! "$pid" =~ ^[0-9]+$ ]]; then
printf 'Not a PID: %s\n' "${pid:-<missing>}" >&2
printf 'Usage: panama-agent-crash <pid> [comm] [exe] [signal] (see: coredumpctl list)\n' >&2
exit 1
fi
comm="${2:-unknown}"
exe="${3:-unknown}"
signal="${4:-unknown}"
skill="$PANAMA_PATH/skills/diagnose-crash/SKILL.md"
# Looked up live so a PID typed by hand still gets a timestamp. A core that has
# already been rotated away costs only the timestamp, so this is allowed to
# fail: the other four facts are enough to start on.
when="$(coredumpctl list "$pid" --no-pager --no-legend 2>/dev/null | tail -1 | cut -d' ' -f1-4)" || true
when="${when:-unknown}"
[[ -n "${when// }" ]] || when="unknown"
prompt="$(
cat <<PROMPT
A process crashed on this Panama machine and I want to know why.
What systemd-coredump recorded:
process: $comm
PID: $pid
binary: $exe
signal: $signal
time: $when
Use the diagnose-crash skill. It covers how to investigate, what to rule out
first, and what to report. If your harness has no skill mechanism, read the
skill file directly and follow it instead:
$skill
PROMPT
)"
exec "$PANAMA_PATH/bin/panama-agent" --prompt "$prompt"
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# "The shell would not reload" -> an agent already holding the error.
#
# Quickshell keeps the old shell running when a reload fails, which is what
# makes this rung possible at all: the desktop that just refused the new code is
# still there to notify you about it, and still there to click. shell.qml's
# onReloadFailed sends that notification; this builds the prompt behind it.
#
# panama-agent-reload "<what Quickshell said>"
#
# The failure string on its own is usually one line naming one file. The journal
# around it is where the rest is -- the QML warnings that preceded the fatal
# one, the property that was already undefined two saves ago -- so both go in.
#
# Environment seams, for the contract:
#
# PANAMA_PATH the repository
# PANAMA_RELOAD_UNIT the unit to read (default panama-quickshell.service)
set -euo pipefail
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
UNIT="${PANAMA_RELOAD_UNIT:-panama-quickshell.service}"
summary="${1:-}"
if [[ -z "${summary// }" ]]; then
printf 'Usage: panama-agent-reload "<the reload failure>"\n' >&2
exit 1
fi
# Read a generous window and filter it down, rather than asking journalctl for
# forty lines and hoping they were the relevant ones. A failed reload usually
# arrives after a burst of unrelated shell chatter.
#
# Each line is truncated because the prompt leaves as one argv element and the
# kernel caps that at 128KB; a single Quickshell backtrace can be most of it.
context=""
if command -v journalctl >/dev/null 2>&1; then
context="$(journalctl --user -u "$UNIT" -n 400 --no-pager --output=cat 2>/dev/null \
| grep -iE 'quickshell|\.qml|qml:|panama' \
| tail -40 \
| cut -c 1-300)" || true
fi
[[ -n "${context// }" ]] || context="(nothing in the journal for $UNIT)"
prompt="$(
cat <<PROMPT
The Panama shell refused to reload on this machine. The old shell is still
running, so the desktop is up, but the change that was just saved is not live.
What Quickshell reported:
$summary
The last relevant lines from $UNIT:
$context
The shell lives in config/dot/quickshell in this repository, symlinked into
~/.config/quickshell -- so the file that failed to parse is a tracked file here,
not a copy. Find what broke the reload and say what it is. Read before you
write: a bad guess saved into this tree is live in the desktop immediately.
PROMPT
)"
exec "$PANAMA_PATH/bin/panama-agent" --prompt "$prompt"
+77 -7
View File
@@ -16,10 +16,18 @@
# few minutes for something the user can do nothing about. The first one is
# news; the fortieth is why people turn notifications off. The health page
# carries the running count for anyone who wants it.
#
# When an agent has been chosen, the notification stops being a dead end. It
# carries the diagnosis command as data in a `panama-exec` hint, which the shell
# runs on click. Command-as-data rather than a libnotify action, because an
# action would tie the click to this process still being alive to hear it, and
# this process is a `journalctl -f` that outlives nothing in particular. The
# hint survives a shell restart and never blocks the watcher.
set -uo pipefail
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
SETTINGS="${PANAMA_AGENT_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}"
# systemd-coredump's MESSAGE_ID. Matching on this rather than on text keeps
# working when the wording changes and never matches a program that merely
@@ -37,6 +45,27 @@ for _ in $(seq 1 60); do
sleep 1
done
# Read per crash rather than once at startup, so choosing an agent in Settings
# takes effect on the next crash instead of on the next login. This service runs
# for the life of the session; nothing restarts it when a preference changes.
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"
}
# What to call the agent in a sentence aimed at a person.
agent_label() {
case "$1" in
claude) printf 'Claude Code' ;;
codex) printf 'Codex' ;;
*) printf '%s' "$1" ;;
esac
}
declare -A reported=()
# -f from now, not from the boot: a session that starts after a crash should
@@ -46,9 +75,15 @@ journalctl --user -f -n 0 --output=json MESSAGE_ID="$COREDUMP_MESSAGE_ID" 2>/dev
| while IFS= read -r line; do
[[ -n "$line" ]] || continue
uid="$(jq -r '.COREDUMP_UID // empty' <<<"$line" 2>/dev/null)"
exe="$(jq -r '.COREDUMP_EXE // empty' <<<"$line" 2>/dev/null)"
comm="$(jq -r '.COREDUMP_COMM // empty' <<<"$line" 2>/dev/null)"
# One jq per entry rather than one per field: the fields are read
# together, and the click payload needs all of them.
IFS=$'\t' read -r uid exe comm pid signal < <(
jq -r '[(.COREDUMP_UID // ""),
(.COREDUMP_EXE // ""),
(.COREDUMP_COMM // ""),
(.COREDUMP_PID // ""),
(.COREDUMP_SIGNAL_NAME // "")] | @tsv' <<<"$line" 2>/dev/null
)
# Another user's crash is not this session's business, and reporting it
# would leak what they are running.
@@ -63,11 +98,46 @@ journalctl --user -f -n 0 --output=json MESSAGE_ID="$COREDUMP_MESSAGE_ID" 2>/dev
else
program="$comm"
fi
# Never announce our own machinery. A crash watcher that notifies about
# the crash watcher, or about the agent it just launched to investigate
# the last crash, is a loop with a toast in it.
[[ "$program" == panama-crash-* || "$program" == panama-agent* ]] && continue
[[ -z "${reported[$program]:-}" ]] || continue
reported[$program]=1
notify-send --icon=dialog-error-symbolic --app-name=Panama \
"$program stopped unexpectedly" \
"It crashed and was not able to recover. System Health has the details." \
2>/dev/null || true
# The toast can only offer a diagnosis if there is something to diagnose
# with. No agent, or the offer switched off, and it stays exactly the
# actionless notification it has always been.
agent="$(read_setting preferredAgent none)"
offer="$(read_setting crashDiagnoseOffer true)"
if [[ -n "$agent" && "$agent" != "none" && "$offer" != "false" && "$pid" =~ ^[0-9]+$ ]]; then
# By absolute path, not by name. The shell runs this hint, and the
# shell is started by systemd -- whose environment does not carry
# the repository's bin directory on PATH, so a bare name would
# click into "command not found".
exec_command="$(printf '%q %q %q %q %q' \
"$PANAMA_PATH/bin/panama-agent-crash" \
"$pid" "${comm:-$program}" "${exe:-unknown}" "${signal:-unknown}")"
# The hint is data, not privilege. Any process on this session bus
# could send one, and running it grants nothing a local process
# could not already do for itself.
# Same urgency as the plain report, deliberately. Making the
# clickable one critical would let a crash break through Do Not
# Disturb, which is a louder desktop than anybody asked for in
# exchange for an offer that keeps until it is read anyway.
notify-send --icon=dialog-error-symbolic --app-name=Panama \
--hint="string:panama-exec:$exec_command" \
"$program stopped unexpectedly" \
"Click to diagnose with $(agent_label "$agent")." \
2>/dev/null || true
else
notify-send --icon=dialog-error-symbolic --app-name=Panama \
"$program stopped unexpectedly" \
"It crashed and was not able to recover. System Health has the details." \
2>/dev/null || true
fi
done
+1
View File
@@ -112,6 +112,7 @@ cmd_run() {
if (( failed )); then
warn "Re-running 'panama migrate' is safe and will retry from the failure."
warn "If it keeps failing, hand it to an agent: panama diagnose"
return 1
fi
ok "This machine now matches the checkout."