05fd5346dbfe8ed4bb6cffa3f614a1afd1cd30a9
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
05fd5346db |
Turn a website into an application, and notice the charger
Two of Section E's small wins. panama-webapp gives a site its own icon, its own window with no browser chrome, and its own launcher entry, which is most of what "installed" means in practice and what both macOS and Windows now ship. It scrapes the site's apple-touch-icon, falls back twice, and never fails an install over a favicon: a web app with a generic icon still works. Names are slugged, so "../../../../tmp/pwn" lands inside the applications directory as tmp-pwn rather than anywhere else, and remove refuses anything without the marker it writes -- sharing a name with a real application must not delete that application. A browser that cannot do app mode is told so rather than handed something that opens an ordinary window and pretends. The charger now announces itself through StatusEvents, which was already the right layer and only wanted a producer. Ambient priority, so Do Not Disturb quiets it, because a charger is exactly what DND is for. A critically low battery is published at a priority DND does not silence, because the one message you must not miss is the one saying the machine is about to stop. Both join the existing silent-startup window rather than announcing the state they found. The keyboard-layout toast the plan also listed is deliberately not built. Hyprland exposes the active keymap but not a change event Quickshell already consumes, so it would need either polling or new event plumbing, and this machine has one layout and could not test it. |
||
|
|
333ab9f6b7 |
Press the browser key twice, get one browser
The application keys ran their command unconditionally, so pressing the browser key twice gave you two browsers. That is the single most common "Linux feels wrong" moment and it is a twenty-line fix: match the window class, focus it if it is there, launch if it is not. Two details the obvious version gets wrong. Patterns are anchored, because an unanchored "mail" matches gmail-notifier and the mail key would raise somebody's notifier. And class alone is not enough here: the terminal and the editor are both kitty, so the editor bind narrows by title or it raises whatever terminal happened to be open. The contract's fixture contains both traps. Found and fixed a bug this shipped with earlier today. Hyprland 0.56 dispatches through Lua, so `hyprctl dispatch focuswindow address:0x...` is parsed as Lua source and fails with a syntax error -- which is what the launcher's Switch Window command had been doing since it landed. Its own contract stubbed hyprctl and recorded the arguments, so the call looked correct and never ran. Both now use the selector form hl.focus actually accepts, and the contract asserts the shape rather than only that something was dispatched. |
||
|
|
50a99a5ad0 |
Closing the lid at a desk is not closing it in a bag
logind handles the lid correctly except for the one case it cannot see: an external display means a closed lid is a docked machine, not one being put away. Its own docked test looks for an ACPI docking station that modern hardware does not have. Panama does not take the lid over to fix that. It holds a logind handle-lid-switch inhibitor while an external display is connected and releases it when the last one goes, which needs no lid watcher, no polling, and no drop-in. The direction it fails in is the point: if the guard dies, logind's default comes back and a docked laptop suspends, which is annoying. A drop-in setting HandleLidSwitch=ignore plus a watcher of our own fails the other way, leaving a lid that does nothing at all on a machine being carried out of a building. Locking on the way down needed no work: hypridle's before_sleep_cmd already runs loginctl lock-session, so a lid-close suspend is a locked suspend. The contract fails anything that duplicates it. Not yet verified against a real lid, which is stated in the helper's header rather than implied by silence. The decision logic, the inhibitor's shape, and every machine that should hold none of it are covered. |
||
|
|
3c359f3f7e |
Notice the battery, and the machine it is or is not in
Panama had no idea whether it was running on a laptop. No upower, no battery, no lid, no AC: hypridle.conf says "This is a desktop" in its own header, and that was true of the code as well as the machine. panama-hw answers hardware questions one at a time, exits 0 or 1, and prints nothing, so scripts, services and contracts all ask the same way. The definition the rest of the laptop work hangs on is one line: clamshell is lid-closed AND an external monitor. A machine with no mains supply at all reports as being on wall power, because a desktop cannot run out of it. The battery service follows Vitals: sysfs through FileView, an availability flag, and no subprocess on the timer. Globbing is the one thing QML cannot do -- a battery is BAT0 or BAT1 or CMB0, mains is AC or ADP1 or ACAD -- so panama-battery resolves the names once and the shell reads the files directly after. Nothing falls back to a plausible zero: a desktop shows no indicator, no card, and no charge limit control where the firmware has no ceiling. Also repairs two contracts that were already failing and had not been noticed, because only the full suite runs them. The dependency scanner treated line-initial variable assignments, case labels, comments and heredoc bodies as commands, and `count`, `host`, `cancel` and `import` are all real binaries on Fedora, so `command -v` could not filter them out. It now drops comments and heredoc bodies and requires a command to be followed by whitespace. Verified it still catches a genuinely undeclared dependency rather than passing quietly. The launcher command contract had not been told about the fourteen commands added earlier today. |
||
|
|
e446a1072c |
Give an installed machine a way to catch up
./install only ever adds. It copies over /, links dotfiles, installs packages -- and has no way to say "remove that file", "disable that unit", "that symlink points nowhere now". So a machine set up months ago keeps whatever this repository has since decided was wrong, and the only thing that ever fixes it is somebody reading a commit message. With a curl installer in the README, that stopped being hypothetical. A migration is one script that performs one repair, exactly once, on the machines that need it. Named by the commit timestamp that authored it, so glob order is chronological without a sequence number two branches could both pick. Marked in ~/.local/state on success and only on success, so a repair that failed stays pending rather than being recorded as done and hidden forever. Ordered, and stopped at the first failure, because a later repair may assume an earlier one landed. A fresh install marks everything without running it, the way Migrations.qml stamps a pre-versioning settings file at its baseline. The first real one removes the dangling ~/.config/forge symlink left behind when the GNOME session was cut: link-dotfiles could link it but never unlink it. Verified both ways -- a no-op on a machine that never had it, an actual repair on one that did. Root work goes through panama-sudo --reason so the password prompt names the repair, and the contract fails any migration reaching for bare sudo. |
||
|
|
c02329ac3c |
Copy a password without leaving a trace of it
The launcher's Copy Password command, built alone and last as the plan required, because every line of it is the security design: the secret travels rbw to wl-copy through a pipe -- never argv, never a file -- and the copy carries wl-clipboard's --sensitive hint, which vicinae's clipboard history documents it ignores. That claim was not taken on faith: a plain probe landed in the live history database and a sensitive one did not, before any of this was written. A transient timer clears the clipboard after thirty seconds. An unconfigured rbw gets a setup message; a vault that locks between list and get gets an honest failure instead of an empty copy claiming success. rbw joins desktop-packages, and the contract pins the whole journey with a stub vault, including that the secret never appears on a command line. |
||
|
|
4e978bf3b7 |
Teach the launcher what an operating system knows
The OS-parity batch from the vicinae plan, tasks 1 through 7. The audit came back better than the plan guessed: the calculator already links libqalculate, the built-in file index answers in under 100ms across all of home, quicklinks and snippets ship as built-in stores -- so zero new packages, and `vicinae dmenu` replaces the planned compiled extension outright. What was missing gets built: a power menu (lock, suspend, log out through uwsm, restart, power off), reminders as transient systemd timers with a pick-to-cancel list, a color picker over hyprpicker, and dmenu pick-lists for window switching, force quit, kill process, SSH hosts, and recent files -- all through one panama-pick helper. The launcher commands contract exercises the reminder parsing and every pick-list against stubs, including killing its own sacrificial sleep. |
||
|
|
51ceb19480 |
One command from fresh Fedora to the front door
`boot` is the script the README now leads with: curl it, and it installs git if the machine lacks it, clones the repository to PANAMA_PATH, and hands off to ./install -- reattaching the terminal first, because a piped stdin would strand the interview. Deliberately dumb: a curled copy leaves the repository the moment it runs, so nothing that can drift lives in it. Re-running is the recovery path: an existing clone is fast-forwarded, never re-cloned, and a refused fast-forward installs from what is there rather than stopping mid-repair. All of it pinned by the boot contract, against stub git and a throwaway clone. |
||
|
|
bd9a55c8eb |
Fail when failing, stop when stopped, and survive what is neither
Four installer bugs, all in the space between exit codes and intent: - A flatpak-only extras category -- most of them -- died at the grep that filters out its dnf half, because grep exits 1 on zero matches and set -e read that as failure. sed deletes lines without editorial comment. The extras contract now runs a flatpak-only category under the installer's own strict options so this stays fixed. - A rate-limited GitHub API call aborted the whole package stage while resolving the RustDesk URL, even though the empty-result fallback was sitting right below it. The pipeline is now guarded so the fallback is reachable. - Ctrl-C did not stop the install: the INT trap ran cleanup and bash carried on with the remaining stages, MOK enrollment and firmware included. INT and TERM now exit explicitly; cleanup rides EXIT. - change-settings and link-dotfiles ran without set -e, so a failed copy over / or a failed symlink fell through to guarded no-ops and the stage reported success. Turning strictness on immediately caught what it had been hiding: link-dotfiles never created ~/.config, so on a truly fresh HOME every symlink was failing silently. |
||
|
|
89417cd6d4 |
Take Claude Desktop from the repository that now carries it
It was `panama app claude-desktop`: a source build, because nothing packaged it. Upstream publishes an RPM repository now, so the exception shrinks to adding that repository -- and the result upgrades with `dnf upgrade` along with everything else instead of needing a slow rebuild every time a version ships. That moves it out of setup/apps/ and into the third-party section of install-packages, beside Bun, Claude Code and RustDesk. It also means it is part of ./install for the first time. The reason it was kept out was the build: slow, wants the network throughout, and fails on an upstream that moved, which is the failure the interview exists to prevent. A dnf install is none of those, and the default dock pins com.anthropic.Claude, so a fresh machine was shipping a pin for something the install had never put there. The repository is added with upstream's own setup script rather than a .repo file written out here. A baseurl copied into this repository is a pin by another name; the script is the part upstream keeps correct. Both halves are skipped when already present, so a re-run costs nothing. The desktop entry id does not change -- it is still com.anthropic.Claude -- so the dock pin and the notification rules carry over untouched. The new package drops the nodejs dependency, which is what used to hide the missing-npm bug the launcher search contract guards; its comment said "depends" in the present tense and now says what actually happened. |
||
|
|
185d7edaa5 |
Open the windows you open together, together
Arrange the desktop, run "Save Layout as Project" from the launcher, name it. "Open Project" lays it out again on free workspaces, so it never lands on top of what you are already doing. Saved layouts are listed on the Desktop settings page, which is where they are removed. Recorded rather than written by hand, and a terminal's directory is most of why it is worth having: without it a project opens three terminals in your home folder and you change directory three times. This machine had two terminals in the same project when it was written, and reopening there is the difference between a layout and a working desktop. Four things had to be found by running it, none of which reading would have shown. A terminal's directory is not the terminal's working directory -- that is where it was launched from. The shell inside it is what has been cd'd. Reading the wrong one looked correct for exactly as long as the terminals under test had been started from the right place, which they had. gtk-launch cannot place a window. It activates over D-Bus, so the process Hyprland started exits and a [workspace N silent] rule has nothing left to apply to; Nautilus landed on whatever workspace was in front of you. The Exec line from the desktop entry is run directly instead. But DBusActivatable applications do the same thing regardless, so the window is found afterwards and moved by address -- which also means a window that never appeared is reported rather than assumed. /proc/PID/task/PID/children is a file of pids, not a directory. Listing it as one always raised, so the fast path was never once taken and everything went through pgrep. And kitty's --directory needs an equals sign or the short -d; the space-separated long form is accepted and silently ignored. The contract exercises a save and open round trip against a stubbed compositor, and reads a terminal's directory out of a real process tree rather than grepping the source for a shell name -- an earlier version passed against a helper that had been changed back, because the constant was still there. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
033d5b21f8 |
Build the launcher's extension on a machine that has only nvm
The search extension is compiled, and the stage that compiles it checked for npm and skipped quietly when it found none. On this machine it always found one -- but only because Claude Desktop depends on nodejs and dragged a system npm in. Nowhere else would. Node moved to nvm when the shell config turned out to have been assuming it for months, and nvm is a shell function in a file only an interactive shell sources; a stage is not one. So a fresh install would have set up the launcher, printed one line about extensions not being built, and left somebody wondering why typing in it suggested nothing. The stage sources nvm before looking, and the contract pins that it does -- checking for npm is not the same as being able to find it. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
7cd4131327 |
Hold a key, speak, and the words are typed
Super+D holds the microphone open, releasing it transcribes on the GPU and types the result wherever the cursor is. Roughly 150ms for a normal utterance once the model is resident, measured rather than hoped for. Getting there meant discarding two approaches. Fedora 44 cannot install any GPU-capable Whisper for Python -- openai-whisper needs a numba that needs an llvmlite that does not exist for 3.14, and faster-whisper needs a ctranslate2 nobody packaged. The whisper-cpp package IS built with HIP but ships libraries with no binary and no bindings, and hand-writing ctypes for a large by-value struct is a segfault waiting for a version bump. So a container, as suggested. Vulkan rather than ROCm, and upstream's image rather than one built here. ROCm is seven gigabytes and serves AMD alone; Vulkan compute runs on the AMD, Intel and NVIDIA machines this config is used on, in a twentieth of the space. The Vulkan tag already contains whisper-server, so there is no Containerfile to keep working -- an earlier draft of this commit had one, and it was strictly worse. Two bugs found by using it rather than by reading it. Whisper describes silence as the literal text "[BLANK_AUDIO]", and the first working version pasted that string into the clipboard; a transcription that is nothing but such markers is now discarded. And the server answers with a line per segment, which typed into a window is an Enter press -- sending the half-written message, submitting the form. Whitespace is collapsed to one line. Neither the image nor the model is installed by ./install. Together they are over two gigabytes that want the network, and Settings offers both as one action instead. Nothing starts at login either: whisper-server holds the model from the moment it starts, so the first press of the key is what brings it up. The contract pins both text bugs, that the server stays on loopback, and that it does not start at login. Reverting the [BLANK_AUDIO] guard did not fail it at first -- the check was still correct, it had simply stopped being called -- so it now checks the call site too. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
9092a80f66 |
Search from the launcher, and give the touchpad something to do
Four things a Hyprland desktop can do that this one was not. Searching from the launcher needed no launcher work at all: Vicinae already models it, so this is a script command with one percent-encoded argument. Make it the fallback command and anything typed that matches nothing else offers to search it. Bangs come free -- they are a property of where the query is sent, not of the launcher -- so !yt reaches YouTube without a line of bang parsing. Suggestions could not be a script command. They need a view that reacts as you type, which is an extension: TypeScript, compiled, querying the same endpoint Firefox's address bar uses. It debounces, and aborts the request in flight on every keystroke -- typing is faster than the network, and an older answer landing after a newer one leaves the list describing a query that is no longer on screen. A bang skips suggestions entirely, because Google has no useful guesses about "!yt". The engine is now written down twice, once in each. The contract pins that they agree, since searching from the fallback and searching from the suggestions reaching different places is the kind of wrong that looks fine. Gestures mirror GNOME: three fingers sideways for workspaces, up for the overview, down to dismiss it. Open and close rather than toggle both ways -- toggling means swiping up from an open overview closes it, which is not what the fingers meant. Hyprland reads gesture registrations at startup so they cannot be a setting, but distance and direction can be, and are. Window swallowing is off by default and a preference like every other misc setting here. A terminal that vanishes when you did not ask for it is confusing rather than broken, which is worse. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
f09763ef5d |
Check the two facts the README states about itself
It claimed 121 contracts when there were 125, one day after the number was written, and it documented every panama subcommand except the one added last -- so `panama apps` existed and the README did not mention it. A number in prose is worth something as a claim somebody relies on and nothing once it is wrong, so it is either checked or it should not be there. This checks it, counted the way the runner collects the suite rather than by a second idea of what a contract is, and checks that every subcommand the README documents is one the dispatcher actually handles -- a listed command that errors reads as a broken install rather than a stale document. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
215da285f3 |
Let applications be chosen a few at a time
The catalog held fourteen applications. This machine runs thirty-four flatpaks, so most of what is actually used had no way to be installed from here at all -- Zoom, Slack, Obsidian, Spotify, LibreOffice, OBS and its sixteen plugins. So the catalog is seeded from the machine, and `panama apps` opens it: pick a category, tick what you want, install just those. ./install still offers the same catalog as whole categories, because during a first install you want coarse and fast. Both read setup/lib/extras-catalog. Two parsers would eventually disagree about what a category contains, and the one that disagreed quietly would be the one that runs unattended. Two pieces of syntax earn their keep. A `| Name` suffix gives the menu something readable, since com.obsproject.Studio is not a name anybody wants to pick from a list. An indented line belongs to the entry above it, which is how OBS carries its plugins as one thing to tick rather than seventeen -- they are extensions of the flatpak, useless alone. That is also why creative moved from dnf to Flathub: the plugins attach only to the flatpak, so the dnf build cannot have them. The rest of the category followed rather than leave one machine with GIMP from dnf and its neighbour from Flathub. The contract now reads the catalog through the same parser instead of keeping a third idea of the format, and checks the two things this syntax can break silently: a label leaking into an install command, and a bundle that installs the application without its plugins. It caught a typo in the Pixelorama id on the first run. It also got slow enough to be worth fixing -- fifty-one names, each its own network call. One bulk query per manager took it from minutes to four seconds. That query needs `flatpak remote-ls --all`: without it, end-of-life applications are hidden and read as missing, which reported yuzu as gone from Flathub when it installs perfectly well. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
f457c1eb9f |
Build the two applications nobody packages, on purpose rather than in passing
Claude Desktop and ChatGPT Desktop ship for macOS and Windows. The Linux path for both is a community wrapper that converts the official build into an RPM -- so what lands is still a package dnf owns and can remove, which is the part of the dnf/flatpak rule that actually matters. What they need an exception for is the build itself, and there is no packaged form to prefer over it. `panama app` builds one by name, and is deliberately not part of ./install. A source build is slow, wants the network throughout, and depends on an upstream that moves -- twenty minutes in, an error, with nobody at the keyboard, which is the exact failure the interview exists to prevent. Asking for one is something you do on purpose, and it is also the rebuild path when a new version ships. Nothing is pinned. Each build takes the current default branch and the current upstream release, and reports a failure rather than working around it, leaving the tree where the error can be read. sunhat pinned versions and every pin was a 404 within a release cycle. Adding one is adding a file to setup/apps/, and the file has to say why the exception exists -- the contract fails a definition that does not, because the guard against this list growing by habit is having to write the reason down. sunhat had seventy-odd installers and a reason recorded for none of them. The contract had a bug worth recording: `while read` on the right of a pipe runs in a subshell, so two of its three per-definition checks recorded findings into an array that went out of scope at the end of the loop. It reported PASS on a definition with no description and no build function. Found by standing one in deliberately and noticing only the third check spoke up. Also: nautilus-open-any-terminal is now declared, and Panama's copy of the extension is gone. Fedora packages that extension AND its gsettings schema, and Panama shipped its own fork of the .py over the same path while declaring neither -- so a fresh machine got an extension whose schema did not exist. It worked here only because the RPM has been installed since sunhat. The fork was also 63 lines behind the packaged version, missing its newer Nautilus and Caja handling. Auditing the rest of config/copy for the same shape found nothing else: dnf.conf is a config file its package expects to be replaced, and the GPU udev rules are Panama's own. 125 contracts pass. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj |
||
|
|
88497826ec |
Let a machine say what it is for, and give Firefox its face back
Phase 4: the optional application categories, and the Firefox chrome. Everything Panama installed until now was what every machine gets, which meant a work laptop acquired emulators and a desktop that wanted Steam had to be told about it by hand. The interview now offers the categories in setup/packages/extras/ as a checklist -- gaming, creative, communication, virtualization -- and nothing is preselected, because a default here installs applications nobody chose on a machine whose owner answered a question they thought was about something else. A category is one file, and a category mixes both package managers because the applications do: Steam is in RPM Fusion, Slack publishes only a flatpak. So a bare line is a dnf package and a flatpak: line is a Flathub ID, and one file holds the whole answer rather than splitting each category across two. The menu is read from the directory rather than written down, so adding a category is adding a file. Every name in all four was resolved against the actual repositories before being written down, and the contract re-resolves them -- the point of admitting applications one at a time is that they stay installable, and a typo here fails on somebody else's machine, not this one. Firefox is declared, and its chrome is Edge-Frfox, vendored into config/firefox. sunhat carried that theme with no license and no attribution; it is MIT, and now it says so and says whose it is. It is the only piece of Panama's configuration that does not go to a path this repository chooses. Firefox owns the profile directory, names it with a random salt, and does not create one until the browser has been run -- so link-dotfiles finds or creates a profile and links both halves into it. Both, or neither works: chrome/ is the CSS and user.js sets the preference that makes Firefox read chrome/ at all, without which the theme is a directory of dead files. Two assumptions there were wrong, and the contract exists for both. Firefox has moved to the XDG directories -- the profile root is ~/.config/mozilla/firefox on this build, not ~/.mozilla/firefox, and writing to the wrong one themes nothing and says nothing about it. And -CreateProfile turns out to be non-interactive, so a fresh machine gets the theme on the first install rather than the second. The contract runs link-dotfiles for real against a throwaway home with no profile in it and looks at what came out; it was checked by pointing the search at the legacy path only and watching it fail. Also: the enrolment/enrollment spellings from the last commit are corrected. This repository is US-spelled everywhere else -- color 1131 times against colour never -- and consistency in prose is worth as much as it is in code. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj |
||
|
|
b319d1a5e1 |
Stop handing dnf the comments that explain the package lists
Every list in setup/packages/ is annotated -- which package exists for which settings page, why an exception was made -- and install-packages passed the whole file to dnf, comment lines included. dnf does not ignore an argument it cannot match. It reports "No match for argument: #" and exits 1, and with set -euo pipefail at the top of that script the first annotated list ends the stage. initial-packages carries four comments and is the first list read, so a fresh machine got the repositories, the group updates, and then nothing. Two things hid it. On a machine that already has everything, a re-run matches every real name and fails only on the comments, so the failure looks like noise rather than the stage dying. And every contract that reads these lists strips comments with sed before comparing -- the tests were reading a file the installer was not, which is why a repository with a dependency contract, an assets contract and a doctor still reported PASS across the board. The fix is one filter used at all five call sites. The contract lifts that filter out of the script and runs it, rather than describing what it should do, so deleting or renaming it fails here instead of passing quietly. It also checks the inverse -- that stripping comments does not strip packages -- because trading a loud failure for a silent one would be worse than the bug. Found while adding the extras lists for phase 4, which are annotated the same way and would have hit the same wall. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj |
||
|
|
13f3648e4d |
Install the driver, enrol the key, and still never ask twice
Phase 3 of the fresh-install spec: the parts of a run that depend on what the machine actually is. NVIDIA, Secure Boot, Fedora's preinstalled extras, firmware. Two of these looked like they would force a compromise, and neither did. sunhat opened an editor in the middle of its run so grub could be hand-corrected, and that single step is why walking away from an install did not work. The step existed to delete duplicated kernel arguments -- and grubby replaces an argument that already exists rather than appending a second copy, so the duplicates cannot accumulate and there is nothing to correct. The editor was load-bearing for a problem that a different tool does not have. MOK enrolment needs a password now and the same password at the next boot's blue screen, which reads like a prompt that has to happen mid-run. mokutil has --generate-hash and --hash-file for exactly this: the interview asks, hashes it on the spot, and records only the hash. The plaintext never reaches the answers file, the environment, or a command line, and the stage runs without asking. The stage runs last rather than fourth as the spec's table had it. The constraint was always "late" and fourth of eight is not late: enrolment arms a prompt for the next boot and firmware may want a reboot, so a machine that reboots out of this stage should already be completely configured. Every question names what was found -- the card, the packages actually installed -- and is not asked at all on a machine it would do nothing to. sunhat's debloat list no longer describes Fedora 44: totem became showtime and LibreOffice is not preinstalled, so the list is curated and a package that is not installed is never passed to dnf, which is what lets it outlive a release. This stage cannot be verified by running it. It installs a proprietary driver and queues a Secure Boot enrolment, and this machine is an AMD desktop. So every privileged command is stood in on PATH and the contract asserts which answer led to which call: that no answers means no commands, that a failed driver install is not followed by arguments and services for a driver that is not there, that the hash reaches mokutil through a file and never a command line, and that removal is offered only for packages that are installed. The contract was checked by breaking the stage three ways and confirming it caught each. It does not verify that akmod-nvidia builds, and says so where a reader would otherwise assume it did. The README's stage table listed three of seven stages; the interview and identity work never reached it. Corrected rather than extended, since a table that lists three of seven is worse than one that lists none. The Desktops section still describes a GNOME session nothing installs -- that is phase 5. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj |
||
|
|
15d54b16f6 |
Ask everything first, then run without needing anybody
sunhat's failure mode was a question twenty minutes into a run. Walking away from an install meant coming back to a prompt that had been waiting an hour. So the questions move to the front. A new interview stage asks what Panama needs to be told -- hostname, git identity, whether to sign in to GitHub, whether to make an SSH key -- shows the answers back, and asks once to proceed. After that nothing asks again. gum is bootstrapped before it runs, because the interview is built on gum and gum arrives with a stage that has not run yet. Answers reach the stages through a mktemp file that install sources and the existing trap deletes, since a child process cannot export into its siblings. They are not remembered between runs: there is no state file to go stale, and one of the answers is an email address. The interview asks only what a stage in this repository actually consumes. Extras, hardware and debloat questions arrive with the stages that act on them -- a prompt whose answer nothing reads is a control that lies. The new contract pins that in both directions, and four deliberate mutations confirmed it catches a question nobody reads, a stage reading something nobody asks, an answers file left on disk, and a declined interview that fails to stop the run. The run now ends with panama-doctor, because a failed-stage count says nothing about a service that did not start. It never changes the exit code: on a fresh machine, unconfigured is the honest answer, not a failure. espanso and oh-my-posh stop being exceptions -- Terra packages espanso-wayland and Fedora packages oh-my-posh, so the curl installer is gone. bun is now the only remaining one. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |