aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-20 20:30:57 +0200
committerDanilo M. <danix@danix.xyz>2026-10-20 20:30:57 +0200
commit0602623806b7c0984544913d663804ed5f54e07f (patch)
treefdd838b21d4dd0775f98760ca11d9e91e92d3745 /docs/superpowers/specs
parentcd9ca18cf12ab24e0d1532d01c0f621b06869545 (diff)
downloadsb-builder-0602623806b7c0984544913d663804ed5f54e07f.tar.gz
sb-builder-0602623806b7c0984544913d663804ed5f54e07f.zip
Docs ready to publish: VM setup, specs in docs/designHEADmaster
README says what the GUI does today and what is planned, which defaults are the author's setup, and how to set up the VM. The specs move to docs/design/; the implementation plans are dropped (working files, now kept out of git). AGENTS.md links slackrepo upstream instead of a local clone path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/specs')
-rw-r--r--docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md335
-rw-r--r--docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md295
-rw-r--r--docs/superpowers/specs/2026-10-20-sb-builder-bundled-deps-design.md115
-rw-r--r--docs/superpowers/specs/2026-10-20-sb-builder-phantom-deps-design.md74
-rw-r--r--docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md266
-rw-r--r--docs/superpowers/specs/2026-10-20-sb-builder-ui-design.md163
6 files changed, 0 insertions, 1248 deletions
diff --git a/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md b/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md
deleted file mode 100644
index 3ec3fc0..0000000
--- a/docs/superpowers/specs/2026-10-19-sb-builder-phase1-design.md
+++ /dev/null
@@ -1,335 +0,0 @@
-# sb-builder phase 1: core design
-
-Date: 2026-10-19
-Status: approved
-
-## Goal
-
-One Qt6 desktop GUI to drive the SlackBuild tooling on the build VM
-(`buildsystem`): slackrepo, mkhint, overlay sync (sbpull, sbshadow), full repo
-rebuild, and SBo orphan discovery. Long-running work runs as jobs on the VM
-that survive the GUI disconnecting, and the GUI can reattach to them later.
-
-## Phases
-
-Each phase gets its own spec, plan and implementation cycle.
-
-1. **Core** (this spec): remote helper with the job model, GUI shell with
- tabs, Dashboard, Jobs tab, VM control, config, notifications. Job kinds:
- `sbpull`, `sbshadow`.
-2. **Packages + slackrepo**: package browser over the large SBo-danix tree
- (search and filter by name, category, overlay, maintainer, build status);
- Build, Rebuild, Bulk update screens.
-3. **Full repo rebuild**: `slackrepo_setup_SBo-danix.sh`, destructive, guarded.
-4. **Hints**: mkhint browse, edit, update, create, `-F`, diff before save.
- Requires non-interactive flags in mkhint (it has about 10 `read -p`
- prompts and wraps slackrepo at `mkhint:1798`). Also: `--no-dl` must write
- `DOWNLOAD="no"`, which slackrepo reads, instead of `NODOWNLOAD=yes`,
- which it does not (keep reading old `NODOWNLOAD` lines for the NoDL column),
- and existing hints on the VM get migrated: every `NODOWNLOAD=yes` line in
- any hint slackrepo searches becomes `DOWNLOAD="no"` (an existing `DOWNLOAD`
- line is replaced, since `DOWNLOAD="no"` overrides it anyway), backed up to
- `.bak` first, idempotent. As of 2026-10-19 no hint carries the line.
- Superseded by `2026-10-20-sb-builder-phase4-design.md`: the hint logic
- moves into the helper instead of extending mkhint.
-5. **Orphans**: SBo orphan discovery (`EMAIL="NOEMAIL"` in `.info`, the
- `sbo-orphans` logic from slackbuilds-superrepo), including orphaned deps
- in REQUIRES. Needs an SBo-stable tree, which is not on the VM today.
-
-Dashboard tiles become entry points as their screens land: Packages opens
-the package browser (phase 2, select packages to build there), Built the
-build screen, Last pull the sync actions, Recent jobs the Jobs tab, SBo
-orphans the orphans screen (phase 5).
-
-Out of scope for all phases so far: overlay git operations (status, commit,
-push in my-slackbuilds and friends).
-
-## Stack
-
-- GUI: Python 3 + PyQt6 (stock in Slackware -current, 6.11), including
- `PyQt6.QtDBus` for notifications. No third-party packages.
-- Helper: Python 3 stdlib only (VM runs 3.12, so `tomllib` is available).
-- Config: TOML, read with `tomllib`.
-- License: GPLv2-only.
-
-## Architecture
-
-```
-desktop buildsystem (root)
-+----------------------+ ssh (ControlMaster) +---------------------------+
-| sb-builder (PyQt6) | ----------------------> | sbb-remote (py3 stdlib) |
-| tabs: Dashboard, | sbb-remote <cmd> | stats / jobs / |
-| Jobs (+later tabs) | <---------------------- | job-start / job-follow / |
-| QProcess per call | JSON on stdout | job-kill, flock, setsid |
-+----------------------+ | /var/lib/sb-builder/ |
- | +---------------------------+
- | virsh (local libvirt)
- v
- domain "buildsystem"
-```
-
-### Repository layout
-
-```
-sb_builder/__main__.py entry point, config load
-sb_builder/config.py GUI config: defaults, load, validate
-sb_builder/remote.py the only code that runs ssh; also virsh
-sb_builder/mainwindow.py tabs, menu, status bar, notifications
-sb_builder/dashboard.py Dashboard tab
-sb_builder/jobs.py Jobs tab
-sb_builder/fmt.py text formatting shared by the tabs
-sb-builder launcher (python3 -m sb_builder)
-remote/sbb-remote VM helper, single file
-config.example.toml GUI config example
-remote/config.example.toml helper config example
-install.sh local install; --remote deploys helper to VM
-tests/test_remote.py
-tests/test_gui.py
-```
-
-### Transport
-
-Every GUI call is a QProcess running:
-
-```
-ssh -o ControlMaster=auto -o ControlPath=<runtime dir>/sbb-%C -o ControlPersist=10m \
- <ssh_options...> <target> sbb-remote <cmd> [args...]
-```
-
-Calls are async. ssh exit 255 means the connection failed. Exit 127 means the
-helper is not installed.
-
-### Trust boundary
-
-The helper runs only a fixed table of job kinds. The GUI sends a kind name
-plus arguments; the helper validates each argument and builds argv itself.
-No shell strings and no paths cross ssh. Validation rules:
-
-- unknown kind: rejected
-- `sbpull` target: must be listed in the helper config `overlays`
-- flags: only the ones the kind declares (`--yes`, `--no`)
-- package names (phase 2 onward): `^[A-Za-z0-9._+-]+$`
-
-### Version check
-
-On connect the GUI runs `sbb-remote version`, which also returns the
-configured `overlays` (the GUI builds its sbpull menu from them). A mismatch with the GUI's own
-version shows a banner telling the user to run `./install.sh --remote`.
-
-## Helper: `sbb-remote`
-
-### State
-
-```
-<state_dir>/lock flock target, held for the whole job
-<state_dir>/jobs/<id>/meta.json kind, args, argv, pid, start, end, exit, status
-<state_dir>/jobs/<id>/log combined stdout+stderr, raw bytes
-```
-
-`<id>` is `YYYYMMDD-HHMMSS-<kind>`. `status` is one of `running`, `ok`,
-`failed`, `killed`, `lost`, `busy`.
-
-Logs are kept forever (`# ponytail:` comment in code; add pruning once disk
-usage is visible on the Dashboard).
-
-### Commands
-
-All print one JSON object on stdout, except `job-follow`.
-
-| command | behaviour |
-|---|---|
-| `version` | `{"version": "...", "overlays": [...]}` |
-| `stats` | Dashboard data: load, uptime, disk free for repo and state dirs, package counts (total, personal, pentesting, shadowed = overlay packages also present in the pristine `current` branch, as sbshadow computes it), last sync per overlay (time of the newest commit touching `<overlay>/` in `repo_dir`), slackrepo summary from its sqlite DB (built, failed, never built). Any missing source yields `null` for that field, never an error |
-| `jobs` | list of all jobs' meta. A job whose meta says `running` but whose pid is gone (`os.kill(pid, 0)` fails) is reported and rewritten as `lost` |
-| `job-start <kind> [args]` | validate; spawn `sbb-remote _run <id>` with `start_new_session=True`, `stdin=/dev/null`, stdout/stderr to `/dev/null`; wait up to 3s for `_run` to write status; return `{"id": ...}`, or `{"busy": "<running id>"}` after deleting the refused job's dir |
-| `job-follow <id> <offset>` | write log bytes from `offset` to stdout, keep following until the job is no longer `running`, then exit 0 |
-| `job-kill <id>` | SIGTERM to the job's process group; `_run` records `killed` |
-| `_run <id>` | internal, see below |
-
-### `_run`
-
-1. Take a non-blocking `fcntl.flock` on `<state_dir>/lock`. If held, write
- status `busy` and exit.
-2. Start the job argv with `stdin=/dev/null`, stdout and stderr to the log
- file, in its own process group. Write `pid` and status `running`.
-3. Wait. Write `end`, `exit` and final status (`ok` on 0, `killed` on
- SIGTERM, else `failed`).
-
-Jobs never read input. A tool that prompts anyway gets EOF on `read` and,
-under `set -e`, exits non-zero: the job fails, it never hangs and it never
-answers yes by accident. Destructive steps are confirmed in the GUI.
-
-### Phase 1 job kinds
-
-| kind | argv built by helper |
-|---|---|
-| `sbpull <overlay> [--yes\|--no]` | `bash -c '. "$1" && sbpull "$2" "$3"' _ <sbpull_script> <overlay> <flag>` |
-| `sbshadow [--yes\|--no]` | `sbshadow <flag>` |
-
-The flag is required in both kinds (a job has no stdin).
-
-### Helper config: `/etc/sb-builder/config.toml`
-
-```toml
-state_dir = "/var/lib/sb-builder"
-repo_dir = "/var/lib/sbopkg/SBo-danix"
-upstream_branch = "current"
-slackrepo_db = "/var/lib/slackrepo/SBo-danix/database_SBo-danix_x86_64.sqlite3"
-sbpull_script = "/usr/local/share/slackrepo_setup/sbpull.bash"
-overlays = ["personal", "pentesting"]
-```
-
-Missing file: these defaults. Unknown key or wrong type: every command
-returns `{"error": "config: <key>: <reason>"}` and exits 1.
-
-### Errors
-
-Top-level handler catches any exception, prints `{"error": "<message>"}`,
-exits 1.
-
-## Changes in slackrepo_setup
-
-Done in that repo, its own commit, redeployed to the VM.
-
-- `sbshadow --yes | --no`: no flag keeps the current interactive prompt.
- `--yes` applies without asking. `--no` prints the plan and leaves the tree
- unchanged (preview). `mkhint -F` runs in all cases, as it does today on
- abort.
-- `sbshadow` honours `SBO_REPO` to override the repo path, so it can be
- tested against a throwaway repo.
-- `sbpull <target> [--yes|--no]`: passes the flag to `sbshadow`, and returns
- `sbshadow`'s exit status (today it returns `popd`'s, so a failure would
- show as a successful job).
-
-GUI flow: `sbpull personal --no` pulls and shows the shadow plan in the log;
-the Jobs tab offers "Apply shadowing", which starts `sbshadow --yes`.
-
-## GUI
-
-### Main window
-
-- QTabWidget. Dashboard is pinned. Other tabs are closable and
- single-instance.
-- Menu: `File > Reload config`, `File > Quit`, `Open > Jobs` (later phases
- add entries).
-- Status bar: connection state (ok, down, VM off, VM paused, VM running but
- ssh unreachable, helper version mismatch), running job if any (click opens
- it in Jobs), transient messages.
-
-### Dashboard
-
-Grid of QGroupBox tiles filled from `stats`, plus `jobs` for the Jobs tile:
-
-- VM: load, uptime, disk free
-- Repo: total packages, personal, pentesting, shadowed
-- slackrepo: built, failed, never built
-- Sync: last pull per overlay
-- Jobs: running job and last 10 (status, duration)
-- Orphans: placeholder tile, filled in phase 5
-
-Refreshes every `refresh.dashboard` seconds while visible, plus a Refresh
-button. `null` fields show "n/a". When the connection is down the last
-values stay, greyed, with "stale since HH:MM".
-
-### Jobs tab
-
-- Left: job list (status icon, kind and args, start, duration), refreshed
- every `refresh.jobs` seconds.
-- Right: log view (QPlainTextEdit, ANSI escapes stripped, `\r` rewrites the
- current line), Kill button with confirm.
-- Toolbar `Start`: one entry per overlay for `sbpull <overlay> --no`, plus
- `sbshadow --no`, `sbshadow --yes` (confirm dialog).
-- After a `--no` run whose log lists `shadow:` or `unshadow:` lines, an
- "Apply shadowing" button starts `sbshadow --yes`.
-- Selecting a job starts `job-follow` from offset 0. If the stream drops
- while the job is still running, the next successful `jobs` poll restarts
- it from the last received offset (the poll itself backs off while the VM
- is down, see Error handling).
-- `job-start` returning busy: dialog "busy: <id>" with [Show job].
-
-### Notifications
-
-freedesktop `org.freedesktop.Notifications.Notify` via QtDBus. Events:
-`finished`, `failed`, `killed`, `lost`, and `vm-up` (VM started and ssh
-answering). Clicking a notification (`ActionInvoked`) raises the window and
-opens the job. The GUI tracks the job states it has seen, so each job end
-notifies once.
-
-### VM control
-
-When ssh fails and `vm.domain` is set, the GUI runs
-`virsh -c <libvirt_uri> domstate <domain>` locally:
-
-| domstate | behaviour |
-|---|---|
-| `shut off` | status "VM off", [Start VM] (automatic when `vm.autostart`): `virsh start`, then poll ssh with backoff, notify `vm-up` |
-| `paused` | status "VM paused", [Resume]: `virsh resume` |
-| `running` | status "VM running, ssh unreachable", keep retrying |
-| virsh missing or error | plain "down" |
-
-No shutdown control.
-
-### GUI config: `$XDG_CONFIG_HOME/sb-builder/config.toml`
-
-```toml
-[remote]
-target = "buildsystem" # any ssh destination
-ssh_options = [] # extra ssh args; ControlMaster options always added
-
-[refresh]
-dashboard = 30
-jobs = 5
-
-[notify]
-enabled = true
-events = ["finished", "failed", "killed", "lost", "vm-up"]
-
-[vm]
-libvirt_uri = "qemu:///system"
-domain = "buildsystem" # empty disables VM control
-autostart = false
-```
-
-Missing file: these defaults. Invalid key or type: dialog naming the key,
-then defaults are used. Read at startup and on `File > Reload config`. No
-settings dialog yet.
-
-## Error handling summary
-
-| case | behaviour |
-|---|---|
-| ssh down | status bar, VM check, stale tiles; the `jobs` poll interval doubles from `refresh.jobs` up to 30s, and resets once ssh answers |
-| helper missing or version mismatch | banner with `./install.sh --remote` |
-| busy | dialog with [Show job] |
-| follow drop | reconnect from offset, job unaffected |
-| helper exception | `{"error"}` shown in status bar |
-| `_run` died, VM rebooted mid-job | `lost`, red in list, notification |
-| config invalid | dialog naming key, defaults used |
-
-## Testing
-
-stdlib `unittest`, no frameworks.
-
-- `tests/test_remote.py`: imports `remote/sbb-remote` as a module with a
- temp `state_dir`. Covers argument validation (unknown kind, overlay not in
- config, unknown flag), lock busy, `lost` detection, `job-follow` from an
- offset, and a full `_run` of a fake kind patched into the kind table
- (ok, failed, killed).
-- `tests/test_gui.py`: config load and validation, `remote.parse`, GUI and
- helper versions equal, both example configs equal the defaults. Smoke tests
- under `QT_QPA_PLATFORM=offscreen` with a fake Remote: tabs, Dashboard from
- a canned `stats` dict with `null` fields, job-end notification sent once,
- VM-off offer, carriage-return handling in the log view.
-- slackrepo_setup: sbshadow `--yes` and `--no` verified in a throwaway git
- repo, the same way the current behaviour was tested.
-- First run against the real VM: manual, with the user.
-
-## Install
-
-- `install.sh`: symlinks the `sb-builder` launcher into `~/bin` (`BINDIR`
- overrides), so repo edits are live; copies `config.example.toml` to the
- config path only if no config exists.
-- `install.sh --remote`: scp `remote/sbb-remote` to `<target>:/usr/local/bin/`
- (`SBB_TARGET`, default `buildsystem`),
- create `/etc/sb-builder/config.toml` from the example only if missing.
- Asks before deploying. A SlackBuild comes later.
diff --git a/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md b/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md
deleted file mode 100644
index 21a041f..0000000
--- a/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md
+++ /dev/null
@@ -1,295 +0,0 @@
-# sb-builder phase 2: packages and slackrepo builds
-
-Date: 2026-10-19
-Status: approved
-Builds on: `2026-10-19-sb-builder-phase1-design.md` (helper, job model, GUI shell)
-
-## Goal
-
-Browse and search the ~10k packages of the SBo-danix tree, select packages
-and run slackrepo `build`, `rebuild`, `update` and `remove` on them, plus
-`update` of everything, as jobs. Every run is previewed first. Publishing
-(the rsync push of the package repository) can be held back per run and
-done later with a separate Publish action. Package signing works from the
-GUI although jobs have no terminal.
-
-## Facts this design rests on
-
-Verified on the VM and in the slackrepo source (`~/Programming/GIT/GITHUB/slackrepo`):
-
-- slackrepo usage: `slackrepo {build|rebuild|update|remove} [--preview|--dry-run] [item...]`,
- `--repo=SBo-danix`. Items may be bare package names.
-- `--preview` on build/update does **not** set `OPT_DRY_RUN` (only revert and
- remove do, `libexec/functions.d/cmdfunctions.sh:231,398`). Finish hooks that
- test only `OPT_DRY_RUN` still run during a preview: today `gotify_hook`
- sends a message and `manage_packages_hook` runs without `--dry-run`.
-- The live config `/etc/slackrepo/slackrepo_SBo-danix.conf` defines start hook
- `danix_gitfetch_hook` (fetch, rebase `danix-current` onto upstream; runs on
- every invocation, previews included) and finish hooks
- `manage_packages_hook bell_hook genrepos_hook gen_web_hook dep_template_hook rsync_push_hook recap_hook gotify_hook`.
- It contains a Gotify key and site hostnames. Stale copies exist in
- `repo-html-structure/` and in the upstream slackrepo clone.
-- `genrepos_hook` runs `gen_repos_files.sh`, which signs with gpg.
- `/root/.genreprc` sets `USE_GPGAGENT=1` and `REPOSOWNERGPG=<fingerprint>`.
- The agent uses `pinentry-curses` (needs a tty) with
- `default-cache-ttl 14400`, `max-cache-ttl 21600`. With the key uncached
- and no tty, `gen_repos_files.sh` aborts ("GPG test failed").
-- slackrepo DB (`database_SBo-danix_x86_64.sqlite3`): `revisions` row with
- `dep='/'` holds the built `version` and `built` time per itemid
- (`category/pkg`); `buildresults` holds the last `result` per itemid
- (`ok`, `failed`, `bad`, `aborted`, `removed`). 213 of 799 `buildresults`
- rows have legacy keys without a `/` and match no package.
-- Hints: 623 in `/etc/slackrepo/SBo-danix/hintfiles/`; the default hint dir
- (`default_hintfiles/15.0`) is empty. Real usage: 594 `DELREQUIRES`, 13 each
- of `VERSION`, `DOWNLOAD`, `MD5SUM`, `ARCH`, 1 `OPTIONS`.
-- Hint grammar (`libexec/functions.d/parsefunctions.sh:405-640`,
- `man/man5/slackrepo.hint.5`): slackrepo searches `<SBREPO>/<cat>/<pkg>/`,
- `HINTDIR/`, `HINTDIR/<cat>/<pkg>/`, `DEFAULT_HINTDIR/`,
- `DEFAULT_HINTDIR/<cat>/<pkg>/` for `<pkg>.hint`, first match wins, and
- sources it. Recognised keys: `SKIP VERSION OPTIONS GROUPADD USERADD
- NOWARNING NOWARNINGS ADDREQUIRES DELREQUIRES BUILDTIME CONFLICTS INSTALL
- NUMJOBS ANSWER CLEANUP PRAGMA SPECIAL ARCH DOWNLOAD MD5SUM SHA256SUM`, plus
- `DOWNLOAD_<arch>`, `MD5SUM_<arch>`, `SHA256SUM_<arch>` resolved through
- `ARCH`. The man page says `ANSWERS`; the code reads `ANSWER`.
-- mkhint `--no-dl` writes `NODOWNLOAD=yes`, which slackrepo does not read
- (its switch is `DOWNLOAD="no"`). No current hint has it. Phase 4 changes
- mkhint to write `DOWNLOAD="no"`.
-
-## Part 1: VM side, in the slackrepo_setup repo
-
-slackrepo_setup becomes the source of truth for the slackrepo config.
-
-- `slackrepo_SBo-danix.conf`: imported from the VM. Site values move out to
- `/etc/slackrepo/SBo-danix.local` (root-only, mode 600, not in git), which the
- config sources: rsync target, rsync password and excludes files, Gotify URL
- and key. The repo ships `SBo-danix.local.example` with placeholders.
-- `sb-publish`: new script, the rsync push of `PKGREPO` that
- `rsync_push_hook` does today. Reads its settings from the `.local` file.
- `rsync_push_hook` calls it.
-- Gates in the config:
- - `rsync_push_hook`: return 0 when `SBB_PUBLISH=n`.
- - `gotify_hook`: return 0 when `OPT_PREVIEW=y` (as well as `OPT_DRY_RUN=y`).
- - `manage_packages_hook`: pass `--dry-run` when `OPT_PREVIEW=y` too.
-- `gpg-agent.conf` for root, managed here: today's content plus
- `allow-preset-passphrase`. Deploy runs `gpgconf --reload gpg-agent`.
-- Terminal runs without `SBB_PUBLISH` behave as today, except that previews
- no longer notify Gotify or touch packages.
-- The stale config copy in `repo-html-structure` (untracked, may still hold
- the Gotify key) is deleted after asking the user.
-- Deploy (asks first): config to `/etc/slackrepo/`, `.local` created from the
- current live values only if missing, `sb-publish` to `/usr/local/bin`,
- `gpg-agent.conf` to `/root/.gnupg/`.
-
-## Part 2: helper (`sbb-remote` 0.2.0)
-
-### New config keys
-
-```toml
-slackrepo_repo = "SBo-danix"
-hintdir = "/etc/slackrepo/SBo-danix/hintfiles"
-default_hintdir = "" # optional, searched after hintdir
-genreprc = "/root/.genreprc"
-```
-
-### `inventory`
-
-One JSON object `{"columns": [...], "rows": [[...], ...]}`, a row per
-package directory `<cat>/<pkg>/<pkg>.SlackBuild` in `repo_dir`:
-
-| column | source |
-|---|---|
-| `path` | `cat/pkg` |
-| `version` | hint `VERSION` if the hint has one, else `.info` `VERSION` |
-| `info_version` | `.info` `VERSION` |
-| `hint_version` | hint `VERSION` or `null` |
-| `built_version`, `built_time` | `revisions`, `dep='/'`, by path; `null` if none |
-| `result` | `buildresults` by path; `null` if none |
-| `maintainer`, `requires` | `.info` `MAINTAINER`, `REQUIRES` (list) |
-| `summary` | `slack-desc` line 1, text inside the parentheses |
-| `hint_path` | the hint found by slackrepo's search order (repo package dir, `hintdir`, `hintdir/<cat>/<pkg>`, `default_hintdir`, `default_hintdir/<cat>/<pkg>`), or `null` |
-| `hints` | `{key: raw value}` of every assignment in that hint, or `null` |
-
-`.info` and hint files are parsed, never sourced: continuation lines joined,
-then `shlex.split(line, comments=True)`, keeping `NAME=value` tokens. A
-value containing `$` is kept as written and flagged `"shell": true` for the
-GUI, since it would be expanded by the shell. A missing DB yields `null` build
-fields, not an error.
-
-### Job kinds
-
-| kind | argv | env |
-|---|---|---|
-| `slackrepo <action> [--preview] [--no-publish] [pkg...]` | `slackrepo --repo=<slackrepo_repo> <action> [--preview] pkg...` | `SBB_PUBLISH=n` with `--no-publish` |
-| `publish` | `sb-publish` | |
-
-- `action` is one of `build`, `rebuild`, `update`, `remove`.
-- Package names match `^[A-Za-z0-9._+-]+$` and must name a package in the
- tree. Duplicates are dropped.
-- `build`, `rebuild`, `remove` need at least one package; `update` with none
- means all.
-- Kinds now return `(argv, env)`; `_run` merges `env` into the environment.
- Existing kinds return an empty env. `meta.json` records `env`.
-- While the signing key is locked, `job-start` refuses a non-preview
- `slackrepo` job with `{"error": "signing key locked"}`. Previews and
- `publish` (which does not sign) are not affected.
-
-### Signing key
-
-- Key: `REPOSOWNERGPG` read from `genreprc` with a regex over
- `REPOSOWNERGPG="..."` (never sourced). Keygrips from
- `gpg --with-colons --with-keygrip --list-secret-keys <key>` (`grp` lines).
-- `gpg-status`: `gpg-connect-agent "keyinfo <grip>" /bye` per keygrip; the key
- is unlocked when every keygrip reports cached. Returns
- `{"unlocked": bool, "key": "<fingerprint>"}`; `{"unlocked": null}` when no
- key is configured.
-- `gpg-unlock`: reads the passphrase from stdin, runs
- `gpg-preset-passphrase --preset <grip>` per keygrip with the passphrase on
- its stdin, then verifies by signing a temp file with
- `gpg --batch --pinentry-mode error -u <key> -bas`. On failure it runs
- `gpg-preset-passphrase --forget` per keygrip and returns
- `{"error": "bad passphrase"}`. The passphrase is never put on a command
- line, in the environment, in a log or on disk.
-- Found 2026-10-20: the preset passphrase is lost when gpg-agent exits, and
- it exits when root's last login session ends. elogind then removes
- `/run/user/0` (root does not linger), where the agent's sockets live; the
- agent notices its socket is gone and terminates. The GUI's ssh
- ControlMaster keeps a session open while sb-builder runs, so the key stays
- unlocked as long as the GUI does, and locks about 10 minutes after it
- quits (ControlPersist). Accepted as is; `loginctl enable-linger root`
- would keep the agent across sessions. The GUI checks state before every
- real run either way.
-
-### Version
-
-`VERSION = "0.2.0"` in both helper and GUI; the existing banner asks for
-`./install.sh --remote` until the VM is updated.
-
-## Part 3: GUI
-
-### Packages tab (`sb_builder/packages.py`, `Open > Packages`)
-
-Layout B from brainstorming: query bar, chip rows, table, detail pane, action
-bar.
-
-- Model: `QAbstractTableModel` over the cached inventory, with a
- `QSortFilterProxyModel` subclass doing the filtering. Columns: check, name,
- category, version, built, status, maintainer. Sortable.
-- Status, derived in the GUI:
-
- | status | rule |
- |---|---|
- | `never` | no `built_version` and no `result` |
- | `failed` | `result` in `failed`, `bad`, `aborted` |
- | `outdated` | `built_version` != `version` |
- | `ok` | otherwise |
-
- Shown as a colored glyph plus the word (status colors from the Dashboard).
- `# ponytail:` outdated compares versions only; slackrepo also rebuilds on
- SlackBuild or dependency changes, which its preview shows.
-- Query bar: whitespace-separated terms, all must match, case-insensitive
- substring. Plain term: name or summary. `cat:`, `maint:`, `req:`,
- `status:` match that field.
-- Chips: overlay group (`personal`, `pentesting`, `upstream` = any other
- category) and status group (`ok`, `failed`, `never`, `outdated`). OR within
- a group, AND across groups and with the query; an empty group does not
- filter.
-- Detail pane for the current row: path, summary, `.info`, hint and built
- versions, built time, last result, maintainer, and the hint view below.
-
-### Hint view (`sb_builder/hints.py`, in the detail pane)
-
-Read-only and structured; never the raw file. `hints.interpret(hints, info)`
-returns sections, the detail pane renders only those that apply:
-
-| section | keys | shown as |
-|---|---|---|
-| Skipped (banner) | `SKIP` | `no` = no banner; `yes` = always skipped; other text = skipped, the text as reason |
-| Version override | `VERSION`, `ARCH`, `DOWNLOAD[_arch]`, `MD5SUM[_arch]`, `SHA256SUM[_arch]` | `.info` version -> hint version; URLs after `ARCH` resolution (`DOWNLOAD_<ARCH>` wins when set); "checksums not checked" when `MD5SUM="no"` or `VERSION` set without `MD5SUM` (slackrepo forces `no`); "no download, source must be in the cache" for `DOWNLOAD="no"` and for `NODOWNLOAD=yes` (shown identically, by user decision) |
-| Dependencies | `.info` `REQUIRES`, `DELREQUIRES`, `ADDREQUIRES`, `BUILDTIME`, `PRAGMA python3`, `CONFLICTS` | one chip row computed as slackrepo does: `.info` deps minus `DELREQUIRES` and `%README%` (struck through), `python3` added by the pragma, `ADDREQUIRES` added (+), `BUILDTIME` added and marked build-time only (not in package metadata); `CONFLICTS` listed apart. With no `.info` `REQUIRES`, `ADDREQUIRES` alone defines the deps |
-| Build settings | `OPTIONS`, `NUMJOBS`, `INSTALL`, `ANSWER`, `NOWARNING`/`NOWARNINGS` | rows; `OPTIONS` split into `OPT=value` pairs; `INSTALL` as yes/no by its first letter (`Y`/`1`, `N`/`n`/`0`); `ANSWER` with `\n` shown as a return glyph; `NOWARNING` labelled as a regex |
-| Pragmas | `PRAGMA`, `SPECIAL` | one row per pragma with its meaning from `slackrepo.hint(5)`; `x86arch=<arch>` and `unset_<VAR>` parsed; unknown pragmas shown as unknown |
-| Host changes | `GROUPADD`, `USERADD` | table rows: gid, name; uid, name, group, home, shell, extra args, with slackrepo's defaults filled in (name = package, group = user, home `/dev/null`, shell `/bin/false`) |
-| On uninstall | `CLEANUP` | the command |
-| Ignored | any other key | "not read by slackrepo" (except `NODOWNLOAD`, see above) |
-
-Values flagged `shell` show as written, marked "expanded by the shell". The
-footer shows `hint_path`.
-- Selection: check column, "Select visible", "Clear", "N selected" label.
- Actions apply to the checked packages, or to the current row when none is
- checked.
-- Action bar: Build, Update, Rebuild, Remove, Update all, and Publish
- (confirm dialog, starts the `publish` job).
-- Build/Update/Rebuild/Remove/Update all start the preview job
- (`slackrepo <action> --preview ...`) and open it in the Jobs tab.
-- Inventory: fetched when the tab opens, on its Refresh button, and after any
- `slackrepo` or `publish` job ends. While loading, the old rows stay and the
- status line says "loading...".
-
-### Jobs tab additions
-
-- A finished `ok` preview of kind `slackrepo` shows [Run it] and a ☑ publish
- checkbox (initial state from config `builds.publish`). Run it starts the
- same action and packages without `--preview`, adding `--no-publish` when the
- box is unticked.
-- Before starting any non-preview `slackrepo` job (Run it, or anywhere else)
- the GUI calls `gpg-status`; when locked it asks for the passphrase
- (`QInputDialog` in password mode), calls `gpg-unlock` with the passphrase on
- stdin, repeats on `bad passphrase`, then starts the job. Cancel aborts.
-- `remote.call` gains an optional `stdin` bytes argument, written to the
- QProcess and closed.
-
-### Main window and Dashboard
-
-- Status bar: signing key state, 🔒 locked / 🔓 unlocked, refreshed with the
- stats poll; hidden when the helper reports no key.
-- Dashboard: clicking the Packages tile opens the Packages tab, clicking the
- Recent jobs card opens the Jobs tab.
-- `Open` menu: Packages, Jobs.
-
-### GUI config
-
-```toml
-[builds]
-publish = true # initial state of the publish checkbox
-```
-
-## Error handling
-
-| case | behaviour |
-|---|---|
-| inventory slow (cold disk ~7s) | async; old rows stay, "loading..." |
-| inventory call fails | status bar message, old rows stay |
-| preview fails | no [Run it]; the log says why |
-| key locked | password dialog; `bad passphrase` asks again; Cancel aborts |
-| `job-start` refuses (locked) | same dialog, then retry once |
-| busy | existing busy dialog |
-| `genrepos` fails anyway | job `failed`, notification, slackrepo's message in the log |
-| unknown package | helper rejects by name |
-
-## Testing
-
-stdlib `unittest`, headless, no VM.
-
-- Helper: `inventory` on a fake tree, hint dirs and sqlite DB (hint version
- wins, hint search order precedence, shlex parsing with quotes, comments and
- continuation lines, `$` values flagged, legacy DB keys ignored, missing DB
- gives nulls);
- `slackrepo` kind (unknown package, `build` without packages, `update`
- without packages, duplicate names, `--no-publish` sets
- `SBB_PUBLISH=n` in env and in the child process); locked key refuses a real
- run, not a preview (gpg check patched); `gpg-unlock` feeds stdin to a fake
- `gpg-preset-passphrase` on `PATH` and forgets on failed verification.
-- GUI: `hints.interpret` table-driven per key and derived rule (ARCH
- resolution, forced checksum skip, dependency computation incl. python3
- pragma and `ADDREQUIRES`-only, USERADD/GROUPADD defaults, SKIP forms,
- INSTALL letters, `NODOWNLOAD` shown as `DOWNLOAD="no"`, ignored keys,
- shell-flagged values); query parser and filter (prefixes, chips, OR/AND); status
- derivation; action target is checked rows else current row; [Run it] after
- an ok preview, with `--no-publish` when unticked; locked key opens the
- dialog before the real job (dialog patched); Dashboard tile clicks open
- tabs.
-- slackrepo_setup: `bash -n` on config and `sb-publish`; source the config
- with stub `rsync`, `curl` and `manage-packages.sh` on `PATH` and check the
- gates with and without `SBB_PUBLISH=n` and `OPT_PREVIEW=y`.
-- First real run with the user: unlock, preview an update of one package,
- run it without publish, then Publish.
diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-bundled-deps-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-bundled-deps-design.md
deleted file mode 100644
index de6ad1d..0000000
--- a/docs/superpowers/specs/2026-10-20-sb-builder-bundled-deps-design.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# sb-builder: bundled deps (phase 4b, part 2)
-
-Date: 2026-10-20
-Status: approved
-Builds on: `2026-10-20-sb-builder-phase4-design.md`, `2026-10-20-sb-builder-phantom-deps-design.md`
-
-## Goal
-
-`hint-update` handles packages with several downloads, so mkhint's bundled-dep
-code (url mode) is no longer needed and part 3 can remove mkhint from the VM.
-
-## Facts
-
-- 64 packages are tracked in `nvchecker.toml`; 4 have more than one download
- (686 in the whole tree):
- - neovim, 13 extra: upstream lists them in `cmake.deps/deps.txt`
- (`NAME_URL <url>` lines), at a path fixed per tag.
- - openvino, 9 extra: git submodules pinned by commit. Lives in the personal
- overlay, its `.info` is edited there; not hint-managed.
- - feroxbuster, 1 extra: a self-hosted vendor tarball carrying the main
- version (`feroxbuster-<ver>-vendor.tar.gz`).
- - exploitdb, 1 extra: `exploitdb-bin-sploits`, its own version, unchanged
- since 2022.
-- `apply_version` swaps the version through the whole hint text but
- downloads only the first URL of each `DOWNLOAD*` for its md5; other URLs
- keep their old md5. A feroxbuster bump today writes the new vendor URL
- with the old md5.
-- mkhint's url mode (`/usr/local/bin/mkhint`, `reconcile_bundle_deps_url` and
- helpers, about 200 lines of bash) fetches the manifest at the new version,
- takes the `_URL` lines, matches each extra hint URL to one, compares
- versions and swaps the changed ones, downloading them for the md5.
-
-## Scope
-
-In: md5 for every changed URL (all packages), manifest reconcile (packages
-in `[manifests]`).
-
-Out: git submodule mode (openvino, not hint-managed), separately versioned
-extras with no manifest (exploitdb), a GUI for the manifest map, adding deps
-the manifest has and the hint lacks (logged only), part 3 (removing mkhint,
-nvchecker and their config from the VM).
-
-## Helper (`sbb-remote` 0.8.0)
-
-### Config
-
-`[manifests]` table, package -> manifest URL template, `{VERSION}` replaced
-by the new version. The default holds neovim's, so the VM needs no new line.
-Replaces `manifest_packages`, and with it the refusal of manifest packages by
-`hint-update` and `hint-new`. `remote/config.example.toml` shows the default:
-
- [manifests]
- neovim = "https://raw.githubusercontent.com/neovim/neovim/v{VERSION}/cmake.deps/deps.txt"
-
-`manifest_packages` becomes an unknown key: the deploy drops its line from
-the VM's `/etc/sb-builder/config.toml`, or the helper refuses to start.
-
-### `apply_version`, every package
-
-After the version swap, for each `DOWNLOAD*`/`MD5SUM*` pair, download every
-URL that differs from the one at the same position before the swap, plus the
-first URL as today, and set its md5. Unchanged continuation URLs keep their
-md5. `SKIP_URLS` (`UNSUPPORTED`, `UNTESTED`) and `--no-dl` behave as today.
-
-### `apply_version`, packages in `[manifests]`
-
-Runs after the version swap and before the downloads, only when a new
-version is given and `--no-dl` is not:
-
-1. Fetch the manifest at the new version.
-2. Extract every `https?://` URL from it (regex, any file format).
-3. For each extra URL (position 1 and later) of `DOWNLOAD`, find its match:
- first by GitHub `owner/repo`, lowercased, skipping the shared blob repo
- `neovim/deps`; then by stem, the basename without archive extension and
- without a trailing `-<version>` (`[vV]?` digit, or 7+ hex).
-4. Version of a URL: basename without archive extension; on GitHub, strip a
- leading `<repo>-` or `<repo>_` (case-insensitive) and the rest is the
- version; elsewhere, the trailing `-<version>` or `-<sha>`. Then drop a
- leading `v` and turn `-` into `_`. The first URL's manifest counterpart is
- never reconciled.
-5. A match whose version differs replaces the hint URL; the download step
- above then gives it its md5.
-
-Log lines: `<pkg>: <dep> <old> -> <new>` per changed dep, `<pkg>: <dep> no
-manifest match, left as is`, `<pkg>: manifest has N deps not in the hint`.
-
-### Errors
-
-A manifest fetch failure, or a download failure, fails that package: its
-hint and `.bak` stay untouched, `<pkg>: failed: <why>` is logged, the job
-moves on, as for the main download today. An empty manifest (no URLs) is a
-failure too: a moved file must not pass as "all current".
-
-## GUI
-
-None. The hint diff dialog already shows the changed `DOWNLOAD`/`MD5SUM`
-lines.
-
-## Testing
-
-`test_remote.py`, no network (the existing `Served` localhost http server):
-
-- URL version and match against neovim's real URL shapes: github archive
- `v<ver>/<name>-<ver>`, LuaJIT by commit, luv release asset, lpeg off
- GitHub, mixed-case owner (JuliaStrings).
-- manifest reconcile: one dep changed, one unchanged, one unmatched; the
- changed one gets the new URL and md5, the rest keep theirs.
-- a continuation URL carrying the main version gets a new md5 without a
- manifest (feroxbuster).
-- manifest fetch failure leaves the hint untouched.
-
-## Version
-
-0.8.0 in `remote/sbb-remote` and `sb_builder/__init__.py`; deploy with
-`./install.sh --remote` after asking.
diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-phantom-deps-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-phantom-deps-design.md
deleted file mode 100644
index 46bc41c..0000000
--- a/docs/superpowers/specs/2026-10-20-sb-builder-phantom-deps-design.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# sb-builder: phantom deps (phase 4b, part 1)
-
-Date: 2026-10-20
-Status: approved
-Builds on: `2026-10-20-sb-builder-phase4-design.md`, `2026-10-20-sb-builder-ui-design.md`
-
-## Goal
-
-Manage the -current phantom deps (deps SBo packages still require but
-Slackware -current ships) from sb-builder: the list, suggestions from the
-tree, and the `DELREQUIRES` sweep that mkhint's `-F` does today. Found when
--current added zenity: 8 SBo packages require it, nothing in the GUI could
-mark it.
-
-## Facts
-
-- When a package enters the official tree, SBo removes it. A `REQUIRES`
- name that matches no package of the repo is then left behind. On
- 2026-10-20 the tree has 140 such names, zenity among them; the 17 on
- mkhint's list (`/root/.config/mkhint/phantom-deps`) are all among them.
-- sbshadow ends with `mkhint -F` (skipped on `--no`). mkhint's `-F` merges
- the phantom deps found in each `.info` `REQUIRES` into the hint's
- `DELREQUIRES`, `.bak` first, a minimal hint when there is none; it never
- removes entries.
-- openvino moved into the personal overlay; only neovim still needs mkhint's
- bundled-dep logic.
-
-## Helper (`sbb-remote` 0.7.0)
-
-- Config: `phantom_deps_file`, default `/etc/sb-builder/phantom-deps`: one
- name per line, `#` comments, mkhint's format. Missing file = empty list.
- `manifest_packages` default becomes `["neovim"]`.
-- `phantom-list`: `{"deps": [...], "suggestions": [{"name", "needed_by":
- [...]}]}`. Suggestions: every `REQUIRES` name (without `%README%`) that
- matches no package of the repo and is not on the list, sorted, with the
- packages requiring it.
-- `phantom-add <name>...`, `phantom-remove <name>...`: names must match
- `PKGNAME`, need not exist in the repo. Rewrites the file atomically,
- comments kept, one name per line, no duplicates. Returns the new list.
-- Job kind `fix-current`: for every package whose `.info` `REQUIRES` names a
- listed dep, merge those deps into `DELREQUIRES` of `<hintdir>/<pkg>.hint`
- (union, first seen order; `.bak` only when it changes; a new hint has just
- that line and no `.bak`). Unmanaged hint paths fail that package. Log one
- line per changed hint, then `SBB-RESULT`. Add only, like mkhint.
-- `fix-current-now`: the same sweep run inline, its log on stdout, for
- sbshadow.
-
-## sbshadow (slackrepo_setup)
-
-The end of a non-preview run calls `sbb-remote fix-current-now` in place of
-`mkhint -F`; without sbb-remote it says so and skips, as it does for mkhint
-today.
-
-## GUI
-
-Hints ribbon: **Phantom deps…** (between Delete hint and the end). A
-non-modal dialog:
-
-- the list, one name per row, with **Remove** for the selected rows;
-- a name field with **Add**;
-- **Suggestions**: tickable rows `name needed by a, b, c`, **Add ticked**;
-- **Apply**: starts `fix-current` (the list itself is saved by each Add or
- Remove at once); the usual diff dialog follows, with Revert per package.
-
-## Deploy (asks first)
-
-1. Helper 0.7.0.
-2. `/etc/sb-builder/phantom-deps` from mkhint's file (17 entries).
-3. sbshadow.
-
-## Out
-
-neovim bundled deps (phase 4b, part 2), removing mkhint and nvchecker from
-the VM (part 3), removing `DELREQUIRES` entries when a dep leaves the list.
diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md
deleted file mode 100644
index b746044..0000000
--- a/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# sb-builder phase 4: hints
-
-Date: 2026-10-20
-Status: approved
-Builds on: `2026-10-19-sb-builder-phase1-design.md` (helper, job model, GUI shell),
-`2026-10-19-sb-builder-phase2-design.md` (inventory, hint view, slackrepo jobs)
-
-## Goal
-
-Manage slackrepo hint files from sb-builder: check upstream versions, update
-version hints, create and delete hints, and see a diff of every change
-with a way to revert it.
-
-Direction set by the user: the repo is run from the desktop, the VM shell is
-for upgrading Slackware only. The VM-side tools were built for a terminal; a
-tool may lose its in-VM usability if that makes sb-builder simpler. So the
-hint logic of mkhint moves into `sbb-remote` (Python, JSON answers) and the
-upstream check moves to the desktop. mkhint is not extended.
-
-## Facts this design rests on
-
-Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`,
-1.4.0, 2291 lines) and in slackrepo (`../GITHUB/slackrepo`):
-
-- The VM has 607 hints, all in `hintdir`, none in subdirectories and none in
- the SBo tree. 594 carry only `DELREQUIRES`, 13 carry
- `VERSION` + `ARCH` + `DOWNLOAD` + `MD5SUM`, one carries `OPTIONS`. No hint
- carries `NODOWNLOAD`. Pinned hints carry `DOWNLOAD_x86_64`/`MD5SUM_x86_64`
- lines, mostly empty (`claude-code`: `DOWNLOAD="UNSUPPORTED"` with the URL in
- `DOWNLOAD_x86_64`), and `ARCH="x86_64"`.
-- slackrepo reads `DOWNLOAD="no"` as "no download" and ignores `NODOWNLOAD`.
- `DOWNLOAD_$ARCH` replaces `DOWNLOAD` when set
- (`libexec/functions.d/parsefunctions.sh:457`), so `DOWNLOAD="no"` next to a
- `DOWNLOAD_x86_64` line has no effect.
-- mkhint's version update: back up to `.bak`, replace the old version by the
- new one everywhere in the file, also the `_` -> `-` form when either version
- contains `_`, also the dotless form of a dotted version inside URLs as a
- whole number (`_swap_dotless_url_version`), then for `DOWNLOAD`/`MD5SUM` and
- `DOWNLOAD_x86_64`/`MD5SUM_x86_64`: download the first URL and recompute its
- md5, keep the md5 of continuation URLs unless the user typed a new URL.
- `UNSUPPORTED`/`UNTESTED` values are skipped.
-- mkhint's new hint from `.info`: copy the `.info`, drop `PRGNAM`, `HOMEPAGE`,
- `MAINTAINER`, `EMAIL`, comment out `REQUIRES`, set `ARCH="x86_64"`, append
- `DELREQUIRES` for phantom deps in `REQUIRES`, then apply a version as above
- when one is given.
-- mkhint's `-F`: for every `.info` in the repo whose `REQUIRES` contains a
- phantom dep, merge those deps into the hint's `DELREQUIRES` (union, first
- seen order, `.bak` only when something changes, minimal hint created when
- absent). It edits only the `DELREQUIRES` line. sbshadow
- (slackrepo_setup) runs it after every sync: that is where the 594
- DELREQUIRES-only hints come from, and why they exist (slackrepo must not
- build deps -current does not need). Until now sbshadow ran it on `--no`
- previews too.
-- The VM's nvchecker config has 64 stanzas named after the packages, plus a
- `[__config__]` with oldver, newver and keyfile. nvchecker v2.22 is
- installed on the desktop, with a GitHub token in
- `~/.config/nvchecker/keys.toml`. With `--logger json` it prints one JSON
- object per line on stdout: `{"name", "version", "event": "updated"}` per
- result, `{"name", "error", "level": "error"}` per failure; it exits 0 when
- entries fail and needs no oldver or newver to run.
-- A DELREQUIRES-only hint has no `VERSION`; mkhint's update cannot bump it.
- Updating one pins it from the `.info` first; the next `mkhint -F` keeps the
- pin. A package with a phantom dep always has such a hint, so `hint-new`
- never sees one.
-- slackrepo `update <item>` builds an item that exists, built or not
- (`update_command` calls `build_command`).
-- The phantom deps list on the VM (`/root/.config/mkhint/phantom-deps`) has
- 18 entries.
-- Two packages use bundled-dep manifests: neovim (url mode) and openvino
- (sha mode, GitHub submodules). That code is about 470 lines of mkhint.
-
-## Scope
-
-In: upstream check, version update, new hint, delete hint,
-`--no-dl` as `DOWNLOAD="no"`, diff and revert.
-
-Out, for good (the Hints screen covers them): mkhint `--list`, `--versions`,
-`--review`, `--strip-version`, `--info`, `--clean`.
-
-Out, later (phase 4b, which then removes mkhint and nvchecker from the VM):
-bundled-dep reconcile for neovim and openvino, and a helper `fix-current`
-that sbshadow calls in place of `mkhint -F`, with the phantom deps list
-moved into the helper config as its single source. Also out: generating nvchecker stanzas
-for new packages (add them to `nvchecker.toml` by hand), free text hint
-editing, migration of `NODOWNLOAD=yes` lines (none exist).
-
-## Part 1: upstream check, on the desktop
-
-- `nvchecker.toml` in the repo root: the VM's 64 stanzas, without
- `[__config__]`. Stanza name = package name. Git-tracked, edited by hand.
-- `sb_builder/upstream.py` runs nvchecker through `QProcess` (as `remote.py`
- runs `virsh`) with a fixed argv: `nvchecker -c <tmp>/nvchecker.toml
- --logger json`. The temp dir holds a generated config: a `[__config__]`
- with `keyfile` (from the GUI config, when the file exists), followed by the
- repo file's text. No oldver, newver or `nvtake`: the comparison is against
- the hints.
-- Result: `{pkg: version}` and `{pkg: error}` from the JSON log. A package
- without a result, or a failed run (exit status, stderr), shows as
- `nvchecker error` with the reason in the tooltip.
-- Upstream versions are stored with `-` as `_` (mkhint's `_normalize_version`):
- `-` is illegal in a SlackBuild version, so upstream `2026-10-14` is
- packaged `2026_10_14`. The comparison and `hint-update` use that form.
-- A package is outdated when its upstream version differs from the hint
- `VERSION`, or from the `.info` `VERSION` when the hint has none. Versions
- are compared as strings; no ordering is attempted.
-
-GUI config gains `[hints] nvchecker_keyfile`, default
-`~/.config/nvchecker/keys.toml` (expanded). A missing keyfile is not an
-error: nvchecker runs without a token.
-
-## Part 2: helper (`sbb-remote` 0.5.0)
-
-### Config
-
-New key `manifest_packages` (below). `remote/config.example.toml` shows it.
-
-### Arguments
-
-- package: `PKGNAME` and present in the repo (as for slackrepo jobs).
-- version: `^[A-Za-z0-9._+-]+$`, at most 64 characters.
-- hint path: always `<hintdir>/<pkg>.hint`. If `find_hint` returns a
- different file for the package (a hint earlier in slackrepo's search
- order), every hint command refuses: "hint lives at <path>, not managed".
-- packages with a bundled-dep manifest are named in config key
- `manifest_packages`, default `["neovim", "openvino"]`. `hint-update` and
- `hint-new` with a version refuse them: "bundled deps: use mkhint until
- phase 4b".
-
-### Job kinds
-
-- `hint-update <pkg>=<ver>... [--no-dl]`: per package, in order: compute the
- new text (apply the version, below), then back the hint up to `.bak` and
- write atomically. A failure (download, missing `.info` VERSION) leaves the
- hint and its old `.bak` untouched, logs `<pkg>: failed: <why>` and
- continues with the next package. A package without a hint gets a new one
- built as `hint-new` does. The last log line is
- `SBB-RESULT {"ok": [pkg...], "failed": [[pkg, why]...]}`. Exit status 1
- when any package failed.
-- `hint-new <pkg> [<ver>] [--no-dl]` (since 0.6.0 `hint-new <pkg>... [--no-dl]`,
- no version, see the UI design): refuses when the hint exists. Builds
- the hint from the `.info` as mkhint does, without phantom deps (see the
- facts), then applies the version when given. No `.bak` (a stale
- one is removed); revert means delete.
-
-These jobs never sign, so they do not check the signing key. Their argv is
-the helper itself: `python3 sbb-remote _hint <kind> <args>`, validated again
-there. Job ids now allow a hyphenated kind (`...-hint-update`).
-
-`job-result <id>`: the last `SBB-RESULT` object of a job's log.
-
-### Applying a version
-
-1. A hint without `VERSION` is first pinned from the `.info`: its
- `VERSION`, `DOWNLOAD*` and `MD5SUM*` lines, plus `ARCH="x86_64"` when the
- hint has no `ARCH`, are put in front of the hint's text. Old version = the
- hint's `VERSION`. With no version given (`hint-new <pkg>`), steps 2 and 3
- are skipped.
-2. Replace old by new everywhere in the file except inside `MD5SUM*`
- values. When either contains `_`, also the `-` forms. When old contains
- `.`, also its dotless form inside URLs, as a whole number (no digit before
- or after).
-3. Without `--no-dl`: for `DOWNLOAD`/`MD5SUM` and
- `DOWNLOAD_x86_64`/`MD5SUM_x86_64`, split the values on whitespace (hint
- values may continue over lines with `\`). Skip a pair whose value is empty,
- `UNSUPPORTED` or `UNTESTED`. Download the first URL with `urllib`
- (timeout 60 s, streamed into `hashlib.md5`, nothing kept on disk) and
- replace its md5; keep the md5s of the other URLs. Write values back one
- URL per line with ` \` continuation, as mkhint does.
-4. With `--no-dl`: nothing is downloaded. Every `DOWNLOAD*` and `MD5SUM*`
- assignment is removed (whole multi-line values) and `DOWNLOAD="no"` is
- written, because `DOWNLOAD_$ARCH` would override it.
-
-Hint text is edited as text, not regenerated, so comments, order and other
-keys stay as they are. An assignment is `KEY="..."` (the value may span
-lines inside the quotes) or `KEY=word`, at line start.
-
-### Commands (answer at once, not jobs)
-
-- `hint-diff <pkg>`: `{"bak": text|null, "current": text|null}`. Both null
- is an error ("no backup").
-- `hint-revert <pkg>`: when a `.bak` exists, move it over the hint (also
- recreating a deleted hint); when there is no `.bak` and the hint exists,
- delete the hint (reverting a `hint-new`). Returns `{"ok": true}`.
-- `hint-delete <pkg>`: move the hint to `.bak`. Errors when there is no hint.
-
-`.bak` is one level deep and overwritten by each change, the mkhint
-convention.
-
-### Version
-
-`VERSION` 0.5.0 in `remote/sbb-remote` and `sb_builder/__init__.py`.
-
-## Part 3: GUI
-
-### Hints screen (`sb_builder/hints_tab.py`, ribbon entry and `Open > Hints`)
-
-- Rows: every package with a stanza in `nvchecker.toml` or a hint in
- `hintdir`. Columns: checkbox, package, hint version, `.info` version,
- upstream, state. Versions come from the existing `inventory` call.
-- States: `outdated`, `current`, `no stanza`, `not checked`,
- `nvchecker error` (stderr in the tooltip). Filter chips per state and a
- text filter on the name, as in the Packages tab.
-- Buttons:
- - **Check upstream**: runs Part 1 over the whole file, fills the upstream
- column, checks the outdated rows. Results live for the session.
- - **Update selected**: starts `hint-update pkg=ver ...` for the checked
- rows that have an upstream version; a **No download** checkbox adds
- `--no-dl`.
- - **Delete hint**: confirm dialog, then `hint-delete` for the current row.
-- Jobs go through the Jobs tab like every other job.
-
-### After a hint job
-
-When a `hint-update` or `hint-new` job ends, the GUI reads
-its `SBB-RESULT` line and opens a dialog with one entry per changed package:
-a unified diff (`difflib`) of `hint-diff`, and a **Revert** button per
-package. Failed packages are listed with their reason. The dialog ends with
-**Build these**, which starts `slackrepo update --preview` for the changed
-packages that were not reverted (update builds unbuilt items too), and
-**Close**. Revert is offered once per package: a second one would delete
-the restored hint. After Revert or Delete the inventory is reloaded.
-
-Each diff is fetched when its package is selected.
-
-### Packages detail pane
-
-- No hint: **New hint**, with an optional version field and a **No
- download** checkbox, starts `hint-new`.
-- Hint present: **Open in Hints** selects the package in the Hints screen.
-
-## Error handling
-
-- Bad arguments, unmanaged hint paths and manifest packages are refused by
- the helper before a job starts; the GUI shows the message.
-- A download failure (HTTP status, timeout, DNS) fails that package only and
- leaves its hint as it was.
-- Writes are atomic (temp file in the same directory, `os.replace`).
-- nvchecker missing or failing on the desktop affects only the upstream
- column; the rest of the screen works.
-- A job without an `SBB-RESULT` line (killed, lost) opens no dialog; the
- log shows what happened and `hint-diff` still works per package.
-
-## Testing
-
-- `test_remote.py`: an `http.server` thread serves fixture files for md5.
- Version swap forms (plain, `_`/`-`, dotless in URL only, md5 hex not
- touched), multi-line `DOWNLOAD`/`MD5SUM` with continuation md5s kept,
- `UNSUPPORTED` skipped, `--no-dl` removing `DOWNLOAD_x86_64` and writing
- `DOWNLOAD="no"`, a download failure restoring the hint while the next
- package still updates, `SBB-RESULT` contents, `hint-new` from `.info`,
- a DELREQUIRES-only hint pinned from the `.info`, new -> revert, update -> diff ->
- revert, delete -> revert, manifest package refusal, unmanaged hint path
- refusal, bad version and package rejection.
-- `test_upstream.py`: generated config, JSON log parsing, states (hint
- version, `.info` fallback), the repo file has no `[__config__]`, `Checker`
- against a fake `nvchecker` on `PATH` and a missing one. No network.
-- `test_gui.py`: Hints screen with a fake remote and injected upstream
- results (states, checked rows, Update selected argv), diff dialog Revert
- call.
-
-## Deploy (asks first)
-
-1. `./install.sh --remote` with helper 0.5.0.
-2. slackrepo_setup's sbshadow, which no longer runs `mkhint -F` on `--no`.
-3. mkhint, nvchecker and `/root/.config/{mkhint,nvchecker}` stay on the VM
- until phase 4b.
diff --git a/docs/superpowers/specs/2026-10-20-sb-builder-ui-design.md b/docs/superpowers/specs/2026-10-20-sb-builder-ui-design.md
deleted file mode 100644
index 8a6844d..0000000
--- a/docs/superpowers/specs/2026-10-20-sb-builder-ui-design.md
+++ /dev/null
@@ -1,163 +0,0 @@
-# sb-builder: one UI frame for every tab
-
-Date: 2026-10-20
-Status: approved
-Builds on: phases 1, 2 and 4 (tabs, Jobs, Packages, Results, Hints)
-
-## Goal
-
-Every tab looks and works the same, and a multi-step task (check upstream,
-update hints, preview, build) never sends the user hunting for the next
-button in another place. Rules go into AGENTS.md so later tabs follow them.
-
-## What is wrong today
-
-- Action buttons sit at the bottom in Packages and Hints, in a side pane in
- Results, at the top in Jobs.
-- The ribbon opens tabs; tabs open and close.
-- A step's follow-up lives elsewhere: Packages action -> auto switch to Jobs
- -> Run it at the top of Jobs. Hints: Check upstream (bottom) -> tick ->
- Update selected (bottom) -> diff dialog -> Build these -> Jobs -> Run it
- (top).
-- The Jobs Start menu name says nothing of what it holds.
-
-## Window frame
-
-Top to bottom: main ribbon, small ribbon, tab bar, the tab's view (its
-filter bar first), status bar.
-
-- **Permanent tabs**: Dashboard (default), Packages, Hints, Results, Jobs,
- created at startup, not closable. Each loads its data the first time it is
- shown (Packages, Hints and Results call `inventory`/`results` then, not at
- startup). Navigation: tab bar, Ctrl+PgUp/PgDn, Dashboard tiles (Packages,
- Recent jobs, slackrepo results as today). The ribbon does not open tabs.
- `File` menu unchanged.
-- **Main ribbon**: `QToolBar`, 32 px icons, text under icon, the current
- tab's actions in workflow order. Refilled on tab change.
-- **Small ribbon**: `QToolBar` right under it, 16 px icons, text beside
- icon, the tab's view controls. Hidden when the tab has none.
-- Each tab exposes `actions` and `view_actions`, lists of `QAction` it owns,
- so enabled and checked state survive tab switches. Icons are freedesktop
- theme names with a `QStyle` fallback, as the ribbon does now.
-- An action that does not apply is disabled with a tooltip saying why;
- never hidden, so the ribbon does not shift.
-- Filter bars (search, chips, `N shown · M selected` or `N of M`) stay at
- the top of the view; their Refresh buttons move to the small ribbon.
-
-## Selection
-
-- Change actions act on the **ticked** rows; view actions (Show build log)
- on the **highlighted** row.
-- A click anywhere on a row toggles its tick (done for Packages and Hints).
-- Ctrl+A ticks the visible rows, Ctrl+Shift+A unticks all (done); the
- small ribbon's Select visible and Clear carry the same shortcuts.
-
-## Ribbons per tab
-
-| Tab | Main ribbon | Small ribbon |
-|---|---|---|
-| Dashboard | Refresh | none |
-| Packages | Build, Update, Rebuild, Remove, Update all, New hint, Publish | Select visible, Clear, Refresh |
-| Hints | Check upstream, Update selected, Delete hint | Select visible, Clear, No download (checkable), Refresh |
-| Results | Show build log | Refresh |
-| Jobs | Pull overlay (menu: one entry per overlay), Check shadowing, Run it, Kill | none |
-
-Disabled reasons (tooltips):
-
-- Build, Update, Rebuild, Remove: "tick packages first".
-- New hint: "none of the ticked packages is without a hint".
-- Update selected: "tick outdated packages after Check upstream".
-- Delete hint: "none of the ticked packages has a hint".
-- Show build log: "select a result with a log".
-- Run it: "select a finished preview".
-- Kill: "select a running job".
-- Check upstream while running: "checking…".
-
-Update all, Publish, Pull overlay and Check shadowing are always enabled
-(the helper refuses a second job with the busy answer, as today).
-
-Packages' detail pane keeps **Open in Hints** as a link (navigation, not an
-action). Its New hint button moves to the ribbon.
-
-### New hint on ticked packages
-
-`hint-new` takes one or more packages and no version: `hint-new <pkg>...
-[--no-dl]`. Each gets the `.info` pin; Update selected from upstream sets a
-version later. The GUI sends no `--no-dl` here (Update selected has the
-toggle); the helper keeps the flag. Every package must be without a hint, else the job is
-refused before it starts, as today for one. Helper `VERSION` 0.6.0 in both
-files, VM redeploy. The New hint dialog goes away.
-
-## Next-step dialogs
-
-- Shown only for jobs started from this GUI session: the main window keeps
- the ids `job-start` returned. Jobs seen on reconnect or started elsewhere
- only notify, as today.
-- Non-modal, one per job, never reopened by polling.
-- **Preview ended ok** (`slackrepo` with `--preview`): dialog titled
- `Preview: <kind and args without --preview>`, with the whole job log
- (`job-follow` from offset 0; the job has ended, so the stream closes by
- itself), a **publish** checkbox (default `builds.publish`), **Run it**
- (default button) and **Not now**. Run it goes through the signing-key
- check (`start_signed`) and starts the real run.
-- **sbpull or sbshadow preview ended ok with a shadow plan** (`shadow:` or
- `unshadow:` lines, as `SHADOW_PLAN` today): same dialog, **Apply
- shadowing** (`sbshadow --yes`, after a confirm: it changes the repo) and
- **Not now**. Without a plan, no dialog.
-- **Hint job ended** (`hint-update`, `hint-new`) or a delete: the existing
- diff dialog; Build these starts `slackrepo update --preview`, whose end
- opens the preview dialog.
-- A real run ending, a failed or killed job: notification only.
-- **Run it** in the Jobs ribbon, with a finished preview selected, reopens
- that preview's dialog (also for a shadowing preview), so Not now loses
- nothing. The Jobs top bar (Start, Apply shadowing, Run it, publish, Kill)
- goes away.
-
-The hugo flow becomes: Hints, Check upstream -> Update selected -> diff
-dialog, Build these -> preview dialog, Run it. No tab switch.
-
-## Jobs tab signal
-
-- Starting a job no longer switches to Jobs; the user stays on the tab.
-- While a job runs, the Jobs tab shows a running icon (`system-run`) and
- reads "Jobs (running)": the tab style sheet's text color would override a
- per-tab color.
-- When it ends, the icon becomes the end status (`✔` ok, `✘` failed,
- killed, lost) and stays until the Jobs tab is shown, then clears.
-- The status bar job button stays.
-
-## AGENTS.md: UI guidelines
-
-A new section, binding for every tab:
-
-1. Tabs are permanent; each loads lazily on first show.
-2. Main ribbon: the tab's actions in workflow order. Small ribbon: view
- controls. Nothing actionable lives inside a view except navigation links.
-3. Change actions act on ticked rows, view actions on the highlighted row.
- A row click toggles its tick; Ctrl+A / Ctrl+Shift+A select visible /
- clear.
-4. An inapplicable action is disabled with a tooltip saying why, never
- hidden.
-5. When a job this session started ends, its next step opens as a
- non-modal dialog where the user is; the Jobs tab shows activity; a
- dismissed step can be taken again from the ribbon.
-6. A modal confirm only for destructive or outward steps (delete hint,
- publish, apply shadowing).
-
-## Testing
-
-`test_gui.py` with the fake remote, offscreen:
-
-- ribbon contents per tab (action texts in order) after switching tabs;
-- disabled states and tooltips (nothing ticked, no preview selected, no job
- running);
-- tabs are not closable and present from the start; data loaded on first
- show only (no `inventory` call before Packages or Hints is shown);
-- a preview this session started ends -> dialog -> Run it starts the real
- run through the signing check; Not now, then Jobs' Run it reopens it;
-- a preview not started by this session ends -> no dialog;
-- Jobs tab icon running -> end status -> cleared on showing Jobs;
-- New hint on two ticked packages -> `hint-new a b`.
-
-`test_remote.py`: `hint-new` with several packages, refusing a version and
-a package that has a hint.