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
@@ -178,6 +178,31 @@ ShellRoot {
});
}
// A command carried as data in the `panama-exec` hint -- the mechanism
// the escalation ladder rides. Three facts in one pass: a notification
// without the hint carries nothing (which is almost all of them), one
// with it carries exactly what was sent, and the entry goes when the
// notification does rather than outliving it in the model.
function execHint(): string {
root.reset();
const plain = root.notification(20, "org.exec.App.desktop", "Exec App");
Notifs.handleNotification(plain);
const carrying = root.notification(21, "org.exec.App.desktop", "Exec App");
carrying.hints = { "panama-exec": " panama-agent-crash 41283 kitty " };
Notifs.handleNotification(carrying);
const carried = Notifs.execCommand(carrying);
carrying.dismiss();
return JSON.stringify({
plain: Notifs.execCommand(plain),
carried: carried,
afterDismiss: Notifs.execCommand(carrying)
});
}
// The override is one answer shared by the bell, the banner duration,
// the breakthrough gate and the card. Only the duration is observable
// from here, and it is the one that would silently keep the old value.
+465 -104
View File
@@ -1,47 +1,149 @@
#!/usr/bin/env bash
# How much of the Claude subscription is gone, in the bar.
# How much of each agent subscription is gone, in the bar.
#
# This is the only thing in Panama that reads an authentication token, so most
# of what is pinned here is about that rather than about the number:
# The Claude collector is the only thing in Panama that reads an authentication
# token, so most of what is pinned here is about that rather than about the
# number:
#
# 1. The token never reaches argv. A header passed as an argument is
# 1. The token never reaches argv. A credential passed as an argument is
# world-readable in /proc/<pid>/cmdline for as long as the request takes,
# which is the same rule the password and MOK paths already follow.
# 2. The token never reaches the output. The widget has no business seeing a
# credential and neither does anyone reading the state file.
# which is the same rule the password and MOK paths already follow. The
# collector makes its request in-process, so there is no child to leak it
# through -- and this proves no child is spawned.
# 2. The token never reaches the output. Neither the panel nor anyone reading
# the state directory has any business seeing a credential, and neither
# does the collector's own cache.
# 3. It NEVER refreshes the token and never writes to the credentials file.
# That token expires hourly and Claude Code refreshes it on demand; two
# processes rotating one credential means being silently signed out of
# Claude Code by a status widget, and no bar indicator is worth that.
# 4. An expired token is reported as waiting, not as an error, and no request
# is made with it.
# 4. An expired token is reported honestly and no request is made with it.
# 5. The widget hides unless it was asked for AND there are real numbers. An
# indicator reading "unknown" is worse than an empty space.
#
# And, since the collectors became a fan-out over a directory of records:
#
# 6. A collector the user switched off does not run, and its stale record
# does not linger.
# 7. A record is replaced only by a whole, parseable one.
# 8. The replacement is atomic -- mktemp and mv, never a redirect into the
# file a reader is watching.
#
# Hermetic throughout. The usage endpoint is a local HTTP fixture, the codex
# app-server is a stub, and the fan-out runs stub collectors: nothing here
# reaches Anthropic or starts a real agent.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
collector="$repo_dir/config/dot/quickshell/scripts/panama-agent-usage"
scripts="$repo_dir/config/dot/quickshell/scripts"
claude="$scripts/panama-agent-usage-claude"
codex="$scripts/panama-agent-usage-codex"
updater="$scripts/panama-agent-usage-update"
service="$repo_dir/config/dot/quickshell/services/AgentUsage.qml"
widget="$repo_dir/config/dot/quickshell/modules/bar/AgentUsageWidget.qml"
panel="$repo_dir/config/dot/quickshell/modules/bar/AgentUsagePanel.qml"
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
aliases="$repo_dir/config/dot/quickshell/config/Settings.qml"
findings=()
note() { findings+=("$1"); }
[[ -x "$collector" ]] || { printf 'agent usage contract: %s is not executable\n' "$collector" >&2; exit 1; }
for required in "$claude" "$codex" "$updater"; do
[[ -x "$required" ]] || {
printf 'agent usage contract: %s is not executable\n' "$required" >&2
exit 1
}
done
[[ -f "$panel" ]] || { printf 'agent usage contract: missing %s\n' "$panel" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
server_pid=""
cleanup() {
[[ -n "$server_pid" ]] && kill "$server_pid" 2>/dev/null
rm -rf "$work"
}
trap cleanup EXIT
stub="$work/bin"
mkdir -p "$stub"
export XDG_STATE_HOME="$work/state"
output="$XDG_STATE_HOME/panama/agent-usage.json"
mkdir -p "$stub" "$work/claude" "$work/cache" "$work/collectors" "$work/usage"
readonly SECRET='sk-fixture-token-must-never-appear'
# ── The endpoint, served locally ────────────────────────────────────────────
#
# A real request to a real socket, so what the collector puts in the header is
# observable -- and so nothing in this file can accidentally reach Anthropic.
cat >"$work/server.py" <<'PY'
import http.server
import os
import sys
work = sys.argv[1]
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
with open(os.path.join(work, "headers.txt"), "a") as handle:
handle.write((self.headers.get("Authorization") or "") + "\n")
try:
status = int(open(os.path.join(work, "status")).read().strip())
except Exception:
status = 200
body = open(os.path.join(work, "payload.json"), "rb").read()
self.send_response(status)
self.send_header("Content-Type", "application/json")
if status == 429:
self.send_header("Retry-After", "30")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
with open(os.path.join(work, "port"), "w") as handle:
handle.write(str(server.server_address[1]))
server.serve_forever()
PY
printf '200\n' >"$work/status"
: >"$work/headers.txt"
cat >"$work/payload.json" <<'JSON'
{
"five_hour": {"utilization": 42, "resets_at": "2099-08-22T14:00:00Z"},
"seven_day": {"utilization": 71, "resets_at": "2099-08-27T00:00:00Z"},
"rate_limit_tier": "default_claude_max_5x",
"limits": [
{"kind": "weekly_scoped", "percent": 31, "resets_at": "2099-08-27T00:00:00Z",
"scope": {"model": {"display_name": "Fable 5", "id": "claude-fable-5"}}}
]
}
JSON
python3 "$work/server.py" "$work" &
server_pid=$!
for _ in $(seq 1 50); do
[[ -s "$work/port" ]] && break
sleep 0.1
done
[[ -s "$work/port" ]] || { printf 'agent usage contract: the endpoint fixture did not start\n' >&2; exit 1; }
endpoint="http://127.0.0.1:$(cat "$work/port")/usage"
# A recording curl. Nothing should ever invoke it: the collector makes its
# request in-process. If it is called, the argv it was called with is evidence.
cat >"$stub/curl" <<STUB
#!/usr/bin/env bash
printf '%s\n' "\$*" >>"$work/curl-argv"
exit 1
STUB
chmod +x "$stub/curl"
: >"$work/curl-argv"
credentials() {
local expires="$1"
cat >"$work/credentials.json" <<CREDS
@@ -50,128 +152,387 @@ credentials() {
CREDS
}
# Records how it was invoked, including whether the secret was in argv.
cat >"$stub/curl" <<STUB
#!/usr/bin/env bash
printf '%s\n' "\$*" >>"$work/curl-argv"
printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}'
STUB
chmod +x "$stub/curl"
run() {
PATH="$stub:$PATH" PANAMA_AGENT_CREDENTIALS="$work/credentials.json" \
PANAMA_AGENT_USAGE_ENDPOINT="https://example.invalid/usage" \
"$collector" >/dev/null 2>&1
# The collector prints its record; the fan-out is what writes files. Both are
# exercised, so both are seamed.
run_claude() {
PATH="$stub:$PATH" \
CLAUDE_CONFIG_DIR="$work/claude" \
PANAMA_AGENT_CREDENTIALS="$work/credentials.json" \
PANAMA_AGENT_USAGE_ENDPOINT="${FIXTURE_ENDPOINT:-$endpoint}" \
PANAMA_AGENT_USAGE_CACHE="$work/cache" \
"$claude" "$@" 2>"$work/claude-stderr"
}
# ── 4. An expired token waits rather than failing ───────────────────────────
future_ms=$(( ($(date +%s) + 3600) * 1000 ))
# ── 4. An expired token is honest, and makes no request ─────────────────────
: >"$work/curl-argv"
credentials 1000
run
[[ "$(jq -r .status "$output")" == "stale" ]] \
|| note "an expired token did not report as stale (got: $(jq -r .status "$output"))"
[[ -s "$work/curl-argv" ]] \
record="$(run_claude --force)"
[[ "$(jq -r .usageStatusText <<<"$record")" == "Sign-in expired" ]] \
|| note "an expired token did not report as expired (got: $(jq -r .usageStatusText <<<"$record"))"
[[ -s "$work/headers.txt" ]] \
&& note 'a request was made with an expired token'
# ── 1 & 2. The token stays out of argv and out of the output ────────────────
# A credential file that is not there at all is a different sentence.
mv "$work/credentials.json" "$work/credentials.away"
record="$(run_claude --force)"
[[ "$(jq -r .usageStatusText <<<"$record")" == "Waiting for auth" ]] \
|| note "a missing sign-in did not report as waiting (got: $(jq -r .usageStatusText <<<"$record"))"
[[ -s "$work/headers.txt" ]] \
&& note 'a request was made with no token at all'
mv "$work/credentials.away" "$work/credentials.json"
: >"$work/curl-argv"
credentials "$(( ($(date +%s) + 3600) * 1000 ))"
run
[[ "$(jq -r .status "$output")" == "ok" ]] \
|| note "a valid token did not produce a reading (got: $(jq -r .detail "$output"))"
grep -qF "$SECRET" "$work/curl-argv" \
&& note 'the token was passed to curl as an argument, where /proc makes it world-readable'
grep -q -- '--config' "$work/curl-argv" \
|| note 'the token is not passed through a curl config file, so it may reach argv'
grep -qrF "$SECRET" "$XDG_STATE_HOME" \
&& note 'the token appears in the state file the widget reads'
# ── 3. Credentials are never written ────────────────────────────────────────
# ── 1, 2 & 3. What happens to the token ─────────────────────────────────────
credentials "$future_ms"
before="$(sha256sum "$work/credentials.json" | cut -d' ' -f1)"
run
record="$(run_claude --force)"
[[ "$(jq -r .ready <<<"$record")" == "true" ]] \
|| note "a valid token did not produce a reading (status: $(jq -r .usageStatusText <<<"$record"))"
grep -qF "Bearer $SECRET" "$work/headers.txt" \
|| note 'the token did not reach the Authorization header, so the request was not authenticated'
[[ -s "$work/curl-argv" ]] \
&& note 'the collector shelled out to curl, where /proc makes an argument world-readable'
grep -qF "$SECRET" <<<"$record" \
&& note 'the token appears in the record the panel reads'
grep -qrF "$SECRET" "$work/cache" \
&& note "the token appears in the collector's own cache"
grep -qF "$SECRET" "$work/claude-stderr" \
&& note 'the token appears on stderr, where the journal keeps it'
[[ "$(sha256sum "$work/credentials.json" | cut -d' ' -f1)" == "$before" ]] \
|| note 'the collector modified the credentials file'
# Comments stripped: the header explains at length that it does not refresh,
# and matching that is matching documentation.
uncommented() { grep -v '^[[:space:]]*#' "$1"; }
uncommented "$collector" | grep -qE 'refreshToken|refresh_token|grant_type' \
uncommented() { python3 - "$1" <<'PY'
import re, sys
text = open(sys.argv[1], encoding="utf-8").read()
# Module docstring first, then whole-line comments.
text = re.sub(r'^\s*(?:"""|\'\'\')(?:.|\n)*?(?:"""|\'\'\')', "", text, count=1)
print("\n".join(line for line in text.split("\n") if not line.lstrip().startswith("#")))
PY
}
uncommented "$claude" | grep -qE 'refreshToken|refresh_token|grant_type' \
&& note 'the collector touches the refresh token, which can sign Claude Code out'
uncommented "$collector" | grep -qE '>[[:space:]]*"?\$?\{?CREDENTIALS' \
uncommented "$claude" | grep -qE 'credentials.*\.write_text|open\([^)]*credentials[^)]*"w"' \
&& note 'the collector writes to the credentials file'
# ── The reading it produced ─────────────────────────────────────────────────
# The endpoint already reports percentages. Treating them as 0..1 fractions and
# multiplying is how the bar came to read 1500%.
[[ "$(jq -r '.usage.fiveHour.used' "$output")" == "42" ]] \
|| note "the five-hour reading is $(jq -r '.usage.fiveHour.used' "$output") for a reported 42%"
[[ "$(jq -r '.usage.week.used' "$output")" == "71" ]] \
|| note "the weekly reading is $(jq -r '.usage.week.used' "$output") for a reported 71%"
# Nothing the widget shows may fall outside the range a percentage has, whatever
# the endpoint says. A readout that can print 1500% is one you learn to ignore.
cat >"$stub/curl" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' '{"five_hour":{"utilization":1500},"seven_day":{"utilization":-4}}'
STUB
chmod +x "$stub/curl"
run
[[ "$(jq -r '.usage.fiveHour.used' "$output")" == "100" ]] \
|| note 'an out-of-range reading was not clamped to 100'
[[ "$(jq -r '.usage.week.used' "$output")" == "0" ]] \
|| note 'a negative reading was not clamped to 0'
cat >"$stub/curl" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}'
STUB
chmod +x "$stub/curl"
# ── It answers a click ──────────────────────────────────────────────────────
#
# Pill's MouseArea is gated on `interactive`, so a widget that sets it false and
# connects onSecondaryActivated has a handler nothing can ever reach.
uncommented "$widget" | grep -q 'interactive: false' \
&& note 'the widget disables Pill''s mouse area, so its click handlers never fire'
uncommented "$widget" | grep -q 'onActivated' \
|| note 'left-clicking the widget does nothing'
# The endpoint reports percentages already. Treating them as 0..1 fractions and
# multiplying is how the bar once came to read 1500%.
# An endpoint that answers with something else must degrade, not crash.
cat >"$stub/curl" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' '{"error":{"message":"nope"}}'
# Whole percents: jq prints 1 and 1.0 for the same number, and a contract that
# fails on the spelling of a float teaches nothing.
percent_for() { jq -r --arg label "$1" '.limits[] | select(.label == $label) | .percent * 100 | round' <<<"$record"; }
[[ "$(percent_for 'Session (5-hour)')" == "42" ]] \
|| note "the five-hour reading is $(percent_for 'Session (5-hour)')% for a reported 42%"
[[ "$(percent_for 'Weekly (7-day)')" == "71" ]] \
|| note "the weekly reading is $(percent_for 'Weekly (7-day)')% for a reported 71%"
# The model-scoped window lives only in the `limits` array. A collector reading
# the flat buckets alone drops a limit the account is really spending against.
[[ "$(percent_for 'Fable 5 Weekly')" == "31" ]] \
|| note 'the model-scoped limit from the limits array was not read'
[[ "$(jq -r .tierLabel <<<"$record")" == "Max 5x" ]] \
|| note "the plan label is $(jq -r .tierLabel <<<"$record"), not the Max 5x the credential names"
# Nothing the panel shows may fall outside the range a percentage has, whatever
# the endpoint says.
cat >"$work/payload.json" <<'JSON'
{"five_hour": {"utilization": 1500}, "seven_day": {"utilization": -4}}
JSON
rm -f "$work/cache/claude-limits.json"
record="$(run_claude --force)"
[[ "$(percent_for 'Session (5-hour)')" == "100" ]] \
|| note "an out-of-range reading was not clamped (got: $(percent_for 'Session (5-hour)'))"
[[ -z "$(percent_for 'Weekly (7-day)')" ]] \
|| note 'a negative reading was reported as a percentage rather than dropped'
# A payload that speaks the older fraction convention is read as fractions.
cat >"$work/payload.json" <<'JSON'
{"five_hour": {"utilization": 0.42}, "seven_day": {"utilization": 0.71}}
JSON
rm -f "$work/cache/claude-limits.json"
record="$(run_claude --force)"
[[ "$(percent_for 'Session (5-hour)')" == "42" ]] \
|| note "a fraction-scaled payload was misread as $(percent_for 'Session (5-hour)')%"
# ── A cached limit outlives its probe, but not its window ───────────────────
#
# Once a window has reset, a cached figure describes a period that is over. A
# stale 78% on a fresh week is a lie the panel must not tell.
dead_endpoint="http://127.0.0.1:1/usage"
cat >"$work/cache/claude-limits.json" <<'JSON'
{"fetchedAtMs":0,"limits":[{"label":"Weekly (7-day)","percent":0.78,"resetsAt":"2099-01-01T00:00:00+00:00"}]}
JSON
record="$(FIXTURE_ENDPOINT="$dead_endpoint" run_claude --force)"
[[ "$(percent_for 'Weekly (7-day)')" == "78" ]] \
|| note 'a cached limit whose window is still open was thrown away'
cat >"$work/cache/claude-limits.json" <<'JSON'
{"fetchedAtMs":0,"limits":[{"label":"Weekly (7-day)","percent":0.78,"resetsAt":"2000-01-01T00:00:00+00:00"}]}
JSON
record="$(FIXTURE_ENDPOINT="$dead_endpoint" run_claude --force)"
[[ -z "$(percent_for 'Weekly (7-day)')" ]] \
&& [[ "$(jq -r .usageStatusText <<<"$record")" == "Claude limits unavailable" ]] \
|| note 'a cached limit was reported after its window had already reset'
# ── A transport failure is not an answer ────────────────────────────────────
#
# Nothing reached a server: retry sooner than the interval. An HTTP status IS a
# server, and pestering it is how a rate limit becomes a ban.
rm -f "$work/cache/claude-limits.json"
record="$(FIXTURE_ENDPOINT="$dead_endpoint" run_claude --force)"
[[ "$(jq -r '.retryAdvised // false' <<<"$record")" == "true" ]] \
|| note 'a transport failure did not ask the shell to retry sooner'
printf '500\n' >"$work/status"
record="$(run_claude --force)"
[[ "$(jq -r '.retryAdvised // false' <<<"$record")" == "false" ]] \
|| note 'an HTTP error asked for a fast retry, which turns a bad status into a hammer'
printf '429\n' >"$work/status"
record="$(run_claude --force)"
grep -q 'retry after 30s' <<<"$(jq -r .authHelpText <<<"$record")" \
|| note "a 429 did not carry its retry-after into the help text (got: $(jq -r .authHelpText <<<"$record"))"
printf '200\n' >"$work/status"
# ── The Codex collector asks read-only, and reads the right field ───────────
cat >"$stub/codex" <<'STUB'
#!/usr/bin/env python3
import json
import os
import sys
with open(os.environ["CODEX_STUB_ARGV"], "w") as handle:
handle.write(" ".join(sys.argv[1:]))
RESULTS = {
"initialize": {"userAgent": "stub"},
"account/read": {"account": {"type": "chatgpt", "planType": "prolite"}},
"account/rateLimits/read": {
"rateLimits": {
"planType": "prolite",
"primary": {"usedPercent": 55, "windowDurationMins": 10080, "resetsAt": 4102444800},
"secondary": None,
},
"rateLimitsByLimitId": {
"codex": {"limitName": None, "primary": {"usedPercent": 55, "windowDurationMins": 10080}},
"codex_spark": {
"limitName": "Spark",
"primary": {"usedPercent": 12, "windowDurationMins": 300, "resetsAt": 4102444800},
"secondary": None,
},
},
},
}
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
message = json.loads(line)
except Exception:
continue
if "id" not in message:
continue
print(json.dumps({"id": message["id"], "result": RESULTS.get(message.get("method"), {})}), flush=True)
STUB
chmod +x "$stub/curl"
run
[[ "$(jq -r .status "$output")" == "unavailable" ]] \
|| note 'an error response was not reported as unavailable'
chmod +x "$stub/codex"
# ── 5. The widget hides itself ──────────────────────────────────────────────
mkdir -p "$work/codex/sessions/2026/08/25"
cat >"$work/codex/sessions/2026/08/25/session.jsonl" <<'JSONL'
{"type":"turn_context","payload":{"model":"gpt-5.6-sol"}}
{"type":"token_count","timestamp":"2026-08-25T10:00:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":999999,"output_tokens":999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}}
{"type":"token_count","timestamp":"2026-08-25T10:05:00Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":1999999,"output_tokens":1999999},"last_token_usage":{"input_tokens":1200,"cached_input_tokens":1000,"output_tokens":300}}}}
JSONL
touch "$work/codex/sessions/2026/08/25/session.jsonl"
codex_record="$(
PATH="$stub:$PATH" \
CODEX_HOME="$work/codex" \
CODEX_STUB_ARGV="$work/codex-argv" \
PANAMA_AGENT_CODEX_BIN="$stub/codex" \
PANAMA_AGENT_USAGE_CACHE="$work/cache" \
"$codex" --force 2>/dev/null
)"
grep -q -- '-s read-only' "$work/codex-argv" \
|| note "the codex app-server was not asked for read-only ($(cat "$work/codex-argv" 2>/dev/null))"
grep -q -- 'app-server' "$work/codex-argv" \
|| note 'the codex collector did not start the app-server'
[[ "$(jq -r '.limits[] | select(.label == "Weekly (7-day)") | .percent * 100 | round' <<<"$codex_record")" == "55" ]] \
|| note 'the codex weekly limit was not read from the app-server'
[[ "$(jq -r '.limits[] | select(.label == "Spark Session (5-hour)") | .percent * 100 | round' <<<"$codex_record")" == "12" ]] \
|| note 'the codex model-scoped limit was not read'
[[ "$(jq -r .tierLabel <<<"$codex_record")" == "prolite" ]] \
|| note 'the codex plan was not read'
# total_token_usage is cumulative for the session. Counting it instead of
# last_token_usage makes usage grow quadratically: these two snapshots are
# 1500 tokens each, not four million.
[[ "$(jq -r .todayTotalTokens <<<"$codex_record")" == "3000" ]] \
|| note "the codex scan counted $(jq -r .todayTotalTokens <<<"$codex_record") tokens where the last-turn rule gives 3000"
# ── 6, 7 & 8. The fan-out ───────────────────────────────────────────────────
cat >"$work/collectors/panama-agent-usage-claude" <<STUB
#!/usr/bin/env bash
printf 'claude\n' >>"$work/ran"
printf '%s\n' '{"schemaVersion":1,"id":"claude","name":"Claude Code","ready":true,"limits":[]}'
STUB
cat >"$work/collectors/panama-agent-usage-codex" <<STUB
#!/usr/bin/env bash
printf 'codex\n' >>"$work/ran"
printf '%s\n' '{"schemaVersion":1,"id":"codex","name":"Codex","ready":true,"limits":[]}'
STUB
chmod +x "$work/collectors"/panama-agent-usage-*
run_update() {
PANAMA_AGENT_USAGE_COLLECTORS="$work/collectors" \
PANAMA_AGENT_USAGE_DIR="$work/usage" \
PANAMA_SETTINGS="$work/settings.json" \
"$updater" "$@" 2>"$work/update-stderr"
}
# Both on: both records appear.
printf '%s\n' '{"agentUsageClaude":true,"agentUsageCodex":true}' >"$work/settings.json"
: >"$work/ran"
run_update
[[ -s "$work/usage/claude.json" && -s "$work/usage/codex.json" ]] \
|| note 'the fan-out did not write a record for every enabled collector'
# One off: it must not run at all. "Off" has to mean "no process" -- the
# collector is the thing that reads a credential and makes a request.
printf '%s\n' '{"agentUsageClaude":true,"agentUsageCodex":false}' >"$work/settings.json"
: >"$work/ran"
run_update
grep -qx 'codex' "$work/ran" \
&& note 'a collector the user switched off still ran'
[[ -e "$work/usage/codex.json" ]] \
&& note "a disabled collector's stale record was left behind for the panel to show"
# Absent means default, and the default is on. `.key // true` would read false
# as absent, which is how a switched-off collector comes back to life.
printf '%s\n' '{}' >"$work/settings.json"
: >"$work/ran"
run_update
grep -qx 'codex' "$work/ran" \
|| note 'an unset per-agent preference was not treated as its default of on'
# 7. A record is replaced only by a whole, parseable one.
printf '%s\n' '{"agentUsageClaude":true,"agentUsageCodex":true}' >"$work/settings.json"
run_update
good="$(sha256sum "$work/usage/codex.json" | cut -d' ' -f1)"
cat >"$work/collectors/panama-agent-usage-codex" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' 'Traceback (most recent call last):'
exit 1
STUB
chmod +x "$work/collectors/panama-agent-usage-codex"
run_update
[[ "$(sha256sum "$work/usage/codex.json" | cut -d' ' -f1)" == "$good" ]] \
|| note 'a collector that died mid-run replaced a good record with rubbish'
jq -e . >/dev/null 2>&1 <"$work/usage/codex.json" \
|| note 'the usage directory holds something that is not JSON'
# 8. Atomic, and tidy: no temp file survives a failure for the panel to read.
compgen -G "$work/usage/.*.??????" >/dev/null \
&& note 'the fan-out left a temporary record behind in the directory the panel watches'
grep -qE '>[[:space:]]*"\$USAGE_DIR/\$agent\.json"' "$updater" \
&& note 'the fan-out redirects straight into the file a reader is watching'
grep -q 'mktemp' "$updater" && grep -q 'mv "\$tmp"' "$updater" \
|| note 'the fan-out does not write through mktemp and mv, so a reader can catch a half-written record'
# ── 5. The widget hides itself, and the panel is what it opens ──────────────
qml_uncommented() { grep -v '^[[:space:]]*//' "$1"; }
grep -q 'Settings.showAgentUsage && AgentUsage.available' "$widget" \
|| note 'the widget does not gate on both the preference and having real numbers'
qml_uncommented "$widget" | grep -q 'interactive: false' \
&& note "the widget disables Pill's mouse area, so its click handlers never fire"
qml_uncommented "$widget" | grep -q 'onActivated' \
|| note 'left-clicking the widget does nothing'
qml_uncommented "$widget" | grep -q 'onSecondaryActivated: ShellState.openSettings' \
|| note 'right-clicking the widget no longer opens the settings that govern it'
qml_uncommented "$widget" | grep -q 'AgentUsagePanel' \
|| note 'the widget does not open the usage panel'
grep -q 'key: "showAgentUsage"' "$schema" || note 'there is no showAgentUsage preference'
grep -A2 'key: "showAgentUsage"' "$schema" | grep -q 'def: false' \
|| note 'the usage widget is on by default; it is a coding-tool readout, not general-purpose desktop furniture'
grep -q 'showAgentUsage' "$aliases" \
|| note 'Settings.qml does not alias showAgentUsage, so the binding reads undefined'
for key in showAgentUsage agentUsageClaude agentUsageCodex agentUsageRefreshMinutes; do
grep -q "\"$key\"" "$aliases" \
|| note "Settings.qml does not alias $key, so the binding reads undefined"
done
# The two-tier colouring is better than one threshold and is not to be lost in
# a refactor: amber before it hurts, red when it is about to.
qml_uncommented "$widget" | grep -q 'AgentUsage.headline >= 90' \
|| note 'the widget lost its red tier at 90%'
qml_uncommented "$widget" | grep -q 'AgentUsage.headline >= 75' \
|| note 'the widget lost its amber tier at 75%'
# The collectors run on a timer measured in minutes, never on a repaint, and
# only while the readout was asked for.
python3 - "$service" <<'PY' || note 'the collectors are not run on a minute-scale timer driven by the preference'
import re
import sys
# The collector runs on a timer measured in minutes, never on a repaint.
python3 - "$service" <<'PY' || note 'the collector is not run on a minute-scale timer'
import re, sys
text = open(sys.argv[1], encoding="utf-8").read()
match = re.search(r"interval:\s*(\d+)\s*\*\s*60\s*\*\s*1000", text)
if not match or int(match.group(1)) < 1:
if not re.search(r"interval:\s*root\.refreshMinutes\s*\*\s*60\s*\*\s*1000", text):
raise SystemExit(1)
if "Settings.agentUsageRefreshMinutes" not in text:
raise SystemExit(1)
if not re.search(r"running:\s*Settings\.showAgentUsage", text):
raise SystemExit(1)
PY
# The panel and the service read records; neither has any business knowing what
# a credential looks like.
for file in "$service" "$panel" "$widget"; do
grep -qE 'credentials|accessToken|Authorization' "$file" \
&& note "$(basename "$file") mentions a credential; only the collector may"
done
# Nothing in the panel repaints on a clock. The one timer it owns advances a
# displayed time and only runs while the panel is open.
python3 - "$panel" <<'PY' || note 'the panel animates or ticks while it is closed'
import re
import sys
text = open(sys.argv[1], encoding="utf-8").read()
for block in re.findall(r"Timer\s*\{(.*?)\n \}", text, re.S):
if "running: root.visible" not in block:
raise SystemExit(1)
if re.search(r"\b(SequentialAnimation|loops:\s*Animation\.Infinite)\b", text):
raise SystemExit(1)
PY
# ── The retired collector stays retired ─────────────────────────────────────
[[ -e "$scripts/panama-agent-usage" ]] \
&& note 'the single-agent collector is still on disk; its consumers moved to the fan-out'
grep -rqF 'scripts/panama-agent-usage"' "$repo_dir/config" "$repo_dir/bin" 2>/dev/null \
&& note 'something still calls the retired single-agent collector'
if (( ${#findings[@]} > 0 )); then
printf 'agent usage contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
@@ -38,9 +38,11 @@ SHELL_WORDS='^(if|then|else|elif|fi|for|while|until|do|done|case|esac|in|functio
# fingerprint aliases in config/bash can rely on it without declaring it.
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|ls|rfkill|lsof|authselect|setsid|nohup|grub2-mkconfig)$'
# bootctl ships in systemd-udev, which every Fedora install carries -- it is
# the udev half of systemd, not an optional tool.
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|bootctl|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
# bootctl and coredumpctl ship in systemd-udev, which every Fedora install
# carries -- it is the udev half of systemd, not an optional tool. Declaring
# systemd-udev in a package list would state a dependency on the thing that
# boots the machine.
SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|bootctl|coredumpctl|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'
# Installed by install-packages itself rather than by a package list. Two
# reasons, both deliberate: bun and claude have no RPM or flatpak at all, and
@@ -463,6 +463,20 @@ jq -e '. == {
before: true, after: false, returned: true, soundAfterReturn: true, forgotUnknown: false
}' <<<"$forgotten" >/dev/null || fail "forgetApp did not delete and restore a rule: $forgotten"
# The command-as-data hint. A notification may name a shell command that the
# CARD runs on a click, which is how a crash watcher that has already exited
# still offers "diagnose this with your agent". Pinned here because the failure
# mode is silent in both directions: a hint that stops being read turns every
# rung of the escalation ladder into an ordinary notification, and an entry
# that outlives its notification hands a later click a stale command.
exec_hint="$(qs_for_test ipc call notification-app-rules-test execHint)"
jq -e '. == {
plain: "",
carried: "panama-agent-crash 41283 kitty",
afterDismiss: ""
}' <<<"$exec_hint" >/dev/null \
|| fail "the panama-exec hint is not carried through the notification model: $exec_hint"
# The urgency override is one answer, read by everything. The timeouts prove
# the banner duration followed it and not only the colour.
urgency="$(qs_for_test ipc call notification-app-rules-test urgency)"
+366
View File
@@ -0,0 +1,366 @@
#!/usr/bin/env bash
# The launcher at the bottom of the escalation ladder.
#
# Every rung -- the crash toast, the failed reload, a red health check,
# `panama diagnose` -- ends at bin/panama-agent, which turns two settings into
# one terminal running one agent. It is the single place where the ladder can
# quietly become a no-op, or launch the wrong thing, or lose the prompt it was
# handed, so it is the single place worth pinning.
#
# What must hold:
#
# 1. "none" is silent. No agent chosen is the shipped state, not an error.
# A rung that shouted about it is a rung that gets switched off, and a
# missing settings file means the same thing as "none".
# 2. The argv per agent is the one the installed binaries actually accept.
# These flags move between releases; the table in panama-agent was read off
# `claude --help` and `codex --help`, and this pins the shape of it.
# 3. agentAutoApprove off means no mode flag at all. The agent's own default
# is a choice the user already made.
# 4. The prompt is ONE argv element. Crash and diagnose prompts are multi-line
# paragraphs; a prompt that arrives as forty words is forty words of
# nothing.
# 5. The working directory is the checkout. The skills the prompts name, the
# repository being asked about, and .claude/settings.json's pre-approved
# read-only diagnostics all live here and nowhere else.
# 6. panama-agent-crash carries all four coredump facts and the absolute path
# to the skill. The path is what makes the ladder work for an agent whose
# harness has no skill mechanism.
#
# Hermetic on purpose: kitty, claude, codex, setsid and coredumpctl are all
# stubbed onto PATH and the settings file is fabricated, so no agent is ever
# launched and no window is ever opened. This is NOT in tests/desktop-hijacking
# for exactly that reason -- it must stay runnable mid-session.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
launcher="$repo_dir/bin/panama-agent"
crash="$repo_dir/bin/panama-agent-crash"
reload="$repo_dir/bin/panama-agent-reload"
findings=()
note() { findings+=("$1"); }
for script in "$launcher" "$crash" "$reload"; do
[[ -x "$script" ]] || { printf 'panama agent contract: %s is not executable\n' "$script" >&2; exit 1; }
done
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
stub="$work/bin"
mkdir -p "$stub"
settings="$work/settings.json"
# The terminal, replaced by something that records what it was asked to run.
# NUL-separated, because the prompts are multi-line and any line-oriented record
# would lose exactly the property this is here to check.
cat >"$stub/kitty" <<STUB
#!/usr/bin/env bash
printf '%s' "\$PWD" >"$work/cwd"
printf '%s\0' "\$@" >"$work/argv"
STUB
# setsid is stubbed rather than real so the launch is synchronous and there is
# no race between the detached child writing and this reading it.
cat >"$stub/setsid" <<STUB
#!/usr/bin/env bash
: >"$work/setsid-used"
exec "\$@"
STUB
# Present so panama-agent's "is it installed" check passes. Reaching one of
# these means the terminal stub was bypassed, which is itself a failure.
for agent in claude codex; do
cat >"$stub/$agent" <<STUB
#!/usr/bin/env bash
: >"$work/agent-actually-ran"
STUB
done
# A hand-run PID has no core here; the real one would be a slow lookup against
# this machine's journal.
cat >"$stub/coredumpctl" <<'STUB'
#!/usr/bin/env bash
exit 1
STUB
# The rungs that quote the journal get a deliberately enormous one. journalctl
# counts entries, not lines, and a real one on this machine answered a
# thirty-entry request with 2,430 lines and a quarter of a megabyte of
# backtrace -- which exec rejects outright, because a single argv element is
# capped at 128KB. The prompt builders have to bound what they quote, and this
# is what proves they do.
cat >"$stub/journalctl" <<'STUB'
#!/usr/bin/env bash
line="quickshell: QML Item: Cannot assign to non-existent property$(printf '%0.sx' {1..500})"
for _ in {1..4000}; do printf '%s\n' "$line"; done
STUB
chmod +x "$stub"/*
# Runs the launcher hermetically and returns its exit status. argv/cwd records
# from the previous run are cleared first, so "was not launched" is a missing
# file rather than a stale one.
launch() {
rm -f "$work/argv" "$work/cwd" "$work/setsid-used" "$work/agent-actually-ran"
PATH="$stub:/usr/bin:/bin" \
PANAMA_PATH="$repo_dir" \
PANAMA_AGENT_SETTINGS="$settings" \
"$@" >"$work/stdout" 2>"$work/stderr"
}
# The recorded argv, as an array.
recorded_argv() {
argv=()
[[ -s "$work/argv" ]] || return 1
mapfile -d '' -t argv <"$work/argv"
return 0
}
# The argv as one string with a separator no prompt contains, so a whole
# element can be matched without a line-oriented search.
argv_joined() {
local IFS=$'\x1f'
printf '%s' "${argv[*]}"
}
# Just the command kitty was told to run: everything after -e, with the
# terminal's own flags dropped. Split out because the agent's argv is what these
# checks are about, and matching it inside the whole line means writing patterns
# that would also match a --title.
agent_command() {
agent_argv=()
local element seen=0
for element in "${argv[@]}"; do
if (( seen )); then
agent_argv+=("$element")
elif [[ "$element" == "-e" ]]; then
seen=1
fi
done
(( ${#agent_argv[@]} > 0 ))
}
# The agent argv from element 1 on, joined -- the flags, without the resolved
# binary path, which differs per machine and is checked separately.
agent_flags() {
local IFS=$'\x1f'
printf '%s' "${agent_argv[*]:1}"
}
# ── 1. "none" is silent ─────────────────────────────────────────────────────
printf '{"preferredAgent":"none"}\n' >"$settings"
launch "$launcher" --prompt "anything"
status=$?
(( status == 0 )) || note "with no agent chosen panama-agent exited $status; choosing none is not an error"
[[ -s "$work/stdout" ]] && note 'with no agent chosen panama-agent printed to stdout; it is meant to be silent'
[[ -s "$work/stderr" ]] && note "with no agent chosen panama-agent complained: $(head -1 "$work/stderr")"
[[ -e "$work/argv" ]] && note 'with no agent chosen panama-agent still opened a terminal'
# A machine that has never been asked the question answers the same way.
rm -f "$settings"
launch "$launcher" --prompt "anything"
status=$?
(( status == 0 )) || note "with no settings file panama-agent exited $status; the default is none, which is silent"
[[ -e "$work/argv" ]] && note 'with no settings file panama-agent opened a terminal anyway'
# ── 2, 4, 5. Claude, auto-approving ─────────────────────────────────────────
prompt=$'first line\nsecond line with spaces'
printf '{"preferredAgent":"claude","agentAutoApprove":true}\n' >"$settings"
launch "$launcher" --prompt "$prompt"
status=$?
(( status == 0 )) || note "panama-agent exited $status launching claude"
[[ -e "$work/setsid-used" ]] || note 'the launch does not go through setsid, so the agent dies with whatever spawned it'
[[ -e "$work/agent-actually-ran" ]] && note 'the contract reached a real agent binary; it must stop at the terminal stub'
if recorded_argv && agent_command; then
joined="$(argv_joined)"
[[ "$joined" == *$'\x1f'"--class"$'\x1f'"panama-agent"* ]] \
|| note 'the terminal is not given the fixed panama-agent window class, so no window rule can find it'
[[ "$joined" == *"--directory"$'\x1f'"$repo_dir"* ]] \
|| note 'the terminal is not opened in the Panama checkout'
# The binary is resolved here, not left for kitty to find. The click that
# reaches this script comes from the shell, whose PATH does not contain
# ~/.local/bin, and kitty would inherit exactly that PATH.
[[ "${agent_argv[0]}" == /* ]] \
|| note "the agent is passed to the terminal as '${agent_argv[0]}' rather than a resolved path"
[[ "${agent_argv[0]}" == */claude ]] \
|| note "the resolved binary is ${agent_argv[0]}, which is not claude"
[[ "$(agent_flags)" == "--permission-mode"$'\x1f'"auto"$'\x1f'"--"$'\x1f'"$prompt" ]] \
|| note "claude with auto-approve on is launched as: $(agent_flags)"
# 4. The prompt survives as one element, newlines and all.
[[ "${argv[-1]}" == "$prompt" ]] \
|| note 'the prompt did not arrive as a single argv element'
# 5. Not just named as a flag -- actually the working directory.
[[ "$(cat "$work/cwd" 2>/dev/null)" == "$repo_dir" ]] \
|| note "the agent starts in $(cat "$work/cwd" 2>/dev/null), not in the checkout"
else
note 'launching claude opened no terminal at all'
fi
# ── 3. Claude, prompting normally ───────────────────────────────────────────
printf '{"preferredAgent":"claude","agentAutoApprove":false}\n' >"$settings"
launch "$launcher" --prompt "$prompt"
if recorded_argv && agent_command; then
[[ "$(agent_flags)" == *"--permission-mode"* ]] \
&& note 'agentAutoApprove is off and claude was still given a permission mode'
[[ "$(agent_flags)" == "--"$'\x1f'"$prompt" ]] \
|| note "claude with auto-approve off is launched as: $(agent_flags)"
else
note 'launching claude with auto-approve off opened no terminal'
fi
# ── 2 & 3. Codex, both ways ─────────────────────────────────────────────────
printf '{"preferredAgent":"codex","agentAutoApprove":true}\n' >"$settings"
launch "$launcher" --prompt "$prompt"
if recorded_argv && agent_command; then
[[ "${agent_argv[0]}" == */codex ]] \
|| note "the resolved binary is ${agent_argv[0]}, which is not codex"
[[ "$(agent_flags)" == "--approve-for-me"$'\x1f'"--"$'\x1f'"$prompt" ]] \
|| note "codex with auto-approve on is launched as: $(agent_flags)"
[[ "${argv[-1]}" == "$prompt" ]] \
|| note 'codex did not receive the prompt as a single argv element'
else
note 'launching codex opened no terminal'
fi
printf '{"preferredAgent":"codex","agentAutoApprove":false}\n' >"$settings"
launch "$launcher" --prompt "$prompt"
if recorded_argv && agent_command; then
[[ "$(agent_flags)" == *"--approve-for-me"* ]] \
&& note 'agentAutoApprove is off and codex was still told to approve for itself'
[[ "$(agent_flags)" == "--"$'\x1f'"$prompt" ]] \
|| note "codex with auto-approve off is launched as: $(agent_flags)"
else
note 'launching codex with auto-approve off opened no terminal'
fi
# ── An agent installed where only the user's own PATH looks ─────────────────
#
# Both agents install themselves into ~/.local/bin. The rungs are clicked from
# the shell, and systemd starts the shell with a PATH that does not contain it,
# so a launcher that trusted the inherited PATH would report every installed
# agent as missing -- from a detached process, into a stderr nobody reads.
home="$work/home"
mkdir -p "$home/.local/bin" "$work/bin-noagent" "$work/empty"
cp "$stub/kitty" "$stub/setsid" "$work/bin-noagent/"
cp "$stub/claude" "$home/.local/bin/claude"
printf '{"preferredAgent":"claude","agentAutoApprove":true}\n' >"$settings"
rm -f "$work/argv" "$work/cwd"
HOME="$home" PATH="$work/bin-noagent:/usr/bin:/bin" \
PANAMA_PATH="$repo_dir" PANAMA_AGENT_SETTINGS="$settings" \
"$launcher" --prompt "$prompt" >"$work/stdout" 2>"$work/stderr"
status=$?
(( status == 0 )) || note "an agent installed in ~/.local/bin was not found: $(head -1 "$work/stderr")"
if recorded_argv && agent_command; then
[[ "${agent_argv[0]}" == "$home/.local/bin/claude" ]] \
|| note "an agent reachable only through ~/.local/bin resolved to ${agent_argv[0]}"
else
note 'an agent installed in ~/.local/bin opened no terminal, so every click from the shell would be a dead end'
fi
# ── An agent that is genuinely not installed says so ────────────────────────
rm -f "$work/argv" "$work/cwd"
HOME="$work/empty" PATH="$work/empty:/usr/bin:/bin" \
PANAMA_PATH="$repo_dir" PANAMA_AGENT_SETTINGS="$settings" \
"$launcher" --prompt "$prompt" >"$work/stdout" 2>"$work/stderr"
status=$?
(( status != 0 )) || note 'a preferred agent that is not installed exited 0, so the rung failed silently'
grep -qi 'not installed' "$work/stderr" \
|| note 'a missing agent binary produced no explanation naming it'
# ── 6. The crash prompt carries the facts ───────────────────────────────────
printf '{"preferredAgent":"claude","agentAutoApprove":true}\n' >"$settings"
launch "$crash" 4242 panama-test-cra /usr/bin/panama-test-crasher SIGSEGV
status=$?
(( status == 0 )) || note "panama-agent-crash exited $status"
[[ -e "$work/agent-actually-ran" ]] && note 'panama-agent-crash reached a real agent binary'
if recorded_argv; then
crash_prompt="${argv[-1]}"
for fact in 4242 panama-test-cra /usr/bin/panama-test-crasher SIGSEGV; do
[[ "$crash_prompt" == *"$fact"* ]] \
|| note "the crash prompt does not mention $fact, which a diagnosis needs"
done
[[ "$crash_prompt" == *"$repo_dir/skills/diagnose-crash/SKILL.md"* ]] \
|| note 'the crash prompt does not give the absolute path to the diagnose-crash skill, so an agent without a skill mechanism has nothing to read'
[[ -r "$repo_dir/skills/diagnose-crash/SKILL.md" ]] \
|| note 'the path the crash prompt points at does not exist'
else
note 'panama-agent-crash opened no terminal'
fi
# Something that is not a PID is a usage error, not a prompt about "unknown".
launch "$crash" not-a-pid
status=$?
(( status != 0 )) || note 'panama-agent-crash accepted a non-PID and launched anyway'
[[ -e "$work/argv" ]] && note 'panama-agent-crash launched an agent for a non-PID'
# ── The reload prompt ───────────────────────────────────────────────────────
launch "$reload" "shell.qml:42 Cannot assign to non-existent property"
status=$?
(( status == 0 )) || note "panama-agent-reload exited $status"
if recorded_argv; then
[[ "${argv[-1]}" == *"Cannot assign to non-existent property"* ]] \
|| note 'the reload prompt does not carry what Quickshell reported'
else
note 'panama-agent-reload opened no terminal'
fi
launch "$reload"
status=$?
(( status != 0 )) || note 'panama-agent-reload with nothing to report launched an agent anyway'
# ── The by-hand rung ────────────────────────────────────────────────────────
#
# `panama diagnose` quotes the journal too, and quoted the whole of it once:
# exec answered with "Argument list too long" and the rung did nothing at all.
# The health summary here is the real one, which is read-only and takes about a
# second.
launch "$repo_dir/bin/panama" diagnose the bar disappears after unplugging
status=$?
(( status == 0 )) || note "panama diagnose exited $status: $(head -1 "$work/stderr")"
if recorded_argv; then
diagnose_prompt="${argv[-1]}"
(( ${#diagnose_prompt} < 131072 )) \
|| note "the diagnose prompt is ${#diagnose_prompt} bytes; one argv element caps at 128KB and exec would refuse it"
[[ "$diagnose_prompt" == *"the bar disappears after unplugging"* ]] \
|| note 'the diagnose prompt dropped the words the person typed, which are the part no collector reports'
else
note 'panama diagnose opened no terminal'
fi
if (( ${#findings[@]} > 0 )); then
printf 'panama agent contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'panama agent contract: PASS\n'
+10 -4
View File
@@ -125,22 +125,28 @@ while read -r pair; do
fi
done <<<"$retired_pairs"
# ── The System strip is eight tabs, and the two it lost are still reachable ──
# ── The System strip is nine tabs, and the two it lost are still reachable ──
#
# System had grown to ten tabs, which is more than a strip can show without
# becoming a second sidebar. Two left: Region & Language merged into Date &
# Time, because a date format and the clock that shows it are one subject, and
# the Manual became a hidden leaf.
#
# Agents is the ninth, added deliberately rather than by accretion: it is where
# the preferred agent is chosen, and every rung of the escalation ladder --
# crash notifications, failed reloads, red health checks -- is dark until that
# choice is made, so it has to be somewhere a person can find without being
# told the id.
#
# The count is pinned rather than derived because the number is the point: this
# is the horizontal space one row of tabs has. Anything that needs an eleventh
# is the horizontal space one row of tabs has. Anything that needs a tenth
# subject needs a decision, not another entry.
python3 - "$routes" <<'PY' || fail 'the System category is not the approved eight-tab strip'
python3 - "$routes" <<'PY' || fail 'the System category is not the approved nine-tab strip'
import re
import sys
expected = [
"about", "updates", "services", "storage",
"about", "updates", "services", "agents", "storage",
"snapshots", "containers", "datetime", "sync",
]
text = open(sys.argv[1], encoding="utf-8").read()
@@ -40,6 +40,12 @@ expected = {
"cursorInactiveTimeout": {"owner": "mouse", "mirrors": {"accessibility"}},
"cursorSize": {"owner": "accessibility", "mirrors": {"mouse"}},
"inactiveOpacity": {"owner": "appearance", "mirrors": {"accessibility"}},
# The bar decides what the bar contains, which is why Bar owns this. Agents
# mirrors it because that is the page somebody is on when they wonder where
# the number went -- the rest of the usage settings (which collectors run,
# how often) live only there, and a master switch missing from the card
# that holds them would be a card that cannot be turned off from itself.
"showAgentUsage": {"owner": "bar", "mirrors": {"agents"}},
# lockMinutes and lockOnSleep used to be mirrored onto Privacy, which was
# the one mirror in this table that nobody had asked for: Privacy carried a
# whole second Screen-lock card, so the same preference had two sliders and
+3 -1
View File
@@ -19,7 +19,9 @@ fail() {
# convention the scaffold carries -- the scroll behaviour, the header, the
# padding -- was hand-rolled there and quietly different from the other
# twenty-five pages.
pages=(Home MyHome Phone Displays Connectivity Firewall Printers Sharing Bar Dock ControlCenter Tiling Workspaces Sync Sound Shortcuts Mouse Dictation Notifications Focus ScreenIntelligence Health About Updates DateTime Containers Manual)
# Agents joined on the day it was written, which is the only moment a page has
# never had a hand-rolled scaffold in it.
pages=(Home MyHome Phone Displays Connectivity Firewall Printers Sharing Bar Dock ControlCenter Tiling Workspaces Sync Sound Shortcuts Mouse Dictation Notifications Focus ScreenIntelligence Health Agents About Updates DateTime Containers Manual)
for page in "${pages[@]}"; do
page_file="$repo_dir/config/dot/quickshell/modules/settings/${page}Page.qml"
[[ -f "$page_file" ]] || fail "${page}Page.qml is missing"