aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-20 11:15:04 +0200
committerDanilo M. <danix@danix.xyz>2026-10-20 11:15:04 +0200
commitb1eb7a1847c42dde679f8f0cdac26fc87a7d9571 (patch)
treed580fae13f5d434f1270cbacb72b11986cdc0cf9 /docs
parente865ba0cb092d7880b017ef2d27e8be9610f5b7d (diff)
downloadsb-builder-b1eb7a1847c42dde679f8f0cdac26fc87a7d9571.tar.gz
sb-builder-b1eb7a1847c42dde679f8f0cdac26fc87a7d9571.zip
Phase 4 plan: drop fix-current, sbshadow owns the DELREQUIRES hints
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-10-20-sb-builder-phase4.md328
1 files changed, 171 insertions, 157 deletions
diff --git a/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md b/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md
index 2608459..1d15e1f 100644
--- a/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md
+++ b/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-**Goal:** Manage slackrepo hints from sb-builder: upstream check on the desktop, hint update/new/delete/fix-current as helper jobs, diff and revert after each.
+**Goal:** Manage slackrepo hints from sb-builder: upstream check on the desktop, hint update/new/delete on the VM through the helper, diff and revert after each.
**Architecture:** mkhint's hint logic is ported into `remote/sbb-remote` as text edits (stdlib: `re`, `urllib`, `hashlib`), run as job kinds whose argv is the helper itself (`_hint`). The GUI runs `nvchecker` locally over a repo-tracked `nvchecker.toml` (`sb_builder/upstream.py`), shows a Hints screen (`sb_builder/hints_tab.py`) and a diff/revert dialog when a hint job ends.
@@ -11,7 +11,7 @@
Spec: `docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md`.
Every code step below was run before this plan was written: the diffs apply
-to `master` at `8ad8b0b` with `git apply` and the suite ends at 113 tests,
+to `master` at `8ad8b0b` with `git apply` and the suite ends at 111 tests,
OK. Two `ResourceWarning: subprocess ... is still running` lines are
expected (detached `_run`).
@@ -19,8 +19,8 @@ expected (detached `_run`).
| File | Change |
|---|---|
-| `remote/sbb-remote` | hint text functions, hint job kinds, `_hint`, `hint-diff`, `hint-revert`, `hint-delete`, `job-result`, config keys `phantom_deps`, `manifest_packages`, hyphenated job ids, VERSION 0.5.0 |
-| `remote/config.example.toml` | the two new keys |
+| `remote/sbb-remote` | hint text functions, hint job kinds, `_hint`, `hint-diff`, `hint-revert`, `hint-delete`, `job-result`, config key `manifest_packages`, hyphenated job ids, VERSION 0.5.0 |
+| `remote/config.example.toml` | the new key |
| `sb_builder/__init__.py` | VERSION 0.5.0 |
| `nvchecker.toml` | new: the VM's 64 stanzas |
| `sb_builder/upstream.py` | new: nvchecker config, JSON log parsing, states, `Checker` |
@@ -50,7 +50,7 @@ and old `.bak`; `hint-revert` without `.bak` deletes (reverting a new hint).
```diff
diff --git c/tests/test_remote.py i/tests/test_remote.py
-index db3d0a1..3e546ef 100644
+index db3d0a1..14622b8 100644
--- c/tests/test_remote.py
+++ i/tests/test_remote.py
@@ -18,6 +18,9 @@
@@ -71,7 +71,7 @@ index db3d0a1..3e546ef 100644
import tempfile
import threading
import time
-@@ -179,6 +183,275 @@ class KindsTest(Base):
+@@ -179,6 +183,255 @@ class KindsTest(Base):
sbb.cmd_job_start(self.cfg, "rm", "-rf", "/")
@@ -110,16 +110,13 @@ index db3d0a1..3e546ef 100644
+ self.assertEqual(sbb.hint_drop(text, r"(?:A|DOWNLOAD\w*)"), "B=plain\n")
+
+ def test_new_hint(self):
-+ self.assertEqual(sbb.new_hint(FLATPAK_INFO, ["ostree", "rust-opt"]),
++ self.assertEqual(sbb.new_hint(FLATPAK_INFO),
+ 'VERSION="1.16.0"\nDOWNLOAD="https://example.org/flatpak-1.16.0.tar.xz"\n'
+ 'MD5SUM="bdf866646fb312a341f84669f625488b"\n'
+ 'DOWNLOAD_x86_64=""\nMD5SUM_x86_64=""\n'
-+ '#REQUIRES="bubblewrap ostree"\nARCH="x86_64"\nDELREQUIRES="ostree"\n')
-+
-+ def test_merge_delrequires(self):
-+ self.assertEqual(sbb.merge_delrequires('DELREQUIRES="a"\n', ["b", "a"]), 'DELREQUIRES="a b"\n')
-+ self.assertIsNone(sbb.merge_delrequires('DELREQUIRES="a b"\n', ["b"]))
-+ self.assertEqual(sbb.merge_delrequires("", ["b"]), 'DELREQUIRES="b"\n')
++ '#REQUIRES="bubblewrap ostree"\nARCH="x86_64"\n')
++ with self.assertRaisesRegex(sbb.Error, "no VERSION"):
++ sbb.new_hint('PRGNAM="x"\n')
+
+
+class Served:
@@ -201,7 +198,6 @@ index db3d0a1..3e546ef 100644
+ def setUp(self):
+ super().setUp()
+ self.www = Served(self, {"x-2.0.tgz": b"two"})
-+ self.cfg["phantom_deps"] = ["rust-opt", "ostree"]
+ self.info = (f'PRGNAM="x"\nVERSION="1.0"\nDOWNLOAD="{self.www.url}/x-1.0.tgz"\nMD5SUM="old"\n'
+ 'DOWNLOAD_x86_64=""\nMD5SUM_x86_64=""\nREQUIRES="rust-opt a"\n')
+ self.hint = os.path.join(self.cfg["hintdir"], "x.hint")
@@ -228,7 +224,6 @@ index db3d0a1..3e546ef 100644
+ self.assertEqual(sbb.KINDS["hint-update"](self.cfg, ["x=2.0", "--no-dl"]),
+ (me + ["hint-update", "x=2.0", "--no-dl"], {}))
+ self.assertEqual(sbb.KINDS["hint-new"](self.cfg, ["x", "2.0"]), (me + ["hint-new", "x", "2.0"], {}))
-+ self.assertEqual(sbb.KINDS["fix-current"](self.cfg, []), (me + ["fix-current"], {}))
+ self.assertFalse(sbb.signs("hint-update", ["x=2.0"]))
+
+ def test_kinds_reject(self):
@@ -249,8 +244,7 @@ index db3d0a1..3e546ef 100644
+ ("hint-update", ["shadowed=1"], "not managed"),
+ ("hint-new", [], "hint-new <pkg>"),
+ ("hint-new", ["x", "1", "2"], "hint-new <pkg>"),
-+ ("hint-new", ["neovim", "1"], "bundled deps"),
-+ ("fix-current", ["x"], "no arguments")):
++ ("hint-new", ["neovim", "1"], "bundled deps")):
+ with self.subTest(kind=kind, args=args), self.assertRaisesRegex(sbb.Error, why):
+ sbb.KINDS[kind](self.cfg, args)
+ sbb.KINDS["hint-new"](self.cfg, ["neovim"]) # no version: no bundled deps to touch
@@ -281,29 +275,15 @@ index db3d0a1..3e546ef 100644
+ rc, result, _ = self.run_hint("hint-update", "x=2.0", "--no-dl")
+ self.assertEqual((rc, result), (0, {"ok": ["x"], "failed": []}))
+ self.assertEqual(self.read(self.hint), 'VERSION="2.0"\n#REQUIRES="rust-opt a"\nARCH="x86_64"\n'
-+ 'DELREQUIRES="rust-opt"\nDOWNLOAD="no"\n')
++ 'DOWNLOAD="no"\n')
+ self.assertFalse(os.path.exists(self.hint + ".bak")) # revert = delete
+
+ def test_new(self):
+ self.make_pkg("misc/x", self.info)
+ rc, result, _ = self.run_hint("hint-new", "x")
+ self.assertEqual((rc, result), (0, {"ok": ["x"], "failed": []}))
-+ self.assertIn('DELREQUIRES="rust-opt"', self.read(self.hint))
-+ self.assertIn('MD5SUM="old"', self.read(self.hint))
-+
-+ def test_fix_current(self):
-+ self.make_pkg("misc/x", self.info, hint='DELREQUIRES="b"\n')
-+ self.make_pkg("misc/z", 'VERSION="1"\nREQUIRES="ostree"\n')
-+ self.make_pkg("misc/w", 'VERSION="1"\nREQUIRES="a"\n')
-+ rc, result, _ = self.run_hint("fix-current")
-+ self.assertEqual((rc, result), (0, {"ok": ["x", "z"], "failed": []}))
-+ self.assertEqual(self.read(self.hint), 'DELREQUIRES="b rust-opt"\n')
-+ z = os.path.join(self.cfg["hintdir"], "z.hint")
-+ self.assertEqual(self.read(z), 'DELREQUIRES="ostree"\n')
-+ self.assertFalse(os.path.exists(z + ".bak"))
-+ rc, result, _ = self.run_hint("fix-current")
-+ self.assertEqual(result, {"ok": [], "failed": []})
-+ self.assertEqual(self.read(self.hint + ".bak"), 'DELREQUIRES="b"\n')
++ self.assertIn('#REQUIRES="rust-opt a"', self.read(self.hint))
++ self.assertIn('MD5SUM="old"', self.read(self.hint)) # no version given: no download
+
+ def test_diff_revert_delete(self):
+ self.make_pkg("misc/x", self.info, hint="A=1\n")
@@ -355,17 +335,16 @@ index db3d0a1..3e546ef 100644
```diff
diff --git c/remote/config.example.toml i/remote/config.example.toml
-index d275401..a489bff 100644
+index d275401..a77ace9 100644
--- c/remote/config.example.toml
+++ i/remote/config.example.toml
-@@ -12,3 +12,5 @@ hintdir = "/etc/slackrepo/SBo-danix/hintfiles"
+@@ -12,3 +12,4 @@ hintdir = "/etc/slackrepo/SBo-danix/hintfiles"
default_hintdir = "" # searched after hintdir when set
genreprc = "/root/.genreprc" # REPOSOWNERGPG names the signing key
slackrepo_logdir = "/var/log/slackrepo/SBo-danix"
-+phantom_deps = [] # deps unneeded on -current, DELREQUIREd by hint-new, fix-current
+manifest_packages = ["neovim", "openvino"] # bundled deps: hint-update refuses them (phase 4b)
diff --git c/remote/sbb-remote i/remote/sbb-remote
-index 25427c0..230dde6 100755
+index 25427c0..2674ed6 100755
--- c/remote/sbb-remote
+++ i/remote/sbb-remote
@@ -25,6 +25,7 @@ its arguments, this file validates them and builds the argv itself.
@@ -387,11 +366,10 @@ index 25427c0..230dde6 100755
DEFAULTS = {
"state_dir": "/var/lib/sb-builder",
"repo_dir": "/var/lib/sbopkg/SBo-danix",
-@@ -51,11 +53,14 @@ DEFAULTS = {
+@@ -51,11 +53,13 @@ DEFAULTS = {
"default_hintdir": "",
"genreprc": "/root/.genreprc",
"slackrepo_logdir": "/var/log/slackrepo/SBo-danix",
-+ "phantom_deps": [], # deps unneeded on -current: hint-new and fix-current DELREQUIRES them
+ "manifest_packages": ["neovim", "openvino"], # bundled deps: left to mkhint until phase 4b
}
FLAGS = ("--yes", "--no")
@@ -403,19 +381,19 @@ index 25427c0..230dde6 100755
ACTIONS = ("build", "rebuild", "update", "remove")
PRESET = "/usr/libexec/gpg-preset-passphrase"
ITEM = re.compile(r"^[A-Za-z0-9._+-]+/[A-Za-z0-9._+-]+$") # category/pkg
-@@ -85,8 +90,9 @@ def load_config(path=None):
+@@ -85,8 +89,9 @@ def load_config(path=None):
if type(v) is not type(DEFAULTS[k]):
raise Error(f"config: {k}: expected {type(DEFAULTS[k]).__name__}")
cfg[k] = v
- if not all(isinstance(o, str) for o in cfg["overlays"]):
- raise Error("config: overlays: expected list of strings")
-+ for k in ("overlays", "phantom_deps", "manifest_packages"):
++ for k in ("overlays", "manifest_packages"):
+ if not all(isinstance(o, str) for o in cfg[k]):
+ raise Error(f"config: {k}: expected list of strings")
return cfg
-@@ -138,8 +144,71 @@ def kind_publish(cfg, args):
+@@ -138,8 +143,67 @@ def kind_publish(cfg, args):
return ["sb-publish"], {}
@@ -472,11 +450,7 @@ index 25427c0..230dde6 100755
+def _self_job(kind):
+ """A job kind run by this file itself (_hint), argv-free like the others."""
+ def build(cfg, args):
-+ if kind == "fix-current":
-+ if args:
-+ raise Error("fix-current takes no arguments")
-+ else:
-+ hint_args(cfg, kind, args)
++ hint_args(cfg, kind, args)
+ return [sys.executable, os.path.abspath(__file__), "_hint", kind, *args], {}
+ return build
+
@@ -484,11 +458,11 @@ index 25427c0..230dde6 100755
KINDS = {"sbpull": kind_sbpull, "sbshadow": kind_sbshadow,
- "slackrepo": kind_slackrepo, "publish": kind_publish}
+ "slackrepo": kind_slackrepo, "publish": kind_publish,
-+ **{k: _self_job(k) for k in ("hint-update", "hint-new", "fix-current")}}
++ **{k: _self_job(k) for k in ("hint-update", "hint-new")}}
def signs(kind, args):
-@@ -549,6 +618,234 @@ def cmd_inventory(cfg):
+@@ -549,6 +613,212 @@ def cmd_inventory(cfg):
return {"columns": COLUMNS, "rows": rows}
@@ -562,23 +536,17 @@ index 25427c0..230dde6 100755
+ return "".join(ln.rstrip("\n") + "\n" for ln in lines)
+
+
-+def new_hint(info, phantom):
-+ """mkhint's hint from an .info: the pin, REQUIRES commented, ARCH, phantom deps."""
++def new_hint(info):
++ """mkhint's hint from an .info: the pin, REQUIRES commented, ARCH.
++
++ No phantom deps: a package that has them already has a hint, the
++ DELREQUIRES one sbshadow's mkhint -F writes after every sync.
++ """
+ text = _pin(info)
-+ requires = hint_tokens(info, "REQUIRES")
+ m = _assignment("REQUIRES").search(info)
+ if m:
+ text += "#" + m.group(0).rstrip("\n") + "\n"
-+ text += 'ARCH="x86_64"\n'
-+ deps = [d for d in phantom if d in requires]
-+ return text + f'DELREQUIRES="{" ".join(deps)}"\n' if deps else text
-+
-+
-+def merge_delrequires(text, deps):
-+ """text with deps added to DELREQUIRES, or None when all are there already."""
-+ have = hint_tokens(text, "DELREQUIRES")
-+ add = [d for d in dict.fromkeys(deps) if d not in have]
-+ return hint_set(text, "DELREQUIRES", " ".join(have + add)) if add else None
++ return text + 'ARCH="x86_64"\n'
+
+
+def _md5_url(url):
@@ -644,41 +612,25 @@ index 25427c0..230dde6 100755
+
+def cmd_hint(cfg, kind=None, *args):
+ """Body of the hint jobs: log lines, then SBB-RESULT {"ok": [...], "failed": [[pkg, why]]}."""
++ if kind not in ("hint-update", "hint-new"):
++ raise Error(f"unknown hint job: {kind}")
+ ok, failed = [], []
+ pkgs = _pkgmap(cfg)
-+ if kind == "fix-current":
-+ for pkg, path in sorted(pkgs.items()):
++ pairs, no_dl = hint_args(cfg, kind, list(args))
++ for pkg, ver in pairs:
++ try:
++ path, target = _hint_target(cfg, pkg, pkgs)
+ info = _read(os.path.join(cfg["repo_dir"], path, pkg + ".info")) or ""
-+ deps = [d for d in cfg["phantom_deps"] if d in hint_tokens(info, "REQUIRES")]
-+ if not deps:
-+ continue
-+ try:
-+ _, target = _hint_target(cfg, pkg, pkgs)
-+ text = merge_delrequires(_read(target) or "", deps)
-+ if text is not None:
-+ _save(target, text)
-+ print(f"{pkg}: DELREQUIRES {' '.join(deps)}", flush=True)
-+ ok.append(pkg)
-+ except (Error, OSError) as e:
-+ failed.append([pkg, str(e)])
-+ elif kind in ("hint-update", "hint-new"):
-+ pairs, no_dl = hint_args(cfg, kind, list(args))
-+ for pkg, ver in pairs:
-+ try:
-+ path, target = _hint_target(cfg, pkg, pkgs)
-+ info = _read(os.path.join(cfg["repo_dir"], path, pkg + ".info")) or ""
-+ text = _read(target)
-+ if text is None:
-+ text = new_hint(info, cfg["phantom_deps"])
-+ _save(target, apply_version(text, info, ver, no_dl,
-+ lambda s, p=pkg: print(f"{p}: {s}", flush=True)))
-+ print(f"{pkg}: ok", flush=True)
-+ ok.append(pkg)
-+ except (Error, OSError) as e:
-+ print(f"{pkg}: failed: {e}", flush=True)
-+ failed.append([pkg, str(e)])
-+ else:
-+ raise Error(f"unknown hint job: {kind}")
++ text = _read(target)
++ if text is None:
++ text = new_hint(info)
++ _save(target, apply_version(text, info, ver, no_dl,
++ lambda s, p=pkg: print(f"{p}: {s}", flush=True)))
++ print(f"{pkg}: ok", flush=True)
++ ok.append(pkg)
++ except (Error, OSError) as e:
++ print(f"{pkg}: failed: {e}", flush=True)
++ failed.append([pkg, str(e)])
+ print("SBB-RESULT " + json.dumps({"ok": ok, "failed": failed}), flush=True)
+ if failed:
+ raise Error(f"{len(failed)} failed")
@@ -723,7 +675,7 @@ index 25427c0..230dde6 100755
# ── signing key ─────────────────────────────────────────────────────────────
def _signing_key(cfg):
-@@ -626,7 +923,12 @@ COMMANDS = {
+@@ -626,7 +896,12 @@ COMMANDS = {
"build-log": cmd_build_log,
"gpg-status": cmd_gpg_status,
"gpg-unlock": cmd_gpg_unlock,
@@ -748,7 +700,7 @@ index e0f577e..21a93ab 100644
+VERSION = "0.5.0" # must match remote/sbb-remote VERSION
```
-- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (104 tests at this point).
+- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (102 tests at this point).
- [ ] **Step 5: Commit.**
@@ -756,7 +708,7 @@ index e0f577e..21a93ab 100644
git add remote/sbb-remote remote/config.example.toml sb_builder/__init__.py tests/test_remote.py
git commit -m "sbb-remote 0.5.0: hint jobs replace mkhint's hint editing
-hint-update, hint-new and fix-current run the helper's own _hint command:
+hint-update and hint-new run the helper's own _hint command:
version bumps as mkhint does them (also '-' and dotless URL forms, never
inside MD5SUM values), md5 of the first URL of each DOWNLOAD pair, --no-dl
as DOWNLOAD=\"no\", .bak before each write and a final SBB-RESULT line
@@ -1051,7 +1003,7 @@ index 0000000..d936706
+ p.start("nvchecker", ["-c", write_config(tmp.name, keyfile, path), "--logger", "json"])
```
-- [ ] **Step 5: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (110 tests).
+- [ ] **Step 5: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (108 tests).
- [ ] **Step 6: Commit.**
@@ -1174,10 +1126,10 @@ index 0da4243..aa9c03b 100644
```diff
diff --git c/sb_builder/hints_tab.py i/sb_builder/hints_tab.py
new file mode 100644
-index 0000000..abe6eb9
+index 0000000..6be57fc
--- /dev/null
+++ i/sb_builder/hints_tab.py
-@@ -0,0 +1,283 @@
+@@ -0,0 +1,277 @@
+# sb-builder - Hints tab
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
@@ -1213,7 +1165,7 @@ index 0000000..abe6eb9
+STATE_COLOR = {"outdated": STATUS["aborted"], "current": STATUS["ok"],
+ "nvchecker error": STATUS["failed"]}
+COLUMNS = ("", "package", "hint", ".info", "upstream", "state")
-+HINT_JOBS = ("hint-update", "hint-new", "fix-current")
++HINT_JOBS = ("hint-update", "hint-new")
+
+
+class HintsTab(QWidget):
@@ -1264,10 +1216,9 @@ index 0000000..abe6eb9
+ bar.addStretch()
+ for w in (self.check_btn, self.update_btn, self.no_dl):
+ bar.addWidget(w)
-+ for label, slot in (("Fix current", self.fix_current), ("Delete hint", self.delete)):
-+ b = QPushButton(label)
-+ b.clicked.connect(slot)
-+ bar.addWidget(b)
++ delete = QPushButton("Delete hint")
++ delete.clicked.connect(self.delete)
++ bar.addWidget(delete)
+
+ layout = QVBoxLayout(self)
+ layout.addWidget(self.query)
@@ -1358,11 +1309,6 @@ index 0000000..abe6eb9
+ if pairs:
+ self.start_requested.emit(["hint-update", *pairs] + (["--no-dl"] if self.no_dl.isChecked() else []))
+
-+ def fix_current(self):
-+ if QMessageBox.question(self, "Fix current", "Add DELREQUIRES for the -current phantom deps "
-+ "to every package that needs them?") == QMessageBox.StandardButton.Yes:
-+ self.start_requested.emit(["fix-current"])
-+
+ def delete(self):
+ name = self.current()
+ if name and QMessageBox.question(self, "Delete hint", f"Delete the hint of {name}? "
@@ -1652,7 +1598,7 @@ index 34ce7f5..146db51 100644
+ *(["--no-dl"] if no_dl else [])])
```
-- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (113 tests).
+- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (111 tests).
- [ ] **Step 5: Look at it.** Render the screens offscreen and read the PNGs (checked while writing this plan: table, chips, button bar, diff dialog with the failure line under the split, New hint under the detail pane):
@@ -1681,8 +1627,7 @@ git add sb_builder/hints_tab.py sb_builder/mainwindow.py sb_builder/packages.py
git commit -m "Hints screen: upstream check, hint jobs, diff and revert
Check upstream fills the upstream column and ticks the outdated hints,
-Update selected starts hint-update, Fix current and Delete hint act on
-the VM. When a hint job ends its SBB-RESULT opens a dialog with each
+Update selected starts hint-update, Delete hint acts on the VM. When a hint job ends its SBB-RESULT opens a dialog with each
changed hint's diff, Revert per package (once) and Build these. The
Packages detail pane gets New hint and Open in Hints.
@@ -1703,7 +1648,7 @@ unbuilt items; 64 stanzas, not 65; pinned hints do carry
```diff
diff --git c/AGENTS.md i/AGENTS.md
-index 9113e34..eda2dd8 100644
+index 9113e34..9a8918c 100644
--- c/AGENTS.md
+++ i/AGENTS.md
@@ -17,7 +17,8 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is
@@ -1716,15 +1661,16 @@ index 9113e34..eda2dd8 100644
- Trust boundary: the helper runs only a fixed table of job kinds and builds
argv itself from validated args. Never pass shell strings or paths from the
GUI to the helper.
-@@ -32,10 +33,15 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is
+@@ -32,10 +33,16 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is
- Hints are parsed (`shlex`), never sourced. `sb_builder/hints.py` mirrors
slackrepo's own rules (`../GITHUB/slackrepo/libexec/functions.d/parsefunctions.sh`);
check that file when changing it.
-+- Hint editing lives in the helper (`hint-update`, `hint-new`, `fix-current`
-+ jobs run the helper's own `_hint` command): text edits, `.bak` before each
++- Hint editing lives in the helper (`hint-update`, `hint-new` jobs run the
++ helper's own `_hint` command): text edits, `.bak` before each
+ write, a final `SBB-RESULT {json}` log line read back with `job-result`.
-+ It replaces mkhint, which stays on the VM only for neovim and openvino
-+ (bundled deps, `manifest_packages`) until phase 4b.
++ It replaces mkhint, which stays on the VM for neovim and openvino
++ (bundled deps, `manifest_packages`) and for the `DELREQUIRES` hints
++ sbshadow regenerates with `mkhint -F` after every sync, until phase 4b.
- Jobs never get input. Any tool run as a job must be non-interactive
(`--yes`/`--no` style flags). If a tool needs a flag it lacks, add it in
- that tool's own repo (e.g. `slackrepo_setup` for sbshadow, `mkhintfile`
@@ -1735,7 +1681,7 @@ index 9113e34..eda2dd8 100644
## Conventions
diff --git c/README.md i/README.md
-index e6434c9..63ebd1a 100644
+index e6434c9..e1f93cc 100644
--- c/README.md
+++ i/README.md
@@ -1,8 +1,8 @@
@@ -1761,7 +1707,7 @@ index e6434c9..63ebd1a 100644
## Install
-@@ -58,6 +57,20 @@ Every key is optional. `File > Reload config` rereads the GUI config.
+@@ -58,6 +57,19 @@ Every key is optional. `File > Reload config` rereads the GUI config.
the package's last attempt (slackrepo keeps one per command: `build.log`,
`update.log`, `rebuild.log`, `remove.log`), where it reports the failure,
with its `resource.log` (load, memory, tmp over the build).
@@ -1771,9 +1717,8 @@ index e6434c9..63ebd1a 100644
+ it by hand to track a package; `hints.nvchecker_keyfile` gives it a GitHub
+ token) and ticks the outdated packages. **Update selected** rewrites their
+ hints on the VM (version, URLs, md5 of the downloaded source; **no download**
-+ writes `DOWNLOAD="no"` instead). **Fix current** adds `DELREQUIRES` for the
-+ -current phantom deps (`phantom_deps` in the VM config). **Delete hint**
-+ keeps a `.bak`. The Packages detail pane has **New hint** for a package
++ writes `DOWNLOAD="no"` instead; a hint sbshadow wrote, `DELREQUIRES` only,
++ gets the `.info` version lines added). **Delete hint** keeps a `.bak`. The Packages detail pane has **New hint** for a package
+ without one.
+- After a hint job a dialog shows the diff of each changed hint, with
+ **Revert** per package and **Build these** (a slackrepo update preview).
@@ -1783,10 +1728,10 @@ index e6434c9..63ebd1a 100644
(status bar 🔒), the GUI asks for its passphrase first and hands it to the
VM's gpg-agent; it is never stored.
diff --git c/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md i/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md
-index df360eb..4b659c7 100644
+index df360eb..c45c720 100644
--- c/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md
+++ i/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md
-@@ -1,7 +1,7 @@
+@@ -1,15 +1,15 @@
# sb-builder phase 4: hints
Date: 2026-10-20
@@ -1795,9 +1740,22 @@ index df360eb..4b659c7 100644
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)
-@@ -25,7 +25,9 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`,
+ ## Goal
+
+ Manage slackrepo hint files from sb-builder: check upstream versions, update
+-version hints, create and delete hints, sweep `DELREQUIRES` for -current
+-phantom deps, and see a diff of every change with a way to revert it.
++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
+@@ -23,9 +23,11 @@ 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 `DELREQUIRES` (from `mkhint -F`), 13 carry
+- the SBo tree. 594 carry `DELREQUIRES` (from `mkhint -F`), 13 carry
++ the SBo tree. 594 carry only `DELREQUIRES`, 13 carry
`VERSION` + `ARCH` + `DOWNLOAD` + `MD5SUM`, one carries `OPTIONS`. No hint
- carries `NODOWNLOAD` or a `DOWNLOAD_x86_64`/`MD5SUM_x86_64` line.
+ carries `NODOWNLOAD`. Pinned hints carry `DOWNLOAD_x86_64`/`MD5SUM_x86_64`
@@ -1806,11 +1764,17 @@ index df360eb..4b659c7 100644
- 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
-@@ -45,11 +47,17 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`,
+@@ -44,12 +46,24 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`,
+ - 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).
+- absent).
-- The VM's nvchecker config has 65 stanzas named after the packages, plus a
++ 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
@@ -1821,13 +1785,33 @@ index df360eb..4b659c7 100644
+ 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.
++ 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
-@@ -70,20 +78,22 @@ editing, migration of `NODOWNLOAD=yes` lines (none exist).
+@@ -57,33 +71,37 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`,
+
+ ## Scope
+
+-In: upstream check, version update, new hint, delete hint, fix-current,
++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: bundled-dep reconcile for neovim and openvino (phase 4b, which
+-then removes mkhint and nvchecker from the VM), generating nvchecker stanzas
++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
@@ -1858,7 +1842,17 @@ index df360eb..4b659c7 100644
`~/.config/nvchecker/keys.toml` (expanded). A missing keyfile is not an
error: nvchecker runs without a token.
-@@ -108,27 +118,38 @@ the 18 entries at deploy. `remote/config.example.toml` shows the key.
+@@ -91,8 +109,7 @@ error: nvchecker runs without a token.
+
+ ### Config
+
+-New key `phantom_deps`, list of strings, default `[]`. The VM config gets
+-the 18 entries at deploy. `remote/config.example.toml` shows the key.
++New key `manifest_packages` (below). `remote/config.example.toml` shows it.
+
+ ### Arguments
+
+@@ -108,27 +125,36 @@ the 18 entries at deploy. `remote/config.example.toml` shows the key.
### Job kinds
@@ -1877,13 +1871,14 @@ index df360eb..4b659c7 100644
+ `SBB-RESULT {"ok": [pkg...], "failed": [[pkg, why]...]}`. Exit status 1
+ when any package failed.
- `hint-new <pkg> [<ver>] [--no-dl]`: refuses when the hint exists. Builds
- the hint from the `.info` as mkhint does, phantom deps from
+- the hint from the `.info` as mkhint does, phantom deps from
- `phantom_deps`, then applies the version when given. No `.bak` (nothing to
- back up); revert means delete.
-+ `phantom_deps`, then applies the version when given. No `.bak` (a stale
+-- `fix-current`: mkhint's `-F` with `phantom_deps`. Logs one line per
+- changed hint, then `SBB-RESULT {"ok": [...], "failed": []}`.
++ 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.
- - `fix-current`: mkhint's `-F` with `phantom_deps`. Logs one line per
- changed hint, then `SBB-RESULT {"ok": [...], "failed": []}`.
-These jobs never sign, so they do not check the signing key.
+These jobs never sign, so they do not check the signing key. Their argv is
@@ -1910,7 +1905,7 @@ index df360eb..4b659c7 100644
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,
-@@ -141,9 +162,8 @@ These jobs never sign, so they do not check the signing key.
+@@ -141,9 +167,8 @@ These jobs never sign, so they do not check the signing key.
written, because `DOWNLOAD_$ARCH` would override it.
Hint text is edited as text, not regenerated, so comments, order and other
@@ -1922,7 +1917,18 @@ index df360eb..4b659c7 100644
### Commands (answer at once, not jobs)
-@@ -187,9 +207,10 @@ When a `hint-update`, `hint-new` or `fix-current` job ends, the GUI reads
+@@ -177,22 +202,21 @@ convention.
+ - **Update selected**: starts `hint-update pkg=ver ...` for the checked
+ rows that have an upstream version; a **No download** checkbox adds
+ `--no-dl`.
+- - **Fix current**: starts `fix-current`.
+ - **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`, `hint-new` or `fix-current` job ends, the GUI reads
++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
@@ -1934,10 +1940,21 @@ index df360eb..4b659c7 100644
+**Close**. Revert is offered once per package: a second one would delete
+the restored hint. After Revert or Delete the inventory is reloaded.
- For `fix-current` the dialog lists the packages without opening every diff
- (it can touch hundreds); clicking one shows its diff.
-@@ -224,8 +245,9 @@ For `fix-current` the dialog lists the packages without opening every diff
- changes nothing and writes no `.bak`), new -> revert, update -> diff ->
+-For `fix-current` the dialog lists the packages without opening every diff
+-(it can touch hundreds); clicking one shows its diff.
++Each diff is fetched when its package is selected.
+
+ ### Packages detail pane
+
+@@ -219,13 +243,13 @@ For `fix-current` the dialog lists the packages without opening every diff
+ 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`
+- with phantom deps, `fix-current` merge and idempotence (second run
+- changes nothing and writes no `.bak`), new -> revert, update -> diff ->
++ 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 text, newver parsing from a fixture,
@@ -1948,6 +1965,14 @@ index df360eb..4b659c7 100644
- `test_gui.py`: Hints screen with a fake remote and injected upstream
results (states, checked rows, Update selected argv), diff dialog Revert
call.
+@@ -233,6 +257,6 @@ For `fix-current` the dialog lists the packages without opening every diff
+ ## Deploy (asks first)
+
+ 1. `./install.sh --remote` with helper 0.5.0.
+-2. Add `phantom_deps` (the VM's 18 entries) to `/etc/sb-builder/config.toml`.
++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.
```
- [ ] **Step 2: Commit and push.**
@@ -1964,17 +1989,6 @@ git log --format='%h %G? %s' -5 # every line G
### Task 5: deploy (ask the user first) and check on the VM
- [ ] **Step 1: Ask**, then deploy the helper: `./install.sh --remote`. Expected last line: `{"version": "0.5.0", ...}`.
-- [ ] **Step 2: Phantom deps into the helper config.** Build the line locally (a JSON array of strings is a TOML array), show it and the VM config to the user, then append it:
-
-```bash
-line=$(ssh buildsystem cat /root/.config/mkhint/phantom-deps | python3 -c '
-import json, sys
-deps = [l.split("#")[0].strip() for l in sys.stdin]
-print("phantom_deps = " + json.dumps([d for d in deps if d]))')
-echo "$line"; ssh buildsystem cat /etc/sb-builder/config.toml
-printf '%s\n' "$line" | ssh buildsystem 'cat >> /etc/sb-builder/config.toml && sbb-remote version'
-```
-
-Expected: an 18-entry array, then the version JSON (a config error would print `{"error": ...}`).
+- [ ] **Step 2: sbshadow.** slackrepo_setup `f5b14ff` (`--no` skips `mkhint -F`): show the user the VM copy's diff, then `scp ~/Programming/GIT/slackrepo_setup/sbshadow buildsystem:$(ssh buildsystem command -v sbshadow)` and `ssh buildsystem 'sbshadow --no | tail -2'`, expecting `sbshadow: preview, phantom-dep hintfiles not regenerated.`
- [ ] **Step 3: Read-only check on real data:** `ssh buildsystem sbb-remote hint-diff flatpak` returns `{"bak": null, "current": "VERSION=..."}` (or the `.bak` mkhint left).
-- [ ] **Step 4: User's manual run** in the GUI, reported back: Hints > Check upstream fills the column; Update selected on one outdated package, dialog shows the diff, Revert restores it; Update again and Build these opens the slackrepo preview; Fix current reports no changes (`mkhint -F` already ran); New hint and Delete hint on a throwaway package, each reverted.
+- [ ] **Step 4: User's manual run** in the GUI, reported back: Hints > Check upstream fills the column; Update selected on one outdated package, dialog shows the diff, Revert restores it; Update again and Build these opens the slackrepo preview; New hint and Delete hint on a throwaway package, each reverted.