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:
+81
@@ -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 "$@" ;;
|
||||
|
||||
Executable
+195
@@ -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[@]}"
|
||||
Executable
+85
@@ -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"
|
||||
Executable
+67
@@ -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
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user