Two pages described Dankclaw's search stack from memory, and one of them invented a port.
`searxng-tuning.md` claimed "`:8081` (UI) / `:8082` (JSON API)". Re-probed 2026-10-21: JSON is
served on **8081** (`/search?q=…&format=json` returns results) and **8082 is not SearXNG at
all** — it publishes container port 8085, which is Marreta's nginx, moved off 8080 because
SearXNG owns that port inside the shared namespace. Four containers print the same port list in
`podman ps` because they share gluetun's namespace, which is exactly the detail that gets
mistaken for "the JSON API is on another port". The policy-drift flag now carries today's
reading: `/config` still reports 262 engines and zero disabled, unchanged since 2026-10-19, so
the tuned engine policy is still not live — still an operator decision, still not silently
re-applied.
`vpn-routing.md` gave a mullvad-shaped gluetun example and no fleet shape at all, while the
bundle's own work log says the manual-WireGuard law lives "on concept pages". It lives here now,
read from the running container: `VPN_SERVICE_PROVIDER=custom` + `VPN_TYPE=wireguard` + the
single-peer `WIREGUARD_*` names (`protonvpn` and the plural `WIREGUARD_PEERS_*` family are both
wrong for this image), `FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on` — whose name
is the warning that disabling it leaks egress while every check keeps passing — and
`tun0` present. The mullvad block stays as the generic illustration, now labelled as one.
Refs #64
An index is the skim layer: its entries decide whether a reader opens the page, so an entry
that runs 250 characters is a paragraph wearing a bullet. Four entries in `tools/index.md`
(`dotfiles-deploy`, `packages`, `pcie-probe`, `remote-job`) now say what the page is for in one
line and let the page carry the detail — which is the whole argument for progressive disclosure
having an index at all.
`engineering/infra/index.md` pointed at `../sdlc.md` with no description at all; it now carries
the one from that page's frontmatter, like its four siblings.
Refs #64
504 lines, 4,496 words — the log had become the largest page in the bundle, and the reason was
not history: twelve entries dated 2026-10-18 → 2026-10-21 each re-narrated the incident AND
re-stated the law AND pointed at the page that owns the law. Three copies of one fact, dated
three ways. 384 lines / 3,251 words now.
What was cut: the restatements. The refuse-by-default guard belongs to
`tools/pcie-probe.md`, the completion-sentinel law to `tools/remote-job.md`, the verbatim
mode-string law to `tools/niri/displays.md`, the enable-snapshots-a-path trap to
`tools/dotfiles-deploy.md`, the sorted-directive rule to `caddy/caddyfile.md`, the effort-map
contract to `pi/models.md`, the tier law and the footer/`xdg-open` fleet laws to `AGENTS.md`,
and the gate's mechanics to `.githooks/pre-commit` plus its tests — each now named once, from a
short entry.
What was kept deliberately: laws with no other owner anywhere in the bundle. Fail-closed on a
blind `git diff`; a stub that echoes its environment instead of its argument cannot see the bug;
a name is data, never let the shell expand it; extensions must never await work that can exceed
30 s; `systemctl --user enable` snapshots an absolute path; agents never run
sudo/yay/pacman/pkexec (faillock incident, with the reset command); a claim about what the
harness sends is worth exactly its last capture; a number in a page is a claim, not a
measurement. Deleting those would have been the actual loss.
The 2025-08-05 entry is annotated rather than deleted: this repo's history dates that file's
arrival at 2026-08-28 (the OKF rebuild copied it), so 2025-08-05 is a legacy-tree date and the
entry now says so instead of silently contradicting `git log`.
Refs #64
The note in `references/okf-spec.md` claimed the bundle keeps a "`tags` discipline". It does
not: 22 of 76 concept pages carry `tags`, and neither `kb-lint.sh` nor `kb-coverage.py`
reads that field or `verified` — so the note was a claim about intent that an agent would
believe and pay the wrong cost for (adding tags to a page helps no tool, while `verified`
silently does the real work: it is the date someone last re-checked the page against the
fleet, and its absence means nobody ever has).
The note now states the measured usage instead of the aspiration, records the house link
style (bundle-absolute, 99 links already written that way), and says plainly that Obsidian
`[[wikilinks]]` are not OKF links — with the count this bundle had and the date they were
converted. The spec's own repository is now cited under `# Citations`, which the page was
missing while it lectured others about skipping citations.
Refs #64
`key-bindings-syntax.md` told you to run `wev` and read `sym: XXXX` — a placeholder nobody
fills in, for a tool that is not on the machine: `command -v wev` is empty, `pacman -Ss
--sync wev` is empty on CachyOS, and the AUR carries it as `wev-git`. The bind token is an
XKB keysym, and the complete list is already on every box that links libxkbcommon, so the
page now leads with the grep against `/usr/include/xkbcommon/xkbcommon-keysyms.h` (run here:
`a A Escape Left Return Tab`), and keeps `wev` as the "press the key and see" option with its
real package name and where to get it.
`screenshots.md` line 35 read `(FIXME upstream).`, which scans as unfinished work in a page
that is finished: the defect is upstream's, tracked in the two issue links six lines below.
Reworded to say that.
Refs #64
The page explained matcher ordering beautifully and never said where the file lives — so the
next agent hunts for a Caddyfile on the host and finds nothing. Measured on Dankclaw
2026-10-21: Caddy is a rootless Quadlet container (`caddy.container`, a *user* unit, so a
root `systemctl is-active caddy` answers `inactive` and means nothing), its config is
`/etc/caddy/Caddyfile` **inside the container**, `/etc/caddy` does not exist on the host, and
`pgrep -a caddy` prints a config path that no host path satisfies. Version is v2.11.7, which
closes the one version-dependent rule on the page: the leading-`/` `rewrite` form needs
2.8.0+, so the `*` disambiguator is legacy in this fleet's config.
Also: the seven caddyserver.com links sat in a mid-page "Sources:" paragraph. OKF wants
external claims under `# Citations`, so they move there and each entry says which claim it
supports. The validate section now points at the container instead of telling you to run the
validator on a file that is not there.
Refs #64
`[[page|Label]]` is Obsidian syntax, not OKF. `kb-lint` resolves it Obsidian-style — by
filename, anywhere in the bundle — so it has never tripped the link gate, while every other
consumer of the bundle (an OKF renderer, a Gitea wiki export, `grep` for reachability) sees
bracketed text where a link should be. That is how two generated pages ended up with no
inbound link at all and nobody noticed: the my-first link census of the bundle reported them
as orphans because their only inbound edges were wikilinks.
Mechanical conversion, 52 links across 17 pages, each resolved against the same page set
`kb-lint` uses: an explicit `|Label` is kept verbatim, an unlabelled link takes the target's
frontmatter `title`. Targets are emitted bundle-absolute (`/tools/niri/tips.md`), the form
the spec recommends and the form 99 existing links in this bundle already use.
Two links were `[[index]]`, which resolves by filename to 21 pages — the ambiguity that
filename-resolution invites. Both meant their own directory's index, so both are now written
out in full: `/tools/gitea/index.md`, `/tools/niri/index.md`.
The linter still tolerates wikilinks; whether it should reject them outright is an open
question on #64, because that would replace the two tests pinning today's tolerance.
Refs #64
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
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
`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
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 <name>` before adding a row.
Refs #60
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
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
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
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
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
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/<id> 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
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
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 <panel> mode <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
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.
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.
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.
The RDNA4 offload campaign left three HIP probes and a driver sitting loose in
$HOME, where the next agent cannot find them and a stray `rm -rf` ends them. They
answer a recurring question on the R9700 box — is decode limited by the link or by
the engine's read path — so they move into scripts/probes/ with a driver, tests,
and a page.
Two things changed in the code, not just the address:
- The driver measures only after a guard that runs BEFORE anything is built:
gpu_busy_percent over PCIE_PROBE_MAX_BUSY, or any process holding /dev/kfd, is a
refusal (exit 3) rather than a measurement. A benchmark taken beside a live
endpoint is both disruptive and worthless, and silently unloading a production
service is not a tool's decision — --force is the human saying otherwise. The
refusal-before-build ordering is pinned by a test, because the sysfs and kfd
paths are overridable and a refusal is otherwise untestable off the card box.
- hip_smoke.hip and pcie_probe.hip called hipGetDeviceProperties, which SEGFAULTS
on this ROCm build even though hipGetDeviceCount works; only pcie_bw.hip avoided
it, and that is the one that produced the campaign's numbers. Device identity now
comes from the sysfs topology block the driver prints, so all three probes are
runnable on the build we ship. Verified here: refusal on a real busy APU (exit 3),
missing toolchain (exit 4), missing sources (exit 2). Compile-and-run still needs
the box with hipcc.
Refs #53
`ssh host 'cmd | tail -f log | sed -n /ready/q'` reported success on jobs that
never finished, three structural ways: the broker aborts the long connection, a
readiness pattern can match a log header printed before the work starts, and
`sed -n /x/q` exits the instant it matches — tearing down the very command it
was watching. The replacement runs the job detached on the host and writes
NAME.out, NAME.rc, then NAME.done LAST, so status is a cheap read of files over
a short-lived connection. That survives the connection, the restart, and the job
that restarts the endpoint the agent itself is running on.
Black-box tests pin the sentinel ordering (rc before done), that a finished-but-
failed job is DONE rc=N with the report still exiting 0, and that a name with no
state is never reported DONE.
Refs #53
install.sh installs desired − installed from packages/*.pkgs, which guarantees a fresh box reaches
the declared state — and nothing anywhere checked that the declared state still describes the
machines. Measured against the Calamares install day, blankpad carries 41 hand-installed packages
and packages/blankpad.pkgs declared one line.
pkg-inventory.sh walks the other direction: installed − declared, with the pacman log as the
baseline so installer furniture is never reported as a hand install. It splits additions by
whether a sync db can serve them (Chaotic-AUR counts, so only true AUR routes to aur.pkgs),
excludes orphans from --emit, reports flatpak apps without pretending install.sh installs them,
and exits 3 rather than guessing when the sync databases are unreadable.
16 black-box tests over fixtures and a stubbed pacman. Two of them exist because the first
implementation was wrong in ways a small fixture cannot show: sort|head under pipefail dies of
SIGPIPE on a real 1000-line install-day log (awk-exit closes the pipe exactly as head does), and
tr without a trailing echo glues the next report label onto the last folded name.
Docs: omp-docs/tools/packages.md now owns the package tier — install.sh was never documented.
Refs #51
Two statements in the law added by #48 need measurement rather than inference, and one of them was
wrong.
**Attributed.** `niri` and `noctalia` both started `Mon Oct 12 16:22:31 2026` (`ps -o lstart=`);
swayidle's last state change was `16:22:23`. The compositor session restarted and the watcher died
with it — eight seconds of agreement, which is what `PartOf=graphical-session.target` is for. The
fix that #48 justified by reasoning is now justified by the event itself. The user journal holds no
entries for that window, so the evidence is process ages, not logs.
**Corrected.** #48 said closing the lid suspended blankpad unlocked, because `before-sleep` lived in
the dead unit. `systemd-inhibit --list` says otherwise: `noctalia … sleep "Lock before sleep" delay`
— the shell holds its own sleep inhibitor and locks on suspend independently of swayidle, so the
suspend path held all along. What was dead for eight days is the **idle** lock and `after-resume`.
The method error is worth keeping: `pgrep` for lockers came back empty and I read that as "no lock
path", when a locker can be a hook inside a running shell. The inhibitor table is the instrument.
Added with them, because it answers a question people will ask: while an `omp` session runs, swayidle
logs `Not enabling timeouts: idle inhibitor found` and arms no idle timeout (`omp … idle "omp agent
session" block`). A box with an agent working does not lock on inactivity, by harness design.
Docs only; the unit file from #48 is unchanged and the live watcher is `active` with `NRestarts=1`
after the SIGTERM proof. `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0 issues, `bats tests/`
71/71. Refs #47.
`swayidle-lock.service` was `enabled`, in `default.target.wants/`, and **dead for eight days**:
Active: inactive (dead) since Mon 2026-10-12 16:22:23 PDT; 1 week 1 day ago
Duration: 2d 21h 57min Main PID: 3181 (code=exited, status=0/SUCCESS)
Result=success NRestarts=0
`Restart=on-failure` cannot see a clean exit, and `swayidle -w` exits 0 on SIGTERM. So the
watcher that owns both the 5-minute idle lock and the `before-sleep` lock stopped existing, and
nothing on the box noticed. The consequence is not cosmetic: with `HandleLidSwitch=suspend` and
`HandleLidSwitchExternalPower=""` (which falls back to it), **closing the lid suspended the
machine unlocked**, because the hook that locks on sleep lived inside the unit that had died. No
alternative was running: `pgrep -c -u blankpad 'swayidle|swaylock|hyprlock|gtklock'` = 0, and
neither `autostart.kdl` launches a locker.
Three settings, each with a job:
- `Restart=always` — a supervisor must not assume it can only die badly.
- `StartLimitIntervalSec=0` — with `always`, a fast loop must never wedge the unit into `failed`;
a lock that keeps trying beats a lock that stopped trying.
- `PartOf=graphical-session.target` — it is wanted by `default.target`, so a session restart
brought the target back without it; now the watcher is restarted with the session.
What triggered the 16:22 exit is **unattributed**: the user journal has no entries in that window,
so all that can be said is that swayidle exited with status 0 — precisely what the old policy was
blind to. The fix does not depend on knowing the trigger, which is the point.
Verification is behavioural, per the law recorded with it: start the unit, `kill -TERM` the main
PID, and watch `NRestarts` increment with a new `MainPID` — the October event, survived. Not
performed here: starting it arms a 5-minute idle lock on a machine that has gone a week without
one, and that is Frank's call. `systemd-analyze --user verify` clean, `bats tests/` 71/71,
`kb-lint.sh omp-docs` 0, `kb-coverage.py` 0.
Refs #47
The first real `deploy.sh` run on blankpad printed:
SKIP live-sync.service (ExecStart '%h/.omp/scripts/live-sync.sh' absent on this host)
while `/home/blank/.omp/scripts/live-sync.sh` sits right there. `%h` is expanded by systemd,
never by the shell, so the literal string was handed to `command -v`, failed, and the unit was
declared to belong to another machine — through the same code path whose whole purpose is to
keep a *foreign* unit inert, and silently. The victim was this repo's own heartbeat, and the
verdict was the same on every box that ever deployed it, because `%h` is exactly where the
checkout lives.
Nothing broke tonight only by luck of install state: `live-sync.service` is `linked`, driven
by its timer, so the heartbeat kept running while the deploy lied about it. A unit whose only
install route was this loop would simply never have been enabled anywhere.
`%h`, `%u` and `%U` are now expanded before resolution; an unrecognised specifier is left
alone so it fails loudly rather than being guessed at. Pinned by a test that writes a
`%h/.local/bin/helper` unit into the scratch home — red before (`SKIP`), green after
(`already linked; not started`), which also keeps the #45 rule intact: resolving a unit is not
licence to start it. Refs #44.
Verification: `bats tests/` 71/71, `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0
issues, `bash -n` clean.
`sync` on a box with a live session on it was unsafe in two independent ways, found by
preflighting the run rather than performing it. Refs #44.
**`enable --now` would have armed a screen lock mid-session.** The unit loop started every
`systemd-user/` unit whose `ExecStart` resolves. `swayidle-lock.service` on blankpad is
`enabled`/`inactive` — nothing started it, no swayidle process exists, `autostart.kdl` does
not launch one — so one `deploy.sh` would have begun blanking and locking the monitors while
the operator was mid-sentence. `live-sync.service` is `linked`, driven by its timer, and
`--now` forces a heartbeat commit at an arbitrary moment. A unit is now started only when
systemd reports it as nothing-yet (`""`/`disabled`), which is precisely a fresh box; anything
already installed is merely `enable`d. The state has to come from the **printed state**:
`is-enabled` exits 1 for `linked` exactly as for `disabled`, so exit status cannot
distinguish "installed, timer-driven" from "never installed" — the first version of this fix
got that wrong and the test caught it.
**`--check` output was being read as more than it can mean.** It prints `RELINK` for every
path the `zz` tier carries, even when the live link already points at the override
(`rules.kdl`, `keybinds.kdl`, `misc.kdl`, `autostart.kdl`, `.zshrc` all did on blankpad),
because the walk visits the base package first and reports the intermediate owner change.
The sentence committed in #43 — RELINK noise is "the sound of a stale override waiting" — is
too strong and is corrected: the line proves a pending change only when `readlink` shows the
live target is not already the `zz-$HOST` file, and `actions=0` is the honest summary. The
stale-override hazard #43 removed was and is real; the signal for it is the target, not the line.
**The dconf dump carried six of its 24 keys from another box**, none of them preferences:
a Firefox file-chooser `last-folder-path` of `/home/dank/Downloads`, a nautilus
`window-state` section, and `picture-uri`/`picture-uri-dark` pointing at
`file:///home/dank/.config/background`. The last five showed no conflict because blankpad's
live tree *already* held those `/home/dank` values — an earlier deploy wrote them here — and
a foreign value that agrees with live is invisible to any diff while still being wrong. They
are dropped; `picture-options='zoom'` stays, because it is a preference. The live wallpaper
URIs are cleared on the box as part of landing this (niri draws the wallpaper, noctalia owns
it, nothing reads a dead path).
`tests/deploy.bats` grows by four, over a stub `systemctl` — the real one on this box is
exactly what must not be invoked from a test. Red before: an already-`enabled` unit and a
`linked` unit both get `--now`. Green after, including the two directions that keep the
change honest: a `disabled` unit is still `enable --now`'d so a fresh box comes up, and a
unit whose `ExecStart` is absent here is never touched at all.
Verification: `bats tests/` 70/70, `kb-lint.sh omp-docs` 0, `kb-coverage.py` 0. The dump now
reconciles against live state with **0 conflicts and no `/home/` values in 18 keys**, proven
by key-by-key comparison rather than `diff` — `dconf load` writes only the keys present, so a
diff overstates the damage one way and hides it the other.
Three findings from verifying the #41 merge against the live box with
`deploy.sh --check`. One commit because they share the deploy tier, its
documentation, and the first test file that tier ever had.
**1. Generated palette output was tracked, and sync would have restored it over a
live theme.** `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl` (`b3d3df4`,
2026-10-18) sat byte-identical to the live file — harmless while the palette held, and
`--check` was already proposing `ADOPT … noctalia.kdl`. The moment the theme changed, the
next sync would have quarantined the live file and reinstated 2026-10-18's colours.
`deploy.sh` now refuses the class outright (`SKIP … (generated theme output)`), `.gitignore`
carries the matching patterns so it cannot be re-added, and the file is untracked.
**2. A `zz` override older than the file it overrides.** `dotfiles/zz-blankpad/niri/…/
layout.kdl` held focus ring `width 2` / `#e01b24` (`7b7dbf5`, 2026-10-17) while base
absorbed `b3d3df4` (2026-10-18): `width 4`, `#ff8c00`. Stow order makes `zz-*` win, and a
cross-package owner change relinks *silently* — no quarantine, no warning. Live was linked
to base, the newer intent, so `--check` printed `RELINK … layout.kdl` every run and one
unattended `deploy.sh` would have reverted the amber focus ring. The duplicate is deleted;
base owns the file.
**3. Two topologies I had asserted and not measured.** The media law and the `.gitignore`
comment both said `~/.local/share/noctalia/wallpapers` symlinks into the repo, so deleting
the tracked copies would blank the desktop. Measured: it is a real directory of real files
that Noctalia owns; the repo held a duplicate, and the #41 merge deleted that duplicate
without the desktop flickering. The correct general law replaces the false one — untracking
a path deletes the file in every other checkout, `git rm --cached` on the branch
notwithstanding — and the restore procedure now points at the app's directory, which
`deploy.sh` does not manage.
The guard is `tests/deploy.bats`, black-box over exit status, stdout, the symlink shape
under `$HOME`, and the quarantine directory. Before the change: test 1 and test 2 fail,
test 2 because the live palette really is quarantined and reverted. After: 4/4, including
the control that an ordinary identical config is still adopted — an exclusion nobody can
turn off is just a broken tool.
Verification: `bats tests/` 66/66, `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0
issues, and live `deploy.sh --check` on blankpad reports `SKIP … (generated theme output)`
with `actions=0` instead of `ADOPT … noctalia.kdl`. The real `deploy.sh` run that clears the
remaining `RELINK` lines is deliberately left until this is in main — running it from a
worktree would repoint live symlinks into a checkout the heartbeat reaps.
Refs #42
The repo still tracked `dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded`
— a retired output config committed 2026-10-09, five days before `.gitignore` grew the
`.AGENTTRASH/` rule. Tracked-then-ignored: an ignore rule never reaches an already-tracked
path, so `git rm --cached` is the only repair. The working copy joins the rest of today's
tracked debt.
Found while answering "does this repo still track my noctalia and niri settings", which
also falsified two rows of the census committed yesterday: niri is 9 tracked files, not
12; noctalia is 3 authored files against 2672 files of app state and downloaded catalog,
not 2/0. The page's own classifier command reproduces the new numbers — it never
reproduced the old ones, which is the only reason they were wrong and not merely
different.
Two laws recorded alongside, both from the same probe: `config.kdl`'s
`include "noctalia.kdl"` is not `optional=true`, so a fresh box cannot start niri until
the theme pipeline has run once; and `cfg/display.kdl -> desk-1-home.kdl` is desk state a
fresh box does not receive — by design, but nowhere written down.
Refs #40
`inventory/home-config-census.md` was dated 2026-08-28, tagged `unverified`,
and every path in it was `/home/dank` — it described another box, yet
`inventory/index.md` still advertised it as the input for the pruning session,
so the pruning session would have pruned against fiction.
Rebuilt from this box, purpose stated in line one. Classification is per FILE
because the tracked unit is a symlink into `dotfiles/`: a directory can look
"real" while every file inside it is tracked (`niri` 12/1, `gtk-3.0` 3/0), and
a directory can be one whole symlink (`lazyvim`). The classifier command is in
the page so tomorrow's agent reruns it instead of trusting the table.
The generated/authored split is the load-bearing part: 24 `noctalia*` palette
files across 20 apps are one generator pass, proven by the
`2026-10-14 19:05:40.330…40.813` same-second cluster and later single re-stamps
(`bat` 10-18, `niri` 10-18, `mango` 10-17). A same-second mass write is
generation, never authorship — so the source palette stays tracked and the
expansion never is. Adoption candidates are limited to programs that pass
`command -v`, with authorship read from the file; the prune list extends the
expected 14 with `pywalfox`, `qt5ct`, `qt6ct`, `turborepo`, and notes that six
of the orphan dirs contain nothing but generated palette.
`fib-watchdog.{service,timer}` is recorded as owned by the bench repo, not
adopted here: its `ExecStart` and `WorkingDirectory` are that project's
internals. It is flagged as a reinstall risk instead, which is the honest
status. Deleted: the `/home/dank` rows, `~/.pi`, Steam library sizes, and the
old offender/summary tables — numbers for software that is not installed here
fail the purpose test.
Refs #40
Three things were in history that should not have been, all invisible to the
tool that deploys them:
- `home/dank/.omp/dotfiles/**` — 3 files re-hosting `niri/cfg/autostart.kdl`,
`niri/cfg/keybinds.kdl` and `systemd-user/…/swayidle-lock.service` under a
prefix `scripts/deploy.sh` never reads (it walks `dotfiles/<pkg>/`). The live
package is deployed and symlinked, so the copy asserted nothing; it only
misled. Moved to `.AGENTTRASH/20261020-tracked-debt/`.
- `.pre-pull-backup/**` — 2 managed-skill files, a local backup in history,
which `.gitignore` itself declares must stay on disk only. Same destination.
- `dotfiles/noctalia/.local/share/noctalia/wallpapers/` — 23 images, 80.4 MiB.
Untracked, files left in place: `~/.local/share/noctalia/wallpapers` symlinks
into this directory and the theme reads it, so deleting them would have gone
black. Every filename carries its wallhaven ID, so a fresh box restores the
set with one `curl` per file plus `deploy.sh`; losing the set is an acceptable
outcome, 80 MiB per clone forever is not.
New page `omp-docs/tools/dotfiles-deploy.md` owns the deploy contract (the
`--check`/sync/`--adopt` modes, `zz-$(hostname)` override semantics, the dconf
and systemd-user special cases) because nothing documented it, plus two laws:
media the pipeline consumes is content not config, and `systemctl --user
enable` snapshots an absolute path — so enabling from a worktree means the
seven-day reap silently kills the unit while `is-enabled` keeps answering
`enabled`. That second law is not theoretical:
`default.target.wants/blankpad-desk-autoswitch.service` pointed into the reaped
`wt/de-redesign-20261009` and desk auto-switching had been off since. It is
relinked to `dotfiles/zz-blankpad/` and left inactive rather than started mid-
session; starting it applies a desk profile.
Refs #40
The Matt Pocock engineering skills (triage, to-tickets, to-spec, research,
implement) assume per-repo config for three things: where work items live, what
the triage label vocabulary is, and where domain docs sit. Nothing here declared
any of them, so every skill run re-guessed.
Frank's decisions, 2026-10-20: Gitea on Dankclaw as the tracker (no gh, no tea on
any box, so it is recorded as prose — auth via a 600 curl config, the endpoint
table, the 404-on-empty-token / form-encoded-merge / empty-200-on-merge traps,
and the AGENTS.md laws a skill must not renegotiate); the canonical triage label
vocabulary kept as-is, with the five labels created on the tracker (the repo had
zero, so nothing collides); single-context domain docs.
The configuration moved out of the skill's default docs/agents/ and into the one
knowledge base this repo allows: three pages under omp-docs/skills/, an empty
omp-docs/decisions/ carrying the ADR format rule, an "## Agent skills" pointer
block in AGENTS.md, index entries, and a log entry. The glossary is deliberately
not created — every candidate term already has an owner in system/, core/ or
skills/, and a term list that paraphrases them is a second owner of facts already
written down. Known cost: re-running setup-matt-pocock-skills will not recognise
omp-docs/skills/* and will offer to recreate docs/agents/; the AGENTS.md pointer
is what stops that.
Also closes a verification gap found on the way: kb-lint.bats only ever points
the linter at fixtures, so a page with no frontmatter or an index that stops
listing a file passes the suite silently. Two tests now run the linter and the
coverage checker against the shipped omp-docs/ tree.
Refs #18
The Limits bullet still described the pre-fix behaviour ("thinking and the
final message only"), which dank/dankvoice#3 just retired: narration rides
`turn_end`, so the assistant's prose and thinking are spoken while the agent
works. Records the two rules an operator needs when audio seems missing —
arming is read at startup only, and a turn landing during playback is dropped
rather than queued — plus where the design rationale lives.
kb-lint 0 violations, index-coverage 0 issues.
Neither setting did what it looked like it did, and the knowledge base had no
page for either, so both were invisible until the operator asked.
tools/neovim.md: the two entry points (vanilla `nvim`, `lazyvim` as an
NVIM_APPNAME profile), the fresh-box install, the base16 wallpaper-palette hook
with its SIGUSR1 contract, and the trap that matters — the official recipe
clones over ~/.config/nvim and destroys the vanilla quick-edit path.
tools/applications.md gets the editor row (Ghostty's row had lost its label)
and Neovim's verified version.
core/dankvoice.md gains the arming rule: arming is session-scoped, a silent
session start is a config state, and the two checks — `/say status` then
`pgrep -a pw-play` — prove the chain without blaming the endpoint.
kb-lint 0 violations, index-coverage 0 issues.
The extension's full behaviour now lives in dank/dankvoice (41/41
black-box suite against the built artifact; this branch's 26 drop-in
tests are a strict subset, names ported 1:1). Delete the drop-in and
its bats suite; repoint omp-docs/core/dankvoice.md at the package,
keeping only fleet wiring (frank-tts + @tiny resolution, the
enable_thinking hybrid law, voice/roster contract) and the cutover
procedure. kb-lint 0 violations; bats tests/ 45/45.
Refs #34
Refs #32
The humanizer's raw fetch to frank-zealot omitted
chat_template_kwargs.enable_thinking: false, so the Qwen35 hybrid thought
instead of answering (6.9 s reasoning_content, empty content - timing out
the 6 s abort; or answering "" and muting the unit). Pin the kwarg, treat
empty-content-with-reasoning as failure over silence, and stop the menu's
test phrase from blocking its handler. Three red-captured regressions;
26/26 green, verified red against HEAD.
PR #30 merged the throwaway checker under its .tmp- name. Promoted to
scripts/kb-coverage.py and documented on okf-maintenance.md § Automated
Gates alongside kb-lint (the bundle previously documented neither gate).
Self-tested: kb-coverage.py reports 0 issues on the current bundle.
Refs #29
Changelog entries were essay-length incident writeups duplicating the
concept pages they created. New entry law recorded at top of log.md:
one dated entry = what changed + durable lesson + concept-page link;
full narratives recoverable via git log --follow. Every date, headline,
incident, and durable lesson preserved; narrative prose that already
lives on concept pages (caddy handle-ordering, gluetun/limiter field
notes, quadlet gotchas, faillock law, credential-store trap) replaced
by pointers. Historical pi-era entries tagged '(pi era)'.
Also: self-hosted-web-agents.md '44.5% peer-reviewed CUA' figure could
not be re-attributed to any published benchmark via web search today
— dropped with an explicit note; the valid core claim (87.4% = managed
Cloud, not the self-hostable library) kept.
Refs #29
- open-models.md: Qwen2.5-VL 'best open vision model' superseded by
Qwen3-VL (Apache 2.0 dense sizes; repo QwenLM/Qwen3-VL, arXiv
2511.21631, verified via web search); decision table + mermaid +
citations updated
- chezmoi-evaluation.md: fleet facts were fabricated — servers run
Ubuntu 26.04 LTS (probed via ssh, both boxes), not 'Arch/FreeToken'
(distro does not exist); dev workstation is AMD Ryzen 5 PRO 5650U,
not Intel Ultra 7; frank-default-* endpoints retired 2026-10-18
- agentic-web-research/index.md: pi-era topology blurb replaced with
current fleet endpoints
- self-hosted-web-agents.md: sample model frank-default-small ->
frank-archon (endpoint/IP already correct: dankserver)
- mcp.md reference-server table verified against upstream
modelcontextprotocol/servers README (exact match, no change)
- 'peer-reviewed BrowseComp' claim from scout ledger: does not exist
in any file — rejected, not applied
Refs #29
Two directories documented one concept (pi npm packages): top-level
packages/ (browser-use, permission-system) and pi/packages/ (peon,
web-access, subagents). Merged into pi/packages/ — one row per package
in a single index, detail pages kept as archive records. The root
index's 'OMP packages and skills' section was doubly wrong (packages/
was pi, not OMP); it becomes a Skills tooling section pointing at the
consolidated /skills/ tree.
Moved the two LIVE omp pages out of the retired-harness banner:
pi/statusline.md -> core/statusline.md, pi/subagent-limits.md ->
core/subagent-limits.md (both document omp behavior against
agent/config.yml + the installed bundle; pi/index.md itself admitted
the mis-shelf). Retargeted every inbound link (core/index, system/
dankserver, log x2, pi/index, settings x2, moved files' own xrefs).
Refs #29
Night fact-check falsified the tree's subject: no `ornith` CLI exists
(npm 404), github.com/rhitching/ornith is 404, and the model IDs the
tree cites are wrong — the real family is ornith-ai/Ornith-1.0/1.5
(9B, 35B-A3B, 397B), built on Qwen3.5/Gemma4 per its model card, not
the 'Ornith-9B-Thinking-1.5T based on Qwen3-8B' the docs assert.
Nothing in the tree describes this fleet: no ornith binary on PATH
(audit 2026-10-18 said the same). Real facts it echoed (PrismML fork
on frank-zealot) already live in /system/.
Also: tools/index.md de-duplicated (same files were listed twice, once
as wikilinks once as links, live-sync missorted) and links normalized;
llama-cpp setup/index xrefs retargeted.
Refs #29
Three parallel trees documented the same ecosystem: omp-docs/skills/
(two overview pages), tools/npx-skills/ (10 pi-era npm pages), and
tools/vercel-labs-skills/ (11 CLI pages), plus a fourth leaderboard page
at engineering/skills-ecosystem.md. Verbatim overlap (identical SKILL.md
field tables in skills-spec.md vs skill-format.md; 13-agent vs 68-agent
copies of the same upstream table). Consolidated to four concept pages:
- skills/agent-skills.md: standard, format, loading, authoring (merges
skills-spec, skill-format, skill-file-structure, skill-loading,
best-practices, skills-vs-extensions)
- skills/skills-cli.md: CLI reference (merges commands, source-formats,
installation-scope, skill-discovery, lock-file, env-variables,
agent-support, related-links)
- skills/store-wiring.md: moved, fleet ops, links retargeted
- skills/ecosystem.md: replaces skills-ecosystem.md; stale 2026-08-12
top-50 table dropped, replaced with a live top-10 re-queried from the
skills.sh API tonight and the API contract re-verified (zero-based,
200/page, {skills,total,hasMore,page}, total 9909)
All facts re-verified 2026-10-19 against primary sources:
agentskills.io, vercel-labs/skills README + src/cli.ts (command set,
--full-depth) + src/skill-lock.ts (v3 lock, XDG path, wipe-on-old),
live skills.sh API. Deleted superseded core/index-doc.md (old pi-era
docs index). Retargeted all inbound links (tools/index, engineering x3,
packages x3, pi/skills, core/index). Pi-era npm skill packaging is
listed as archived under /pi/.
Refs #29
- references/okf-spec.md: replace v0.2 note with the deltas verified
against upstream SPEC.md 2026-10-19 (timestamp->generated.at and
# Citations->sources are the two breaking changes; trust fields and
root-index okf_version are additive; bundle stays v0.1).
- index.md: restore the agentic-web-research section lost in the
2026-08-28 rebuild and link log.md (index-coverage doctrine).
- gen-keybinds.sh emits OKF frontmatter so its generated artifact
(tools/niri/keybindings.md, regenerated, 120 bindings) passes kb-lint;
test added red->green.
- fix the two broken [[niri]] wikilinks (handy, noctalia-shell).
Refs #29
Answers spoken with /say were verbatim markdown-cleaned text while the
built-in vocalizer speaks prose rewritten by a small model. Port that
enhancer (bundle Qyr/T6o: same prompt, 1536 tokens, 6 s, 4000-char
window, null-fallback/empty-skip contract) and resolve its model from
modelRoles.tiny exactly as the harness does, so auto-speak sounds like
the enhanced TUI voice instead of a pager. agent_end now also speaks the
last assistant message's thinking blocks as their own unit before the
answer; narration-flagged thinking counts as answer text. Both knobs are
menu-toggleable, persisted, and env-overridable; the fixture gains a
chat route (echo/prose) and seven scenarios cover the round-trip, the
404 fallback, ordering, the kill switches and role resolution (22 green,
58 suite-wide; live smoke through real frank-zealot + frank-tts).
Refs #27
agent_end awaited one whole-answer synthesis; long answers hit the
harness 30s extension-handler timeout (handler timed out after 30000ms)
and the single frank-tts request was the stall. Port the built-in
speak-aloud vocalizer's utterance segmenter (fHe in dist/cli.js) so
segmentation matches the known-working stock path: [.!?…]-run
boundaries, abbreviation/decimal/inline-code guards, fenced code and
tables swallowed, markup cleaned. Play segment N to player exit before
synthesizing N+1 (audio never stacks); fire-and-forget the chain so
utterances outlive the handler timeout; a run token retires superseded
chains. DANKVOICE_MAX becomes a per-answer budget of whole segments;
stripForSpeech is gone. Refs #23 follow-up.