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

262 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```sh
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
```sh
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:
```sh
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.