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/.githooks/pre-commit b/.githooks/pre-commit index 5c1a608..1024624 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 @@ -67,17 +79,48 @@ while IFS= read -r 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 - 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/.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/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/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/agent/models.yml b/agent/models.yml index 81dc64d..86273d0 100644 --- a/agent/models.yml +++ b/agent/models.yml @@ -16,7 +16,7 @@ providers: auth: none models: - id: frank-archon - # Model id renamed from frank-archon on 2026-10-08 (official + # Model id renamed from frank-medium on 2026-10-08 (official # unit-name scheme; the systemd service, scripts and runbooks keep # the frank-archon name — service names are unchanged). # Server-side compaction routes to compaction.remoteEndpoint in @@ -82,9 +82,11 @@ providers: auth: none models: - id: frank-dragoon - # Model id renamed from frank-dragoon on 2026-10-08 (official + # Model id renamed from frank-large on 2026-10-08 (official # unit-name scheme; the service/unit/scripts keep the frank-dragoon - # name — service names are unchanged). + # name — service names are unchanged). The pre-rename id is + # `frank-large`, recorded here because the callsign rename overwrote + # its own explanation and left "renamed from frank-dragoon". name: "frank-dragoon (dankclaw-gpu)" # Server-side compaction runs on the compaction.remoteEndpoint in # config.yml (192.168.0.143:7771 — frank-zealot's 3090); this steers the @@ -111,31 +113,80 @@ providers: maxTokens: 45056 reasoning: true # VISION ON as of 2026-10-06: the v2 vision tower loads beside the - # checkpoint (HALOGEN_VISION_TOWER; red-PNG probe verified on the - # card). image_url content parts only — the legacy top-level - # images field is the frank-zealot trap, do not repeat it here. + # checkpoint (HALOGEN_VISION_TOWER; priced at 0.8 GiB in the boot + # ledger, and re-probed 2026-10-21 — a 512 px solid-red PNG comes back + # "Red" at 282 prompt tokens). image_url content parts only — the + # legacy top-level images field is the frank-zealot trap, do not + # repeat it here. input: [text, image] compat: # flash-server emits thinking as `reasoning_content` — same # field as every engine this endpoint has run, no harness # change needed. reasoningContentField: "reasoning_content" - # Same Qwen3.8 template family (it ships inside the tokenizer - # bundle): any reasoning_effort outside {xhigh, medium, low} is - # rejected, not clamped — the fold stays mandatory. + # flash-server validates reasoning_effort against its own list and + # 400s anything else, naming it: "reasoning_effort 'maximum' + # unsupported; use minimal|low|medium|high|xhigh|max|none" (measured + # 2026-10-21). Every omp level is already spelled by the engine, so + # the map is identity — it exists to record that, not to fold. An + # earlier version rewrote high->xhigh and minimal->low to stop + # rejections that do not exist, and the rewrite asked the engine for + # a different thinking budget than the caller named. Levels really do + # differ here: thinking length on one prompt at max_tokens 2048 + # measured low 766 tokens, xhigh 614, max 1049 with the block closed + # at 1024 (reasoning_closed_at). The harness passes an unmapped level + # through verbatim (`compat.reasoningEffortMap?.[effort] ?? effort`), + # so removing an entry is a pass-through, never a 400. + # + # `max` still does not reach the engine, and the map is not why: the + # harness clamps the requested effort to the model's own effort ladder + # before the map applies, and its ladder for this model tops out at + # xhigh — captured on the wire 2026-10-21, `:max` left as + # `reasoning_effort: "xhigh"` with this identity map in place. The + # `max` row below is the engine-side half of the contract; it takes + # effect the moment the harness ladder gains `max`, and nothing here + # can force it sooner. + # + # Thinking OFF needs an explicit mode. The default disable mode sends + # the LOWEST effort rather than a switch — `:off` left the wire as + # `reasoning_effort: "minimal"` (captured), and minimal still thinks: + # a canned 26-token block on every "off" request. `none-effort` makes + # the harness send `reasoning_effort: "none"`, which this engine + # honours as a real switch: `reasoning_tokens: 0`, answer only, no + # thinking block in the rendered output (both verified 2026-10-21). + # + # Re-measure before changing any of it. The allow-list, in the + # engine's own words: + # curl -s :7770/v1/chat/completions -H 'content-type: application/json' \ + # -d '{"model":"frank-dragoon","messages":[{"role":"user","content":"x"}], + # "reasoning_effort":"maximum"}' + # -> 400 "reasoning_effort 'maximum' unsupported; use + # minimal|low|medium|high|xhigh|max|none" + # and read usage.completion_tokens_details.reasoning_tokens at + # max_tokens 2048 to see the levels separate. The wire itself is + # settled by pointing a scratch HOME at a logging proxy on :7799 that + # forwards to :7770 and running `omp --model …:off` — config claims + # are worth exactly what their last capture says. + reasoningDisableMode: none-effort reasoningEffortMap: - minimal: low + minimal: minimal low: low medium: medium - high: xhigh - max: xhigh - # The card's thinking recipe, on the wire. The flash-server - # engine HAS a server-side sampler (HALOGEN_TEMPERATURE/TOP_P/ - # TOP_K = 1.0 / 0.95 / 20 in frank-dragoon.env) — these match it, - # so per-request values are consistent, not load-bearing. + high: high + xhigh: xhigh + max: max + # The card's thinking recipe, on the wire. The flash-server engine + # HAS a server-side sampler (HALOGEN_TEMPERATURE/TOP_P/TOP_K = + # 1.0 / 0.95 / 20 in frank-dragoon.env), but an omitted parameter is + # the engine's choice, not a pinned one: top_k is sent explicitly so + # the recipe survives someone editing HALOGEN_TOP_K. The engine + # accepts top_k, min_p, presence_penalty and repetition_penalty + # (measured 2026-10-21, all 200) — only the values this endpoint is + # meant to run are pinned here. extraBody: temperature: 1.0 top_p: 0.95 + top_k: 20 "llamacpp (dankclaw-3090)": baseUrl: http://192.168.0.143:7771/v1 @@ -143,7 +194,7 @@ providers: auth: none models: - id: frank-zealot - # Model id renamed from frank-zealot on 2026-10-08 (official + # Model id renamed from frank-small on 2026-10-08 (official # unit-name scheme; the service/unit/scripts keep the # frank-zealot name — service names are unchanged). name: "frank-zealot (dankclaw-3090)" 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/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/core/index.md b/omp-docs/core/index.md index 56f4eb1..b6dfe70 100644 --- a/omp-docs/core/index.md +++ b/omp-docs/core/index.md @@ -16,6 +16,4 @@ These files define the structural conventions that govern the agent system itsel * [OMP Status Line Customization](statusline.md) — How presets, composer shapes, and segment arrays actually interact; the fleet's tuned bar. (moved from /pi/, 2026-10-19) * [OMP Subagent Limits & Fan-out Policy](subagent-limits.md) — Live caps: concurrency 2, no nested spawns (the `maxRecursionDepth: 0` trap), per-endpoint in-flight ceilings, isolation policy. (moved from /pi/, 2026-10-19) -## Change Tracking - -* [Bundle Change Log](log.md) — Chronological update history. +**Change log:** the bundle keeps exactly one, one level up — [Bundle Change Log](../log.md). A directory here never has its own. diff --git a/omp-docs/core/log.md b/omp-docs/core/log.md deleted file mode 100644 index f0509ab..0000000 --- a/omp-docs/core/log.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -type: changelog -title: Core Bundle Change Log -description: Change log for the core documentation directory — tracks migrations and structural changes. -tags: - - core - - changelog - - okf -timestamp: 2026-07-18 ---- - -# Core Bundle Change Log - -## 2026-07-18 -- **Migration**: Converted `index.md` (root) to `core/index-doc.md` — moved root bundle index into core domain -- **Migration**: Converted `agents-md-format.md` (root) to `core/agents-md-format.md` — agents-md format belongs in core -- **Creation**: Established `core/` directory with `index.md` as the domain entry point -- **Creation**: This change log for tracking core directory modifications - -## 2026-07-15 -- **Initialization**: OKF v0.1 conversion of the entire docs bundle — frontmatter applied, structure established, first `log.md` entries created 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..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) — 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/log.md b/omp-docs/log.md index 207ad8b..dcf5b87 100644 --- a/omp-docs/log.md +++ b/omp-docs/log.md @@ -6,7 +6,7 @@ tags: - okf - maintenance - changelog -timestamp: 2026-10-19 +timestamp: 2026-10-21 --- # Bundle Change Log @@ -16,6 +16,115 @@ 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 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 + +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. + +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 +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. + +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 — an effort map folding against rejections that never existed + +`agent/models.yml` rewrote `minimal→low`, `high→xhigh` and `max→xhigh` for +frank-dragoon to dodge 400s the engine does not send: flash-server's own error names +its allow-list (`minimal|low|medium|high|xhigh|max|none`) and the harness passes an +unmapped level through verbatim, so the fold only asked for a different thinking +budget than the caller named. The map is identity now, and +`reasoningDisableMode: none-effort` was added because the default disable mode does +not disable — `:off` left as `reasoning_effort: "minimal"` and paid for a canned +thinking block, where `none` returns `reasoning_tokens: 0`. Both settled by capturing +the wire through a logging proxy rather than reading the config: a claim about what +the harness sends is worth exactly its last capture. The same pass restored the three +pre-callsign model ids (`frank-medium` / `frank-large` / `frank-small`) that the +2026-10-08 rename had overwritten inside its own explanatory comments. Refs #62 + +## 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. +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): + +- **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.** 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 +temporary per-output path, so no config reload and no logged-out session. + +## 2026-10-20 — one work log per bundle + +The bundle had two: this page and `core/log.md`, created "for tracking core +directory modifications" — two entries of pure migration bookkeeping (file moved, +directory created, log created), and a date for the OKF conversion that neither this +page nor git agrees with: 2026-07-15 there, 2026-01-15 here, while `git log` dates the +OKF rebuild 2026-08-28. `core/index.md` was still pointing readers at it, which is how +a stale page stays alive. Trashed to `omp-docs/core/.AGENTTRASH/log.md` — its content +is recoverable from git, which is the entire argument against a second log. + +Laws, now written down in [references/okf-spec.md](references/okf-spec.md) and +[references/okf-maintenance.md](references/okf-maintenance.md), and gated: a bundle has +ONE work log, `log.md`, at its root, and `scripts/kb-lint.sh` reports any `log.md` that +is not the root's — so the sprawl that made logging useless once cannot restart +quietly. The agent-side duty is in `agent/APPEND_SYSTEM.md` § 1: log here, read here, +never start another. + ## 2026-10-20 — two instruments rescued from `$HOME` script sediment `$HOME` had accumulated 123 loose scripts at its root (archived to diff --git a/omp-docs/references/okf-maintenance.md b/omp-docs/references/okf-maintenance.md index 609da21..2846550 100644 --- a/omp-docs/references/okf-maintenance.md +++ b/omp-docs/references/okf-maintenance.md @@ -21,32 +21,46 @@ 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` file. It is not optional. It is the bundle's memory. +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:** Date-grouped entries, newest first. One line per change with timestamp, file path, and what changed. +**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. ```markdown # Bundle Change Log -## 2026-01-15 -- `/docs/ghostty.md` — Updated keybind table after v1.2 release -- `/docs/pi-permission-system/` — Added `policy.md` and `audit.md` -- `/docs/index.md` — Synced with directory contents +## 2026-01-15 — the gate was reading the wrong files + +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). ``` **Rules:** -- **Newest entries first.** Putting old entries at the top inverts the timeline and makes it impossible to see what just happened. -- **One line per change.** A single line is sufficient. The git history is there if someone wants the gory details. -- **Group by date.** Batch all changes from the same day together. -- **Always date-stamp the section.** Use `## YYYY-MM-DD`. -- **Mention `index.md` whenever you change a bundle.** If you added or removed a file, update the index and note it in the log. +- **Newest entries first.** Old entries at the top invert the timeline. +- **What changed + the durable lesson, a few lines.** Full narratives are recoverable + from git (`git log --follow log.md`); never re-expand one in place. +- **Laws and incidents, never bookkeeping.** No per-file inventories, no run-by-run + numbers, no transcripts. Measured numbers still have to pass the purpose test. +- **Link, do not duplicate.** Knowledge that outlives the entry belongs on its concept + page; the entry points there. +- **One dated section per day** (`## YYYY-MM-DD`), and mention `index.md` whenever a + file is added or removed. ### Common Mistakes -- ❌ Writing `"Updated ghostty.md"` — Updated it with what? Be specific. -- ❌ Forgetting to write to `log.md` — This is the equivalent of doing surgery without keeping a record. -- ❌ Using relative paths — Use absolute paths (`/docs/ghostty.md`). -- ❌ Leaving stale entries from manual edits — The log is the single source of truth. +- ❌ `Updated ghostty.md` — updated it with what, and what did it teach? +- ❌ 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 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. --- @@ -84,6 +98,14 @@ OKF is built on the principle that a file is one concept. - ❌ Don't create a file called `misc.md` — `misc` is a graveyard, not a category. - ❌ Don't use `# Other` or `# Additional Notes` sections — these are taping things to the wall. +## Quarantine, Not Deletion + +A page is never deleted outright: it moves to `.AGENTTRASH/` beside where it lived and +waits for a pruning session with Frank present. The quarantine is not part of the +bundle — `scripts/kb-lint.sh` and `scripts/kb-coverage.py` skip any path containing +`.AGENTTRASH`. That exemption is deliberate: a gate that goes green only once the +quarantined copy is gone is a gate that deletes history. + --- ## Auto-Generation Strategies diff --git a/omp-docs/references/okf-spec.md b/omp-docs/references/okf-spec.md index e5099c5..6add03b 100644 --- a/omp-docs/references/okf-spec.md +++ b/omp-docs/references/okf-spec.md @@ -42,7 +42,7 @@ An OKF knowledge bundle is a directory tree. Every concept is one file. The file ``` my-bundle/ ├── index.md # Optional directory listing -├── log.md # Optional update history +├── log.md # The bundle's ONE update history — root only ├── concept-one.md # A concept at the root level └── subdirectory/ ├── index.md # Optional subdirectory listing @@ -55,7 +55,13 @@ my-bundle/ | Filename | Purpose | |----------|---------| | `index.md` | Directory listing for progressive disclosure | -| `log.md` | Chronological update history | +| `log.md` | Chronological update history — **exactly one, at the bundle root** | + +**One work log per bundle.** `log.md` is reserved, and it is reserved for a single +file: the bundle root's. A subdirectory never keeps its own change log — a bundle +that logs in fifty places has no log, because the entry is never where the next +reader looks and every copy re-dates the same event. `scripts/kb-lint.sh` reports +any `log.md` that is not the root's. --- diff --git a/omp-docs/tools/applications.md b/omp-docs/tools/applications.md index e4c39e4..3802392 100644 --- a/omp-docs/tools/applications.md +++ b/omp-docs/tools/applications.md @@ -33,7 +33,6 @@ Applications that ship with or are configured on CachyOS with Niri on the workst | **Noctalia Shell** | 4.7.7 | Desktop shell (launcher, wallpaper, session menu) | | **Shelly Notifications** | — | Notification daemon | | **pavucontrol** | — | Audio mixer | -| **wlr-randr** | — | Display management | ## Screenshot and Recording diff --git a/omp-docs/tools/cachyos.md b/omp-docs/tools/cachyos.md index 4315b2a..28e0310 100644 --- a/omp-docs/tools/cachyos.md +++ b/omp-docs/tools/cachyos.md @@ -95,7 +95,6 @@ CachyOS with Niri ships with a curated set of applications: - **wf-recorder** — Screen recording - **hyprpicker** — Color picker - **wl-clipboard** — Clipboard management -- **wlr-randr** — Display management - **NetworkManager** — Network management - **BlueZ** — Bluetooth - **pavucontrol** — Audio mixer diff --git a/omp-docs/tools/dotfiles-deploy.md b/omp-docs/tools/dotfiles-deploy.md index e558953..ed4aa7d 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 @@ -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*. @@ -54,8 +59,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 @@ -158,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/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/omp-docs/tools/niri/displays.md b/omp-docs/tools/niri/displays.md index e686f5b..6d7a41b 100644 --- a/omp-docs/tools/niri/displays.md +++ b/omp-docs/tools/niri/displays.md @@ -1,14 +1,14 @@ --- type: reference title: Display Configuration -description: Niri output configuration on blankpad — EDID-identity-keyed desk profiles, hotplug auto-switching, portrait transforms, position pinning, and GPU notes (AMD Cezanne now; Intel Arc render-node pin is dankware-legacy). +description: Niri output configuration on blankpad — EDID-identity-keyed desk profiles, hotplug auto-switching, portrait transforms, position pinning, the phantom-mode fallback trap, and GPU notes (AMD Cezanne now; Intel Arc render-node pin is dankware-legacy). tags: - niri - display - monitors - rotation - desk-profiles -verified: 2026-10-09 +verified: 2026-10-21 --- # Display Configuration @@ -24,7 +24,11 @@ active profile (`desk-1-home.kdl`, `desk-2-portable.kdl`, …). `config.kdl` includes it with `include optional=true` so a missing link is a warning, not a parse failure. **Probe:** `niri msg outputs` is authoritative (connector, full EDID long -name, modes, transform, scale, position, VRR). `wlr-randr` remains available. +name, modes, transform, scale, position, VRR). Per-output changes go live with +`niri msg output ` and are +temporary — never written to the config file. `wlr-randr` is **not installed** +on blankpad (`pacman -Qs wlr` matches nothing, verified 2026-10-21); pages that +list it as an installed tool are stale. --- @@ -33,21 +37,23 @@ name, modes, transform, scale, position, VRR). `wlr-randr` remains available. ``` ┌──────────────┬──────────────────────────────┬──────────┐ │ PM1601 │ Sceptre M34 ultrawide │ eDP-1 │ -│ portrait │ 3440x1440 @ 239.999 │ 1920x1080│ +│ portrait │ 3440x1440 @ 180.000 │ 1920x1080│ │ 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@239.999"`, `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. @@ -84,7 +90,7 @@ graph LR ```kdl // desk-1-home.kdl — one block per panel, keyed by identity output "Sceptre Tech Inc Sceptre M34 Unknown" { - mode "3440x1440@239.999" + mode "3440x1440@180.000" scale 1 transform "normal" position x=1581 y=-600 @@ -98,6 +104,10 @@ Four rules learned the hard way: name *and* long name) fight over properties — transform reverts are the classic symptom. Desk profiles are self-contained: a panel's identity, mode, transform, **and** position live in exactly one block per file. + `desk-1-home.kdl` still carries legacy connector-keyed `DP-1` / `DP-2` + blocks next to the identity block. Their modes were aligned to 180 Hz on + 2026-10-21 so a connector-named reconnect cannot drop the panel to 120, but + they belong folded into the identity block. 2. **Key by EDID identity, not connector.** Connector names swap between docks; the EDID long name travels with the panel. Serials on these panels read `Unknown`, so the full long name (manufacturer model serial) is the @@ -105,8 +115,57 @@ 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. **Modes must match what the panel reports** (`niri msg outputs` → - `Mode: current …`). A mode the panel cannot drive leaves the output dark. +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 + +A display complaint belongs to one of three layers. Sort it before editing +config, because the config is usually innocent — and when it is guilty, the +guilt may have been copied from this page. + +| Layer | Log signature | Reality | Move | +|-------|---------------|---------|------| +| Config | `configured mode … could not be found, falling back to preferred` | a mode/scale the panel never offered | re-read the mode list with `niri msg outputs` and correct the file; do **not** trust a documented mode | +| Link | `disconnecting connector: "DP-1"` followed seconds later by `new connector: DP-1`, recurring | the DP link itself dropping — cable, dock, the monitor's DP-version/DSC setting, or panel power saving | every drop re-runs mode selection, so a config error is re-enacted in public; swap the cable before blaming the compositor | +| Driver | `Page flip commit failed … (Invalid argument)` plus kernel `*ERROR* … DMCUB error` | amdgpu's display engine wedged, typically after an s2idle resume | survives until a reboot; niri keeps running while the panel keeps dropping. No config edit clears it | + +Probes, in this order: + +```bash +niri msg outputs # connector, EDID long name, real mode list, current mode +journalctl --user -b | grep -E 'disconnecting connector|Page flip commit failed|falling back to preferred' +journalctl -k -b | grep -iE 'amdgpu.*(ERROR|DMCUB)|PM: suspend exit' +``` + +Correlate the drop timestamps against `PM: suspend exit` first. A resume a +minute before the first drop is the cause; the config file is not. + +To fix a refresh rate without disturbing the session, use the temporary +per-output path — it touches one output, applies no config, reloads nothing: + +```bash +niri msg output "Sceptre Tech Inc Sceptre M34 Unknown" mode "3440x1440@180.000" +``` + +It is temporary by design, so correct the profile too and let the next +reconnect or `load-config-file` make it permanent. With the display engine +already throwing `EINVAL`, prefer the per-output path over a full config +reload; on a healthy machine either is fine. ## GPU assignment @@ -132,4 +191,4 @@ symlink matrix]]). - [Niri](/tools/niri/index.md) — compositor overview and config layout - [[tips|tips]] — reload path, `niri msg layers/casts`, includes - [[noctalia-shell|noctalia-shell]] — notification shown on desk switch -- [[cachyos|cachyos]] — operating system, including `wlr-randr` +- [[cachyos|cachyos]] — operating system 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. --- diff --git a/omp-docs/tools/packages.md b/omp-docs/tools/packages.md index 41d80b5..c01abfd 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 --- @@ -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 @@ -48,7 +52,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 +82,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 @@ -102,5 +114,6 @@ install the same dead weight. They are printed as `orphans ignored` and excluded - [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/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/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/packages/common.pkgs b/packages/common.pkgs index 470f2a5..5853882 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 # 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 flameshot # screenshot tool; bound to CTRL+Shift+1/2/3 in niri keybinds 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/scripts/gen-keybinds.sh b/scripts/gen-keybinds.sh index dcab227..3feeeec 100755 --- a/scripts/gen-keybinds.sh +++ b/scripts/gen-keybinds.sh @@ -42,17 +42,27 @@ gen() { echo "> GENERATED by \`scripts/gen-keybinds.sh\` from \`$(basename "$KBD")\` — do not edit by hand." echo awk ' - # section header: // ─── Name ─── - /^[[:space:]]*\/\/[[:space:]]*─+ (.+) ─+[[:space:]]*$/ { + # Section marker: a comment decorated with box-drawing dashes on both sides, + # `// ─── Name ───`. The dash is matched as a LITERAL and removed with gsub — never + # with a quantifier on the multibyte character. Under LC_ALL=C (cron, systemd units, + # agent shells) gawk works byte-wise, `─+` then means "the last byte of ─, repeated", + # and every header vanishes while the script still exits 0. Plain comments stay + # comments: only a dash-decorated one opens a section. + /^[[:space:]]*\/\/[[:space:]]*─/ { s = $0 - sub(/^[[:space:]]*\/\/[[:space:]]*─+ /, "", s) - sub(/ ─+[[:space:]]*$/, "", s) - if (in_table) print "" - print "## " s - print "" - print "| Key | Action |" - print "|-----|--------|" - in_table = 1 + sub(/^[[:space:]]*\/\/[[:space:]]*/, "", s) + sub(/[[:space:]]+$/, "", s) + if (s ~ /─ .*─/) { + gsub(/─/, "", s) + gsub(/[[:space:]]+/, " ", s) + sub(/^ /, "", s); sub(/ $/, "", s) + if (in_table) print "" + print "## " s + print "" + print "| Key | Action |" + print "|-----|--------|" + in_table = 1 + } next } # bind line: KEY [flags] [hotkey-overlay-title="..."] { action; } 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/kb-coverage.py b/scripts/kb-coverage.py index e1809ed..f665481 100755 --- a/scripts/kb-coverage.py +++ b/scripts/kb-coverage.py @@ -4,7 +4,15 @@ import re, sys from pathlib import Path kb = Path(sys.argv[1]) issues = [] -dirs = sorted({p.parent for p in kb.rglob("*.md")}) + + +def kept(p): + # `.AGENTTRASH/` is quarantine, not bundle content: a gate that flags it pushes an + # agent to delete a page that is only waiting for a pruning session with the operator. + return ".AGENTTRASH" not in p.parts + + +dirs = sorted({p.parent for p in kb.rglob("*.md") if kept(p)}) for d in dirs: idx = d / "index.md" if not idx.is_file() and d != kb: @@ -21,12 +29,16 @@ for d in dirs: # wikilinks too wl_names = {m.group(1).strip().rsplit("/", 1)[-1] for m in re.finditer(r"\[\[([^\]|#]+)", text)} for sib in sorted(d.iterdir()): + if sib.name == ".AGENTTRASH": + continue if sib.suffix != ".md" or sib.name == "index.md": continue if sib.resolve() not in linked and sib.stem not in wl_names: issues.append(f"{d.relative_to(kb)}/index.md: does not link {sib.name}") # subdirs of d linked as their index? for sub in sorted(d.iterdir()): + if sub.name == ".AGENTTRASH": + continue if sub.is_dir() and (sub / "index.md").is_file(): subidx = (sub / "index.md").resolve() dirref = sub.resolve() diff --git a/scripts/kb-lint.sh b/scripts/kb-lint.sh index aa27243..4b8b933 100755 --- a/scripts/kb-lint.sh +++ b/scripts/kb-lint.sh @@ -12,7 +12,19 @@ from pathlib import Path kb = Path(sys.argv[1]) violations = [] -for p in sorted(kb.rglob("*.md")): +def pages(): + """Markdown pages that belong to the bundle. `.AGENTTRASH/` is a quarantine, not + content: linting a deleted-but-not-yet-pruned page would demand its destruction to + make the gate go green, which is the wrong incentive.""" + for p in kb.rglob("*.md"): + 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") lines = text.splitlines() @@ -38,9 +50,21 @@ for p in sorted(kb.rglob("*.md")): if not isinstance(fm, dict) or not str(fm.get("type", "")).strip(): violations.append(f"{rel}: frontmatter lacks required 'type:'") + +# One work log per bundle, at the bundle root. A `log.md` is a reserved name, but +# reserved does not mean "allowed in every directory": per-directory logs are how a +# bundle ends up with fifty of them, the entry never where the next agent looks, and +# each copy dating the same event differently. A bundle with no log at all is left to +# the maintenance page to nag about; this gate's job is sprawl. +logs = sorted(str(p.relative_to(kb)) for p in pages() if p.name == "log.md") +if len(logs) > 1 or (logs and logs[0] != "log.md"): + violations.append( + "a bundle has one work log, log.md, at the bundle root; found: " + ", ".join(logs) + ) + # link integrity: fenced code blocks hold example paths, not live links — strip first. # Vendored spec is exempt: its example paths are illustrations, not live links. -for p in sorted(kb.rglob("*.md")): +for p in pages(): if str(p.relative_to(kb)) == "references/okf-spec.md": continue text = re.sub(r"^```[^\n]*$^```[^\n]*$", "", p.read_text(errors="replace"), flags=re.M) @@ -50,9 +74,12 @@ for p in sorted(kb.rglob("*.md")): 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/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/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/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/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/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" +} diff --git a/tests/gen-keybinds.bats b/tests/gen-keybinds.bats index 70edf8b..8823e11 100755 --- a/tests/gen-keybinds.bats +++ b/tests/gen-keybinds.bats @@ -48,6 +48,16 @@ teardown() { [[ "$output" != *"a plain comment"* ]] } +@test "section headers survive a C locale, where awk reads bytes instead of characters" { + # Cron, systemd units and agent shells get LC_ALL=C while Frank's terminal does not. + # A matcher that puts a quantifier on a multibyte character then loses EVERY section + # header and prints a sheet with no sections at all — silently, exit status 0. + run env LC_ALL=C "$GEN" "$TMP/keybinds.kdl" + [[ "$output" == *"## Applications"* ]] + [[ "$output" == *"## Screenshots"* ]] + [[ "$output" != *"a plain comment"* ]] +} + @test "row count matches bind count" { run "$GEN" "$TMP/keybinds.kdl" local rows diff --git a/tests/kb-coverage.bats b/tests/kb-coverage.bats new file mode 100755 index 0000000..49f551d --- /dev/null +++ b/tests/kb-coverage.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats +# tests/kb-coverage.bats — black-box tests for scripts/kb-coverage.py +# +# Contract: exit 0 when every directory of a knowledge bundle has an index.md and each +# index links its siblings. Observable surface: stdout lines and exit status. + +setup() { + CHECK="$BATS_TEST_DIRNAME/../scripts/kb-coverage.py" + KB="$(mktemp -d)/kb" + mkdir -p "$KB/sub" + printf '# Index\n\n* [Good](good.md)\n* [Sub](sub/)\n' > "$KB/index.md" + printf '# Sub\n\n* [Deep](deep.md)\n' > "$KB/sub/index.md" + printf 'body\n' > "$KB/good.md" + printf 'body\n' > "$KB/sub/deep.md" +} + +teardown() { rm -rf "$(dirname "$KB")"; } + +@test "a bundle whose directories index their contents passes" { + run python3 "$CHECK" "$KB" + [ "$status" -eq 0 ] + [[ "$output" == *"0 issue(s)"* ]] +} + +@test "a directory nobody indexes is reported" { + mkdir "$KB/stray" + printf 'body\n' > "$KB/stray/thing.md" + run python3 "$CHECK" "$KB" + [ "$status" -ne 0 ] + [[ "$output" == *"stray: no index.md"* ]] +} + +@test "an index that forgets a sibling page is reported" { + printf 'body\n' > "$KB/orphan.md" + run python3 "$CHECK" "$KB" + [ "$status" -ne 0 ] + [[ "$output" == *"does not link orphan.md"* ]] +} + +@test "the quarantine trash directory is not part of the bundle" { + # Pages deleted from the KB wait in .AGENTTRASH/ for a pruning session with Frank. + # Requiring an index there, or listing its contents, would push an agent to delete + # the quarantined copy just to make the gate green. + mkdir -p "$KB/.AGENTTRASH" + printf 'retired page\n' > "$KB/.AGENTTRASH/old-page.md" + run python3 "$CHECK" "$KB" + [ "$status" -eq 0 ] + [[ "$output" == *"0 issue(s)"* ]] +} diff --git a/tests/kb-lint.bats b/tests/kb-lint.bats index b7868fb..cc8dfc8 100755 --- a/tests/kb-lint.bats +++ b/tests/kb-lint.bats @@ -91,6 +91,57 @@ EOF [ "$status" -eq 2 ] } +@test "a second work log in a subdirectory is reported" { + # One bundle, one work log. Per-directory logs are how a bundle ends up with fifty + # of them: the entry is never where the next agent looks, and each copy re-dates the + # same event on its own. + printf -- '---\ntype: changelog\n---\n## 2026-01-01\n- created a log\n' > "$KB/log.md" + printf -- '---\ntype: changelog\n---\n## 2026-01-02\n- also a log\n' > "$KB/sub/log.md" + run bash "$LINT" "$KB" + [ "$status" -ne 0 ] + [[ "$output" == *"one work log"*"at the bundle root"* ]] + [[ "$output" == *"sub/log.md"* ]] +} + +@test "a work log that is not at the bundle root is reported" { + printf -- '---\ntype: changelog\n---\n## 2026-01-02\n- misplaced log\n' > "$KB/sub/log.md" + run bash "$LINT" "$KB" + [ "$status" -ne 0 ] + [[ "$output" == *"one work log"*"at the bundle root"* ]] +} + +@test "the quarantine trash directory is not part of the bundle" { + # Deleted pages wait in .AGENTTRASH/ for a pruning session with Frank. Holding them + # to frontmatter and one-log rules would push an agent to delete history to go green. + mkdir -p "$KB/.AGENTTRASH" + printf 'no frontmatter, and a second log besides\n' > "$KB/.AGENTTRASH/log.md" + run bash "$LINT" "$KB" + [ "$status" -eq 0 ] + [[ "$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 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* ]] +} 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" 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" { 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 ] diff --git a/tests/secret-gate.bats b/tests/secret-gate.bats index c354765..03d38cf 100755 --- a/tests/secret-gate.bats +++ b/tests/secret-gate.bats @@ -157,3 +157,48 @@ 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' +} + +@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"* ]] +}