Files
Panama/docs/superpowers/specs/2026-08-23-panama-update-command-design.md
T

11 KiB
Raw Blame History

Panama: one command to update a machine

Why now

Updating a machine currently takes three commands, and one of them asks the full first-install questionnaire every time:

panama update      # actually: commit & push my edits, or pull if clean
panama migrate     # the repairs
panama upgrade     # ./install, interview and all

Nobody will do that routinely, which means machines drift. The interview in particular is pure ceremony on an already-configured machine: it asks about NVIDIA, Secure Boot, git identity and extras categories, none of which have changed since the machine was built.

What is broken

1. The interview runs on every upgrade, and nothing needs it.

Five of the seven stages read no interview answer at all:

Stage Reads from the interview
install-packages PANAMA_EXTRAS only, for the optional categories. Empty means base lists still install.
link-dotfiles nothing
link-user PANAMA_USER_CONTENT, and falls back to the decision recorded at $XDG_STATE_HOME/panama/user-content
change-settings nothing
link-vicinae-scripts nothing
setup-identity git name/email/editor, gh login, ssh key — genuinely first-run
install-hardware NVIDIA, MOK hash, debloat, firmware — genuinely first-run

link-user already anticipates this in its own comment: "Running this stage by hand outside an install honours the recorded answer instead, so panama upgrade on an already-configured machine does not need re-asking." The capability is there; install:91 just calls the interview unconditionally with no way past it.

2. panama update is named for the wrong job.

It means "review, commit & sync local changes" — a git workflow command. README and config/dot/quickshell/manual/05-making-it-yours.md both document it that way. The command a person reaches for to update their machine is therefore the one command that does not.

3. Most updates need no stages at all, and there is no way to tell.

Dotfiles are symlinks into the checkout, so an edit to any existing config/dot/** file is live the instant git pull returns. A pull only needs stages when it brings something structurally new: a new config/dot/<name>/ directory, anything under config/copy/, a new package, or a gsettings change. Today the only way to be sure is to run everything.

Decisions

  1. panama update becomes the machine-update command. The git workflow moves to panama sync, unchanged in intent. One verb per job; nothing branches on state the user cannot see.
  2. install gains --upgrade, which skips the interview and trims STAGES to the five that need no answers. One stage list in the repository, so the two paths cannot drift.
  3. install-packages runs when a content hash of setup/packages/** differs from the recorded one. Not a git range: Panama is developed in place, and a hand-edited uncommitted package list must still trigger an install.
  4. A dirty tree is stashed, pulled over, and popped. The machine update is never blocked by uncommitted work.
  5. A conflicted pop resets the tree, keeps the stash, warns, and continues. Conflict markers must never reach a live config; the update proceeds against the clean upstream checkout.
  6. The migrate notification is untouched. It says "repairs are waiting" and it will keep doing exactly that.
  7. panama sync commits before pulling, so it needs no stash at all.

Design

Command surface

Command Job
panama update Bring this machine up to date. The routine command.
panama sync Commit & push my edits. Today's cmd_update, renamed and simplified.
panama upgrade Full ./install, interview included. New machine, or to change an answer.
panama migrate Repairs only. Unchanged; still what the login notification runs.

panama update

1. Record pre-pull HEAD.
2. Dirty tree?  git stash push --include-untracked -m "panama-update-<epoch>"
3. git pull --ff-only          (failure is reported, never fatal)
4. Stashed?     git stash pop
                on conflict: git reset --hard HEAD
                             leave the stash in place
                             record a warning for the summary
5. exec install --upgrade

Steps 14 are the only new logic in panama update; the packages gate below is the only new logic in install. Everything after step 5 is install's existing tail.

The pull is --ff-only. A diverged branch is a thing to resolve deliberately, not something an update command should paper over; it reports and continues to the stages, because the machine should still be brought in line with what is checked out.

install --upgrade

install has no argument parsing today, so this adds the first. cmd_upgrade already forwards "$@", so panama upgrade --upgrade would work but is pointless and undocumented.

--upgrade changes exactly four things:

  • The interview block (install:91-98) is skipped. No PANAMA_ANSWERS file is created, so every PANAMA_* answer is unset and each stage takes its documented empty-answer path.
  • hostnamectl is skipped — there is no answer to apply.
  • STAGES drops setup-identity and install-hardware.
  • The closing message says the machine was updated rather than "Panama installed. Log out and choose the Hyprland session to start it."

Everything else is inherited unchanged and deliberately so: the sudo -v keepalive (change-settings and install-packages both need root), the per-stage failure collection, the migrations block, panama-doctor --summary, the post-upgrade hook, and the exit code.

One correction to the migrations block is required. It currently baselines when the marker directory is absent, on the reasoning that a machine with no markers was just built from this checkout. That inference is only sound during a real install. Under --upgrade it must always take run; the migrations are all self-guarding and a no-op on a machine that does not need them.

The packages hash

STATE="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
hash_packages() {
  find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \
    | sort | sha256sum | cut -d' ' -f1
}

install-packages is skipped when the hash matches $STATE/packages-hash.

-maxdepth 1 is deliberate and excludes setup/packages/extras/. An answer-free run has an empty PANAMA_EXTRAS and installs no optional category, so hashing those files would flip the hash, run the stage, install nothing extra, and record the new hash as though it had. Optional categories cannot be re-applied by panama update at all, because which ones this machine chose is never recorded — the interview's answers are transient by design. Adding to a category you already have is panama apps, which is what it is for. This is a known limitation, not an oversight.

The hash is written only after the stage succeeds, mirroring the rule panama-migrate already documents for its markers: a step that did not complete has not happened, and recording it as done hides it forever.

Two consequences worth stating: a full ./install always runs the stage and writes the hash regardless of any recorded value, and the first panama update after this ships will run install-packages once, because no hash exists yet. Both are correct.

panama update --packages forces the stage regardless of the hash, forwarding --packages to install --upgrade. This is the one flag worth adding, because the hash cannot see a package that dnf removed behind Panama's back.

panama sync

Today's cmd_update with the stash dance removed. Because the changes are committed before the pull, there is nothing to stash:

1. Clean tree?  Say so and stop. Pulling is `panama update`'s job now.
2. Show status + diff, including untracked files as new-file diffs.
3. Confirm, read a commit message, git add -A && git commit.
4. Behind upstream?  git pull --rebase
5. Offer to push.

A rebase conflict lands on committed work, which is an ordinary and recoverable place to be, rather than on a stash.

Conflict handling

Verified against a throwaway repository rather than assumed:

  • A conflicted git stash pop keeps the stash entry.
  • Conflict markers are written into the working file. On this repository that means directly into a live ~/.config path via the symlink — a broken .qml can cost the running shell.
  • git reset --hard HEAD clears the markers, and stash@{0} survives it.

So on conflict: reset, keep the stash, and report it in the final summary rather than only in scrollback, where twenty minutes of dnf output will bury it.

! Your local changes conflicted with what was pulled.
  They are safe at stash@{0}: panama-update-1787680000
  Restore with: git stash pop

What does not change

  • bin/panama-migrate — no change of any kind.
  • bin/panama-migrate-notify — still runs panama-migrate run in a terminal.
  • The seven stage scripts — none of them are edited. This works precisely because they already handle absent answers.
  • panama upgrade — same behaviour, same interview.

Files touched

File Change
install Argument parsing; --upgrade skips interview + hostname, trims STAGES, forces migrate run, alters the closing message. Packages-hash gate around install-packages.
bin/panama cmd_update rewritten; cmd_sync added (the old body, minus the stash); dispatcher, usage block and header comment updated.
README.md The upgrade-path paragraph and the command table.
config/dot/quickshell/manual/05-making-it-yours.md The line telling the reader panama update will offer to commit.
tests/setup/ New contracts, below.

Tests

Four contracts, each pinning a decision that would otherwise rot silently:

  1. install --upgrade never invokes the interview. The regression that started this. Assert setup/scripts/interview is not executed.
  2. --upgrade runs exactly the five answer-free stages, and never setup-identity or install-hardware.
  3. The packages hash gates the stage, and is not written when the stage exits non-zero. Note this is a narrow guarantee: install-packages logs and steps over individual package failures internally and still exits 0, so a single missing package does not hold the hash back. Only a stage-level failure does.
  4. A conflicted pop leaves no conflict markers in the tree and keeps the stash. Runs against a temporary repository, as the verification above did.

Contract 2 is the one that matters most over time: it fails the moment a stage is added to STAGES without a decision about which path owns it.

Verification

On this machine, after implementation:

panama test update      # the new contracts
panama update           # expect: no questions, one sudo prompt, packages skipped
panama update           # expect: idempotent, still no questions

The second run is the real check. Everything must be a no-op, and install-packages must be skipped on both.

Out of scope

  • Any change to how migrations work. They are correct.
  • A GUI entry point for updates. The notification covers repairs; a full update is a terminal action because it needs a password and prints a summary worth reading.
  • Rewriting the seven stage scripts. Their empty-answer behaviour is what makes this design a flag rather than a refactor.