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