From 71a7e5cb9a0922ab6e64edf6d41e4087949af118 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:50:45 -0700 Subject: [PATCH 01/13] fix(pcie-probe): select a card by ordinal only, honour probe failures, refuse blind Per-card measurement was unreachable for every card but ordinal 0: the driver set HIP_VISIBLE_DEVICES=N and still passed N as the device argument, so the probe called hipSetDevice(N) on a runtime where the mask left only ordinal 0. The bats stub reported the env var rather than the argument it received, so the suite asserted the mask was set and never noticed the collision. - one selector: the ordinal is passed as argv, the mask is never set - a probe that fails is now exit 5 and names probe + device; the sweep still measures the remaining cards instead of swallowing the failure - the guard fails closed when `fuser` is missing (gpu_busy_percent reads 0 for a resident-but-idle service), with PCIE_PROBE_FUSER as the test seam - stub probes now model the HIP mask rule; the pre-existing per-device test goes red against the old driver, which is the regression proof Refs #60 --- omp-docs/tools/pcie-probe.md | 28 ++++++++++++++-------- scripts/pcie-probe.sh | 45 +++++++++++++++++++++++++++++------- tests/pcie-probe.bats | 43 ++++++++++++++++++++++++++++++++-- 3 files changed, 97 insertions(+), 19 deletions(-) diff --git a/omp-docs/tools/pcie-probe.md b/omp-docs/tools/pcie-probe.md index 5a17f05..34acca3 100644 --- a/omp-docs/tools/pcie-probe.md +++ b/omp-docs/tools/pcie-probe.md @@ -8,7 +8,7 @@ tags: - rdna4 - benchmarking - dankserver -verified: 2026-10-20 +verified: 2026-10-21 timestamp: 2026-10-20 --- @@ -40,7 +40,10 @@ scripts/pcie-probe.sh --build-only # compile, never touch the cards The guard runs **before anything is built or run** and refuses when the GPU is occupied — `gpu_busy_percent` above `PCIE_PROBE_MAX_BUSY` (default 5 %) or any -process holding `/dev/kfd`. Refusal is exit `3`, and it stops nothing. +process holding `/dev/kfd`. Refusal is exit `3`, and it stops nothing. If the holder +probe is missing the guard refuses too rather than assuming idle — see +[seams](#exit-status-and-seams); `gpu_busy_percent` reads `0` for a service that is +resident and idle, so a silent fallback is exactly the benchmark this law forbids. Auto-stopping a production endpoint is not this tool's decision to make, and a measurement taken as noise beside serving traffic is worthless anyway: it is @@ -65,8 +68,11 @@ throughput at *that* size is what matters, not the big-transfer figure: - Pinned versus pageable columns apart means pinning is load-bearing; together means the copies are already pinned by someone else. -Measure one device at a time (`--device`). A run that lets the runtime pick -several devices cannot attribute bandwidth to a card at all. +Measure one device at a time (`--device`, a HIP ordinal). A run that lets the runtime +pick several devices cannot attribute bandwidth to a card at all. The ordinal goes to +the probe as an argument and **the driver never sets `HIP_VISIBLE_DEVICES`**: masking +the device *and* passing its real ordinal asks `hipSetDevice` for a card the runtime +has hidden, so every card but ordinal 0 fails to measure at all. One selector only. ## The trap carried in the sources @@ -79,14 +85,18 @@ when touching these files. ## Exit status and seams -`0` measured · `2` usage or no sources · `3` refused, GPU busy · `4` no HIP -toolchain or build failed. A client box without the SDK exits `4` loudly rather -than reporting an empty success; the probes belong on the box with the cards. +`0` measured · `2` usage or no sources · `3` refused, GPU busy or holders unknowable · +`4` no HIP toolchain or build failed · `5` a probe ran and failed. A client box without +the SDK exits `4` loudly rather than reporting an empty success; the probes belong on +the box with the cards. A probe that dies (the segfault above, an unfundable +allocation) exits `5` and names which probe on which device — a sweep that quietly +swallowed the failure would report success with half its rows missing. Test seams (all documented in the script header): `PCIE_PROBE_SRC`, `PCIE_PROBE_BUILD`, `PCIE_PROBE_SYSFS`, `PCIE_PROBE_KFD`, `PCIE_PROBE_HIPCC`, -`PCIE_PROBE_MAX_BUSY` — the sysfs/kfd paths are what make a refusal testable on -a machine with no cards, which is the only honest way to test a refusal. +`PCIE_PROBE_FUSER`, `PCIE_PROBE_MAX_BUSY` — the sysfs/kfd paths are what make a +refusal testable on a machine with no cards, which is the only honest way to test a +refusal. Measured campaign numbers are not kept here; they belong with the endpoint they were taken on, in the serverAppConfigs LLM runbook vault. diff --git a/scripts/pcie-probe.sh b/scripts/pcie-probe.sh index dd1b03d..215fc84 100755 --- a/scripts/pcie-probe.sh +++ b/scripts/pcie-probe.sh @@ -15,8 +15,12 @@ # # usage: pcie-probe.sh [--device N]... [--build-only] [--force] # -# --device N measure this device; repeat for each card. Without it the probes run once with the -# environment's own device selection — one shared run cannot attribute bandwidth. +# --device N measure HIP device ordinal N; repeat for each card. Without it the probes run +# once with the environment's own device selection — one shared run cannot +# attribute bandwidth. The ordinal is passed to the probes as argv, which is the +# only selector that survives contact with HIP: HIP_VISIBLE_DEVICES=1 makes +# ordinal 0 the sole visible card, so masking AND passing N asks the probe for a +# device that no longer exists. The mask is deliberately not set here. # --build-only compile and stop; never touches the cards. # --force measure even though the guard says the GPU is busy. # @@ -27,10 +31,11 @@ # PCIE_PROBE_SYSFS drm sysfs root (/sys/class/drm) # PCIE_PROBE_KFD KFD device node (/dev/kfd) # PCIE_PROBE_HIPCC compiler (hipcc) +# PCIE_PROBE_FUSER KFD holder probe (fuser) # PCIE_PROBE_MAX_BUSY refusal threshold, percent (5) # -# Exit status: 0 measured · 2 usage or missing sources · 3 REFUSED, GPU busy · 4 no toolchain or -# build failed. +# Exit status: 0 measured · 2 usage or missing sources · 3 REFUSED, GPU busy or unknowable · +# 4 no toolchain or build failed · 5 a probe ran and failed. set -uo pipefail @@ -39,6 +44,7 @@ BUILD="${PCIE_PROBE_BUILD:-$HOME/.cache/pcie-probe}" SYSFS="${PCIE_PROBE_SYSFS:-/sys/class/drm}" KFD="${PCIE_PROBE_KFD:-/dev/kfd}" HIPCC="${PCIE_PROBE_HIPCC:-hipcc}" +FUSER="${PCIE_PROBE_FUSER:-fuser}" MAX_BUSY="${PCIE_PROBE_MAX_BUSY:-5}" force=0 @@ -63,9 +69,19 @@ fi # ---- the guard, before anything touches the card -------------------------------- if (( ! force )); then busy="" - if [[ -e "$KFD" ]] && command -v fuser > /dev/null 2>&1; then - holders="$(fuser "$KFD" 2>/dev/null | tr -s '[:space:]' ' ')" - [[ -n "${holders// /}" ]] && busy="KFD in use by:${holders}" + if [[ -e "$KFD" ]]; then + if command -v "$FUSER" > /dev/null 2>&1; then + holders="$("$FUSER" "$KFD" 2>/dev/null | tr -s '[:space:]' ' ')" + [[ -n "${holders// /}" ]] && busy="KFD in use by:${holders}" + else + # No fuser (psmisc) means the KFD holders are unknowable — and gpu_busy_percent reads 0 + # for a service that is resident but idle. Guessing "idle" here is how you end up + # benchmarking beside a live endpoint, so the guard fails closed instead. + printf 'pcie-probe: REFUSED — cannot see who holds %s: `%s` not found (psmisc)\n' "$KFD" "$FUSER" >&2 + printf ' install psmisc, or pass --force if you have decided the contention is\n' >&2 + printf ' acceptable and read gpu_busy_percent yourself first.\n' >&2 + exit 3 + fi fi for card in "$SYSFS"/card*; do [[ -f "$card/device/gpu_busy_percent" ]] || continue @@ -111,7 +127,11 @@ for card in "$SYSFS"/card*; do done # ---- measure, per device -------------------------------------------------------- +# One selector only (see --device above), and every probe's exit status is honoured: a probe +# that dies must not let the sweep report success. Failures are collected so the remaining +# cards are still measured — losing the second card's numbers is how a campaign lies. (( ${#devices[@]} > 0 )) || devices=(default) +failures=() for d in "${devices[@]}"; do if [[ $d == default ]]; then printf '\n########## engine device selection ##########\n' @@ -121,6 +141,15 @@ for d in "${devices[@]}"; do for name in hip_smoke pcie_bw pcie_probe; do bin="$BUILD/$name" [[ -x "$bin" ]] || continue - if [[ $d == default ]]; then "$bin"; else HIP_VISIBLE_DEVICES="$d" "$bin" "$d"; fi + if [[ $d == default ]]; then + "$bin" || failures+=("$name (engine selection)") + else + "$bin" "$d" || failures+=("$name (device $d)") + fi done done +if (( ${#failures[@]} > 0 )); then + printf 'pcie-probe: %d probe(s) failed:\n' "${#failures[@]}" >&2 + printf ' %s\n' "${failures[@]}" >&2 + exit 5 +fi diff --git a/tests/pcie-probe.bats b/tests/pcie-probe.bats index 9c74afc..85af796 100755 --- a/tests/pcie-probe.bats +++ b/tests/pcie-probe.bats @@ -6,7 +6,8 @@ # built or run, and the tool stops nothing and starts nothing (auto-stopping a production service is # the thing it must not do unasked). # -# Exit status: 0 measured · 2 usage · 3 refused (GPU busy) · 4 no toolchain / build failed. +# Exit status: 0 measured · 2 usage · 3 refused (GPU busy) · 4 no toolchain / build failed · +# 5 a probe ran and failed. # # hipcc and the compiled probes are stubbed: this box has no HIP SDK (it is a client laptop) and a # real measurement belongs on a machine with the cards, beside no running model. @@ -46,7 +47,16 @@ while [ $# -gt 0 ]; do [ "$1" = "-o" ] && out="$2"; shift; done : > "$PCIE_PROBE_BUILD/.built" { echo '#!/usr/bin/env bash' echo ': > "$PCIE_PROBE_BUILD/.ran"' - echo 'echo "PROBE RAN dev=${HIP_VISIBLE_DEVICES:-none} bin=$(basename "$0")"' + # HIP rule, modelled because it is the trap: with HIP_VISIBLE_DEVICES set, the ONLY + # valid ordinal is 0. A driver that masks the device and still passes its real ordinal + # asks for a card the runtime cannot see — which is why the stub used to report the + # env var instead of the argument, and could not see the collision. + echo 'dev="${1:-0}"' + echo 'if [ -n "${HIP_VISIBLE_DEVICES:-}" ] && [ "$dev" != 0 ]; then' + echo ' printf "setDevice(%s) failed\n" "$dev" >&2; exit 1' + echo 'fi' + echo '[ "${STUB_PROBE_FAILS:-0}" = 1 ] && { echo "stub probe died mid-measurement" >&2; exit 1; }' + echo 'echo "PROBE RAN dev=$dev bin=$(basename "$0")"' echo 'echo " slab 2.344 MiB: 31.4 GB/s"' } > "$out" chmod +x "$out" @@ -131,6 +141,35 @@ teardown() { rm -rf "$TEST_TMP"; } [[ "$output" == *"dev=1"* ]] } +@test "a device ordinal the environment mask would hide is still measured" { + # Reproduction, not preference: HIP_VISIBLE_DEVICES=1 leaves ordinal 0 as the only + # selectable card, so masking AND passing the ordinal asks hipSetDevice(1) for a + # device the runtime cannot see. Per-card bandwidth was unobtainable that way. + run "$SCRIPT" --device 0 --device 1 + [ "$status" -eq 0 ] + [[ "$output" == *"PROBE RAN dev=1"* ]] + [[ "$output" != *"setDevice"* ]] +} + +@test "a probe that dies is reported as a failed run, not a green one" { + # The header promises "0 measured". A probe that segfaults (the documented hazard on + # this ROCm build) must not let the sweep exit 0. + run env STUB_PROBE_FAILS=1 "$SCRIPT" + [ "$status" -eq 5 ] + [[ "$output" == *"failed"* ]] + [[ "$output" == *"hip_smoke"* ]] +} + +@test "the guard refuses when it cannot see who holds the KFD, instead of assuming idle" { + # Without fuser the /dev/kfd half of the guard cannot run, and gpu_busy_percent reads + # 0 for a service that is resident but idle — a silent fallback would benchmark beside + # a live endpoint, which is the one thing this tool's LAW forbids. + run env PCIE_PROBE_FUSER="$TEST_TMP/nope-fuser" "$SCRIPT" + [ "$status" -eq 3 ] + [[ "$output" == *"fuser"* ]] + [ ! -e "$BUILD/.built" ] +} + @test "no probe sources is a usage-level error, not an empty success" { rm -f "$SRC"/*.hip run "$SCRIPT" -- 2.54.0 From 671222b881da88a88c3ed69a59e9c5401d0fd73a Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:54:06 -0700 Subject: [PATCH 02/13] docs(60): point-of-use corrections from the 2026-10-20 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured fixes, one line each, from the review of everything that landed on 2026-10-20 (issue #60). Each page keeps its own facts; nothing was moved. - inventory/index, index: the census blurb says what the page actually covers (tracked-vs-box-only per file), not "all userspace configuration" - dotfiles-deploy: the wallpaper re-fetch recipe resolves the CDN URL through the wallhaven API — /w/ answers text/html, so curl -L never yields a JPEG; description widened to the whole page, tools/index blurb matched - okf-maintenance: the link example and the mistake bullet now agree with okf-spec, which RECOMMENDS absolute bundle paths (they said the opposite) - decisions/index: the unlisted-page gate is kb-coverage.py, not kb-lint - AGENTS.md: the three skill-config subsections are pointers again; the pages own issue tracker, labels and domain docs - swayidle-lock.service: Description names what the unit does (arms the idle lock over noctalia-shell IPC); suspend belongs to noctalia's inhibitor - common.pkgs: git-lfs re-justified by the tracked .gitattributes filter contract; the swayidle comment drops its lid-policy claim --- AGENTS.md | 11 ++++------- .../.config/systemd/user/swayidle-lock.service | 2 +- omp-docs/decisions/index.md | 4 ++-- omp-docs/index.md | 4 ++-- omp-docs/inventory/index.md | 2 +- omp-docs/references/okf-maintenance.md | 7 ++++--- omp-docs/tools/dotfiles-deploy.md | 17 +++++++++++++---- omp-docs/tools/index.md | 2 +- packages/common.pkgs | 4 ++-- 9 files changed, 30 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2dc1de5..f905e95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,19 +94,16 @@ knowledge base. Those pages own their facts; this is only the pointer. ### Issue tracker -Work items are Gitea issues on `dank/command-center` (Dankclaw, -`192.168.0.143:3000`) — no `gh`, no `tea`. See -[`omp-docs/skills/issue-tracker.md`](omp-docs/skills/issue-tracker.md). +See [`omp-docs/skills/issue-tracker.md`](omp-docs/skills/issue-tracker.md). ### Triage labels -The five canonical roles, each label string equal to its name. See -[`omp-docs/skills/triage-labels.md`](omp-docs/skills/triage-labels.md). +See [`omp-docs/skills/triage-labels.md`](omp-docs/skills/triage-labels.md). ### Domain docs -Single-context, inside the existing knowledge base: `omp-docs/glossary.md` and -`omp-docs/decisions/`. See [`omp-docs/skills/domain-docs.md`](omp-docs/skills/domain-docs.md). +ADRs: `omp-docs/decisions/`. `omp-docs/glossary.md` is intentionally not created. +See [`omp-docs/skills/domain-docs.md`](omp-docs/skills/domain-docs.md). ## Secret gate (HARD rule) diff --git a/dotfiles/systemd-user/.config/systemd/user/swayidle-lock.service b/dotfiles/systemd-user/.config/systemd/user/swayidle-lock.service index 2bb987e..26af01e 100644 --- a/dotfiles/systemd-user/.config/systemd/user/swayidle-lock.service +++ b/dotfiles/systemd-user/.config/systemd/user/swayidle-lock.service @@ -1,5 +1,5 @@ [Unit] -Description=swayidle idle lock (locks via noctalia-shell IPC, suspends on lid) +Description=swayidle idle lock watcher (arms the idle lock via noctalia-shell IPC; suspend is covered by noctalia's sleep inhibitor) Documentation=https://github.com/swaywm/swayidle After=graphical-session.target # Restart together with the session, not merely at boot: this unit is wanted by diff --git a/omp-docs/decisions/index.md b/omp-docs/decisions/index.md index 9f3755e..b2232b0 100644 --- a/omp-docs/decisions/index.md +++ b/omp-docs/decisions/index.md @@ -1,8 +1,8 @@ # Architecture decision records Decisions this repo has made and will not silently revisit. One file per -decision, named `NNN-.md`, listed here — an unlisted page fails `kb-lint` -and does not exist as far as any agent is concerned. +decision, named `NNN-.md`, listed here — an unlisted page fails +`scripts/kb-coverage.py` and does not exist as far as any agent is concerned. **No ADRs yet.** The first one lands the moment a decision stops being reversible. diff --git a/omp-docs/index.md b/omp-docs/index.md index 376e07a..9949b1b 100644 --- a/omp-docs/index.md +++ b/omp-docs/index.md @@ -57,8 +57,8 @@ search practices, open models, MCP. ## [Inventory](inventory/index.md) -Pre-pruning census of every userspace config on this box — the input for the -pruning session. +Per-file tracked-vs-box-only census of `~/.config` on blankpad — the input for the +dotfiles pruning/adoption session. ## [Decisions](decisions/index.md) diff --git a/omp-docs/inventory/index.md b/omp-docs/inventory/index.md index 49d5478..aa03f78 100644 --- a/omp-docs/inventory/index.md +++ b/omp-docs/inventory/index.md @@ -2,4 +2,4 @@ Census of all userspace program configuration on the workstation. -- [home-config-census.md](home-config-census.md) — Complete inventory of top-level dotfiles, ~/.config, ~/.local/share state, and special directories (~/.pi, ~/.omp) +- [home-config-census.md](home-config-census.md) — Per-file tracked-vs-box-only census of ~/.config on blankpad — the input for the dotfiles pruning/adoption session. diff --git a/omp-docs/references/okf-maintenance.md b/omp-docs/references/okf-maintenance.md index b361b3a..9100be3 100644 --- a/omp-docs/references/okf-maintenance.md +++ b/omp-docs/references/okf-maintenance.md @@ -38,7 +38,7 @@ happened, and the durable lesson. It is not a file manifest. A gate that gets an empty file list reports success. Law: a check MUST fail closed; a silently-passing gate is worse than none, because it buys the belief of safety. -Details: [secret gate](../tools/index.md). +Details: [secret gate](/tools/index.md). ``` **Rules:** @@ -58,8 +58,9 @@ Details: [secret gate](../tools/index.md). - ❌ Forgetting the entry — the change lands undocumented and the next agent rediscovers the failure the hard way. - ❌ Starting a second log in a subdirectory — the sprawl that makes logging useless. -- ❌ Invented absolute paths in links — links resolve relative to the page, and the - linter checks them. +- ❌ Invented paths in links — the RECOMMENDED form is an absolute bundle path + (`/tools/index.md`), resolved against the bundle root; the mistake is a path that + names no file there, and that is what the linter reports. - ❌ Re-expanding an old entry in place — the entry is a pointer; git is the transcript. --- diff --git a/omp-docs/tools/dotfiles-deploy.md b/omp-docs/tools/dotfiles-deploy.md index e558953..182c1df 100644 --- a/omp-docs/tools/dotfiles-deploy.md +++ b/omp-docs/tools/dotfiles-deploy.md @@ -1,14 +1,14 @@ --- type: reference title: Dotfiles deployment tier -description: How dotfiles/ packages become live configuration — stow-equivalent symlinks, host override packages, and the two laws about theme media and systemd enable symlinks. +description: How dotfiles/ packages become live configuration — deploy.sh modes, host override packages, and the tier's hard-won laws, from media and generated theme output to systemd enable/start semantics, dconf dumps, and idle-lock watcher triage. tags: - dotfiles - deployment - systemd - noctalia timestamp: 2026-10-20 -verified: 2026-10-20 +verified: 2026-10-21 --- # Dotfiles deployment tier @@ -54,8 +54,17 @@ checkout, `git rm --cached` on the branch notwithstanding. Restore on a fresh box: the library lives in `~/.local/share/noctalia/wallpapers/`, a path `deploy.sh` does not manage. Every filename is `wallhaven_.` and the ID is the -whole address — `curl -L -o "https://wallhaven.cc/w/"` into that directory puts -the set back. Losing it is an acceptable outcome; 80 MB per clone forever is not. +whole address — but `https://wallhaven.cc/w/` answers `text/html`, the web page, so +resolve the direct CDN URL through the API and download that: + +```bash +id=73g8xo # the wallhaven_ stem of any file in the set +url=$(curl -s "https://wallhaven.cc/api/v1/w/$id" | grep -o '"path":"[^"]*' | cut -d'"' -f4) +curl -L -o ~/.local/share/noctalia/wallpapers/"${url##*/}" "$url" +``` + +`data.path` already ends in `wallhaven_.`, so the filename keeps itself. Losing it +is an acceptable outcome; 80 MB per clone forever is not. ## Law: never `systemctl --user enable` a unit from a worktree diff --git a/omp-docs/tools/index.md b/omp-docs/tools/index.md index 91a202e..866b884 100644 --- a/omp-docs/tools/index.md +++ b/omp-docs/tools/index.md @@ -9,7 +9,7 @@ Configuration guides and reference material for installed software. Each tool ha * [applications](applications.md) — Default userspace applications on CachyOS with Niri — browsers, editor, terminal, file manager, audio, screenshots, gaming. * [cachyos](cachyos.md) — Arch-based rolling release with performance-optimized kernels, repositories, and gaming stack. Running on the workstation. * [comfyui](comfyui/index.md) — Node-based AI image generation engine on dankclaw. FLUX.2 klein 4B, CPU backend, systemd managed. -* [dotfiles-deploy](dotfiles-deploy.md) — How `dotfiles/` packages become live config: `deploy.sh` modes, host override packages, and why theme media and worktree-enabled units are traps. +* [dotfiles-deploy](dotfiles-deploy.md) — How `dotfiles/` packages become live config: `deploy.sh` modes, host override packages, and the tier's hard-won laws, from media and generated theme output to systemd enable/start semantics, dconf dumps, and idle-lock watcher triage. * [ghostty](ghostty/index.md) — GPU-accelerated terminal emulator with extensive configuration, keybindings, and shaders. * [gitea](gitea/index.md) — Private Git hosting on dankclaw. Quadlet-managed containers: Gitea, PostgreSQL, Caddy. * [handy](handy.md) — Offline speech-to-text transcription with WebKitGTK backend and Tauri v2 UI. diff --git a/packages/common.pkgs b/packages/common.pkgs index 470f2a5..cb4c7bd 100644 --- a/packages/common.pkgs +++ b/packages/common.pkgs @@ -18,8 +18,8 @@ btop # system / fleet tooling yay # AUR helper bootstrap: ships in the cachyos repo; aur.pkgs installs need it -git-lfs # LFS smudge/clean for tracked wallpapers (see .gitattributes) -swayidle # idle lock + lid policy via swayidle-lock.service unit +git-lfs # .gitattributes still routes image types through the LFS filter; no image is tracked since 2026-10-20 +swayidle # idle lock watcher via swayidle-lock.service; suspend locking is noctalia's sleep inhibitor # desktop apps flameshot # screenshot tool; bound to CTRL+Shift+1/2/3 in niri keybinds -- 2.54.0 From 40b97254335cd260a2548d1b7c66b347350ff05c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:54:14 -0700 Subject: [PATCH 03/13] fix(pkg-inventory): state the orphan law, test the boundary it rests on, stop --help mid-code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things from the review (#60), one of them a finding of my own that did not survive measurement: - The reported defect — additions never subtract `pacman -Qtdq`, so an orphan can be emitted as a manifest line — is wrong. A package carries one install reason: `-Qtdq` selects installed-as-dependency, additions come from `-Qqe`, so the sets are disjoint (blankpad: 206 explicit, 3 orphans, none in common). A subtraction would be dead code that looks like a guard, so the header now says why not to add one. The packages page law was rewritten to the same effect. - What the old test should have pinned: an implicit dependency installed AFTER the baseline. Only `-Qqe` keeps it out of drift — mutate that query to `-Qq` and the new test goes red (verified), where the old assertion passed no matter what. - `--help` printed `set -euo pipefail` and the LC_ALL line as documentation: the hardcoded sed range ran past the comment block. It now stops at the first line of code, so the header can keep growing. Refs #60 --- omp-docs/tools/packages.md | 22 +++++++++++++++------- scripts/pkg-inventory.sh | 19 +++++++++++++------ tests/pkg-inventory.bats | 38 ++++++++++++++++++++++++++++++++------ 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/omp-docs/tools/packages.md b/omp-docs/tools/packages.md index 41d80b5..f685f13 100644 --- a/omp-docs/tools/packages.md +++ b/omp-docs/tools/packages.md @@ -8,7 +8,7 @@ tags: - provisioning - fleet - blankpad -verified: 2026-10-20 +verified: 2026-10-21 timestamp: 2026-10-20 --- @@ -48,7 +48,7 @@ undeclared additions (declare these): N ← exit 1 when N > 0 aur (needs a helper): → belongs in packages/aur.pkgs installer-provided, undeclared: N + names → a reinstall supplies these; do not declare declared, not installed: → install.sh installs these -orphans ignored: → pacman -Qtdq +orphans ignored: → pacman -Qtdq, never drift (see the law below) flatpak: → reported only, install.sh installs no flatpaks ``` @@ -78,15 +78,23 @@ how a manifest becomes a pile of source builds. Classify with `pacman -Sl` (world-readable). **`pacman -Sqq` requires root** — a tool that only reads the machine must not need sudo to answer a question about it. -**Known gap:** `aur.pkgs` is read for *every* host, so a desktop-only AUR package (a niri module, a -GUI client) cannot be declared there without asking the headless servers to build it. Such a name is -still reported as an AUR addition; leave it undeclared until `install.sh` grows a per-host AUR +**Known gap:** `aur.pkgs` is read for *every* host, so a desktop-only AUR package — the niri +module `nirimod-git`, say, or a GUI client — cannot be declared there without asking the +headless servers to build it. Such a name is still reported as an AUR addition and stays +undeclared (see the note in `packages/blankpad.pkgs`) until `install.sh` grows a per-host AUR manifest. ## Law: orphans never reach a manifest -`pacman -Qtdq` names are packages nothing depends on; emitting them would teach a fresh machine to -install the same dead weight. They are printed as `orphans ignored` and excluded from `--emit`. +Drift is `pacman -Qqe` — *explicitly* installed, therefore yours. `pacman -Qtdq` names packages +installed **as a dependency** that nothing requires any more, and a package carries exactly one +install reason, so the two sets cannot overlap (blankpad 2026-10-21: 206 explicit, 3 orphans, none +in common). That is why nothing in `pkg-inventory.sh` subtracts the orphan list from the additions: +the query is already the guard. Do not add such a subtraction — it cannot fire, and it would bury +the invariant that matters, *an implicit install is never drift no matter when it arrived*, which +`tests/pkg-inventory.bats` pins with a dependency installed after the baseline. What the +`orphans ignored` line is for is the other half: dead weight on the machine that a manifest must +never teach a fresh box to install. ## Two traps that produced this tool diff --git a/scripts/pkg-inventory.sh b/scripts/pkg-inventory.sh index 97f5386..f6e4ae9 100755 --- a/scripts/pkg-inventory.sh +++ b/scripts/pkg-inventory.sh @@ -19,11 +19,19 @@ # aur (needs a helper): → belongs in packages/aur.pkgs (yay) # installer-provided, undeclared: N a reinstall supplies these; do not declare # declared, not installed: N install.sh installs these -# orphans ignored: pacman -Qtdq — never emitted +# orphans ignored: pacman -Qtdq — never drift, never emitted # flatpak: reported only, install.sh installs no flatpaks # # --emit prints the additions as manifest lines, ready to fold into packages/. Names are bare # tokens so install.sh's `sed 's/#.*//'` + first-field parser reads them unchanged. + +# Drift is `pacman -Qqe`: explicitly installed, therefore something you asked for and a rebuild +# must declare. `pacman -Qtdq` orphans are packages installed AS A DEPENDENCY and no longer +# required, and a package carries exactly one install reason, so the two sets are disjoint by +# construction (blankpad, 2026-10-21: 206 explicit, 3 orphans, none in common). Nothing here +# subtracts the orphan list from the additions and nothing should — the query is the guard. Do not +# add such a subtraction: it cannot fire, and it would hide the real invariant (implicit installs +# are never drift, whenever they arrived) behind a line that looks like protection. # # Exit status: 0 clean · 1 drift (undeclared additions exist) · 2 usage · 3 cannot classify # (pacman log unreadable or sync databases unavailable — worse to guess than to say so, because @@ -42,23 +50,22 @@ MODE="report" while [[ $# -gt 0 ]]; do case "$1" in --emit) MODE="emit"; shift ;; - -h | --help) sed -n '2,40p' "$0"; exit 0 ;; + -h | --help) awk '/^set /{exit} NR>1' "$0"; exit 0 ;; *) echo "usage: pkg-inventory.sh [--emit]" >&2; exit 2 ;; esac done +HERE="$(dirname "$(realpath "$0")")" REPO="${PKGINV_REPO:-}" if [[ -z "$REPO" ]]; then - REPO="$(git -C "$(dirname "$(realpath "$0")")" rev-parse --show-toplevel 2>/dev/null || true)" + REPO="$(git -C "$HERE" rev-parse --show-toplevel 2>/dev/null || true)" fi [[ -n "$REPO" && -d "$REPO/packages" ]] || { echo "pkg-inventory: no packages/ under ${REPO:-}" >&2; exit 2; } HOST="${PKGINV_HOST:-$(hostname)}" TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT -read_manifest() { # same parser install.sh uses, so a name means the same thing to both - sed 's/#.*//' "$1" | tr -d '\r' | awk '{if ($1) print $1}' -} +source "$HERE/lib/manifest.sh" # the same parser install.sh installs with # ---- installed state --------------------------------------------------------- # Four queries are the tool's whole interface to pacman. -Sl is the unprivileged sync listing: # -Sqq asks for root, and a read of the machine must not need sudo. Field 2 of -Sl is the package diff --git a/tests/pkg-inventory.bats b/tests/pkg-inventory.bats index 7446e94..9c22d95 100755 --- a/tests/pkg-inventory.bats +++ b/tests/pkg-inventory.bats @@ -39,15 +39,18 @@ EOF [2026-03-04T09:00:00-0800] [ALPM] installed bun (1.2-1) [2026-03-04T09:30:00-0800] [ALPM] upgraded filesystem (2026.01.01-1 -> 2026.03.01-1) [2026-03-05T09:00:00-0800] [ALPM] installed nirimod-git (r7-1) +[2026-03-07T09:00:00-0800] [ALPM] installed libstray (1.0-1) [2026-03-06T09:00:00-0800] [ALPM] removed base (3-1) EOF # --- installed state -------------------------------------------------------- # ghostty/zoxide/neovim/typora declared. libfoo: explicit, arrived with the installer. - # bun/nirimod-git: installed by hand afterwards, undeclared. python-hatchling: orphan. - printf '%s\n' base bun filesystem ghostty libfoo neovim nirimod-git typora zoxide > "$TEST_TMP/local-all" + # bun/nirimod-git: installed by hand afterwards, undeclared. python-hatchling and libstray: + # installed as dependencies and now unrequired (pacman -Qtdq). libstray arrived AFTER the + # baseline, so the date filter alone cannot keep it out of drift — only the explicit query can. + printf '%s\n' base bun filesystem ghostty libfoo libstray neovim nirimod-git typora zoxide > "$TEST_TMP/local-all" printf '%s\n' bun ghostty libfoo neovim nirimod-git typora zoxide > "$TEST_TMP/local-explicit" - printf '%s\n' python-hatchling > "$TEST_TMP/orphans" + printf '%s\n' python-hatchling libstray > "$TEST_TMP/orphans" # nirimod-git is deliberately ABSENT here: no sync db can serve it, which is the fact that # routes it to aur.pkgs in the test below. printf '%s\n' bun ghostty neovim typora zoxide libfoo vlc python-hatchling base filesystem > "$TEST_TMP/sync-all" @@ -131,11 +134,34 @@ ADDITIONS() { section "^undeclared additions" "^installer-provided"; } [[ "$(section "^declared," "^orphans")" == *vlc* ]] } -@test "orphans are reported as ignored and never emitted" { +@test "orphans are named on their own line, not mixed into the drift answer" { run "$SCRIPT" - [[ "$output" == *orphans*python-hatchling* ]] + [[ "$(section "^orphans" "^flatpak")" == *python-hatchling* ]] + [[ "$(ADDITIONS)" != *python-hatchling* ]] +} + +@test "a dependency installed after the baseline is never drift, orphan or not" { + # Drift means `pacman -Qqe` — you asked for this one. libstray arrived as a dependency + # after install day and is now unrequired, so the baseline filter does not exclude it: + # only the explicit query does. Swap -Qqe for -Qq and every dependency on the machine, + # dead ones included, becomes a manifest line install.sh will reinstall on a rebuild. + run "$SCRIPT" + [[ "$(ADDITIONS)" != *libstray* ]] + [[ "$output" == *"undeclared additions (declare these): 2"* ]] + [[ "$(section "^orphans" "^flatpak")" == *libstray* ]] run "$SCRIPT" --emit - [[ "$output" != *python-hatchling* ]] + [[ "$output" != *libstray* ]] +} + +@test "--help prints the header and stops before the shell" { + # The header IS the contract (report shape, exit status, seams). A hardcoded line range + # that runs past the comment block hands the reader `set -euo pipefail` as if it were + # documentation. + run "$SCRIPT" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Exit status"* ]] + [[ "$output" != *"pipefail"* ]] + [[ "$output" != *"LC_ALL"* ]] } @test "--baseline moves packages installed on that date into the installer set" { -- 2.54.0 From 06f45e83c1dbf16f5f3c6131ea8615321453ae9a Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:54:31 -0700 Subject: [PATCH 04/13] refactor(packages): one manifest parser, so a name means one thing to both tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install.sh and pkg-inventory.sh carried byte-identical copies of read_manifest. That is the duplicated-code half of the problem; the worse half is untested — neither script had any test for the grammar that decides what gets installed. - scripts/lib/manifest.sh is the parser; both scripts source it - tests/manifest-parser.bats writes the contract first (comments, CRLF, first field only, unreadable file fails loudly instead of yielding "nothing desired") - an unreadable manifest now fails inside the helper rather than depending on whichever `set` options the caller happens to run under - install.sh check mode verified end to end against a stubbed pacman: a CRLF manifest with trailing comments resolves to bare names, installed names drop Refs #60 --- omp-docs/tools/packages.md | 13 +++++++--- scripts/install.sh | 7 +++--- scripts/lib/manifest.sh | 23 +++++++++++++++++ tests/manifest-parser.bats | 51 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 8 deletions(-) create mode 100755 scripts/lib/manifest.sh create mode 100755 tests/manifest-parser.bats diff --git a/omp-docs/tools/packages.md b/omp-docs/tools/packages.md index f685f13..c01abfd 100644 --- a/omp-docs/tools/packages.md +++ b/omp-docs/tools/packages.md @@ -25,9 +25,13 @@ if both directions are checked. ## Manifests `packages/common.pkgs` (every host) + `packages/.pkgs` (this host) + `packages/aur.pkgs` -(AUR-only, installed through `yay`). The parser is `sed 's/#.*//'` then the first field, so `#` -comments trail freely and one token per line is the whole grammar. An unknown `--host` gets the -common manifest only: a wrong-host run is a no-op, not a hazard. +(AUR-only, installed through `yay`). **One parser reads them for both tools**: +`scripts/lib/manifest.sh` — `sed 's/#.*//'` then the first field, so `#` comments trail freely and +one token per line is the whole grammar; an unreadable manifest is a loud failure rather than an +empty desired list. `tests/manifest-parser.bats` owns that contract. The two scripts carried +verbatim copies of the body until 2026-10-21, which is exactly how a manifest line can come to mean +two things to the two halves of the same check. An unknown `--host` gets the common manifest only: +a wrong-host run is a no-op, not a hazard. ## Rebuild a workstation @@ -110,5 +114,6 @@ never teach a fresh box to install. - [tools/index](index.md) · [dotfiles-deploy](dotfiles-deploy.md) (the config tier that follows) · [live-sync](live-sync.md) (the heartbeat that commits machine state) -- Tests: `tests/pkg-inventory.bats` (fixtures + stubbed `pacman`, no real package database touched). +- Tests: `tests/pkg-inventory.bats` (fixtures + stubbed `pacman`, no real package database touched) + and `tests/manifest-parser.bats` (the grammar above, observed as names in / names out). - Refs #51. diff --git a/scripts/install.sh b/scripts/install.sh index 4f787a2..dd2c001 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -17,7 +17,8 @@ # otherwise sudo prompts normally. The value is never stored in this repo. set -euo pipefail -REPO="$(git -C "$(dirname "$(realpath "$0")")" rev-parse --show-toplevel 2>/dev/null || true)" +HERE="$(dirname "$(realpath "$0")")" +REPO="$(git -C "$HERE" rev-parse --show-toplevel 2>/dev/null || true)" REPO="${INSTALL_REPO:-$REPO}" [ -n "$REPO" ] && [ -d "$REPO/packages" ] || { echo "install.sh: no packages/ under ${REPO:-}" >&2; exit 1; } @@ -36,9 +37,7 @@ SUDO() { else sudo "$@"; fi } -read_manifest() { # $1 = manifest path; emits package names, strips comments/blanks - sed 's/#.*//' "$1" | tr -d '\r' | awk '{if ($1) print $1}' -} +source "$HERE/lib/manifest.sh" # the same parser pkg-inventory.sh reads with # ---- desired-minus-installed delta ------------------------------------------ PKGS=() for m in common.pkgs "$HOST.pkgs"; do diff --git a/scripts/lib/manifest.sh b/scripts/lib/manifest.sh new file mode 100755 index 0000000..c20284e --- /dev/null +++ b/scripts/lib/manifest.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# scripts/lib/manifest.sh — the one parser for packages/*.pkgs +# +# install.sh walks desired → installed; pkg-inventory.sh walks installed → declared. Both read the +# same hand-written manifests, so both must agree on what a line MEANS: if one forgives an inline +# comment and the other does not, the same name is simultaneously "desired" and "drift", and the +# pair stops being a check on the machine. Source it — do not paste the body into a caller again +# (Refs #60: it was pasted twice, in install.sh and in pkg-inventory.sh). +# +# Contract, and tests/manifest-parser.bats is that contract: +# name first field of a line, emitted as one bare token — callers hand it to pacman +# comment `#` and everything after it is dropped, whole-line or trailing +# CRLF stripped (the manifests are edited on more than one box) +# blank empty and whitespace-only rows vanish +# unreadable non-zero exit, no names: a silent empty list reads as "nothing desired", and a +# rebuild would then install nothing while reporting success +# +# Sourced, never executed; callers own `set -euo pipefail`. + +read_manifest() { # $1 = manifest path; prints one package name per line + [[ -r "$1" ]] || { printf 'read_manifest: cannot read %s\n' "$1" >&2; return 2; } + sed 's/#.*//' "$1" | tr -d '\r' | awk '{if ($1) print $1}' +} diff --git a/tests/manifest-parser.bats b/tests/manifest-parser.bats new file mode 100755 index 0000000..6641e15 --- /dev/null +++ b/tests/manifest-parser.bats @@ -0,0 +1,51 @@ +#!/usr/bin/env bats +# tests/manifest-parser.bats — the one manifest parser, shared by install.sh and pkg-inventory.sh +# +# `packages/*.pkgs` is hand-written, so the parser's whole job is to forgive the ways a hand-edited +# list is wrong. The same function decides what install.sh tries to install and what pkg-inventory +# calls drift, so a manifest line must mean the same thing to both — which is why it lives in one +# file instead of being pasted into each caller (Refs #60: it was pasted twice). +# +# Observed through the function's exit status and stdout only: +# names first field of a line, one bare token; `#` and everything after it is a comment +# CRLF tolerated (the manifests get edited on more than one box) +# blank lines vanish +# unreadable non-zero with no names — an empty desired list must never be a silent success, +# because "nothing desired" is exactly what a rebuild would install + +setup() { + TEST_TMP="$(mktemp -d)" + LIB="$BATS_TEST_DIRNAME/../scripts/lib/manifest.sh" +} + +teardown() { rm -rf "$TEST_TMP"; } + +parse() { # $1 = manifest contents, verbatim + printf '%s' "$1" > "$TEST_TMP/m.pkgs" + run bash -c 'source "$1"; read_manifest "$2"' _ "$LIB" "$TEST_TMP/m.pkgs" +} + +@test "inline notes, full-line comments, blank rows and CRLF come out as bare names" { + parse $'# desired state\r\nghostty # terminal\r\n\r\n \r\nzoxide\r\n' + [ "$status" -eq 0 ] + [ "$(printf '%s\n' "$output" | tr '\n' ' ')" = "ghostty zoxide " ] +} + +@test "only the first field of a line is a package" { + # install.sh hands every emitted token straight to pacman, so a stray second token on a line + # must never become an install target; whatever it is, the manifest did not declare it. + parse $'neovim extra tokens here\n' + [ "$output" = "neovim" ] +} + +@test "a comment that names a package is not a package" { + parse $'# vlc\n#typora\nbun\n' + [ "$output" = "bun" ] +} + +@test "an unreadable manifest fails loudly instead of yielding an empty desired list" { + run --separate-stderr bash -c 'source "$1"; read_manifest "$2"' _ "$LIB" "$TEST_TMP/nope.pkgs" + [ "$status" -ne 0 ] + [ -z "$output" ] + [[ "$stderr" == *nope.pkgs* ]] +} -- 2.54.0 From 812be6a0a6e9096698d94bf6daaf3ab4fe7ea161 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:55:47 -0700 Subject: [PATCH 05/13] fix(secret-gate): a path git quotes, a placeholder comment, a glued report line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three ways the fail-closed gate failed open, all reproduced by new tests first: - `git diff --name-only` quotes any path it cannot print as UTF-8 (core.quotePath, on by default). The quoted string was handed back to `git diff -- `, which matches nothing, and an empty diff is `continue`d — so a credential under `café/` committed cleanly. The list is now NUL-delimited and read from a temp file, because command substitution deletes NUL bytes and the names must survive to the loop that reads them. - The placeholder filter matched the whole added line, so `API_TOKEN= # example, placeholder, changeme` was exempt on the strength of its own comment. The filter now reads the assigned value only — the token up to whitespace or `#`. Truncation errs toward blocking; ALLOW_SECRETS remains the way out. - `report+=` ended on a command substitution, so the next file's name was glued onto the previous file's last hit line and the report blamed the wrong file. Three tests, all red before the change: a quoted path, a placeholder comment beside a live value, and two leaking files each named on their own line. Refs #60 --- .githooks/pre-commit | 46 +++++++++++++++++++++++++++++++++--------- tests/secret-gate.bats | 32 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 5c1a608..1e41f1c 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -34,24 +34,36 @@ placeholders='changeme|change[-_]|placeholder|redacted|example|sample|dummy|noke # how a gate gets permanently overridden. assign_exempt='(_FILE|_PATH|_DIR|_URL|_ENDPOINT|_SOCKET)[[:space:]]*[:=]|[:=][[:space:]]*["'"'"']?(/|\.\./)' # Fail closed: a gate that cannot read the staged diff must not report success. -if ! files=$(git diff --cached --diff-filter=ACM --name-only 2>&1); then +# +# `-z`, and the list lives in a file. Two traps decide this: git quotes any path it cannot print +# as UTF-8 (core.quotePath is on by default), and a quoted name handed back to +# `git diff -- ` matches nothing — the file is skipped, and a skipped file used to be a +# silent pass. Command substitution cannot hold the list either, because $() deletes NUL bytes, +# so the names go to a temp file and the loop reads them with a NUL delimiter. +TMPD=$(mktemp -d) || TMPD="" +if [ -z "$TMPD" ]; then + echo "SECRET GATE: cannot create a temp dir — refusing to commit." >&2 + [ -n "${ALLOW_SECRETS:-}" ] && exit 0 + exit 1 +fi +trap 'rm -rf "$TMPD"' EXIT +if ! git diff --cached --diff-filter=ACM --name-only -z > "$TMPD/files" 2> "$TMPD/err"; then { echo "SECRET GATE: cannot read the staged diff — refusing to commit." - printf ' %s\n' "$files" + sed 's/^/ /' "$TMPD/err" echo "Override deliberately: ALLOW_SECRETS=1 git commit ..." } >&2 [ -n "${ALLOW_SECRETS:-}" ] && exit 0 exit 1 fi -# stderr is captured so the failure branch can quote it; on success it must not -# leak a `warning:` line into the file list and have it mistaken for a path. -files=$(printf '%s\n' "$files" | grep -vE '^(warning|fatal|error):' || true) -[ -z "${files//[[:space:]]/}" ] && exit 0 +# git's stderr stays out of the name list on purpose: a `warning:` line mistaken for a path +# becomes a file that "cannot be read", which is how a gate starts skipping the real ones. +[ -s "$TMPD/files" ] || exit 0 report="" exempt="" count=0 -while IFS= read -r f; do +while IFS= read -r -d '' f; do [ -z "$f" ] && continue # Self-exemption, deliberately an exact filename list and not `tests/`: the # gate's fixtures must contain credential-shaped strings or there is nothing to @@ -69,15 +81,29 @@ while IFS= read -r f; do [ -z "$added" ] && continue hits=$(printf '%s\n' "$added" | grep -Ei "$shapes" || true) if [ "${f##*.}" != "md" ]; then - assigns=$(printf '%s\n' "$added" | grep -Ei "$assign" | grep -Ev "$assign_exempt" | grep -Eiv "$placeholders" || true) + # The placeholder filter reads the VALUE, never the whole line. A trailing comment that + # says `# example` says nothing about the token the same line assigns; scoping the filter + # to what follows the separator is what keeps `API_TOKEN= # placeholder` a block. + assigns=$(printf '%s\n' "$added" | grep -Ei "$assign" | grep -Ev "$assign_exempt" \ + | while IFS= read -r line; do + # The judged value is the assigned token alone: it ends at whitespace or `#`, so a + # trailing comment cannot vouch for it. Truncating a quoted value early only ever errs + # toward blocking, which is the safe direction for a gate that can be overridden. + value=${line#*[:=]} + value=${value#"${value%%[![:space:]]*}"} + value=${value%%[[:space:]]*} + value=${value%%#*} + printf '%s\n' "$value" | grep -Eiq "$placeholders" && continue + printf '%s\n' "$line" + done) hits=$(printf '%s\n%s\n' "$hits" "$assigns" | sed '/^[[:space:]]*$/d') fi if [ -n "$hits" ]; then n=$(printf '%s\n' "$hits" | wc -l) count=$((count + n)) - report+=" $f"$'\n'"$(printf '%s\n' "$hits" | cut -c1-100 | sed 's/^/ /')" + report+=" $f"$'\n'"$(printf '%s\n' "$hits" | cut -c1-100 | sed 's/^/ /')"$'\n' fi -done <<< "$files" +done < "$TMPD/files" # Say so out loud: an exemption that prints nothing is how a carve-out becomes # invisible, and invisible is the property a secret wants. diff --git a/tests/secret-gate.bats b/tests/secret-gate.bats index c354765..04f6ffb 100755 --- a/tests/secret-gate.bats +++ b/tests/secret-gate.bats @@ -157,3 +157,35 @@ export API_KEY_FILE=/run/secrets/api_key' [ "$status" -ne 0 ] [[ "$output" == *"tests/other.bats"* ]] } + +@test "a credential under a path git quotes is still read" { + # core.quotePath is on by default, so `git diff --name-only` prints "caf\303\251/…" for a + # non-ASCII directory. Feed that quoted string back to `git diff -- ` and the diff is + # empty — and a gate that skips an unreadable file reports success. This is the fail-closed + # promise breaking on its own terms, not a cosmetic quoting complaint. + try_commit "café/postgres.env" 'POSTGRES_PASSWORD=hunter2corp2026' + [ "$status" -ne 0 ] + [[ "$output" == *"postgres.env"* ]] +} + +@test "a placeholder word elsewhere on the line does not exempt a live value" { + # The rule is about the VALUE. `# example` in a comment says nothing about the token the + # line assigns, but a whole-line placeholder filter lets the comment vouch for the secret. + try_commit "app.env" 'API_TOKEN=hunter2corp2026 # example, placeholder, changeme' + [ "$status" -ne 0 ] + [[ "$output" == *"app.env"* ]] +} + +@test "each hit is attributed to the file it came from" { + # The report is how the next agent finds the leak. If one file's last hit line runs into the + # next file's name, the audit starts at a clean file and the leaker keeps its name. + mkdir -p "$REPO/a" "$REPO/b" + printf 'A_PASSWORD=hunter2corp2026\n' > "$REPO/a/one.env" + printf 'B_PASSWORD=hunter2corp2026\n' > "$REPO/b/two.env" + run git -C "$REPO" add -A + run git -C "$REPO" commit -q -m "two leakers" + [ "$status" -ne 0 ] + printf '%s\n' "$output" | grep -qx ' a/one.env' + printf '%s\n' "$output" | grep -qx ' b/two.env' + ! printf '%s\n' "$output" | grep -qE 'hunter2corp2026 +b/two\.env' +} -- 2.54.0 From f3ce42eb807f5594921c7b64e0b8e1fda9e9de69 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:59:13 -0700 Subject: [PATCH 06/13] fix(deploy): report what enable actually did, and arm the tier's timers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two lies in the unit loop, both reported as success: - `systemctl --user enable live-sync.service` fails on every deploy — the file has no [Install] section, because live-sync.timer is what arms it. The result was swallowed by `|| true` and the tool printed ENABLE anyway, so the line an operator reads after a rebuild was false on every box, on the repo's own heartbeat. The [Install] section is readable from the file being deployed, so the tool now checks it and prints INERT with the reason instead of making a call that cannot succeed; a real enable failure prints INERT too. - The loop walked *.service only. live-sync.timer was never enabled by anything, so a rebuilt box got the heartbeat and no schedule for it. Timers are walked now; a timer has no ExecStart, so the "does this unit belong on this box" test runs against the service it activates. The systemctl stub models systemd's refusal (no [Install] section -> non-zero), which is what makes the new tests red against the old loop; both new tests were run red first, and the five existing unit tests still pin #44's semantics. Refs #60 --- omp-docs/tools/dotfiles-deploy.md | 22 +++++++++--- scripts/deploy.sh | 44 ++++++++++++++++------- tests/deploy.bats | 60 ++++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/omp-docs/tools/dotfiles-deploy.md b/omp-docs/tools/dotfiles-deploy.md index 182c1df..ed4aa7d 100644 --- a/omp-docs/tools/dotfiles-deploy.md +++ b/omp-docs/tools/dotfiles-deploy.md @@ -25,10 +25,15 @@ is stowed last and its files replace symlinks coming from base packages. | `deploy.sh` (sync) | adopt identicals, link new, quarantine divergents into `.quarantine/` and replace them | | `deploy.sh --adopt` | like sync, but divergents are reported and skipped rather than quarantined | -Two package shapes are not symlinks. `dconf/dump.txt` is loaded with `dconf load /`, and -`systemd-user` units get `daemon-reload` plus `enable`; a unit is **started** only when -systemd reports it as nothing-yet (`""`/`disabled`), and never when its `ExecStart` binary -is absent here, so a unit belonging to another box stays inert instead of crash-looping. +Two package shapes are not symlinks. `dconf/dump.txt` is loaded with `dconf load /`. The +`systemd-user` tier gets `daemon-reload`, then **every `*.service` and `*.timer` whose start +command resolves on this box** is enabled — a timer is the schedule that makes a oneshot service +run at all, and it carries no `ExecStart` of its own, so the belonging test is applied to the +service it activates. A unit is **started** (`--now`) only when systemd reports it as nothing-yet +(`""`/`disabled`), and never when its binary is absent here, so a unit belonging to another box +stays inert instead of crash-looping. Three outcomes are printed, and each says what actually +happened: `ENABLE` only when `systemctl enable` returned success, `INERT` when it could not or did +not, `SKIP` when the unit is foreign. Refs #44, #60. Starting something already installed is what makes `deploy.sh` unsafe to run beside a live session — see *Never start a unit a session may already be running*. @@ -167,6 +172,15 @@ fresh box — where everything reports `disabled` — still gets its units up. B are pinned in `tests/deploy.bats` over a stub `systemctl`, because the real one on this box is exactly what must not be invoked from a test. Refs #44. +**A third state exists, and it is not an error.** A unit file with no `[Install]` section cannot be +enabled at all — `systemctl --user enable` answers with a non-zero exit and a paragraph about +missing `WantedBy`. `live-sync.service` is one on purpose: `live-sync.timer` pulls it in, which is +also why systemd reports it as `linked`. Until 2026-10-21 the deploy swallowed that failure and +printed `ENABLE` anyway, so the line an operator reads to learn what a rebuild armed was false on +every box, on the repo's own heartbeat. The section is readable from the file being deployed, so +the tool checks it and prints `INERT …(no [Install] section — another unit arms it, enable cannot)` +without making the call. Refs #60. + ## Law: expand systemd specifiers before calling a unit foreign `ExecStart=%h/.omp/scripts/live-sync.sh` is valid systemd and meaningless to a shell. The diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 391cb32..99c9f34 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -120,9 +120,20 @@ done if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user" ]; then systemctl --user daemon-reload || true - for unit in "$PKGS_DIR"/systemd-user/.config/systemd/user/*.service; do + UNITS_DIR="$PKGS_DIR/systemd-user/.config/systemd/user" + # Services AND timers. A timer is the schedule that makes a oneshot service run at all, and it + # carries no ExecStart of its own — the binary that decides whether the unit belongs on this + # box lives in the service it activates. Walking *.service only deployed live-sync.service on + # every rebuild and never armed live-sync.timer: the heartbeat was present and never ticked. + for unit in "$UNITS_DIR"/*.service "$UNITS_DIR"/*.timer; do [ -e "$unit" ] || continue - bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$unit" | head -1)" + name="$(basename "$unit")" + probe="$unit" + if [ "${name##*.}" = timer ]; then + act="$(sed -n 's/^Unit=\(.*\)$/\1/p' "$unit" | head -1)" + probe="$UNITS_DIR/${act:-"${name%.timer}.service"}" + fi + bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$probe" 2>/dev/null | head -1)" # systemd expands specifiers inside ExecStart; this shell does not. Matching the literal # `%h/…` makes a unit of THIS box look foreign and skips it in silence — which is exactly # how live-sync.service, the repo's own heartbeat, has been reported "absent on this @@ -132,22 +143,31 @@ if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user" bin="${bin//%u/$(id -un)}" bin="${bin//%U/$(id -u)}" if [ -n "$bin" ] && command -v "$bin" >/dev/null 2>&1; then - name="$(basename "$unit")" # Read the state, not the exit status: `is-enabled` exits 1 for a `linked` unit exactly # as for a `disabled` one. Starting something already installed is how a deploy locks a # live session — swayidle-lock sits enabled/inactive on blankpad and `--now` arms it # mid-conversation. Only a unit nothing has installed yet gets started. Refs #44. state="$(systemctl --user is-enabled "$name" 2>/dev/null || true)" - case "$state" in - "" | disabled) - systemctl --user enable --now "$name" || true - echo "ENABLE $name (ExecStart resolves here; nothing running it yet)" ;; - *) - systemctl --user enable "$name" || true - echo "ENABLE $name (already $state; not started — a live session decides what runs)" ;; - esac + if ! grep -q '^\[Install\]' "$unit"; then + # systemd refuses to enable these: no [Install] section means nothing can want the unit + # at login, and live-sync.service has none on purpose — its timer pulls it in. The old + # branch swallowed that failure and printed ENABLE anyway, so the one line a rebuild is + # checked by was false on every box. The section is readable right here, so do not make + # the call at all. Refs #60. + echo "INERT $name (state=${state:-none}; no [Install] section — another unit arms it, enable cannot)" + elif [ -z "$state" ] || [ "$state" = disabled ]; then + if systemctl --user enable --now "$name"; then + echo "ENABLE $name (ExecStart resolves here; nothing running it yet)" + else + echo "INERT $name (enable --now failed — systemd's message above says why)" + fi + elif systemctl --user enable "$name"; then + echo "ENABLE $name (already $state; not started — a live session decides what runs)" + else + echo "INERT $name (already $state; enable failed — systemd's message above says why)" + fi else - echo "SKIP $(basename "$unit") (ExecStart '$bin' absent on this host)" + echo "SKIP $name (ExecStart '$bin' absent on this host)" fi done fi diff --git a/tests/deploy.bats b/tests/deploy.bats index e76ddb6..9cb4a03 100755 --- a/tests/deploy.bats +++ b/tests/deploy.bats @@ -37,7 +37,22 @@ if [ "$2" = is-enabled ]; then printf '%s\n' "$state" case "$state" in disabled | "") exit 1 ;; *) exit 0 ;; esac fi -exit 0 +if [ "$2" = enable ]; then + # Modelled, not stubbed flat: systemd refuses to enable a unit whose file has no [Install] + # section, and the deployed file is what it reads. A stub that always says yes hides the + # one case this repo actually ships (live-sync.service is driven by its timer). + target="" + for a in "$@"; do + case "$a" in + *.service | *.timer | *.socket) target="$a" ;; + esac + done + file="$HOME/.config/systemd/user/${target##*/}" + if [ -e "$file" ] && ! grep -q '^\[Install\]' "$file"; then + printf 'The unit files have no installation config (WantedBy=, RequiredBy=, Also= settings in the [Install] section). This means they are not meant to be enabled using this command.\n' >&2 + exit 1 + fi +fi STUB chmod +x "$TEST_TMP/bin/systemctl" export PATH="$TEST_TMP/bin:$PATH" @@ -57,6 +72,21 @@ systemd_unit() { > "$REPO/dotfiles/systemd-user/.config/systemd/user/$1" } +# systemd_unit_no_install — a unit nothing can enable (live-sync.service: +# it is pulled in by live-sync.timer, so it carries no [Install] section on purpose). +systemd_unit_no_install() { + mkdir -p "$REPO/dotfiles/systemd-user/.config/systemd/user" + printf '[Unit]\nDescription=%s\n[Service]\nType=oneshot\nExecStart=%s\n' "$1" "$2" \ + > "$REPO/dotfiles/systemd-user/.config/systemd/user/$1" +} + +# systemd_timer — a schedule; it has no ExecStart of its own. +systemd_timer() { + mkdir -p "$REPO/dotfiles/systemd-user/.config/systemd/user" + printf '[Unit]\nDescription=%s\n[Timer]\nOnCalendar=hourly\nPersistent=true\n[Install]\nWantedBy=timers.target\n' "$1" \ + > "$REPO/dotfiles/systemd-user/.config/systemd/user/$1" +} + @test "generated theme output is never adopted into a symlink" { repo_file "ghostty/.config/ghostty/themes/noctalia" 'background = "#f43841"' live_file "$HOME/.config/ghostty/themes/noctalia" 'background = "#f43841"' @@ -152,3 +182,31 @@ systemd_unit() { grep -q 'systemctl --user enable heartbeat.service' "$STUB_LOG" ! grep -q -- '--now' "$STUB_LOG" } + +@test "a unit nothing can enable is reported as what it is, never as ENABLE" { + # `systemctl --user enable live-sync.service` fails on every deploy: the file has no + # [Install] section (its timer arms it), and systemd says so with a non-zero exit. The + # deploy swallowed that and printed ENABLE, so the one line the operator reads to learn + # what a rebuild armed was false on every box, on the repo's own heartbeat. + systemd_unit_no_install heartbeat.service /bin/sh + printf 'linked\n' > "$UNIT_STATE" + run "$REPO/scripts/deploy.sh" + [ "$status" -eq 0 ] + [[ "$output" == *"heartbeat.service"*"[Install]"* ]] + [[ "$output" != *"ENABLE heartbeat.service"* ]] + # No futile call: the section is readable from the file being deployed. + ! grep -q 'systemctl --user enable heartbeat.service' "$STUB_LOG" +} + +@test "a timer in the tier is armed, because a service on its own has no schedule" { + # The loop walked *.service only. On a rebuilt box live-sync.service is deployed and the + # timer is never enabled, so the heartbeat that commits machine state simply does not run + # — and the deploy report says nothing about it. + systemd_unit heartbeat.service /bin/sh + systemd_timer heartbeat.timer heartbeat.service + printf 'disabled\n' > "$UNIT_STATE" + run "$REPO/scripts/deploy.sh" + [ "$status" -eq 0 ] + [[ "$output" != *"SKIP heartbeat.timer"* ]] + grep -q 'systemctl --user enable --now heartbeat.timer' "$STUB_LOG" +} -- 2.54.0 From 71601f72a8c892c6109d97f45af2d3c0e87c4c15 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:59:18 -0700 Subject: [PATCH 07/13] fix(secret-gate): a documented fake id must not stop the machine tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live-sync heartbeat failed three hourly ticks (22:25, 23:27, 00:28) because a managed skill that documents how to write a secret gate quotes the vendor's all-zeros placeholder access-key id, and the shape rule fired on the documentation. The machine tier is prose ABOUT credentials — the gate's own header says a gate that fires there gets overridden until it means nothing — and a heartbeat that cannot commit is a fleet that silently stops syncing config. - shape hits are judged on the matched token, not the line: a documented fake is inert, one live token sharing the file still blocks. The match is judged with its following context, because a fixed quantifier truncates `EXAMPLE` to `EXA` and the placeholder would vanish from what is examined. - assignment values keep the stricter rule from the previous commit: a comment on the same line cannot vouch for the token beside it. - `ALLOW_SECRETS` remains the only override; nothing here widens what is scanned, it only stops flagging text that was never a credential. Verified against the payload that is stuck right now: the skill page that the heartbeat cannot commit passes the fixed hook and was refused by the live one. Refs #60 --- .githooks/pre-commit | 19 ++++++++++++++- omp-docs/log.md | 55 ++++++++++++++++++++++++++++++++++-------- tests/secret-gate.bats | 13 ++++++++++ 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 1e41f1c..1024624 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -79,7 +79,24 @@ while IFS= read -r -d '' f; do # gate refused the redaction of the password it exists to catch. added=$(printf '%s\n' "$diff" | grep -E '^\+' | grep -vE '^\+\+\+' | sed 's/^+//' || true) [ -z "$added" ] && continue - hits=$(printf '%s\n' "$added" | grep -Ei "$shapes" || true) + # Shape hits are judged on the MATCHED TOKEN, not on the whole line. Documentation about + # credentials quotes vendor placeholders — AWS publishes an all-zeros access-key id that ends + # in EXAMPLE for exactly this purpose — and + # a gate that blocks prose over an inert example gets worked around until it means nothing: + # the live-sync heartbeat failed three ticks on 2026-10-21 because a managed skill page quotes + # exactly that id. A line is exempted only when EVERY shape on it is inert, so one live token + # sharing the file with a documented fake still blocks. + hits=$(printf '%s\n' "$added" | while IFS= read -r line; do + live=0 + while IFS= read -r m; do + # The shape regexes carry fixed quantifiers, so the match they return is TRUNCATED: + # `AKIA` + exactly 16 characters stops inside the word EXAMPLE and the placeholder + # disappears from what is judged. Judge the match plus what follows it instead. + ctx=$(printf '%s' "$line" | awk -v m="$m" '{ i = index($0, m); print (i > 0 ? substr($0, i, length(m) + 8) : m) }') + printf '%s' "$ctx" | grep -Eiq 'EXAMPLE|X{4,}|Y{4,}|Z{4,}|_{4,}|REDACTED|CHANGEME|PLACEHOLDER' || live=1 + done < <(printf '%s\n' "$line" | grep -Eio "$shapes" || true) + [ "$live" = 1 ] && printf '%s\n' "$line" + done) if [ "${f##*.}" != "md" ]; then # The placeholder filter reads the VALUE, never the whole line. A trailing comment that # says `# example` says nothing about the token the same line assigns; scoping the filter diff --git a/omp-docs/log.md b/omp-docs/log.md index 5f9f27d..62dc325 100644 --- a/omp-docs/log.md +++ b/omp-docs/log.md @@ -20,28 +20,63 @@ This page is the bundle's **only** work log: one file, at the root. Never start second one in a subdirectory or under another name — `scripts/kb-lint.sh` reports any `log.md` that is not this one. +## 2026-10-21 — reviewing a day of work, and finding checks that lie + +Two-axis review (Standards / Spec) of everything landed 2026-10-20 → 2026-10-21: 51 commits, +30 findings, worked on `fix/60-review-findings` (issue #60). Three findings were the same shape — +a check that reports confidence it did not earn: + +- **A guard that cannot see must refuse.** `pcie-probe.sh` dropped the `/dev/kfd` half of its + busy check when `fuser` was absent, and `gpu_busy_percent` reads `0` for a resident-but-idle + service: the tool measured beside a live endpoint while its own LAW said not to. Missing + visibility is now a refusal (exit `3`), not a narrower check. +- **A swallowed status is a false report.** `deploy.sh` printed `ENABLE` for units systemd + refuses to enable (`live-sync.service` has no `[Install]` section; its timer arms it), and the + loop walked `*.service` only, so a rebuilt box deployed the heartbeat and never armed its + timer. Every outcome now reports what actually happened. +- **A gate that reads a quoted path reads nothing.** git quotes non-ASCII paths in + `--name-only` output; the quoted name came back as an empty diff and the secret gate + `continue`d past it. Fail closed means `-z`, and it means the placeholder filter reads the + assigned value — a comment saying `# example` cannot vouch for the token beside it. + +The law that cost the most, from the probe: **a test that observes the tool's environment instead +of what the tool passed on cannot see the bug.** The stub probe echoed `$HIP_VISIBLE_DEVICES` +rather than its argument, so the suite asserted the mask was set and per-card bandwidth was +unobtainable on the 2×R9700 box. Model the platform rule in the stub and the existing test goes +red against the old code — that is the red-proof worth having. + +Incident, found while verifying the gate: the live-sync heartbeat had failed three hourly ticks, +because a managed skill that documents how to write a secret gate quotes the vendor's all-zeros +placeholder id and the gate's shape rule fired on the documentation. Prose about credentials is +what the machine tier is made of; a gate that blocks it is worked around, and the workaround is +the hole. Placeholder judgement reads the matched token — plus what follows it, because a fixed +quantifier truncates `EXAMPLE` to `EXA` — never the whole line. `ALLOW_SECRETS` stays the only +override, and the heartbeat's own payload was replayed through the fixed hook to prove it lands. + ## 2026-10-21 — a documented mode the panel never had The home ultrawide had been running at 60 Hz for weeks while `desk-1-home.kdl` asked for `3440x1440@239.999`. The page that owns that file documented the same phantom mode, so every reader — including tonight's inspection — trusted it. -`niri msg outputs` says the Sceptre M34's EDID tops out at 180 Hz; there is no -240 Hz mode to ask for. +The panel does offer a 240-class mode: `niri msg outputs` lists `3440x1440@239.998`. The defect +was one character — niri matches mode strings verbatim, and `239.999` is not `239.998`. Laws, now on [tools/niri/displays](tools/niri/displays.md): -- **An impossible mode does not fail, it falls back.** niri logs - `configured mode … could not be found, falling back to preferred` and drives - the EDID preferred mode — 60 Hz here. A config typo therefore looks like a - dying monitor, and every connector drop re-enacts it. +- **A mode string must match the advertised string verbatim.** niri does not round: + `3440x1440@239.999` does not select `3440x1440@239.998`. It logs + `configured mode … could not be found, falling back to preferred` and drives the EDID + preferred mode — 60 Hz here. A one-character typo therefore looks like a dying monitor, and + every connector drop re-enacts it. - **Three layers, three signatures:** config fallback, DP link drops, and amdgpu `DMCUB` errors with `EINVAL` page flips after s2idle. Only the first yields to a file edit; only the last yields to a reboot. Triage table and probes are on the page. -- **A number in a page is a claim, not a measurement.** Modes, refresh rates and - tool availability were wrong on four pages at once — `wlr-randr` was listed as - installed while `pacman -Qs wlr` matches nothing. Read the hardware before - writing a mode into a runbook, and destroy the stale copies. +- **A number in a page is a claim, not a measurement.** Mode strings, refresh rates and tool + availability were wrong on several pages at once — `wlr-randr` was listed as installed while + `pacman -Qs wlr` matches nothing. Read the hardware before writing a mode into a runbook, and + destroy the stale copies. A correction is itself a measurement: the first pass at this entry + published a second wrong hardware fact, with a `verified:` date stamped on it. Fix: `desk-1-home.kdl` 239.999 → 180.000 on the identity block and both legacy connector blocks, applied live with `niri msg output mode ` — the diff --git a/tests/secret-gate.bats b/tests/secret-gate.bats index 04f6ffb..03d38cf 100755 --- a/tests/secret-gate.bats +++ b/tests/secret-gate.bats @@ -189,3 +189,16 @@ export API_KEY_FILE=/run/secrets/api_key' printf '%s\n' "$output" | grep -qx ' b/two.env' ! printf '%s\n' "$output" | grep -qE 'hunter2corp2026 +b/two\.env' } + +@test "a documented fake id in prose is committable; a live-shaped id in the same file is not" { + # Measured on blankpad 2026-10-21: the live-sync heartbeat failed three ticks in a row + # because a managed-skill page documents how to write a secret gate and quotes AWS's own + # placeholder id. A gate that blocks the machine tier over an inert example gets worked + # around, and a workaround is the hole. The exemption reads the matched token, so a real + # id in the same file still blocks. + try_commit "docs.md" 'Inert fixtures look like AKIAIOSFODNN7EXAMPLE or AKIA0000000000000EXAMPLE.' + [ "$status" -eq 0 ] + try_commit "docs.md" 'Inert fixture AKIAIOSFODNN7EXAMPLE and a live one AKIAQZR4W7DBPCFG7V3Q.' + [ "$status" -ne 0 ] + [[ "$output" == *"docs.md"* ]] +} -- 2.54.0 From 66fc40de65c9f9434ec7f636660087b95a837ce0 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:59:51 -0700 Subject: [PATCH 08/13] fix(kb-lint): a wikilink must not resolve into the quarantine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolver copied Obsidian's filename-anywhere semantics with a raw rglob, which walked .AGENTTRASH/ as well. [[log]] therefore stayed "valid" by landing on the quarantined core/log.md — a page that is out of circulation until a pruning session and, being gitignored, does not exist on any other machine. The gate reported a green bundle while pointing readers at a file git does not carry: exactly the #57 failure, hidden by the check that exists to catch it. Resolution now uses the same filtered set linting uses (pages()), so the two can never disagree about what exists. A companion test pins that filename-anywhere resolution for live pages still works, so the narrowing costs nothing; mutating the resolver back to rglob turns the new test red. Refs #60 --- scripts/kb-lint.sh | 11 +++++++++-- tests/kb-lint.bats | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/scripts/kb-lint.sh b/scripts/kb-lint.sh index ef25c22..4b8b933 100755 --- a/scripts/kb-lint.sh +++ b/scripts/kb-lint.sh @@ -20,6 +20,10 @@ def pages(): if ".AGENTTRASH" not in p.parts: yield p +# Names that a wikilink may point at: exactly the pages pages() reports. Deriving this +# once keeps linting and link resolution from disagreeing about what exists. +live = {p.stem for p in pages()} + for p in pages(): rel = p.relative_to(kb) text = p.read_text(errors="replace") @@ -70,9 +74,12 @@ for p in pages(): if not Path(target).is_file(): violations.append(f"{rel}: broken relative link -> {m.group(1)}") for m in re.finditer(r"\[\[([^\]|#]+)", text): - # wikilinks resolve by filename anywhere in the KB (Obsidian semantics) + # wikilinks resolve by filename anywhere in the KB (Obsidian semantics) — but only + # among pages that BELONG to the bundle. A raw rglob also walked .AGENTTRASH/, so a + # link to a quarantined page stayed "valid" here while pointing at nothing on every + # other machine (the trash is gitignored). Same exclusion as pages(), one source. name = m.group(1).strip().rsplit("/", 1)[-1] - if not list(kb.rglob(name + ".md")): + if name not in live: violations.append(f"{rel}: broken wikilink -> [[{m.group(1).strip()}]]") for v in violations: diff --git a/tests/kb-lint.bats b/tests/kb-lint.bats index e145b54..cc8dfc8 100755 --- a/tests/kb-lint.bats +++ b/tests/kb-lint.bats @@ -120,6 +120,28 @@ EOF [[ "$output" == *"0 violation(s)"* ]] } +@test "a wikilink must not resolve into the quarantine" { + # Obsidian resolves [[name]] by filename anywhere under the vault, and the resolver copied + # that with a raw rglob — so [[log]] stayed "valid" by landing on the copy waiting in + # .AGENTTRASH/. That is #57's failure wearing a green check: the page is out of circulation + # until a pruning session with Frank, and it is not even tracked, so on another machine the + # link points at nothing while the gate here says it is fine. + mkdir -p "$KB/.AGENTTRASH" + printf 'no frontmatter, quarantined\n' > "$KB/.AGENTTRASH/log.md" + printf -- '---\ntype: reference\n---\nSee [[log]] for the history.\n' > "$KB/good.md" + run bash "$LINT" "$KB" + [ "$status" -ne 0 ] + [[ "$output" == *"broken wikilink -> [[log]]"* ]] +} + +@test "a wikilink to a live page still resolves, in any directory" { + # The narrowing above must cost nothing: filename-anywhere semantics is the point. + printf -- '---\ntype: reference\n---\nSee [[good]].\n' > "$KB/sub/other.md" + run bash "$LINT" "$KB" + [ "$status" -eq 0 ] + [[ "$output" == *"0 violation(s)"* ]] +} + @test "the shipped knowledge base itself is conformant" { # The tests above exercise the linter against fixtures; nothing else in the # suite ever points it at the real bundle, so a page added without frontmatter -- 2.54.0 From 334e5d0ee06e896037a722f53fcd56caa8739593 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 01:06:04 -0700 Subject: [PATCH 09/13] docs(niri): the ultrawide's refresh ceiling was a typo, not a limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page asserted the Sceptre M34's EDID tops out at 180 Hz. Measured on blankpad after the merge, `niri msg outputs` lists 3440x1440@239.998 — the claim was false in main, and it was load-bearing: it is the reason nobody questioned the profile while the panel ran at 60 Hz for weeks. The real defect was one character: the profile asked for 239.999, the panel offers 239.998, and niri matches mode strings verbatim, falling back to *preferred* without failing. Corrected to the measured mode list, and the deployed 180 is now documented as a deliberate choice (the link is trained at x8 32.0 GT/s against a max of x16 and the display engine throws EINVAL), so the next reader neither "restores 240" blind nor re-pins 180 as a hardware ceiling. The kdl profile comment carries the same facts at the point of edit, because that is where the next change to this mode gets typed. Refs #60 --- .../niri/.config/niri/cfg/desk-1-home.kdl | 12 ++++--- omp-docs/tools/niri/displays.md | 32 +++++++++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-1-home.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-1-home.kdl index 96a4bcd..9558369 100644 --- a/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-1-home.kdl +++ b/dotfiles/zz-blankpad/niri/.config/niri/cfg/desk-1-home.kdl @@ -7,10 +7,14 @@ // requires: Lenovo Group Limited 0x40A9 Unknown // optional: HDMI-A-1 (PM1601, dock-dependent) -// This panel's EDID tops out at 3440x1440@180 (read live from niri: no 240 Hz mode -// exists). Ask for a mode the panel does not offer and niri does not fail — it logs -// "could not be found, falling back to preferred" and drops to 60 Hz on every -// reconnect. Keep 180 here unless a re-read EDID says otherwise. +// The panel offers 3440x1440@239.998 as well as @180.000 (`niri msg outputs`, 2026-10-21), and +// 180 is driven DELIBERATELY: the link trained at x8 32.0 GT/s against a max of x16 and the +// display engine is throwing DMCUB errors with EINVAL page flips after s2idle, so the 240-class +// mode stays undriven until a reboot clears that state. niri matches mode strings VERBATIM and +// does not round: "@239.999" is not "@239.998", and a mode it cannot find does not fail — it +// logs "could not be found, falling back to preferred" and drops the panel to 60 Hz on every +// reconnect, which is how this desk ran for weeks. Copy the string from `niri msg outputs`. +// Reasoning and triage: omp-docs/tools/niri/displays.md ("The config pattern", rules 4 and 5). output "Sceptre Tech Inc Sceptre M34 Unknown" { mode "3440x1440@180.000" scale 1 diff --git a/omp-docs/tools/niri/displays.md b/omp-docs/tools/niri/displays.md index 5de8498..6d7a41b 100644 --- a/omp-docs/tools/niri/displays.md +++ b/omp-docs/tools/niri/displays.md @@ -41,17 +41,19 @@ list it as an installed tool are stale. │ 1200x1920 │ landscape, scale 1 │ scale 1 │ │ rot 90 │ position 1581,-600 │ 5021,-240│ └──────────────┴──────────────────────────────┴──────────┘ - HDMI-A-1 DP-2 (focus-at-startup) eDP-1 (fallback focus) + HDMI-A-1 DP-1 (focus-at-startup) eDP-1 (fallback focus) +Connector names are today's (`niri msg outputs`, 2026-10-21). They reshuffle with +the dock and the cable, which is why the config keys on the EDID long name instead. ``` | Panel | EDID long name | Keyed by | Notes | |-------|----------------|----------|-------| -| Sceptre M34 ultrawide | `Sceptre Tech Inc Sceptre M34 Unknown` | identity | `mode "3440x1440@180.000"` — the EDID maximum; no 240 Hz mode exists — `focus-at-startup` | +| Sceptre M34 ultrawide | `Sceptre Tech Inc Sceptre M34 Unknown` | identity | `mode "3440x1440@180.000"`, **deliberate** — the panel also offers `3440x1440@239.998`, `144.001` and `120.000`; 180 is driven while the link is degraded (see the law below) — `focus-at-startup` | | PM1601 portrait | *not captured* | connector `HDMI-A-1` | dock-dependent; run `desk.sh capture` when plugged in, then re-key | | eDP-1 laptop panel | `Lenovo Group Limited 0x40A9 Unknown` | identity | scale **1.0** (deliberate; Ghostty grid at 1.25 rejected 2026-10-09), second `focus-at-startup` | -VRR: DP-2 supports it; **left disabled by decision** (2026-10-09). Add -`variable-refresh-rate` per output if that ever changes. +VRR: the M34 supports it (its connector today is `DP-1`); **left disabled by +decision** (2026-10-09). Add `variable-refresh-rate` per output if that ever changes. `focus-at-startup` on both M34 and eDP-1: niri focuses in config order, so the ultrawide wins when present and the laptop panel is the fallback. @@ -113,12 +115,22 @@ Four rules learned the hard way: 3. **Positions are absolute, not relational.** A disconnected panel leaves a hole; that is exactly why positions belong to the *desk profile*, not a global file. -4. **Ask only for modes the panel reports** (`niri msg outputs` → `Mode:` list). - A mode the panel never offered does **not** leave the output dark: niri logs - `configured mode … could not be found, falling back to preferred` and quietly - drives the panel at its EDID *preferred* mode — **60 Hz** on the M34. Nothing - looks broken except the refresh rate, and every reconnect repeats the - fallback. Check Hz, not just resolution, after any "monitor is acting up". +4. **Ask only for modes the panel reports, and copy the string exactly.** The M34's + `Available modes` (`niri msg outputs`, 2026-10-21) are `3440x1440@239.998`, + `3440x1440@180.000`, `144.001`, `120.000`, … with `59.998` marked preferred. niri + matches mode strings **verbatim** and does not round, so `239.999` — one digit from + the real mode, which is what this file and the config both carried for weeks — is not + `239.998`. A mode it cannot find does **not** leave the output dark: niri logs + `configured mode … could not be found, falling back to preferred` and quietly drives + the panel at its EDID *preferred* mode — **60 Hz**. Nothing looks broken except the + refresh rate, and every reconnect repeats the fallback. Check Hz, not just + resolution, after any "monitor is acting up". +5. **180 Hz here is a decision, not a ceiling.** The panel offers 239.998 and is driven + at 180.000 deliberately: the link trained at `x8 32.0 GT/s` against a max of `x16`, + and the display engine is throwing `DMCUB` errors with `EINVAL` page flips after + s2idle, so the 240-class mode is not driven until a reboot clears that state. Before + changing the mode, re-read `niri msg outputs` **and** the link sysfs; treat "restore + 240" as a change to verify on a healthy link, never as the obvious fix. Refs #60. ## "The monitors are broken" — three layers, three signatures -- 2.54.0 From d0587a5bc5e1847be7876608a48a76cef06e5a80 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 01:06:08 -0700 Subject: [PATCH 10/13] docs(noctalia): the dependency table described packages this box does not have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three rows claimed `wlr-randr`, `qt6-multimedia` and `brightnessctl` were dependencies. `pacman -Q` on blankpad says none is installed, and the two features they were cited for do not come from them at all: display information comes from `niri msg outputs` (the niri page already said so), and brightness is driven through the kernel — `/sys/class/backlight/amdgpu_bl1` exists with no helper installed — so no package is doing that work. A dependency table is a claim about a machine, and every row of it is now measured against `pacman -Q` on blankpad (2026-10-21). Verified against the package database: check `pacman -Q ` before adding a row. Refs #60 --- omp-docs/tools/noctalia-shell.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/omp-docs/tools/noctalia-shell.md b/omp-docs/tools/noctalia-shell.md index 92592ca..ad8a265 100644 --- a/omp-docs/tools/noctalia-shell.md +++ b/omp-docs/tools/noctalia-shell.md @@ -413,17 +413,21 @@ noctalia msg templates-apply ## Dependencies -| Package | Purpose | -|---------|---------| -| `noctalia` (Qt) | Native desktop environment | -| `qt6-base` | Qt6 runtime | -| `qt6-multimedia` | Multimedia support | -| `niri` | Wayland compositor | -| `wlr-randr` | Display information | -| `brightnessctl` | Brightness control | -| `imagemagick` | Image processing | -| `ffmpeg` | Media processing | -| `polkit` | PolicyKit integration | +| Package | Purpose | Version on blankpad (2026-10-21) | +|---------|---------|----------------------------------| +| `noctalia` (Qt) | the shell: bar, widgets, `noctalia msg` | 5.3.0-1.1 | +| `qt6-base` | Qt6 runtime | 6.12.0-3.1 | +| `niri` | Wayland compositor | 26.04-1.1 | +| `polkit` | PolicyKit integration | 128-1.1 | +| `imagemagick` | image processing for palette wallpapers | 7.1.2.34-2.1 | +| `ffmpeg` | media processing | 2:9.0.2-2.1 | + +**Three rows this page used to claim are not installed**, verified with `pacman -Q` and +`command -v`: `wlr-randr` (display information is `niri msg outputs` — no package needed, and +[displays](niri/displays.md) owns that procedure), `qt6-multimedia` (audio reaches PipeWire +through noctalia's own dependency), and `brightnessctl` (`/sys/class/backlight/amdgpu_bl1` +exists and nothing on this box drives it — laptop brightness is a gap to close, not a feature +to document). A dependency table is a claim about the machine: check it before writing it. --- -- 2.54.0 From 8393dc10903f37ac09e7240028e8ad357c8187b8 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 01:06:11 -0700 Subject: [PATCH 11/13] fix(remote-job): a job name is data, never a shell pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `status` built its job list with `for n in $names`, which word-splits AND globs. The second half is the bug: a job named `x*` was expanded against the caller's working directory, so the report described unrelated files there as jobs and never mentioned the one that ran — on a harness whose whole purpose is a report you can trust after the session that started the job is gone. Splitting stays (several names in one argument is the documented call shape); expansion goes. `tail` was honest-by-accident in the wrong direction: with no jobs it handed a literal `$JOBS/*.out` to tail, which reports a missing *file* — indistinguishable from a broken tool — and a missing name produced tail's errno rather than "no job called that". Both now say what is missing and exit 1. Red first: the name test reads the report from a directory containing an unrelated file, so the old glob is visible as a phantom job; both new tests fail against the previous code and pass against this one. Refs #60 --- omp-docs/tools/remote-job.md | 5 +++-- scripts/remote-job.sh | 26 ++++++++++++++++++++------ tests/remote-job.bats | 25 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/omp-docs/tools/remote-job.md b/omp-docs/tools/remote-job.md index 94b526d..2dec74f 100644 --- a/omp-docs/tools/remote-job.md +++ b/omp-docs/tools/remote-job.md @@ -38,10 +38,10 @@ read afterwards by a short-lived connection. That is what this is. ```bash remote-job.sh start NAME [--] CMD... # launch detached, return at once -remote-job.sh status [NAME] # one line per job; exit 0 only if all DONE +remote-job.sh status [NAME] # one line per job; names literal, never globs remote-job.sh wait NAME [SEC] [N] # block until DONE or timeout, then tail N lines remote-job.sh out NAME [N] # tail a job's output -remote-job.sh tail [NAME] # follow one job, or every job at once +remote-job.sh tail [NAME] # follow one job, or all; exit 1 if nothing to follow remote-job.sh watch # refreshing status, for a spare pane ``` @@ -77,6 +77,7 @@ can be made from a fresh session on a machine that rebooted underneath it. | Command name is wrong | `PENDING (no output yet)`, exit 1 — never a false DONE | | Job exits non-zero | `DONE rc=N`; the code is data, not the report's status | | Two jobs, same NAME | the second replaces the first's state files | +| `tail` with no job, or a name that is a glob | exit 1 saying there is nothing to follow; names are matched literally, never expanded | | Job dir missing | created on first use | Pinned by `tests/remote-job.bats` (black-box: output, exit codes, sentinel diff --git a/scripts/remote-job.sh b/scripts/remote-job.sh index 20082b3..8f64105 100755 --- a/scripts/remote-job.sh +++ b/scripts/remote-job.sh @@ -51,13 +51,21 @@ case "${1:-}" in ;; status) + # Job names are data, never patterns. `for n in $names` word-split AND globbed: a job named + # `x*` was expanded against the caller's working directory, so the report described unrelated + # files there as jobs and never mentioned the one that ran. Splitting stays (several names in + # one argument is the documented call shape); expansion does not. + names=() if [[ -n "${2:-}" ]]; then - names="$2" + read -r -a names <<< "$2" else - names=$(cd "$JOBS" && ls *.out 2>/dev/null | sed 's/\.out$//' | sort) + while IFS= read -r -d '' f; do + names+=("$(basename "$f" .out)") + done < <(find "$JOBS" -maxdepth 1 -name '*.out' -print0 2>/dev/null | sort -z) fi + if [[ ${#names[@]} -eq 0 ]]; then echo " (no jobs)"; exit 0; fi pending=0 - for n in $names; do + for n in "${names[@]}"; do if [[ -f "$JOBS/$n.done" ]]; then printf ' %-20s DONE rc=%s %s lines\n' "$n" \ "$(cat "$JOBS/$n.rc" 2>/dev/null)" "$(wc -l < "$JOBS/$n.out" 2>/dev/null | tr -d ' ')" @@ -69,7 +77,6 @@ case "${1:-}" in pending=1 fi done - if [[ -z "$names" ]]; then echo " (no jobs)"; exit 0; fi exit "$pending" ;; @@ -95,8 +102,15 @@ case "${1:-}" in tail) # Multiplexed follow: no NAME means every job at once, so one pane shows the whole harness. - if [[ -n "${2:-}" ]]; then exec tail -f "$JOBS/$2.out"; fi - exec tail -f "$JOBS"/*.out + # Both branches check for something to follow first: an unexpanded `*.out` handed to tail + # reports a missing *file*, which reads as a broken tool instead of an empty harness. + if [[ -n "${2:-}" ]]; then + [[ -f "$JOBS/$2.out" ]] || { echo "remote-job: no job '$2' (no $JOBS/$2.out)" >&2; exit 1; } + exec tail -f "$JOBS/$2.out" + fi + mapfile -d '' -t follow < <(find "$JOBS" -maxdepth 1 -name '*.out' -print0 2>/dev/null | sort -z) + (( ${#follow[@]} )) || { echo "remote-job: no jobs in $JOBS yet" >&2; exit 1; } + exec tail -f "${follow[@]}" ;; watch) diff --git a/tests/remote-job.bats b/tests/remote-job.bats index c980ffc..2598a12 100755 --- a/tests/remote-job.bats +++ b/tests/remote-job.bats @@ -122,6 +122,31 @@ until_done() { [[ "$output" == *38* ]] # the tail, not the head } +@test "status treats a job name as a name, not a pattern" { + "$SCRIPT" start 'x*' -- bash -c 'echo star' + until_done 'x*' + touch "$TEST_TMP/anchor" + # The report is read from a directory full of unrelated files on purpose: a name that is + # expanded as a glob resolves against the CALLER's cwd, so the tool would describe `anchor` + # as a job and never mention the one that really ran. + run bash -c 'cd "$1" && "$2" status "$3"' _ "$TEST_TMP" "$SCRIPT" 'x*' + [[ "$output" == " x*"*"DONE rc=0"* ]] + [[ "$output" != *anchor* ]] +} + +@test "tail on an empty harness and on a missing job both say so" { + # An unexpanded `*.out` reaching tail is reported as a missing FILE, which reads as a broken + # script; the honest answer is that there is nothing to follow yet. + run timeout 5 "$SCRIPT" tail + [ "$status" -ne 0 ] + [[ "$output" == *"no jobs"* ]] + [[ "$output" != *".out"* ]] + run timeout 5 "$SCRIPT" tail nosuchjob + [ "$status" -ne 0 ] + [[ "$output" == *"nosuchjob"* ]] + [[ "$output" != *"No such file"* ]] +} + @test "an unknown verb is a usage error" { run "$SCRIPT" frobnicate [ "$status" -eq 2 ] -- 2.54.0 From a280c29d2ca3a0c97dc60dca2563fdac4ade4795 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 01:06:47 -0700 Subject: [PATCH 12/13] chore(tier): close the ignore/deploy drift, and stop justifying git-lfs by dead config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `deploy_file` excludes `*/Noctalia*` (noctalia's capital-N palette naming) but `.gitignore` had only the lowercase classes, so a generated palette file could become tracked-but-never-deployed — the fiction class #41 removed, reintroduced by the ignore list falling out of step with the deployer. The patterns are one contract in two files; both halves are now stated and the comment says so. `natives/` (2.4 MB of tree-sitter grammars the harness unpacks into the repo root on first use, measured on blankpad) sat unnamed in the root, one `git add -A` away from being committed as configuration. It is runtime state by every clause of AGENTS.md's doctrine, so it is ignored. The wallpaper re-fetch note pointed at `https://wallhaven.cc/w/`, which answers text/html — it now names the API resolution the deploy page carries. `git-lfs` was installed because `.gitattributes` routes image types through the LFS filter — for wallpapers this week's diff untracked: `git lfs ls-files` is zero, so those four rules match nothing. The manifest now states what the tool is actually for (cloning LFS-backed upstreams such as Hugging Face model/dataset repos) and the file records its own inert state. Deleting `.gitattributes` is a pruning call for a session with Frank, not a cleanup an agent runs alone. Verified with `git check-ignore -v` for both new rules. Refs #60 --- .gitattributes | 10 ++++++++++ .gitignore | 20 ++++++++++++++++---- packages/common.pkgs | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.gitattributes b/.gitattributes index b1b4dc7..1726f76 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,13 @@ +# LFS routing for image types. Measured 2026-10-21: this repo tracks ZERO LFS files +# (`git lfs ls-files` = 0) — the wallpaper library became live theme state and is gitignored +# (omp-docs/tools/dotfiles-deploy.md), so these four rules match nothing today. They are kept +# because tracked media is a plausible future and idle filters cost nothing; deleting the file +# is a pruning call for a session with Frank, not a cleanup an agent runs alone. +# +# Do not cite this file as the reason `git-lfs` is installed on a box. The manifest carries its +# own justification (packages/common.pkgs), and a tool justified by dead config is a tool nobody +# can defend when the config goes. + *.jpg filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 3264ac8..9e26146 100644 --- a/.gitignore +++ b/.gitignore @@ -86,15 +86,27 @@ agent/memories/--home-blank-.omp-wt-* # 2026-10-20), so this path in the repo was a duplicate of it, not its source — and # note that untracking deletes the copy in every other checkout regardless of # `git rm --cached` on the branch. A fresh box starts with an empty library; every -# filename carries its wallhaven ID, so `curl -L -o https://wallhaven.cc/w/` -# puts it back. Law: omp-docs/tools/dotfiles-deploy.md +# filename carries its wallhaven ID; `/w/` answers `text/html`, so resolve the CDN URL at +# `https://wallhaven.cc/api/v1/w/` and download `data.path` — the recipe, with the reason it +# is not a plain `curl -L`, is on omp-docs/tools/dotfiles-deploy.md dotfiles/noctalia/.local/share/noctalia/wallpapers/ # --- generated theme output: machine-written, never reviewable, never deployed --- # noctalia/matugen rewrite these in place; a tracked copy is a stale snapshot that -# deploy.sh would push back over a live palette. Patterns match the exclusion list in -# scripts/deploy.sh. Law: omp-docs/inventory/home-config-census.md, Refs #42 +# deploy.sh would push back over a live palette. These patterns are the ignore half of the +# exclusion list in `deploy_file` (scripts/deploy.sh): change one, change the other, or a +# generated file becomes tracked-but-never-deployed — the fiction class #41 removed. The +# capital-N rule is noctalia's own palette-directory naming, and it was in deploy.sh only. +# Law: omp-docs/inventory/home-config-census.md, Refs #42, #60 dotfiles/**/themes/noctalia* dotfiles/**/colors/noctalia* dotfiles/**/noctalia.* dotfiles/**/noctalia-* +dotfiles/**/Noctalia* + +# --- harness natives: unpacked runtime payloads, never configuration ---------- +# 2.4 MB of tree-sitter grammars the harness unpacks into the repo root on first use (measured +# on blankpad 2026-10-21). Regenerable, unreviewable, not config — every clause of AGENTS.md's +# runtime-state doctrine applies, and it sat unnamed in the root, one `git add -A` away from +# being committed as if it were configuration. +natives/ diff --git a/packages/common.pkgs b/packages/common.pkgs index cb4c7bd..5853882 100644 --- a/packages/common.pkgs +++ b/packages/common.pkgs @@ -18,7 +18,7 @@ btop # system / fleet tooling yay # AUR helper bootstrap: ships in the cachyos repo; aur.pkgs installs need it -git-lfs # .gitattributes still routes image types through the LFS filter; no image is tracked since 2026-10-20 +git-lfs # cloning LFS-backed upstreams (Hugging Face model/dataset repos); this repo tracks zero LFS files — .gitattributes is idle config swayidle # idle lock watcher via swayidle-lock.service; suspend locking is noctalia's sleep inhibitor # desktop apps -- 2.54.0 From 66e1129e6c182312a7e89c826176bf95447e0d6c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 01:06:51 -0700 Subject: [PATCH 13/13] docs: one owner for the one-log rule, and an honest scope on the census MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one-work-log rule was written near-verbatim in four places — okf-spec, okf-maintenance, log.md's preamble, and APPEND_SYSTEM.md. Four copies is four authorities: each can be edited alone, and the next reader cannot tell which one the linter implements. okf-spec.md owns it; maintenance now carries the action (collapse a second log into the root, then re-run the linter) instead of re-arguing it; the log page points at the rule rather than restating it. APPEND_SYSTEM keeps its imperative — a system prompt cannot fetch a link at the moment of writing — but no longer carries the duplicated reasoning. `inventory/index.md` advertised "all userspace program configuration on the workstation". The one page in that directory measures `~/.config` on blankpad: not `~/.local/share`, not system package state, not the servers. A coverage claim that names what was measured is the difference between a census and an alibi. Refs #60 --- agent/APPEND_SYSTEM.md | 2 +- omp-docs/inventory/index.md | 4 +++- omp-docs/log.md | 17 ++++++++++++++--- omp-docs/references/okf-maintenance.md | 11 +++++------ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/agent/APPEND_SYSTEM.md b/agent/APPEND_SYSTEM.md index 66782a1..d73a27e 100644 --- a/agent/APPEND_SYSTEM.md +++ b/agent/APPEND_SYSTEM.md @@ -72,7 +72,7 @@ Documentation exists for ONE reason: **point of use**. A page earns its bytes on #### THE WORK LOG IS ONE FILE. The work log is a single page in the knowledge base: `omp-docs/log.md` in this repo, or whichever change-log page the repo's docs index names. Write the entry when the change lands — in the same commit as the change — and read that page to recover where things stand before starting. It follows its own entry law: a dated entry is what changed plus the durable lesson, a few lines. -- NEVER create a second log: not per directory, not per project, feature, branch or run, not `NOTES.md` / `PROGRESS.md` / `STATUS.md` / `worklog/`, not a dated diary. Logging in fifty places is not logging — the entry is never where the next reader looks, and each copy re-dates the same event on its own authority. In `dank/command-center` the KB linter reports any `log.md` that is not the bundle root's. +- NEVER create a second log: not per directory, not per project, feature, branch or run, not `NOTES.md` / `PROGRESS.md` / `STATUS.md` / `worklog/`, not a dated diary. In `dank/command-center` the KB linter reports any `log.md` that is not the bundle root's; the rule and its reasoning are owned by `omp-docs/references/okf-spec.md`. - A log holds laws and incidents, never bookkeeping: no per-file inventories, no run-by-run numbers, no transcripts. A number in a log still has to pass the purpose test above. - If a knowledge base has no log page, add one at its root and list it in the index. That is the fix — never a log somewhere else. diff --git a/omp-docs/inventory/index.md b/omp-docs/inventory/index.md index aa03f78..bd992ae 100644 --- a/omp-docs/inventory/index.md +++ b/omp-docs/inventory/index.md @@ -1,5 +1,7 @@ # Inventory Index -Census of all userspace program configuration on the workstation. +Tracked-vs-box-only census of userspace configuration on blankpad. Read the scope before +trusting coverage: these pages measure `~/.config`, not `~/.local/share`, not system package +state, not the servers. - [home-config-census.md](home-config-census.md) — Per-file tracked-vs-box-only census of ~/.config on blankpad — the input for the dotfiles pruning/adoption session. diff --git a/omp-docs/log.md b/omp-docs/log.md index 62dc325..99fdc02 100644 --- a/omp-docs/log.md +++ b/omp-docs/log.md @@ -16,9 +16,8 @@ lesson, a few lines. Full incident narratives are recoverable from git (`git log --follow omp-docs/log.md`); knowledge that outlives an entry lives on its concept page, linked here. Never re-expand an entry in place. -This page is the bundle's **only** work log: one file, at the root. Never start a -second one in a subdirectory or under another name — `scripts/kb-lint.sh` reports any -`log.md` that is not this one. +This is the bundle's only work log; [references/okf-spec.md](references/okf-spec.md) owns the +one-log rule and `scripts/kb-lint.sh` enforces it. ## 2026-10-21 — reviewing a day of work, and finding checks that lie @@ -45,6 +44,11 @@ rather than its argument, so the suite asserted the mask was set and per-card ba unobtainable on the 2×R9700 box. Model the platform rule in the stub and the existing test goes red against the old code — that is the red-proof worth having. +Same shape in `remote-job.sh status`: it globbed its arguments, so a job named `x*` was reported +as whatever happened to match in the caller's directory. A name is data — split it (`read -a`), +never let the shell expand it — and a multiplexed `tail` with no jobs says "no jobs" instead of +handing `*.out` to tail and reporting a missing file. + Incident, found while verifying the gate: the live-sync heartbeat had failed three hourly ticks, because a managed skill that documents how to write a secret gate quotes the vendor's all-zeros placeholder id and the gate's shape rule fired on the documentation. Prose about credentials is @@ -53,6 +57,13 @@ the hole. Placeholder judgement reads the matched token — plus what follows it quantifier truncates `EXAMPLE` to `EXA` — never the whole line. `ALLOW_SECRETS` stays the only override, and the heartbeat's own payload was replayed through the fixed hook to prove it lands. +The rest of #60 was claims about the machine that the machine had stopped honouring: +`.gitignore` and `deploy.sh`'s exclusion list had drifted apart (`Noctalia*` was excluded, never +ignored), `.gitattributes` was the stated reason `git-lfs` was installed — for wallpapers that +same week's diff untracked, and the one-log rule was restated in four owners. A coverage claim +names what was measured; a tool justified by dead config is a tool nobody can defend when the +config goes. + ## 2026-10-21 — a documented mode the panel never had The home ultrawide had been running at 60 Hz for weeks while `desk-1-home.kdl` diff --git a/omp-docs/references/okf-maintenance.md b/omp-docs/references/okf-maintenance.md index 9100be3..2846550 100644 --- a/omp-docs/references/okf-maintenance.md +++ b/omp-docs/references/okf-maintenance.md @@ -21,12 +21,11 @@ Knowledge bundles rot. It is the nature of knowledge to attract cobwebs, much li ## Change Tracking with `log.md` -Every OKF bundle has a `log.md`, and it is the bundle's **only** work log: one file, -at the bundle root. A subdirectory never gets its own — not `core/log.md`, not -`notes.md`, not a per-project, per-branch or per-run diary. Sprawl is not caution: -with fifty logs the entry is never where the next agent looks, and each copy re-dates -the same event on its own authority. `scripts/kb-lint.sh` reports any `log.md` that is -not the root's. +Exactly one `log.md` per bundle, at the bundle root — [okf-spec.md](okf-spec.md) owns that rule +and `scripts/kb-lint.sh` enforces it, reporting any `log.md` that is not the root's. The +maintenance action is collapse, never coexistence: when a second log turns up (`core/log.md`, +`notes.md`, a per-run diary), move its entries into the root log under their own dates, delete +the file, and re-run the linter. **Format:** one dated section per day, newest first; a few lines per change — what happened, and the durable lesson. It is not a file manifest. -- 2.54.0