From e777ddca34f5ea8040d9905c55b853a7089d6682 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Tue, 25 Aug 2026 18:54:55 -0600 Subject: docs(spec): split the live upgrade and hold the kernel for a dedicated session I rewrote the guarded-upgrade spec around a script the maintenance panel calls: pacman -Syu --ignore on what the live-update guard would block plus the kernel set, then the AUR remainder and topgrade with the system step off. The boot oneshot now completes only the deferred GPU set. Kernels wait for a session I choose, behind a gate on the DKMS build, the initramfs, and a pre-pacman snapshot, because on velox a failed zfs-dkms rebuild after the swap can't be aborted and leaves the new kernel unbootable. All seven decisions are closed. Also closes the ratio Emacs 31 task (upgraded, verified on Wayland) and files gcalcli in the installer. --- .../2026-08-25-topgrade-guarded-upgrade-spec.org | 112 +++++++++++++++------ todo.org | 102 +++++++++++++++---- 2 files changed, 162 insertions(+), 52 deletions(-) diff --git a/docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org b/docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org index c2461be..d9ec8d4 100644 --- a/docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org +++ b/docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org @@ -8,6 +8,8 @@ :PROPERTIES: :ID: 81cdfd72-db96-43d3-aa03-779878c99f3e :END: +- [2026-08-25 Tue @ 18:45 -0600] decisions closed 7/7. The kernel decision reversed on the velox DKMS failure chain: held on every everyday run, landed only in the dedicated session behind a DKMS/initramfs/snapshot gate. +- [2026-08-25 Tue @ 18:30 -0600] redirected: the everyday path is a live split upgrade (apply everything the guard would not block, defer the rest); the boot-time oneshot becomes the completion step for the deferred set. Decided while running exactly that by hand on ratio. - [2026-08-25 Tue @ 06:39:42 -0600] drafted. Grounded in a live read of the maint engine, the pacman hooks, and the boot path on velox, not memory. The topgrade-freshness diagnosis that motivates it is in this session's log. * Metadata @@ -45,21 +47,23 @@ Two framings of the fix are in tension, and choosing between them is the spec's ** Non-Goals - Weakening or bypassing the =hypr-live-update-guard= hook. It stays exactly as strict; this builds *around* it, not through it. - Making the full topgrade ecosystem sweep (git repos, vim, npm, ...) run at boot. Those never need a stopped compositor and are out of the boot path. -- Solving the live-kernel-upgrade hazard (running-kernel modules vanish until reboot). Related, not guarded by this hook, and deferred. +- Changing how the kernel hazard is *guarded*. The hook stays silent on kernels; the split script holds them back on a live run as a second, separately-reasoned list (see Design), which is a deferral policy rather than a guard. - A general offline-update system for all of pacman. Scope is the guarded-library case. ** Scope tiers -- v1: stamp the safe-completion path; an armed, bounded, non-blocking boot-time =topgrade --only system= unit installed by archsetup; an arming affordance in maint that fires when guarded libs are pending. -- Out of scope: full-sweep-at-boot; kernel-reboot coordination; touching the guard's policy. -- vNext: fold the same arm-and-reboot pattern into a live-kernel-upgrade prompt (log to =todo.org=). +- v1: the split-upgrade script (live: apply the non-blocked remainder, defer the rest, run the ecosystem sweep with the system step off, report the deferred set); maint's UPDATE/TOPGRADE levers route through it; an "apply on reboot" affordance that installs the held kernel live and arms the boot-time oneshot for the GPU/compositor set. +- Out of scope: full-sweep-at-boot; touching the guard's policy. +- vNext: none open — the kernel deferral that was vNext is now part of v1's held set. * Design The shape follows one principle: the only part of topgrade that needs a stopped compositor is its =system= step when a guarded library is pending. Everything else runs fine live and rarely fails. So the safe path is small and targeted — apply the guarded system upgrade with Hyprland down, once, and stamp it — while the ordinary full sweep stays a normal live =topgrade= run. -Three pieces, at two altitudes. +Three pieces, at two altitudes — but the everyday gesture is not a reboot. It is a normal live update that simply leaves the dangerous few behind. -*For the user.* When the maintenance panel sees a guarded library in the pending set (the exact condition that fails a live topgrade), it offers a new action: "apply on reboot." Choosing it arms a persistent flag and offers to reboot. On the next boot, before the autologin shell starts Hyprland, the guarded upgrade runs in the console — the guard passes freely because nothing is live — the stamp is written, the flag is cleared, and boot continues into the session. No second reboot: the libraries are already current before anything maps them. If anything goes wrong, the machine still boots into Hyprland and the panel still shows the pending upgrade, so you are never worse off than before arming. +*The split script.* A pacman =PreTransaction= hook can only abort or allow the transaction it is handed; it cannot drop targets from it. So "upgrade everything except the guarded set" cannot live in the hook — it lives one layer up, in a script the panel calls. On a live run the script: refreshes the sync db and reads the pending set (=checkupdates=); computes the *blocked set* = the guard's own trigger list (read from the installed hook's =Target= lines, so there is one source of truth, and version-aware the way the guard is — a same-version reinstall is not a swap) plus the *kernel set* (every installed kernel with its =-headers=, always as a set; held on every everyday run because a failed DKMS rebuild on velox's ZFS root leaves the machine unbootable — see the kernel decision); clears the news hook (=informant read=) where installed; runs =pacman -Syu --noconfirm --ignore==; runs the AUR-only remainder (=yay -Sua --noconfirm=, AUR packages pinning a guarded version hold themselves back); then runs =topgrade --disable system,git_repos -y= so the other ecosystems still get their sweep and topgrade can actually exit 0. It writes the deferred set to a state file the panel reads, and exits 0 when the live part succeeded, whatever was deferred. The guard hook stays installed as the backstop for a bare =pacman -Syu= typed at a shell; on the driven path it never fires. Proof of concept: this exact sequence, run by hand on ratio on 2026-08-25 while Hyprland was live, resolved 724 of 730 pending packages (Emacs 31.1 among them) with the six guard hits deferred — after one unrelated fix, an orphaned =qemu-block-gluster= that had been dropped from the repo. + +*For the user.* UPDATE and TOPGRADE on the panel run the split script; they succeed, and the panel shows "N deferred" when the script held anything back. Landing the deferred set is a dedicated session, chosen on purpose, run in the foreground from the panel's action or =guarded-upgrade --complete= in a terminal: first the kernel set, live, with the desktop still up; then the gate — every DKMS module built for the new kernel, a fresh initramfs, and on a ZFS root a pre-pacman snapshot to fall back on. If the gate fails the script stops there, names what failed, and does not reboot; the machine keeps running on the old kernel and the desktop is available for the fix. If it passes, the script arms a persistent flag for the GPU/compositor set and offers to reboot (or, from a TTY with no compositor, applies that set directly). On the next boot, before the autologin shell starts Hyprland, the deferred guarded upgrade runs in the console — the guard passes freely because nothing is live — the stamp is written, the flag is cleared, and boot continues into the session. No second reboot: the libraries are already current before anything maps them. If anything goes wrong, the machine still boots into Hyprland and the panel still shows the pending upgrade, so you are never worse off than before arming. *For the implementer.* A persistent arm flag (a file on a non-tmpfs path, e.g. =/var/lib/archsetup/apply-upgrade-on-boot=, so it survives the reboot the =/run= guard sentinel cannot). A system oneshot, =archsetup-boot-upgrade.service=, =ConditionPathExists= on the flag, ordered =Before=getty@tty1.service= so it completes before autologin execs Hyprland — this ordering is mandatory, because a parallel run would let Hyprland start mid-swap and reintroduce the exact crash the guard prevents. The unit is bounded (=TimeoutStartSec=) and best-effort: its failure or timeout must not fail any target the session needs, so boot proceeds past it regardless. Its =ExecStart= runs, as the user: =informant read= (clear the news hook that would otherwise abort the transaction), then =topgrade --only system= (or the equivalent =yay -Syu=), then =maint stamp topgrade= on success, then removes the flag unconditionally (a one-shot arm — a failed attempt disarms rather than retrying every boot). =sudo= works unattended (=%cjennings NOPASSWD: ALL=), so no password prompt wedges it. @@ -87,47 +91,81 @@ The stamp also needs to happen when the upgrade is completed by other safe means - Bad, because it puts a unit on the boot critical path, which must be bounded and non-fatal with care, and it is the most to build. - Neutral, because it composes with C: the same =maint apply-upgrade= path serves both an interactive TTY run and the boot unit. -* Decisions [/] +** E. Split the live run: apply the non-blocked remainder now, defer the rest (this spec's everyday path) +- Good, because it is what a careful operator does by hand anyway — and did, on ratio, the day this was decided. The live run succeeds on the common day, topgrade exits 0, the AUR and every other ecosystem stay current, and the guard's abort becomes the rare path rather than the default. +- Bad, because Arch calls any =--ignore= run a partial upgrade. In practice pacman still enforces declared dependencies, so anything needing the newer mesa fails resolution instead of installing broken; the residual exposure is a package with an *unversioned* dependency built against a new ABI, which for mesa/wayland/libdrm is rare. Named, accepted. +- Bad, because a deferred set nobody surfaces is a set that silently never lands — the same trap as the freshness stamp, one layer down. So the script must record the deferred set durably and the panel must show it; this is why D stays in the design as the completion step rather than being replaced. +- Neutral, because it does not change the guard at all; it changes who decides the transaction's contents. + +* Decisions [7/7] -** TODO Metric means state, not recency -- Owner / by-when: Craig / at spec-review +** DONE Metric means state, not recency +CLOSED: [2026-08-25 Tue 18:45] +- Owner / by-when: Craig / 2026-08-25 - Context: the stamp gate can mean "ran the sweep" or "system is current." The whole fix differs by which. - Decision: We will keep the state meaning. Freshness stays stale while a guarded upgrade is genuinely un-applied, and the fix is to make *safe completion* stamp — not to loosen the gate. - Consequences: easier — the metric stays trustworthy as an is-current signal, and Alternative A is off the table. Harder — completion now needs a real safe path (the rest of this spec) rather than a one-line wrapper change. -** TODO Primary mechanism is the armed boot-time oneshot (Alternative D) -- Owner / by-when: Craig / at spec-review -- Context: D vs the manual-only C vs the risky live-and-reboot B. -- Decision: We will build D, and structure it so the same completion path also serves an interactive TTY run (C as the manual fallback). B is rejected on the live-swap risk. -- Consequences: easier — one gesture (arm + reboot) applies guarded upgrades safely and keeps the metric honest. Harder — a boot-critical-path unit needs the bounded, non-fatal, ordered-before-autologin wiring exactly right, or it risks boot delay. - -** TODO Boot run is arch-only (=topgrade --only system=), not the full sweep -- Owner / by-when: Craig / at spec-review -- Context: the only step needing a stopped compositor is the guarded system upgrade; the rest run fine live and are what usually fail. -- Decision: We will run only the system step at boot. The full topgrade sweep stays a normal live run. -- Consequences: easier — the boot path is fast (one transaction), has a tiny interactive-prompt surface, and rarely fails. Harder — freshness then reflects the system step specifically; if we want the sweep's other steps to also gate freshness, that is a separate signal. - -** TODO Arm flag lives on a persistent path and is one-shot -- Owner / by-when: Craig / at spec-review +** DONE Everyday mechanism is the split live run (Alternative E); the boot oneshot (D) completes the deferred set +CLOSED: [2026-08-25 Tue 18:30] +- Owner / by-when: Craig / 2026-08-25 +- Context: the first draft made D the primary gesture, which means every guarded-library day is a reboot day. Craig's read while watching the ratio run: when the guard would trip, the rational move is to upgrade everything *except* the guarded and kernel items, then run the rest of topgrade without the yay piece — and that logic should be a script we can keep editing, not something baked into the panel. +- Decision: I will build E as the path UPDATE and TOPGRADE always take on a live session, and keep D as the way the deferred set lands (arm + reboot). C remains the manual fallback through the same script from a TTY (no compositor → nothing blocked → a full run). B stays rejected on the live-swap risk. +- Consequences: easier — the common day is one live run that succeeds; reboots are reserved for the days the deferred set is non-empty, and even then the machine keeps working until the reboot is convenient. Harder — two lists to maintain (the guard's, read from the hook; the kernel list, owned by the script), a state file the panel must render, and the partial-upgrade caveat above to keep an eye on. + +** DONE The script lives in archsetup beside the guard, and maint calls it +CLOSED: [2026-08-25 Tue 18:45] +- Owner / by-when: Craig / 2026-08-25 +- Context: the panel (dotfiles =maint=) and the guard (archsetup =scripts/hypr-live-update-guard=, installed to =/usr/local/bin=) live in different repos, and maint already carries its own copy of the trigger list as =[updates] guard_patterns= in the thresholds TOML. +- Decision: ship the split script in archsetup next to the guard, installed by the same installer step, reading the blocked list from the installed hook so the guard and the script can never disagree. maint's UPDATE/TOPGRADE levers change their =argv= to the script; the TOML patterns stay as the panel's *display-side* mirror (the badge that says a run will defer) and gain a test asserting they match the hook. +- Consequences: easier — one owner for "which libraries are dangerous," and a rebuilt machine gets the script with the guard. Harder — a cross-repo change (archsetup ships it, dotfiles wires it), so the rollout is two commits, archsetup first. + +** DONE What a split run stamps +CLOSED: [2026-08-25 Tue 18:45] +- Owner / by-when: Craig / 2026-08-25 +- Context: under the state framing, a run that deferred six packages left the system *not* current, yet the sweep ran and every other ecosystem is fresh. +- Decision: the script stamps =topgrade_run= only when the deferred set is empty. When it is non-empty it writes the deferred set to its own cache key, and the panel renders that as its own state ("6 deferred — apply on reboot") rather than as stale freshness. The boot oneshot stamps when it completes the deferred set. Freshness keeps meaning "current"; the deferred badge carries the other half. +- Consequences: easier — no signal is thrown away, and the reboot nag has a precise count behind it. Harder — one more cache key and one more probe in maint. + +** DONE Kernel set is held on every everyday run and lands only in the dedicated session, gated on the DKMS result +CLOSED: [2026-08-25 Tue 18:45] +- Owner / by-when: Craig / 2026-08-25 +- Context: I first wrote this as "install the kernel live at apply-on-reboot," on the reasoning that a kernel swap crashes nothing and the modules-vanish window ends with the reboot. Craig asked what happens on velox when the DKMS rebuild fails, and the answer changed the decision. Velox is an encrypted ZFS root with =/boot= inside the root dataset, one kernel (=linux-lts=), and =zfs-dkms=. On a kernel upgrade the DKMS build runs PostTransaction, after the kernel is swapped and the old modules are deleted, so nothing can abort; a failed build leaves a new kernel beside an initramfs built for the old one, whose =zfs.ko= won't load, and the next boot can't import the pool. It is survivable — ZFSBootMenu can boot the pre-pacman snapshot, which holds the old kernel, initramfs, and modules — but it is a recovery session, not an update. Ratio (btrfs root, two kernels, zfs only for a data pool) is exposed only at the pool. The realistic triggers are a kernel major outrunning OpenZFS's supported range, a kernel upgraded without its headers, a toolchain regression, or a full disk. +- Decision: the script holds the kernel set — every installed kernel with its =-headers=, moved as a set, never one without the other — on every everyday run, on both machines, so there is one rule rather than a per-host exception. The kernel set lands only in the dedicated session, live, while a working desktop exists for diagnosing, and the script gates what follows on the result: =dkms status= reports every DKMS module installed for the new kernel version, the initramfs is newer than the kernel image, and on a ZFS root a pre-pacman snapshot exists. A failed gate stops with the failure named and never reboots. The GPU/compositor set follows only after the gate passes — armed for the boot oneshot, or applied from a TTY. Kernels stay off the guard's list (the hook would block a TTY kernel upgrade for no reason). "Install the kernel live at apply-on-reboot" is withdrawn. +- Consequences: easier — an everyday UPDATE can never put velox into the unbootable state, and the day the kernel moves is one Craig chose, sitting at the machine, expecting to handle issues. Harder — the kernel deferral is now standing, so the dedicated session has to happen on a cadence (security fixes ride the kernel), and the panel's deferred count carries a kernel most days; the gate is one more script to test, with fakes for =dkms status= and the image timestamps. + +** DONE Boot run applies exactly the deferred GPU/compositor set, nothing else +CLOSED: [2026-08-25 Tue 18:45] +- Owner / by-when: Craig / 2026-08-25 +- Context: the only packages that need a stopped compositor are the guard's trigger set; the rest run fine live and are what usually fail. The first draft phrased this as =topgrade --only system=; with the split script that wording is stale, and with the kernel decision above the kernel is not part of what boot applies either. +- Decision: the boot oneshot runs the script's =--complete= form scoped to the deferred GPU/compositor set: one pacman transaction, no ecosystem sweep, no kernel. The full topgrade sweep stays a normal live run through the everyday path. +- Consequences: easier — the boot path is fast, has a tiny interactive-prompt surface, and rarely fails. Harder — freshness after a boot run reflects the guarded set specifically, which is what the stamp decision above already accounts for. + +** DONE Arm flag lives on a persistent path and is one-shot +CLOSED: [2026-08-25 Tue 18:45] +- Owner / by-when: Craig / 2026-08-25 - Context: the guard's =/run= sentinel is tmpfs and cleared on reboot, so it cannot carry an intent across the reboot. A boot that retries forever on failure is its own outage. - Decision: We will use a persistent flag (=/var/lib/archsetup/=) that the boot unit removes unconditionally at the end of its attempt — success or failure disarms. - Consequences: easier — the intent survives exactly one reboot and a failed attempt never wedges subsequent boots. Harder — a failed attempt needs re-arming, which is correct (a human decides to try again) but is a manual step. * Implementation phases -** Phase 1 — Stamp the safe-completion path -A single =maint= entry point (=maint apply-upgrade=, or a documented flag on the existing lever) that runs =informant read= then the guarded system upgrade with Hyprland assumed down, and calls =maint stamp topgrade= on a zero exit. Usable immediately from a TTY. Leaves the tree working; no boot machinery yet. Tests: stamps on success, does not stamp on failure, refuses/aborts cleanly if Hyprland is live. +** Phase 1 — The split-upgrade script (archsetup) +=scripts/guarded-upgrade= (name open), installed to =/usr/local/bin= by the step that installs the guard. Behaviour as in Design: pending set → blocked set (hook =Target= lines, version-aware) ∪ held-kernel set when a compositor is live → =informant read= if present → =pacman -Syu --noconfirm --ignore=…= → =yay -Sua --noconfirm= → =topgrade --disable system,git_repos -y= → deferred set written to a state file → stamp only when nothing was deferred → exit 0 on a successful live part. The kernel set is derived from what is installed (every =linux*= kernel package and its =-headers=), never a hardcoded pair, and is always held or applied whole. Flags: =--dry-run= (print the plan and the deferred set, change nothing), =--no-topgrade=, =--no-aur=, =--complete= (the dedicated-session form: apply the kernel set live, run the gate, then arm the GPU/compositor set or, with no compositor live, apply it directly; stamp when the deferred set is empty). The gate is its own small script, =kernel-modules-check=: for each kernel under =/usr/lib/modules=, =dkms status= reports every registered module =installed= for it, and its initramfs is newer than its =vmlinuz=; on a ZFS root, a =pre-pacman_= snapshot of the root dataset exists. It exits non-zero with the failing item named, and =--complete= refuses to arm or reboot on that exit. Usable from a TTY at once. Tests (pytest beside the guard's): blocked-set computation against a fixture hook and version map; the kernel set is derived from the installed kernels and held whole on every everyday run; the =--ignore= list is exactly blocked ∪ kernel set; the state file round-trips; stamps only on an empty deferred set; =--dry-run= is IO-free; the gate passes and fails on fake =dkms status= output, image timestamps, and snapshot listings, and =--complete= never reaches the arm step on a failed gate. -** Phase 2 — The armed boot-time unit (archsetup) -=archsetup-boot-upgrade.service= installed by the installer: =ConditionPathExists= the arm flag, =Before=getty@tty1.service=, =TimeoutStartSec= bounded, non-fatal to every session target, =ExecStart= calls the Phase-1 path as the user and removes the flag unconditionally. Installer step + unit file + the =/var/lib/archsetup/= flag directory. Tests in =tests/installer-steps/= for the install step; a documented manual boot test (arm, reboot, observe) in =todo.org= under Manual testing and validation. +** Phase 2 — Wire maint to it (dotfiles) +UPDATE and TOPGRADE levers change their =argv= to the script; the press-again-to-force sentinel wrap goes away (the driven path never trips the guard). A new probe reads the deferred-set state file and the panel renders "N deferred — apply on reboot" as its own row. A test asserts the TOML =guard_patterns= equal the installed hook's =Target= list. Tests under the maint fake harness. -** Phase 3 — The arming affordance (dotfiles maint) -The panel/CLI detects a guarded library in the pending set (the same trigger list the hook uses) and offers "apply on reboot": writes the arm flag and offers to reboot. Tests: the offer appears only when a guarded lib is pending, writes the flag, and is idempotent. +** Phase 3 — "Apply on reboot" and the boot-time unit (archsetup + maint) +The panel action installs the held-kernel set live, writes the persistent arm flag, and offers to reboot. =archsetup-boot-upgrade.service=, installed by the installer: =ConditionPathExists= the flag, =Before=getty@tty1.service=, =TimeoutStartSec= bounded, non-fatal to every session target, =ExecStart= runs the script's =--complete= form as the user and removes the flag unconditionally. Installer step + unit file + the =/var/lib/archsetup/= flag directory. Tests in =tests/installer-steps/= for the install step; the arm action's tests in maint; a documented manual boot test (defer, arm, reboot, observe) in =todo.org= under Manual testing and validation. ** Phase 4 — Docs, rollout, and both daily drivers Document the flow (arm → reboot → console upgrade → session). Roll the unit to velox and ratio (installer already covers a rebuild; existing machines need the one-time install). Confirm the ratio path matches. * Acceptance criteria +- [ ] With a guarded library pending and Hyprland live, UPDATE applies everything else, exits 0, and the panel shows the exact deferred set; the guard hook does not fire. +- [ ] The same run with no compositor live (a TTY) applies everything but the kernel set and stamps only if nothing was deferred. +- [ ] A =--complete= run whose DKMS build fails stops before arming or rebooting, names the failure, and leaves the machine running on the old kernel; on velox the pre-pacman snapshot it required is bootable from ZFSBootMenu. - [ ] With a guarded library pending, arming and rebooting applies it in the console before Hyprland starts, and =maint status= then reads a fresh =topgrade_age=. - [ ] A boot-upgrade failure (a failed step, a timeout, an aborted transaction) never blocks the session: the machine boots into Hyprland, the flag is cleared, and the panel still shows the pending work. - [ ] Unread Arch news does not wedge the boot run (=informant read= precedes the transaction). @@ -141,7 +179,7 @@ Answer each, or write "N/A because…". - Security & privacy: relies on the existing =%cjennings NOPASSWD: ALL=; the unit runs the upgrade as the user via sudo, adds no new privilege. Note the NOPASSWD breadth as a pre-existing fact, not introduced here. - Observability: the boot run's output is on the console; its systemd unit status and journal record success/failure; the panel reflects the cleared or still-pending state after boot. - Performance & scale: one pacman/yay transaction at boot; bounded by =TimeoutStartSec=. Negligible boot-time cost when the flag is absent (=ConditionPathExists= skips the unit). -- Reuse & lost opportunities: reuses the guard's trigger list (single source of truth for "which libs are dangerous"), =informant=, =maint stamp=, and topgrade's own =--only system=. No reinvention. +- Reuse & lost opportunities: reuses the guard's trigger list by reading the installed hook (single source of truth for "which libs are dangerous"), =informant=, =maint stamp=, =checkupdates=, and topgrade's own step switches. The one duplicate that exists today — maint's TOML =guard_patterns= — is kept as a display mirror and pinned to the hook by a test rather than removed. - Architecture fit & weak points: integration points are the pacman hook set, getty autologin ordering, and the maint cache. Weak point: the =Before=getty@tty1= ordering is load-bearing for safety; a parallel run reintroduces the live-swap crash. Mitigated by making the ordering explicit and tested-by-inspection. - Config surface: the arm flag path and the timeout. Defaults safe (absent flag = no-op). - Documentation plan: a short "reboot to apply guarded upgrades" note in the maint docs; the installer step self-documents in-comment. @@ -152,13 +190,23 @@ Answer each, or write "N/A because…". * Risks, Rabbit Holes, and Drawbacks - Boot critical path: the unit sits ahead of autologin, so a hang would delay boot. Mitigated by =TimeoutStartSec= and non-fatal wiring; worst case is a bounded delay, then a normal session. - Interactive prompts under no stdin: =yay=/pacman can still prompt (provider choice, replace, AUR review) even with =assume_yes=. The =--only system= scope and =--noconfirm=-style flags shrink this to near zero, but a prompt with no stdin fails the run (benign) — needs a genuinely non-interactive invocation, verified in Phase 2. -- Partial ecosystem state: N/A for the hazard — the system step is a single atomic pacman transaction, so there is no half-swapped GPU library. -- Adjacent but out of scope: a live kernel upgrade is not guarded by this hook yet removes running-kernel modules until reboot. Flagged as vNext, not solved here. +- Partial ecosystem state: N/A for the GPU hazard — each pacman run is one atomic transaction, so there is no half-swapped library. The =--ignore= run is a partial upgrade in Arch's sense; pacman's dependency resolution is the safety net, and the residual unversioned-ABI exposure is accepted in Alternative E. +- Orphans that block resolution: a package dropped from the repo but still pinning an old version (ratio's =qemu-block-gluster= on 2026-08-25) fails the whole transaction. The script should detect the "could not satisfy dependencies" case, name the foreign package, and stop with the remedy — never =-Rdd= on its own. +- The kernel on a DKMS ZFS root: a failed =zfs-dkms= build after the kernel swap cannot be aborted (the DKMS hooks are PostTransaction) and leaves velox unbootable on the new kernel. Mitigated by holding the kernel set on every everyday run, landing it only in the dedicated session behind the gate, and by the standing fallback: =/boot= lives in the root dataset, the =05-zfs-snapshot= hook snapshots it before every transaction, and ZFSBootMenu can boot that snapshot. The pacman cache also keeps the previous kernel and =zfs-dkms= for a downgrade. Ratio's exposure is its data pool only (btrfs root, two kernels). +- Standing kernel deferral: because the everyday run never moves the kernel, the dedicated session has to happen on a cadence or kernel security fixes sit unapplied. The panel's deferred row is the reminder; a stale-kernel age in maint is a possible follow-up. * Testing / Verification / Rollout Phase-1 and Phase-3 logic under the maint fake harness; Phase-2 install under =tests/installer-steps/=. The one thing no unit test can cover — that an armed reboot actually applies the upgrade pre-session and stamps — is a scripted manual test in =todo.org= (arm with a guarded lib pending, reboot, confirm the console run, the fresh metric, and a normal session). Roll to velox first, then ratio. * Review and iteration history +** 2026-08-25 Tue @ 18:45 -0600 — Craig Jennings — author +- What: closed all seven decisions. Reversed the kernel decision (hold on every everyday run; land only in the dedicated session, gated on DKMS built, initramfs fresh, snapshot present; withdrew "install live at apply-on-reboot"), reworded the boot-scope decision for the split design, added the =kernel-modules-check= gate to Phase 1 and the acceptance criteria, and wrote the velox failure chain and the ZFSBootMenu fallback into Risks. +- Why: on velox a failed =zfs-dkms= rebuild after a kernel swap is unabortable and unbootable; that belongs in a session I chose, not in an update I expected to touch applications. +- Artifacts: this session's log (velox boot layout verified live: ZBM on the ESP, =/boot= in =zroot/ROOT/default=, one kernel, =zfs-dkms 2.4.4=). +** 2026-08-25 Tue @ 18:30 -0600 — Craig Jennings — author +- What: made the split live run (E) the everyday path and the boot oneshot (D) the completion step; added the script-ownership, stamp-semantics, and kernel-hold decisions; rewrote the phases around the script; added the orphan-blocks-resolution risk. +- Why: watching a 724-of-730 guarded run succeed by hand on ratio made it obvious the guard's abort should be the rare path, and that the logic belongs in an editable script the panel calls rather than in the panel. +- Artifacts: this session's log; the ratio run (=ratio-upgrade.service=, =/var/log/ratio-upgrade.log=). ** 2026-08-25 Tue @ 06:39:42 -0600 — Craig Jennings — author - What: initial draft. - Why: the topgrade-freshness metric reads permanently stale because the guard blocks the arch step; designing a safe completion path rather than loosening the gate. diff --git a/todo.org b/todo.org index 592349f..d6a3e45 100644 --- a/todo.org +++ b/todo.org @@ -45,6 +45,45 @@ below): input-side-spec.org (DRAFT, four decisions open). * Archsetup Open Work +** TODO [#B] gcalcli in the installer, token carried from the other daily driver :feature:velox:tooling:solo: +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:END: +From home's 2026-08-25 handoff: velox's 08-13 reinstall left it without +gcalcli, and on 08-21 both calendar write paths on velox were down at once +(the google-calendar MCP with expired tokens, and no gcalcli), so a booked lab +appointment sat uncalendared for three days. The daily-drivers one-time-setup +drift, exactly. + +Part 1 is done (2026-08-25, this session): =pipx install gcalcli==4.5.1= on +velox to match ratio, then ratio's =~/.local/share/gcalcli/{oauth,cache}= +copied over tailscale (=oauth= is a 1 KB pickled google-auth credential, +=chmod 600=). =gcalcli list= on velox returned all six calendars with no +re-consent, so the token is portable between the daily drivers and the OAuth +click-through is not needed when the other machine is reachable. + +Part 2, this task: make the installer do it. +- =pip_install gcalcli= in the tool set beside =pip_install yt-dlp= (archsetup + ~line 3109; =pip_install= wraps =pipx install= as =$username=). Pin or not: + ratio and velox are both 4.5.1; unpinned matches how yt-dlp is installed. +- The credential can't be installed: add a named post-install manual step + "copy =~/.local/share/gcalcli/oauth= from the other daily driver + (=scp :.local/share/gcalcli/oauth ~/.local/share/gcalcli/=, + =chmod 600=), or run =gcalcli init= per + =assets/2026-02-01-gcalcli-setup.org= when neither machine has it." +- A =post-rebuild-check= item: gcalcli on PATH and the oauth file present, so + the drift is caught by the checker rather than by a missed appointment. +- Tests: an installer-steps pytest asserting the tool set carries + =pip_install gcalcli=; a post-rebuild-check test for the new item, both + states. +- When it lands, confirm back to home (=inbox-send home=) so it can retire + its "gcalcli is not installed on velox" notes. + +Grading: feature, no hard date, real improvement to the install = [#B]. +:solo: — build path (installer + checker + tests) and verify path (pytest; +the live proof already exists on velox) with no open decision. + ** TODO [#A] Topgrade guarded-upgrade spec — decisions, review, decomposition :feature:maint:dotfiles: SCHEDULED: <2026-08-25 Tue> :PROPERTIES: @@ -73,26 +112,6 @@ Then: flip the decisions DONE, run spec-review (DRAFT → READY), run spec-response to decompose the four phases into build tasks here, file the vNext =[#D]= kernel-reboot item, and commit the spec. -** TODO [#A] Ratio: pull .emacs.d before upgrading Emacs to 31.1 :chore:ratio:emacs: -SCHEDULED: <2026-08-25 Tue> -:PROPERTIES: -:CREATED: [2026-08-25 Tue] -:LAST_REVIEWED: 2026-08-25 -:END: -Emacs 31.1's warnings.el defers daemon-startup warnings into a closure holding -the =*Warnings*= buffer; the config's dashboard-only sweep killed that buffer, -so the first client frame of every fresh 31.1 daemon failed on Wayland and -emacsclient silently fell back to =$DISPLAY= (XWayland, pgtk warning dialog). -Fixed in =.emacs.d= commit =63831060= (2026-08-25, velox verified live: -=GdkWaylandDisplay=). Ratio is still on 30.2, which lacks the deferring code, -so it is fine until it upgrades — then it hits the same trap once per daemon -start unless the fix is pulled first. - -Order on ratio: =git -C ~/.emacs.d pull= (the push from velox is the telega -session's; confirm =63831060= is on origin first), then the =pacman -Syu= that -brings =emacs-wayland 31.1=, then restart the daemon. Check afterwards: -=emacsclient -e '(pgtk-backend-display-class)'= → =GdkWaylandDisplay=. - ** TODO [#A] Lock-screen clock stale after a real sleep :bug:hyprland:dotfiles:velox: SCHEDULED: <2026-08-25 Tue> :PROPERTIES: @@ -4316,3 +4335,46 @@ installed. *** 2026-08-21 Fri @ 14:18:03 -0700 Promoted the Phase 5 residual out to its own task Rescoped 2026-07-04 (audit): the tunnels track already shipped most of the original Phase 5. Panel tunnel bring-up/down and detection landed (dotfiles 2d9d060 probes tailscale/NM-wireguard/Proton; 21db05a brings overlays up/down from the panel's Tunnels sub-view; 31ba056 diagnose/doctor understand tunnel routes; archsetup 2e40781 wireguard config import; the net-panel-other-interfaces spec is IMPLEMENTED). What remains for Phase 5 is only the =net vpn ...= CLI subcommand — cli.py still has no vpn/tunnel parser. Fold the panel's existing tunnel operations into a CLI surface; spec separately when picked up. +** DONE [#A] Ratio: pull .emacs.d before upgrading Emacs to 31.1 :chore:ratio:emacs: +CLOSED: [2026-08-25 Tue] +:PROPERTIES: +:CREATED: [2026-08-25 Tue] +:LAST_REVIEWED: 2026-08-25 +:END: +Emacs 31.1's warnings.el defers daemon-startup warnings into a closure holding +the =*Warnings*= buffer; the config's dashboard-only sweep killed that buffer, +so the first client frame of every fresh 31.1 daemon failed on Wayland and +emacsclient silently fell back to =$DISPLAY= (XWayland, pgtk warning dialog). +Fixed in =.emacs.d= commit =63831060= (2026-08-25, velox verified live: +=GdkWaylandDisplay=). Ratio is still on 30.2, which lacks the deferring code, +so it is fine until it upgrades — then it hits the same trap once per daemon +start unless the fix is pulled first. + +Order on ratio: =git -C ~/.emacs.d pull= (the push from velox is the telega +session's; confirm =63831060= is on origin first), then the =pacman -Syu= that +brings =emacs-wayland 31.1=, then restart the daemon. Check afterwards: +=emacsclient -e '(pgtk-backend-display-class)'= → =GdkWaylandDisplay=. + +*** 2026-08-25 18:10 — pull already landed; the upgrade half remains +Checked ratio over tailscale: =~/.emacs.d= is clean at =91fbac72= (= =origin/main=), +and =63831060= is an ancestor of HEAD — =modules/undead-buffers.el= carries the +=*Warnings*= entry. Ratio is on =emacs-wayland 30.2-3= with =31.1-1= pending among +720 updates (last full upgrade 2026-08-01; kernel 7.1.5 → 7.1.9 also pending, +btrfs root, uptime 3.5 weeks). The daemon is a plain =emacs --daemon= (not a +user unit) holding 2 live frames, so the restart step will drop those frames. +What remains: the =pacman -Syu= on ratio, the daemon restart, and the +=(pgtk-backend-display-class)= check. + +*** 2026-08-25 Tue @ 18:35:00 -0600 Upgraded ratio to Emacs 31.1 and verified the Wayland backend +Ran the upgrade over tailscale as a transient unit (=ratio-upgrade.service=, +log at =/var/log/ratio-upgrade.log=): 714 packages, =--ignore= on the six +packages the live-update guard would have blocked (aquamarine, hyprland, +hyprutils, mesa, vulkan-radeon, wayland — still pending, apply from a TTY +before the reboot). One orphan cleared first: =qemu-block-gluster= had been +dropped from the repo and pinned =qemu-common=; the new =qemu-full= no +longer needs it. Killed the plain =emacs --daemon= (no modified buffers, no +graphical frames), started =emacs.service= instead so the daemon carries the +systemd user environment, and probed from a throwaway frame: +=(pgtk-backend-display-class)= → =GdkWaylandDisplay=, =*Warnings*= alive. +Ratio still wants a reboot for =linux 7.1.9=. Pacnews to review there: +=/etc/ssh/sshd_config.pacnew= and two =/etc/tpm2-tss/fapi-profiles/*.json=. -- cgit v1.2.3