Commit Graph
407 Commits
Author SHA1 Message Date
Gabriel Brown 1192ad64dc Test: Classify every contract capability 2026-08-26 21:52:16 -04:00
Gabriel Brown 044139ed63 Plan verification gate remediation 2026-08-26 21:11:25 -04:00
Gabriel Brown 0d894afe3e Plan repository audit remediation 2026-08-26 20:42:59 -04:00
Gabriel Brown 54dfa977ee The notification menu learns what every other popover already knew
Popover's container is a plain Item and never measures its children, so
the three-dots menu on a notification card opened as a 240x28 sliver
with its one row clipped out of existence. TrayMenu and AgentUsagePanel
both carry the antidote — implicit sizes on the Popover itself, taken
from the content column — and now the notification settings menu does
too, with rows filling the window width so the hover reaches the edges.

Claude-Session: https://claude.ai/code/session_01W8icivxZcmFTR2g6DkYhjT
2026-08-26 19:50:31 -04:00
Gabriel Brown 5f09c5ee38 The document portal may not claim started until its mount exists
Third recurrence of the stale-mount bug: the ExecStartPre cleanup ran on
clean ground and the portal still came up mountless, sitting active while
every fresh flatpak sandbox failed to build. Add an ExecStartPost that
polls for the fuse mount and fails the start after five seconds without
one, so a mountless portal shows up failed in the journal and the next
bus activation retries from the cleanup instead of lingering broken.

Claude-Session: https://claude.ai/code/session_01CTL7G5u3iDf1NCM6rTyCES
2026-08-26 10:48:47 -04:00
Gabriel Brown f33da41cc6 Panama learns what a server is: from a root login to running containers
A machine's role is now the interview's first question and the one answer
Panama records. Servers get the same shell minus the screen: core packages,
nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless
ports from 80, firewalld, the nginx-bridge network, and a nightly image
updater that replaced watchtower for cause.

server/containers/ carries junior's 23 compose services -- secrets moved to
per-machine .env files that never enter this public repo, every transformed
compose proven to render byte-identical to what is live. 'panama server'
enables, disables and relinks them; nothing here restarts a running service.
'boot --server' walks a fresh VPS from its root login to a normal install.

Five new contracts pin the secrets rule, the catalog's shape, panama-server's
behavior, the role plumbing, and the dotfile classification.

Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
2026-08-25 23:11:49 -04:00
Gabriel Brown 9b338608ef Center the usage pill like every other tenant of the bar
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 13:15:42 -04:00
Gabriel Brown cc7d91d09c No error is a dead end: crash, click, and your agent is already looking
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 12:50:09 -04:00
Gabriel Brown ada0faf1d1 Let the sidebar breathe: fourteen categories deserve legible names
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 11:20:34 -04:00
Gabriel Brown 045a774847 The machine now carries its own manual for AI hands
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 10:09:38 -04:00
Gabriel Brown beed44dd87 panama update: one command, no questions, and no gap it cannot see
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 07:22:12 -04:00
Gabriel Brown 8105849151 The last two stale needles the great respelling left behind
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 02:31:59 -04:00
Gabriel Brown bd2fd12de2 Coherence: one component per idea, one spelling per word, one truth per claim
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 02:21:45 -04:00
Gabriel Brown 06c53d6c21 Shortcuts you invent, rules you write, gestures you own - all still just data
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 01:51:59 -04:00
Gabriel Brown f9e5d3f470 No forgetting, deleting or clearing on a single press, anywhere
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 00:41:39 -04:00
Gabriel Brown 88371d19f0 The safety layer: two presses for anything you cannot take back
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 00:39:07 -04:00
Gabriel Brown 8b1205e4b8 Tier 0: render what the services already decided, honestly
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 00:24:30 -04:00
Gabriel Brown be0e55214b Finish the wonderland: System told truthfully, in eight tabs instead of ten
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 23:31:52 -04:00
Gabriel Brown 9ffaf45a4d Make every settings row reachable, and every accessibility switch honest
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 21:03:36 -04:00
Gabriel Brown e1ff25fc66 Draw idle as one timeline, and let the power button answer to its owner
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 20:33:44 -04:00
Gabriel Brown 6f0ce639d9 Show every answer the portal remembers, and give SSH keys their missing half
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 19:26:56 -04:00
Gabriel Brown 4ec8bd94d9 Give identity its due: native enrollment, honest deletion, and sign-in that stays home
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 18:55:38 -04:00
Gabriel Brown 5a0643357f Make Applications a real app manager, and clean up storage without the racket
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 17:18:14 -04:00
Gabriel Brown b30bf40407 Own the network: details, VPN, enterprise Wi-Fi, and a firewall that can also allow
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 16:31:52 -04:00
Gabriel Brown aba2d16ffa Give Input keycaps, a shortcut search, and the missing pointer basics
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 15:21:01 -04:00
Gabriel Brown b8f88a91f3 Add the four components the Focus commit referenced but forgot to ship
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 15:14:02 -04:00
Gabriel Brown d5b6e62515 Bound the notification app list, and give Focus a real editor
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 14:25:26 -04:00
Gabriel Brown 07db1068f1 Review everything shipped this weekend, and fix what the reviewers caught
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 13:29:42 -04:00
Gabriel Brown ffce48964e Make the Jira skills call unslop, grilling, and tdd where they pay
All three already delegated in places — ticket calls diagnosing-bugs and
run, review-ticket calls codebase-design — so this extends a habit rather
than starting one.

unslop is the load-bearing part, at four points: create-ticket before the
Jira write, review-ticket's check list where a bare punctuation sweep
stood, and ticket on both mr.md and the Jira fields. Every one is scoped
to pattern detection and skips the "Adding soul" section, which is blog
voice and would fight the rigid house formats. The mr.md hook exempts the
pasted pre-MR handoff, generated output the skill already forbids
editing, and the field hook exempts proof cells. Conrad and Henry read
all of this, and prose that reads as machine generated costs the work its
credibility before anyone weighs what it says.

grilling comes in as a classification gate rather than blanket ceremony.
ticket now names the work bounded or architectural before drafting
plan.md and escalates only on architectural, where a wrong assumption is
expensive to find in Phase 2; bounded stories go straight through as
before. create-ticket escalates only when the epic docs have not settled
scope, which is where invented scope gets in.

tdd is gated the same way: test-first for procedures, derivations,
permission checks, migrations and state machines, and explicitly not for
UI wiring or copy changes, where it would produce the smoke-test padding
House style rejects. That closes a loop already half built, since
review-ticket proposes automated-test subtasks — those subtasks now name
the behavior to demonstrate, and ticket writes them first.
codebase-design gets the same call in ticket's planning step that
review-ticket already makes.

No steps were renumbered, so ticket's Phase 2 cross-references still
resolve.

Claude-Session: https://claude.ai/code/session_01DFgQVLdzLwu56gU8m4gvCS
2026-08-24 11:58:01 -04:00
Gabriel Brown b58371bb35 Give Sound the whole story, and keep the buttons inside the card
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 11:39:16 -04:00
Gabriel Brown 9bc68ba358 Rebuild Displays around the canvas, and let the transaction keep color
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 10:44:11 -04:00
Gabriel Brown 1f694b00b6 Keep .bashrc a bootstrap, and secrets out of the checkout for real
.bashrc is back to its one job: export the Panama paths and source
what it finds. The personal-env block moves into config/bash/shell —
first, because the tmux guard below it reads that file — and the
cargo source that was duplicated between the two files lives only in
shell now.

The real fix is behind that tidying: both Home Assistant helpers
defaulted to the IN-REPO config/bash/env, and the writer rebuilt it
with only its own three lines — which read as "my env vars vanished"
to the person who thought that file was hand-maintained. Both now
prefer ~/.config/panama/env, the migrated home outside the checkout,
with the repo path kept only as a read fallback for unmigrated
machines. The stale legacy copy on this machine is retired; the
working credentials were merged into the migrated file first, after
probing both sets against the live Home Assistant.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 08:49:40 -04:00
Gabriel Brown f8f5b25510 Make Shell a category, the bar legible, and the dock a real dock
Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling,
Workspaces — the home for everything Quickshell draws. The settings-
management cluster moves to System as Sync & Backup, Appearance's
Shell tab dissolves, and 24-hour time finally lives on Date & Time,
which always owned it.

The bar gets what it never had: a way to survive the wallpaper. A
second neutral text family (follow theme, or forced light or dark),
a one-layer shadow under every glyph, and a gradient scrim for
wallpapers nothing else survives — all off by default, pixel-identical
until asked. Widgets earn toggles (weather, media, clipboard, calendar
countdown), the vitals cluster stops leaving a dead pill behind, and
Control Center's sections learn to step aside.

The dock graduates from MVP: a context menu with window rows, pin,
unpin, quit and new-window; scroll an icon to cycle its windows; drag
to reorder on the dock itself; hover previews with one-shot captures;
and "Add App to Dock" in the launcher. Three real bugs died en route —
menus that slid away with the autohide, a readonly-property crash on
every menu open, and a drag that drifted half a slot per icon on side
docks. The pinned-apps editor in Settings becomes a drag strip.

166 contracts; the full suite is green except two live display and
switcher tests that cannot run behind a locked session — re-verified
on unlock.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 04:28:20 -04:00
Gabriel Brown 4d7a194300 Make video wallpapers survive first contact
Shipping met reality tonight, and reality won three rounds before we
did. mpvpaper's -f forks it into the background, which a supervisor
reads as instant death — the respawn loop repainted the desktop black
once a second. The reaper's own kills fired onExited like crashes, so
the supervisor ate its young until deliberate deaths got marked. And
the video's path shared wallpaperPath with the still pipeline, whose
transactional persistence clobbered it — it now lives under its own
videoWallpaperPath key, read by the lock screen and the doctor too.

Restore is the service's own now, reactive and once per session: a
one-shot timer raced the async preference load and availability probe
at cold start and silently lost. A video-wallpaper IPC target drives
start/stop/pause from a terminal and from the contract sweep to come.

Verified live: one player per output, same PID across restarts, VAAPI
engaged, 0.2% CPU steady for 1440p30 h264, pause from the bar pill.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 23:57:04 -04:00
Gabriel Brown cb7c09d208 Give the desktop real themes, video wallpapers, and honest titlebars
Appearance now opens on Themes: light and dark side by side, each
remembering its own choice, over galleries of ten shipped themes —
Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and
Everforest in both modes. A theme is a complete palette: the catalog
lives in themes.json, Theme.qml reads every color token from the
active record, and one render pipeline carries it to kitty, tmux,
btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme
editor builds new ones from four wells — wheel, hex, or eyedropper —
with derived surfaces, a saturation slider, debounced fine-tune, and
effects that save with the theme. Custom edits finally keep GNOME's
accent, kitty's border, and hyprlock in sync.

Wallpapers can be video: mpvpaper per output, hardware-decoded, muted
and looped, supervised and respawned. Panama owns the pausing — games,
battery, and a bar pill for right now — because the compositor
rebuilds full-screen blur for every frame a video wallpaper draws.
The lock screen gets a still frame.

Titlebars stop lying. GNOME apps get close-only on your chosen side,
the maximize and double-click settings are gone, the Settings window
obeys the same rules, and its titlebar can be turned off entirely.
Typography becomes five labeled dropdowns instead of a wall of
samples.

Contracts updated and written throughout (165 now); per the redesign
workflow none were executed — the full sweep runs once at the end.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 23:39:04 -04:00
Gabriel Brown 7578348db1 Merge Home & Phone into a three-tab Home that knows your house
Home is now Overview | My Home | Phone. Overview leads with quick-action
tiles (focus, Do Not Disturb, health, snapshots, storage), keeps the
findings card — updates fold in, the reclaim-space prompt is gone on
purpose — and adds glance cards, the next calendar event, and weather.

My Home groups every light by Home Assistant area: the helper gained an
`areas` command (one REST template render, no websocket), and the rooms
degrade to a flat list on setups without areas. The favorites editor and
connection card moved intact. Phone gains a vitals strip — battery and
cell signal read from KDE Connect's plugin D-Bus objects, where absence
is data, not an error — beside ring, clipboard, send-a-file, and the
BlueBubbles handoff.

The retired home-phone id resolves to my-home forever via a new alias
map in SettingsRoutes (with a hasOwnProperty guard so prototype names
cannot leak into settingsPage). Storage no longer claims 0 B free — the
old page read a field the disks helper never emitted.

Contracts updated alongside; per the new workflow, the full suite runs
once at the end of the redesign (see the test backlog note).

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 22:06:18 -04:00
Gabriel Brown 5490fd285d Fold thirty-one settings pages into fifteen categories with tabs
The sidebar was a flat scan of thirty-one rows; now it reads like a
settings app. Multi-subject categories (Input, Network & Sharing,
Applications, Users & Accounts, Privacy & Security, System) carry an
Appearance-style tab strip above the page, drawn by the shell so the
leaf pages themselves are untouched. The taxonomy lives in one new
file, services/SettingsRoutes.qml; the sidebar, the strip, route
validation, search breadcrumbs, and both generators derive from it.

ShellState.settingsPage still holds leaf ids, so every deep link, IPC
call, and search result keeps working — and now lands on the exact
tab. Dictation moves out of Sound onto its own page under Input, with
a handoff back to Sound for the microphone. The strip scrolls when
System's nine tabs outgrow a tiled window. All 161 contracts pass.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 20:21:31 -04:00
Gabriel Brown 50077a0c31 Fix the second dead settings button, and pin the class shut
A sweep for controls wired to targets the backend does not handle -- the shape
of the dictation bug -- found one more: "Start Orca" on the Accessibility page
called openApplication("orca") with no "orca" in the command map, so it set an
error and launched nothing. orca ships in hyprland-packages; it now runs.

That is the whole count. Every service-method call across 84 settings pages
resolves, every helper subcommand a service invokes is implemented, every
openGnomePanel handoff is allow-listed. Two dead buttons existed in the entire
settings app -- the dictation Download and this -- and both are fixed.

settings-buttons-contract pins the class: every openApplication id and
openGnomePanel panel a QML button passes must be present in SystemSettings'
dispatch maps, both of which return false silently on an unknown name.
Verified it catches the orca button when the fix is reverted.

And a dependency-contract exception the ffmpeg fix needed: ffmpeg is provided
by a swap (ffmpeg-free -> ffmpeg), never a list entry, because listing it is
the conflict that fix removed -- so panama-transcode's use of it is satisfied
without a package name to point at.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 14:11:15 -04:00
Gabriel Brown 14fc4fc8a3 Make the dictation setup button call a command that exists
The Download button on Sound → Dictation ran `panama-dictate download`. There
is no such command: the helper answered {"ok": false, "error":
"unknown-command"} and the button did nothing, every time, for anyone who
tried it. The real command is `setup`, which pulls the speech server image
and downloads the ~490 MB model together -- one trip, by design. And the card
told you the server needed `panama app whisper-vulkan`, an app that exists in
no package or setup/apps entry; the server is a prebuilt image the helper
pulls with podman.

So the card described three things and got all three wrong. It now shows the
real state of both pieces and offers one action -- "Set up dictation" -- wired
to the command that actually fetches them, with progress that names which half
is running (server image, then model). Verified end to end: `panama-dictate
setup` emits the pulling/downloading JSON the service parses.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 13:59:42 -04:00
Gabriel Brown e723fabed3 Stop the session you started, so the next one can begin
Logging out of plain Hyprland left the desktop running. autostart.lua starts
about ten user units, none of them children of the compositor, and the shutdown
handler that was meant to stop them stopped hyprland-session.target -- a unit
Fedora does not ship. systemctl says 'Unit not found', the handler reports
nothing, and hyprpaper, hypridle, vicinae and the polkit agent keep running
after logout. Each of them Requires=graphical-session.target, so that target
stayed active with no session behind it.

The symptom was not a broken Hyprland. Both uwsm and gnome-session refuse to
start into a session that is already running, so 'Hyprland (uwsm-managed)' and
GNOME both bounced back to the login screen, and plain Hyprland -- which makes
no such check -- was the only one that worked. The session that looked healthy
was the one leaving the mess, and it was the one Panama tells people not to use.

Now stops graphical-session.target, which every one of those units is PartOf,
so a unit added to the start handler later cannot be forgotten here.

panama-crash-watch had the same leak and no PartOf at all: a journal follower
per login, and a 'once per program per session' dedup that had quietly become
once per boot.
2026-08-23 13:55:47 -04:00
Gabriel Brown c0eee076b4 Stop installing ffmpeg over ffmpeg-free, which a fresh machine refuses
A clean Fedora Workstation ships ffmpeg-free, and desktop-packages listed
ffmpeg (RPM Fusion). Those two conflict rather than add: dnf will not erase
the preinstalled ffmpeg-free to make room, so the WHOLE desktop transaction
failed to resolve and no desktop package installed. The codec section already
does the trade correctly -- `dnf swap ffmpeg-free ffmpeg --allowerasing` --
so ffmpeg simply does not belong in the eager list. A machine that already
had ffmpeg (every one this repo was ever run on) sailed past this; the first
genuinely fresh Workstation install is what surfaced it.

Also: report_missing false-warned that awk was unavailable. rpm -q
--whatprovides matches a package's named provides, but awk is provided as the
file path /usr/bin/awk (by gawk), which that query misses. Fall back to
command -v so a capability provided by path is not reported as missing.

Found by a fresh-VM certification run, which is exactly the failure mode a
re-run on an already-configured machine cannot reproduce.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 13:20:42 -04:00
Gabriel Brown 31668619a4 A shell that comes back, a recording you can hear, a layout you can see
Quickshell segfaulted mid-session -- a Qt image-teardown bug, three cores in
the journal -- and the desktop stayed bar-less until a person noticed and
knew what to type, because the shell ran as a bare compositor child and
panama-crash-watch's report had no notification server left to arrive on.
The shell is now panama-quickshell.service, started per-session by
autostart.lua like every other Panama unit and never enabled globally:
Restart=on-failure turns the same crash into a two-second flicker, verified
by sending the running shell a real SIGSEGV and watching it return, and the
crash report now lands because the restarted shell is serving the bus by the
time the watcher looks. The two contracts that restart the shell learned to
do it through the unit, or the unit's own restart races them with a second
shell.

Recordings can hear: a recorderAudio preference -- none by default, GNOME's
default too, because a screencast that silently captured the microphone is an
incident -- adds system audio or the microphone through PulseAudio's
@DEFAULT_*@ aliases, so the capture follows whatever device Sound settings
has chosen.

The bar shows the active keyboard layout whenever more than one is
configured, mapped from xkb's own registry (evdev.lst) because deriving a
code from a description guesses wrong immediately -- "German" is de, not ge
-- and updated live from Hyprland's activelayout event. One layout, no
indicator, which is GNOME's behavior too.

And presentation mode: Caffeine plus Do Not Disturb as one quick-settings
tile, restoring both exactly as found -- the half you forget to arm before
plugging into a projector is the one that fires a message preview onto the
big screen.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 12:41:36 -04:00
Gabriel Brown 9530e32326 Give a laptop its first week: the keys, the lid, the battery's endgame
The audit's fourth tier -- what a Framework owner reaches for in the first
week and found missing.

The power button stops being an instant, unconfirmed poweroff: a shipped
logind drop-in tells the daemon to stand down and the compositor binds the
key to the power menu, the way GNOME turns it into a question. Holding it
still hard-cuts through firmware. change-settings restarts logind so the
change applies without waiting for a boot, and the Power page says what the
button does now.

The function row fills in: F10 (XF86RFKill) toggles airplane mode through a
new panama-osd verb that blocks or unblocks every radio and says which way it
went; F9 (XF86Display) opens the Displays page, the honest action until
mirroring exists. And the lid becomes a switch bind: closing a docked lid
turns the internal panel off so nothing renders inside a closed shell and no
workspace strands on an invisible output, and opening it restores the panel
with the mode and scale chosen in Settings. panama-lid owns both decisions;
undocked machines suspend via logind before any of it matters.

The battery gets an endgame. On battery the screen dims to 30% two-thirds of
the way to blanking -- GNOME's single largest idle battery saver -- and
restores exactly the level it saved. At the urgent threshold the machine
suspends after a fifteen-second grace, cancelled by plugging in, because a
suspend preserves the session for days and a hard cut at 0% preserves
nothing; "Only warn" remains a choice on the Power page. Hibernate joins the
power menu, but only where logind answers CanHibernate with yes -- an entry
that fails silently is worse than none.

And brightness stops being two code paths: the Displays page now embeds the
same control the quick-settings panel uses, so the built-in backlight and
DDC/CI monitors share one surface that withdraws itself where neither
exists. The lid contract narrows to what its principle protects -- a
HandleLidSwitch drop-in -- so deliberate policy for other keys can ship.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 12:21:08 -04:00
Gabriel Brown 153554b5df Make ./install something you could hand a stranger
The audit's third tier: everything between this installer and a fresh machine
it has never met.

The one path that could cost a person their display: the interview probes
Secure Boot with mokutil, which install-packages had not installed yet, so on
a minimal base the MOK question silently never fired -- and install-hardware
still installed akmod-nvidia and blacklisted nouveau, arming a reboot into an
unloadable driver with its fallback disabled. The probe tools (pciutils,
mokutil, fwupd) now bootstrap beside gum, and install-hardware re-checks
Secure Boot for itself and refuses the driver rather than the display.

Secrets leave the checkout: the personal environment moves to
~/.config/panama/env at mode 600 by migration, and .bashrc sources it with a
permission check that quietly re-tightens drift. change-settings no longer
overwrites /etc/dnf/dnf.conf -- two performance keys are set additively, the
defaultyes=True that made every `dnf remove` treat Enter as yes is gone, and
a migration strips it from machines that already received it.

Package installation survives the world changing: the initial and desktop
lists run with --skip-unavailable and a report_missing pass that names what
was skipped (resolved through --whatprovides, so capability names like awk
do not cry wolf); the openh264, appstream and core-group extras go through
soft; RustDesk resolves its RPM for the machine's own architecture; and the
Claude Desktop repository script is fetched to a kept file and run, never
piped from the network into root.

The hardware predicates stop guessing: a wireless mouse's scope=Device
battery no longer turns a tower into a laptop, USB-PD-only machines read
their power state from the battery's own status instead of being permanently
"on AC", the lid falls back to logind's LidClosed where ACPI is silent, and
charge limits reach every pack of a two-battery machine in one authorization
-- with the reported percentage summed across packs.

And the parsers stop assuming this machine: snapper is read through
--machine-readable csv with named columns instead of a localized box-drawing
table, and reports whether snapshots are even possible so ext4 and
unconfigured-btrfs stop looking identical; fprintd is parsed under LC_ALL=C;
the hypridle drop-in resolves the binary it points at; the recorder's render
node became an "auto" token resolved at record time; update-grub writes the
config its firmware actually boots; the nvm prompt hook and the SSH tmux
takeover are guarded; hipblas and rocm-opencl move to an opt-in gpu-compute
category; and the two interactive python tools' libraries are declared.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 12:10:03 -04:00
Gabriel Brown 44124d72fa Take one machine's fingerprints off everyone's desktop
The audit's second tier: values that were measurements of the author's
desktop, shipped to every machine as if they were defaults.

Settings greeted every human as Gabriel; it now greets whoever
accountsservice says is signed in, and nobody when it says nothing. The
weather shipped his home coordinates and confidently reported his forecast
anywhere on earth; it now ships unset, fetches nothing until a location is
chosen, and the location row says so. The GTK bookmarks carried seven
/home/gib paths and his file server into every file dialog; they are now
generated per machine from a template and gitignored -- Nautilus edits the
instance freely, the way settings.ini already worked one file over. Web
search routed through his personal bang redirector; the engine is now the
webSearchUrl preference with a DuckDuckGo default, read by both the script
command and the suggestions extension, which the launcher-search contract
already pins to one another. The GPU vitals path defaulted to his card1 and
lost the readout on any machine enumerated differently; a machine with
exactly one GPU now adopts it. And the Containers and Snapshots pages hide
once a scan proves their backing stack absent, instead of rendering
permanently empty on machines that never had podman or snapper.

Lesser residue swept in the same pass: the DP-2 hyprpaper block one machine
needed, the author's username-typo expansions (moved to his personal seed in
user/, where personal content belongs), a capture fallback into /home/gib,
and a parity table asserting one machine's hardware as fact.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 11:55:43 -04:00
Gabriel Brown 3d21e20041 Make every control tell the truth
The audit's first tier, in one change: every case found where the interface
asserted something the system did not do.

Twenty-one compositor-owned preferences -- the whole Mouse & Touchpad page,
plus window layout, snapping, dim-inactive and the magnifier -- had the live
half (hyprctl eval) and not the config-time half, so they quietly reverted on
every hyprctl reload. All 70 hypr-backed keys now have a prefs.get() in the
Lua, and hypr-prefs-contract pins both the presence and that the Lua fallback
equals the schema default, which is how the touchpad page misreported natural
scrolling on first boot.

The idle generator fell back from unwritten battery keys to the AC values
while the Power page displayed the schema defaults: a fresh laptop showed
"suspend at 20 minutes" and generated no suspend listener, then discharged to
zero in a bag. Unwritten keys now use the defaults the page shows
(idle-defaults-contract pins generator to schema; idle-config-contract
re-pinned to the new rule with the tradeoff recorded), and change-settings
enables managed idle on any machine with a battery -- without starting
hypridle in whatever session the installer runs under.

The per-app lock-screen notification switches wrote fields nothing read:
hyprlock cannot render notifications. Removed, with the rule model shrunk to
{enabled}, stale stored fields dropped at normalization, and the contract now
forbidding the page from growing lock-screen switches it cannot honor.

The battery warning thresholds were searchable, documented as "Found on
Power & Lock", and rendered nowhere -- and crossing the low threshold changed
only a glyph's color. Both sliders now exist where search was already sending
people, and low battery publishes a real notification at important priority.

Three handoffs opened GNOME panels that are inert in a Hyprland session. The
keyboard handoff is gone (that panel writes gsettings nothing here reads, and
the working controls sat on the same page); Connectivity gains a Wi-Fi row
that opens GNOME's actual Wi-Fi panel -- hidden SSIDs and 802.1X finally have
a road -- beside the network row that legitimately drives NetworkManager; the
universal-access handoff is gone, its few working toggles being controls this
app already owns. And the accessibility page now gives the true reason sticky
keys are missing: each Wayland compositor implements its own and Hyprland
does not yet -- not "an X11 feature with no Wayland equivalent," which sent
people to the wrong conclusion about the platform.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 11:43:39 -04:00
Gabriel Brown dfc0c49877 Let the laptop say what it is doing: battery percentage, the lid, a fingerprint
Three surfaces the first laptop install showed were missing.

The bar's battery icon gets an optional exact number beside it -- GNOME's
"Show Battery Percentage", off by default for GNOME's reason, one color with
the icon so it reads as one indicator.

The Power page says what closing the lid does. The policy already existed
(LidPolicy holds a suspend inhibitor while an external display is connected)
but was surfaced nowhere, so the machine's most physical behavior was
undiscoverable -- and the deliberate absence of an override deserves stating
rather than leaving someone to hunt for a switch that does not exist.

And the Users page grows a Fingerprint card, because fingerprint login is two
systems that fail silently when they disagree: fprintd holds the enrolled
prints, authselect decides whether PAM ever asks the reader. This machine
arrived with a finger enrolled from its GNOME days and with-fingerprint off,
which reads as "the reader is broken". The card shows both facts, flips the
authselect feature through polkit with a stated reason, and hands enrollment
to GNOME's Users panel, which owns the only good capture dialog -- a named
exception in the handoff contract. Everything through scripts/panama-fingerprint,
pinned by a stub-driven contract.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 11:07:37 -04:00
Gabriel Brown c2eddc2c5b Ask for the password once, and own the plugin the COPR needs
Two ways a fresh machine's unattended run could still stall. The stages call
sudo dozens of times across twenty-plus minutes, and the timestamp expires
five minutes after whichever call came last -- so any single dnf step that
outlasted it turned the next stage into a password prompt nobody was there to
answer. ./install now authenticates once, right after the interview, and a
refresher holds the timestamp for exactly as long as the script lives;
cleanup() kills it on every exit path, so nothing outlives the install with
ambient credentials.

And `dnf copr` comes from dnf5-plugins, which a machine that has used copr
carries and a fresh Workstation does not -- a missing plugin there would have
cost the entire desktop. Declared in initial-packages, where the repositories
stage can rely on it.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 11:07:37 -04:00
Gabriel Brown 6ad2e017e8 Decide how to log out by what the session is, not what is installed
The power menu's Log Out ran `uwsm stop` whenever the uwsm binary existed --
which is always, since Panama installs it. In a plain "Hyprland" session there
is no uwsm unit to stop, so the command failed and the button silently did
nothing. Nor is `uwsm check is-active` the test: it reads
graphical-session.target, which the plain session also activates. The one
thing unique to a managed session is uwsm's own compositor unit,
wayland-wm@*.service, so that is now what chooses between stopping the unit
and asking the compositor to exit. A new contract pins the decision -- and
checks the script line rather than the file, because the comment explaining
the wrong tests has to be allowed to name them.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 10:38:09 -04:00
Gabriel Brown 12f6b2a310 Let the contracts assert properties, not the machine they were written on
The first run of the suite on a laptop found five contracts asserting the
desktop instead of the code. settings-system pinned DP-2 at 4500x3000 in
XRGB2101010; it now asks Hyprland what is actually primary. ssh-keys hardcoded
id_ed25519; it now uses whichever key exists. switcher's live half stepped a
session with one window, which step() deliberately refuses. displays raced the
service's revert readback -- the compositor looks restored while verification
still holds busy, so an immediate apply was refused with its error already
cleared; the harness now exposes settled and the contract waits for it.

declared-dependencies gets an OPTIONAL list for docker: the aliases serve
machines that run Docker deliberately, Panama's runtime is rootless podman,
and a missing docker fails by naming the command, which is loud enough.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 10:32:17 -04:00