docs(log): the work log stops re-teaching what the concept pages teach

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
This commit is contained in:
Frank
2026-10-21 02:16:33 -07:00
parent 88d486b37b
commit c6e2a1daca
+244 -364
View File
@@ -17,488 +17,368 @@ lesson, a few lines. Full incident narratives are recoverable from git
on its concept page, linked here. Never re-expand an entry in place.
This is the bundle's only work log; [references/okf-spec.md](references/okf-spec.md) owns the
one-log rule and `scripts/kb-lint.sh` enforces it.
one-log rule and `scripts/kb-lint.sh` enforces it. A law that a concept page already owns is
not restated here — entry, then pointer. That is what the 2026-10-21 audit pass did to the
newest twelve entries: the facts stayed, the duplicates went, `git log --follow` keeps the
long form.
## 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:
Two-axis review (Standards / Spec) of 51 commits, 30 findings, on `fix/60-review-findings`.
Refs #60, merged PR #61. The findings were one 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.
- A guard that loses half its condition when a tool is missing must refuse, not narrow —
`pcie-probe.sh` measured beside a live endpoint while its own LAW said not to
([tools/pcie-probe.md](tools/pcie-probe.md)).
- A deployer that prints `ENABLE` for a unit systemd refuses to enable (`live-sync.service` has
no `[Install]`; its timer arms it) reports a belief, not an outcome, and walking `*.service`
only left the heartbeat unarmed on a rebuilt box
([tools/dotfiles-deploy.md](tools/dotfiles-deploy.md)).
- A gate handed git's quoted path reads an empty diff and passes. Fail closed means `-z`
(`.githooks/pre-commit`, `tests/secret-gate.bats`).
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.
Two laws no other page owns: **a stub that echoes its environment instead of its argument
cannot see the bug** — the probe suite asserted the mask was set and never read what the tool
passed, so per-card bandwidth stayed unobtainable; and **a name is data — split it, never let
the shell expand it** (`remote-job.sh status` globbed its own arguments, so job `x*` was
reported as whatever matched in the caller's directory: [tools/remote-job.md](tools/remote-job.md)).
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.
The heartbeat's three missed ticks were this gate firing on a managed skill that documents the
vendor's all-zeros placeholder id: prose about credentials *is* the machine tier, and a gate
that blocks it gets worked around — the workaround is the hole. Placeholder judgement reads the
matched token and what follows it (a fixed quantifier truncates `EXAMPLE` to `EXA`), never the
whole line. `ALLOW_SECRETS` stays the only override.
Incident, found while verifying the gate: the live-sync heartbeat had failed three hourly ticks,
because a managed skill that documents how to write a secret gate quotes the vendor's all-zeros
placeholder id and the gate's shape rule fired on the documentation. Prose about credentials is
what the machine tier is made of; a gate that blocks it is worked around, and the workaround is
the hole. Placeholder judgement reads the matched token — plus what follows it, because a fixed
quantifier truncates `EXAMPLE` to `EXA` — never the whole line. `ALLOW_SECRETS` stays the only
override, and the heartbeat's own payload was replayed through the fixed hook to prove it lands.
The rest of #60 was claims about the machine that the machine had stopped honouring:
`.gitignore` and `deploy.sh`'s exclusion list had drifted apart (`Noctalia*` was excluded, never
ignored), `.gitattributes` was the stated reason `git-lfs` was installed — for wallpapers that
same week's diff untracked, and the one-log rule was restated in four owners. A coverage claim
names what was measured; a tool justified by dead config is a tool nobody can defend when the
config goes.
The rest of #60 was claims the machine had stopped honouring: `.gitignore` and `deploy.sh`'s
exclusion list drifted apart, `.gitattributes` justified installing `git-lfs` for wallpapers
that same week's diff untracked, and the one-log rule had four owners. A coverage claim names
what was measured; a tool justified by dead config cannot survive the config.
## 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
Refs #62. `agent/models.yml` rewrote `minimal→low`, `high→xhigh`, `max→xhigh` for frank-dragoon
to dodge 400s the engine does not send — flash-server's own error names its allow-list 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` still sends `reasoning_effort: "minimal"` and pays for a canned thinking block. Contract
owner: [pi/models.md](pi/models.md). Law: **a claim about what the harness sends is worth
exactly its last capture** — settle it through a logging proxy, not by reading config. The same
pass restored the three pre-callsign model ids the 2026-10-08 rename had overwritten inside its
own explanatory comments.
## 2026-10-21 — a documented mode the panel never had
The home ultrawide had been running at 60 Hz for weeks while `desk-1-home.kdl`
asked for `3440x1440@239.999`. The page that owns that file documented the same
phantom mode, so every reader — including tonight's inspection — trusted it.
The panel does offer a 240-class mode: `niri msg outputs` lists `3440x1440@239.998`. The defect
was one character — niri matches mode strings verbatim, and `239.999` is not `239.998`.
Laws, now on [tools/niri/displays](tools/niri/displays.md):
- **A mode string must match the advertised string verbatim.** niri does not round:
`3440x1440@239.999` does not select `3440x1440@239.998`. It logs
`configured mode … could not be found, falling back to preferred` and drives the EDID
preferred mode — 60 Hz here. A one-character typo therefore looks like a dying monitor, and
every connector drop re-enacts it.
- **Three layers, three signatures:** config fallback, DP link drops, and
amdgpu `DMCUB` errors with `EINVAL` page flips after s2idle. Only the first
yields to a file edit; only the last yields to a reboot. Triage table and
probes are on the page.
- **A number in a page is a claim, not a measurement.** Mode strings, refresh rates and tool
availability were wrong on several pages at once — `wlr-randr` was listed as installed while
`pacman -Qs wlr` matches nothing. Read the hardware before writing a mode into a runbook, and
destroy the stale copies. A correction is itself a measurement: the first pass at this entry
published a second wrong hardware fact, with a `verified:` date stamped on it.
Fix: `desk-1-home.kdl` 239.999 → 180.000 on the identity block and both legacy
connector blocks, applied live with `niri msg output <panel> mode <mode>` — the
temporary per-output path, so no config reload and no logged-out session.
The home ultrawide ran at 60 Hz for weeks because `desk-1-home.kdl` asked for
`3440x1440@239.999` and the panel advertises `@239.998`. One character: niri matches mode
strings verbatim, does not round, and a mode it cannot find does not fail — it falls back to the
EDID preferred mode, 60 Hz, on every connector drop. The page that owns the file documented the
same phantom mode, so every reader trusted it. Laws, triage table and probes:
[tools/niri/displays.md](tools/niri/displays.md). Durable: **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` listed as installed while `pacman -Qs wlr` matches nothing). Read the
hardware, then destroy the stale copies. A correction is itself a measurement: this entry's
first pass published a second wrong hardware fact, with a `verified:` date stamped on it.
## 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.
The bundle had two: this page and `core/log.md` — two entries of pure migration bookkeeping and
a conversion date that neither this page nor git agreed with (2026-07-15 there, 2026-01-15 here,
`git log` dates the OKF rebuild 2026-08-28); `core/index.md` still pointed readers at it, which
is how a stale page stays alive. Trashed to `omp-docs/core/.AGENTTRASH/`. The rule lives in
[references/okf-spec.md](references/okf-spec.md) and
[references/okf-maintenance.md](references/okf-maintenance.md) and is gated by
`scripts/kb-lint.sh`; the agent-side duty is `agent/APPEND_SYSTEM.md` § 1.
## 2026-10-20 — two instruments rescued from `$HOME` script sediment
`$HOME` had accumulated 123 loose scripts at its root (archived to
`.AGENTTRASH/home-scripts-2026-10-20`, ~1.8 GiB of stray audio consolidated into
`~/audiosamples`). Two were durable instruments and now live in the repo with
black-box tests and pages: [tools/remote-job](tools/remote-job.md) and
[tools/pcie-probe](tools/pcie-probe.md). Refs #53. Laws learned:
- **A readiness pattern in a log is not evidence of completion.** `ssh host 'cmd |
tail -f log | sed -n /ready/q'` fails three independent ways — the broker aborts
the long connection, the pattern can match a header printed before the work
starts, and `sed -n /x/q` exits the moment it matches, tearing down the job it was
watching. The only honest signal is a sentinel the job writes last, read over a
short-lived connection.
- **Never benchmark beside a live service, and never let a tool decide to stop one.**
The guard refuses before anything is built; unloading a production endpoint is a
human's call. A number taken as noise beside serving traffic is both disruptive and
wrong.
- **An instrument that segfaults is not an instrument.** Two of the three shipped
`.hip` probes called `hipGetDeviceProperties`, which crashes on this ROCm build
while `hipGetDeviceCount` works fine; device identity now comes from sysfs, and the
trap is recorded in the sources so it is not reintroduced.
Refs #53. `$HOME` had accumulated 123 loose scripts (archived to
`.AGENTTRASH/home-scripts-2026-10-20`; ~1.8 GiB of stray audio consolidated into `~/audiosamples`).
Two were durable instruments and now live in the repo with black-box tests and pages:
[tools/remote-job](tools/remote-job.md) — a readiness pattern in a log is not evidence of
completion, the pattern can match a header printed before the work starts, and `sed -n /x/q`
exits the moment it matches, tearing down the job it was watching — and
[tools/pcie-probe](tools/pcie-probe.md) — never benchmark beside a live service, never let a
tool decide to stop one, and an instrument that segfaults is not an instrument.
## 2026-10-20 — the secret gate was reading the wrong files, and one credential had already escaped
Found while deciding what else belongs in `dotfiles/`. (1) The gate whitelisted
extensions (`*.md *.yml *.json *.toml *.sh *.ts *.js`), so a `.env`, `.container`,
`.ini` or extension-less dotfile was never read — exactly the shapes the dotfiles
tier is made of. It now scans the whole staged diff for token shapes and, in
non-prose files, for credential assignments, and it fails closed when `git diff`
itself errors. That last clause earned its place: the rewrite's first attempt
passed `-I` to `git diff`, which is not valid there, got an empty file list, and
reported success on every commit — that is what a blind gate looks like, and it
looks identical to a clean tree. (2) A live credential had already escaped into
tracked content: a Caddy basicauth bcrypt hash, its plaintext password, and the
Pi-hole internal password, in an auto-learned memory file. Redacted — but both
credentials need **rotation**, because redaction does not un-leak history that
every machine has already pulled. (3) `home/dank/.omp/dotfiles/**` and
`.pre-pull-backup/**` were tracked fiction invisible to `deploy.sh`, and 23
wallpaper JPEGs (80 MiB) rode along in every clone. Laws: a gate must fail closed,
since a silently-passing gate is worse than none — it buys the belief of safety;
auto-learned memory is written by a process that has read your terminal, so it is
a leak surface and gets gated like any config; `systemctl --user enable`
snapshots an absolute path, so enabling from a worktree means the seven-day reap
Found while deciding what else belongs in `dotfiles/`. The gate whitelisted extensions, so
`.env`, `.container`, `.ini` and extension-less dotfiles — the shapes the dotfiles tier is made
of — were never read. It now scans the whole staged diff for token shapes and, in non-prose
files, for credential assignments, and fails closed when `git diff` itself errors; the rewrite's
first attempt passed an invalid flag to `git diff`, got an empty file list, and reported success
on every commit — which is what a blind gate looks like, and it looks identical to a clean tree.
Bcrypt hashes and Basic-auth headers joined the shape list (issue #40) because the audit found
a live Caddy basicauth hash, its plaintext password, and the Pi-hole internal password in a
tracked auto-learned memory file. **Redaction does not un-leak history that every machine has
already pulled: both credentials needed rotation.** Auto-learned memory is written by a process
that has read your terminal, so it is a leak surface and gets gated like any config. Also
removed: tracked fiction invisible to `deploy.sh` (`home/dank/.omp/dotfiles/**`,
`.pre-pull-backup/**`) and 23 wallpaper JPEGs (80 MiB) riding in every clone. Mechanism:
`.githooks/pre-commit` and its tests; details in
[dotfiles-deploy](tools/dotfiles-deploy.md) and
[home-config-census](inventory/home-config-census.md). Law no other page owns: **`systemctl
--user enable` snapshots an absolute path** — enabling from a worktree means the seven-day reap
silently kills the unit while `is-enabled` goes on saying `enabled`
(`blankpad-desk-autoswitch.service` was dangling that way). Details:
[dotfiles-deploy](tools/dotfiles-deploy.md),
[home-config-census](inventory/home-config-census.md).
(`blankpad-desk-autoswitch.service` was dangling that way).
## 2026-10-20 — engineering skills configured against this KB
`setup-matt-pocock-skills` ran, and its output moved out of the skill's default
`docs/agents/` into the knowledge base: [issue tracker](skills/issue-tracker.md)
(Gitea on Dankclaw is the tracker — no `gh`, no `tea`, token via a `600` curl
config), [triage labels](skills/triage-labels.md) (the five canonical roles,
created on the repo, which carried zero labels before), [domain
docs](skills/domain-docs.md) (single-context; glossary withheld until a term has
no other owner), plus an empty [decisions](decisions/index.md) with its format
rule. `AGENTS.md` gained an `## Agent skills` pointer block. Law: this repo has
one knowledge base, so per-tool configuration is a concept page in `omp-docs/` —
a second `docs/` tree means a second set of index rules, which is where
documentation starts disagreeing with itself. Known cost: re-running that setup
skill will not recognise `omp-docs/skills/*` and will offer to recreate
`docs/agents/`; the `AGENTS.md` pointer is the thing that stops it.
`setup-matt-pocock-skills` ran and its output moved out of the skill's default `docs/agents/`
into the knowledge base: [issue tracker](skills/issue-tracker.md),
[triage labels](skills/triage-labels.md), [domain docs](skills/domain-docs.md), and an empty
[decisions](decisions/index.md) with its format rule. `AGENTS.md` gained the `## Agent skills`
pointer block. Law: this repo has one knowledge base, so per-tool configuration is a concept
page in `omp-docs/` — a second `docs/` tree means a second set of index rules, which is where
documentation starts disagreeing with itself. Known cost: re-running that setup skill will not
recognise `omp-docs/skills/*` and will offer to recreate `docs/agents/`; the `AGENTS.md` pointer
is what stops it.
## 2026-10-19 — dankvoice auto-speak armed at startup
Sessions spoke only on explicit `/say`: the settings file carried no `auto`
key, and arming is session-scoped by design (`/say on` never persists). Set
`"auto": true` in `~/.omp/cache/dankvoice.json`. Same report, second half:
auto-speak listened only to `agent_end` — a settle that fires once per prompt
and is skipped outright when the harness schedules a continuation — so nothing
was narrated while an agent worked; fixed in dank/dankvoice#3, which narrates
`turn_end` as well. Law: silence at session start is a config state — read
`/say status` for `auto-speak on`, then watch `pgrep -a pw-play` for one player
per segment, before blaming the endpoint. Law: whether the harness delivers an
event to extensions is settled in minutes by a throwaway probe extension that
timestamps `turn_end`/`agent_end` — never inferred from audio timing, which the
humanizer lags by seconds. Details: [dankvoice](core/dankvoice.md).
Nothing spoke because the settings file carried no `auto` key (arming is session-scoped by
design) and narration rode only `agent_end`, a settle that a scheduled continuation skips; fixed
upstream in dank/dankvoice#3. Law: silence at session start is a config state — read `/say
status`, then watch `pgrep -a pw-play` for one player per segment — before blaming the endpoint.
Law: whether the harness delivers an event to extensions is settled in minutes by a throwaway
probe extension that timestamps `turn_end`/`agent_end`, never inferred from audio timing.
Details: [core/dankvoice.md](core/dankvoice.md).
## 2026-10-19 — LazyVim installed as an NVIM_APPNAME profile
`lazyvim` had never been installed here — shell history shows the command typed
twice and dropped; `~/.config/nvim` held only an orphaned `matugen.lua` palette
that nothing loaded. Now: LazyVim starter in `dotfiles/lazyvim/` symlinked to
`~/.config/lazyvim`, a `lazyvim` zsh function, and the palette applied through
`RRethy/base16-nvim` on `ColorScheme`. Law: the distro stays out of
`~/.config/nvim` so bare `nvim` keeps working as a vanilla quick-edit editor.
Details: [neovim](tools/neovim.md).
`lazyvim` had never been installed here: shell history shows the command typed twice and
dropped, and `~/.config/nvim` held only an orphaned `matugen.lua` palette nothing loaded. Now the
starter lives in `dotfiles/lazyvim/` symlinked to `~/.config/lazyvim`, a `lazyvim` zsh function
reaches it, and the wallpaper palette applies through `RRethy/base16-nvim` on `ColorScheme`.
Law: the distribution stays out of `~/.config/nvim` so bare `nvim` keeps working as a vanilla
quick-edit editor. Details: [tools/neovim.md](tools/neovim.md).
## 2026-10-19 — dankvoice cut over to the `dank/dankvoice` package (Refs #34)
Drop-in `agent/extensions/dankvoice.ts` and its 26-test suite retired; the
package carries the behaviour (41/41 black-box suite) and is installed with
`pi install git:`. Law: install and prove `/say status` speaks on a box
*before* pulling the cutover commit — the shared settings file keeps menu
state across both transports, so the swap is stateless once installed.
Drop-in `agent/extensions/dankvoice.ts` and its 26-test suite retired; the package carries the
behaviour (41/41 black-box suite) and installs with `pi install git:`. Law: install and prove
`/say status` speaks on a box *before* pulling the cutover commit — the shared settings file
keeps menu state across both transports, so the swap is stateless once installed.
## 2026-10-19 — dankvoice: silence and timeouts, root-caused (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 of `reasoning_content`, `content: ""` — timing out the rewrite twice
(12 s dead air) and, when it did answer inside the window, speaking silence.
Root-caused by replaying the exact wire body against the live endpoint.
Fix: pin the kwarg in the request body; treat empty-content-with-reasoning as
failure (mechanical voice), not "nothing worth saying"; the menu's test phrase
fires without blocking its handler. Lesson: the harness injects thinking-off
for managed roles, but raw HTTP callers must set it themselves — 26/26 green
with three red-captured regressions.
The humanizer's raw `fetch` omitted `chat_template_kwargs.enable_thinking: false`, so the Qwen35
hybrid *thought* instead of answering: 6.9 s of `reasoning_content`, `content: ""` — dead air
twice, and when it answered inside the window it spoke silence. Root-caused by replaying the
exact wire body against the live endpoint. Fix: pin the kwarg, treat empty-content-with-reasoning
as failure, fire the menu's test phrase without blocking its handler. Lesson: the harness injects
thinking-off for managed roles; raw HTTP callers must set it themselves.
[core/dankvoice.md](core/dankvoice.md).
## 2026-10-19 — dankvoice: humanized narration and thinking aloud (Refs #27)
## 2026-10-19 — dankvoice: humanized narration, segmented speech, `/say` menu (Refs #27, #23, #21)
Spoken units are rewritten by the `@tiny` role (frank-zealot) under the
bundle's speech budgets before synthesis, with mechanical cleanup as fallback;
auto-speak narrates the last message's thinking blocks first; all toggles
persist. 7 new scenarios, 22 green; live smoke through real zealot + frank-tts.
## 2026-10-19 — dankvoice: segmented sequential speech
`agent_end` awaiting one whole-answer synthesis hit the harness's 30 s
extension-handler timeout — lesson: extensions must never await work that can
exceed 30 s. Ported the built-in vocalizer's utterance-splitting state machine
(boundary = `[.!?…]+` + whitespace, abbreviation/code guards, per-answer
segment budget); segments synthesize strictly sequentially. `DANKVOICE_MAX` is
now a segment budget. 17/17 in `tests/dankvoice.bats`.
## 2026-10-18 — dankvoice: `/say` options menu with output-device routing (Refs #23)
`/say menu` (pi-peon-style): auto-speak, voice, PipeWire output device
(`wpctl` → `pw-play --target`), player, char cap, test phrase. Precedence
env > `~/.omp/cache/dankvoice.json` > defaults — born from the incident where
PipeWire's remembered Bluetooth route played answers into nowhere.
## 2026-10-18 — dankvoice: the cloned voices finally speak in the TUI (Refs #21)
The harness gates `--voice` against the bundled Kokoro enum, so frank-tts's
cloned voices were unreachable in-TUI. Added `agent/extensions/dankvoice.ts`
(`/say …`) synthesizing over the OpenAI speech route, endpoint read from the
`api: openai-speech` row of `agent/models.yml` (no host or secret in code).
Spoken units are rewritten by the `@tiny` role under the bundle's speech budgets with mechanical
cleanup as fallback; the utterance-splitting state machine synthesizes segments strictly
sequentially because `agent_end` awaiting one whole-answer synthesis hit the harness's 30 s
extension-handler timeout — **extensions must never await work that can exceed 30 s**. `/say menu`
routes output device (`wpctl` → `pw-play --target`) with precedence env > settings file >
defaults, born from PipeWire's remembered Bluetooth route playing answers into nowhere. The
extension exists at all because the harness gates `--voice` against the bundled Kokoro enum, so
frank-tts's cloned voices were unreachable in-TUI. [core/dankvoice.md](core/dankvoice.md).
## 2026-10-18 — Noctalia bar layout clobbered by trunk-sync stash, recovered
GUI-built bar customizations existed only in the working tree; a trunk-sync
stash-then-drop destroyed them. Recovery: `git fsck --lost-found` surfaced the
dropped stash as a dangling commit; validated in a sandboxed `XDG_*_HOME`
before restore. Lesson: GUI-written config is only durable once committed —
the motivation for the live-sync heartbeat below.
## 2026-10-18 — Fleet law: issue/PR attribution footers
Every agent-opened issue/PR body ends with `cwd:` + `session:` footer, in both
`dank/command-center` and `dank/serverAppConfigs` AGENTS.md: machine-made
artifacts must trace to the box and session that produced them.
GUI-built bar customizations existed only in the working tree; a trunk-sync stash-then-drop
destroyed them. Recovery: `git fsck --lost-found` surfaced the dropped stash as a dangling
commit, validated in a sandboxed `XDG_*_HOME` before restore. Lesson: GUI-written config is
durable only once committed — the motivation for the heartbeat below.
[tools/dotfiles-deploy.md](tools/dotfiles-deploy.md).
## 2026-10-18 — live-sync heartbeat for machine-written paths (Refs #11)
A month of silent divergence in the live checkout proved the checkout-is-
deployment model needs a committer: `scripts/live-sync.sh` + systemd user
timer auto-commits allowlisted tiers (memories, managed-skills, live config,
dotfiles) with hooks armed, merges `origin/main`, pushes, reaps stale `wt/*`.
Non-allowlist dirt never staged; conflicts abort-and-alert. See
[tools/live-sync.md](tools/live-sync.md); tier law in AGENTS.md.
A month of silent divergence in the live checkout proved the checkout-is-deployment model needs
a committer: `scripts/live-sync.sh` + a systemd user timer auto-commits the allowlisted tiers
with hooks armed, merges `origin/main`, pushes, and reaps stale `wt/*`. Non-allowlist dirt is
never staged; conflicts abort and alert. [tools/live-sync.md](tools/live-sync.md); tier law in
`AGENTS.md`. Same day, two fleet laws into both repos' `AGENTS.md`: every agent-opened issue or
PR body ends with a `cwd:` + `session:` footer, so machine-made artifacts trace to the box that
spawned them; and `xdg-open <PR-URL>` the moment a PR opens, so Frank never hunts for it in
Gitea.
## 2026-10-18 — Fleet law: filed PRs open in Frank's browser immediately
## 2026-10-18 — SDLC switched to trunk-based development; extension route closed unmerged
`xdg-open <PR-URL>` the moment a PR opens — codified in both repos' git rules.
## 2026-10-18 — Trunk law: extension route closed unmerged; direct repo alignment
Operator closed PR #9 (cwd-scoped `before_agent_start` extension) in favor of
editing each repo's AGENTS.md. Durable findings: every context-file provider
(AGENTS.md walk-up, `.omp/` dirs, CLAUDE.md) resolves at or below the repo
root — ancestor-scope files can never load; user-level `APPEND_SYSTEM.md`
broadcasts to all sessions; serverAppConfigs never had git-flow.
## 2026-10-18 — SDLC switched to trunk-based development
`develop` retired (single-dev repo). Law in AGENTS.md § Trunk-based SDLC:
branch off freshest `origin/main`, PRs target `main`, `--no-ff` merges, tags
`vX.Y.Z` on main. Managed skill `gitflow-back-merge-plumbing` is historical.
`develop` retired (single-dev repo): branch off freshest `origin/main`, PRs target `main`,
`--no-ff` merges, tags `vX.Y.Z` on main — law in `AGENTS.md` § Trunk-based SDLC, managed skill
`gitflow-back-merge-plumbing` marked historical. The operator also closed PR #9 (cwd-scoped
`before_agent_start` extension) in favour of editing each repo's `AGENTS.md`. Durable finding:
every context-file provider (AGENTS.md walk-up, `.omp/` dirs, CLAUDE.md) resolves at or below
the repo root, so ancestor-scope files can never load; user-level `APPEND_SYSTEM.md` broadcasts
to all sessions.
## 2026-10-18 — Sibling-vault overlap census; system docs realigned to live ground truth
Census of `omp-docs/` vs `serverAppConfigs-docs` (subagents + live-ssh probes)
found four contradicting pairs; ours fixed, theirs flagged for their PR flow.
Recorded then: RTX 3090 moved Dankserver→Dankclaw 2026-09; dankserver = 2×
Radeon AI PRO R9700 (the "board swap" story was false); served models per
live units. Current state always: [/system/index.md](/system/index.md)
(27B-era zealot figures here are historical; live census 2026-10-19 below).
Census of `omp-docs/` vs `serverAppConfigs-docs` (subagents + live-ssh probes) found four
contradicting pairs; ours fixed, theirs flagged for their PR flow. Recorded then: the RTX 3090
moved Dankserver→Dankclaw 2026-09; dankserver is 2× Radeon AI PRO R9700 (the "board swap" story
was false); served models per live units. Current state always:
[/system/index.md](/system/index.md).
## 2026-10-18 — Status line rebuilt on custom segments; full-source audit
New page [core/statusline.md](core/statusline.md) with an audit log — four
first-pass claims were false and corrected against the minified bundle + a
live screenshot matrix (context meter is `box`/`band` only; `composer.shape`
routes segment arrays; `mode` states = Plan/Prewalk/Goal/Vibe/Loop). Bar
rebuilt on `band` + custom segments; frank-archon display name slimmed in
models.yml.
[core/statusline.md](core/statusline.md) carries the audit log — four first-pass claims were
false and corrected against the minified bundle plus a live screenshot matrix (context meter is
`box`/`band` only; `composer.shape` routes segment arrays; `mode` states =
Plan/Prewalk/Goal/Vibe/Loop). Bar rebuilt on `band` + custom segments.
## 2026-10-09 — Blankpad DE redesign: desk profiles, keybinds, Handy wiring
Per-desk niri profiles keyed by EDID long name (connector names reshuffle
between docks; identity travels) + `scripts/desk.sh`; niri 26.04 has **no
hotplug event** (`OutputConfigChanged` is only an ACK per niri-ipc) — the
autoswitch daemon diffs output sets on `WorkspacesChanged`. Keybind cheat
sheet now generated (`scripts/gen-keybinds.sh`); noctalia docs corrected to
the v5 `noctalia msg` API. Handy: niri owns autostart; open breaker —
frank-secondary answers in `reasoning_content` with empty `content`, and
Handy cannot send `chat_template_kwargs.enable_thinking=false`. 27/27 green.
Per-desk niri profiles keyed by EDID long name (connector names reshuffle between docks;
identity travels) + `scripts/desk.sh`. niri 26.04 has **no hotplug event** (`OutputConfigChanged`
is only an ACK per niri-ipc), so the autoswitch daemon diffs output sets on `WorkspacesChanged`.
Keybind cheat sheet generated by `scripts/gen-keybinds.sh`; noctalia docs corrected to the v5
`noctalia msg` API. Handy: niri owns autostart; open breaker — frank-secondary answers in
`reasoning_content` with empty `content`, and Handy cannot send
`chat_template_kwargs.enable_thinking=false`. [tools/niri/displays.md](tools/niri/displays.md),
[tools/handy.md](tools/handy.md).
## 2026-10-09 — Incident: agent sudo-loop tripped pam_faillock, locked out the operator
A subagent retried `sudo yay` (no TTY) for ~15 min; `pam_faillock` deny=3
locked user `blank` — correct-password sudo failed, mimicking a password
change. Recovery: `faillock --user blank --reset`. Law: agents never run
`sudo`/`yay`/`pacman`/`pkexec`; root steps go to the operator as exact
commands. Skill `faillock-sudo-lockout-recovery`.
A subagent retried `sudo yay` (no TTY) for ~15 min; `pam_faillock` deny=3 locked user `blank`,
and correct-password sudo then failed, mimicking a password change. Recovery:
`faillock --user blank --reset`. Law: agents never run `sudo`/`yay`/`pacman`/`pkexec`; root steps
go to the operator as exact commands. Skill `faillock-sudo-lockout-recovery`.
## 2026-10-09 — Full secret audit of the command-center repo: clean
Five layers (live-value scan across 77 revs/765 blobs, whole object store
incl. dangling, shape triage, entropy ≥4.3, LFS payloads) — no secret in any
reachable or dangling object; quarantine list holds. Fixed `hf_token_write`
0644→0600. Open gap: the pre-commit gate lacks `hf_`/`npm_`/`pypi-`/`AIza`/
Stripe/Telegram shapes and basic-auth URLs — widening it is an open decision.
Five layers (live-value scan across 77 revs/765 blobs, whole object store including dangling,
shape triage, entropy ≥4.3, LFS payloads) found no secret in any reachable or dangling object;
the quarantine list holds. Fixed `hf_token_write` 0644→0600. Open gap: the gate lacks
`hf_`/`npm_`/`pypi-`/`AIza`/Stripe/Telegram shapes and basic-auth URLs — widening it is an open
decision.
## 2026-10-09 — Gitea token moved out of every remote URL
Remotes bare; credential in `~/.config/secrets/gitea` (0600) behind a global
`credential.helper store`. Trap (git 2.56.0, measured): `git
credential-store` rejects the `protocol=`/`host=` block shape the man page
shows — the store file must be one URL per line, port included. Verified with
`GIT_TERMINAL_PROMPT=0`. Details: [tools/gitea/client-access.md](tools/gitea/client-access.md).
`credential.helper store`. Trap (git 2.56.0, measured): `git credential-store` rejects the
`protocol=`/`host=` block shape the man page shows — the store file must be one URL per line,
port included. Verified with `GIT_TERMINAL_PROMPT=0`.
[tools/gitea/client-access.md](tools/gitea/client-access.md).
## 2026-10-09 — Working tree reconciled: callsign renames, compaction move, gitignore quarantine
200-entry dirty tree committed as seven atomic units. Harness model ids
renamed `frank-{medium,large,small}` → `frank-{archon,dragoon,zealot}` —
**services keep old names on the wire**; the engines accept any request model
id, so renames are harness-side only. Server-side compaction moved to
zealot's 3090 (threshold 70→85 %). `.gitignore` quarantine for worktree-keyed
memory buckets and runtime dirs. Skill renames are not losses: git renders
directory renames as mass delete+add; pre-cutover content intact at `12c5e88`.
200-entry dirty tree committed as seven atomic units. Harness model ids renamed
`frank-{medium,large,small}` → `frank-{archon,dragoon,zealot}` — **services keep old names on the
wire**; the engines accept any request model id, so renames are harness-side only. Server-side
compaction moved to zealot (threshold 70→85 %). `.gitignore` quarantine for worktree-keyed memory
buckets and runtime dirs. Skill renames are not losses: git renders directory renames as mass
delete+add; pre-cutover content intact at `12c5e88`.
## 2026-09-28 — Retired workstation purged from the knowledge base
Sold box: hostname, both LAN addresses, Wi-Fi SSID gone bundle-wide; the
knowledge distilled into [system/network.md](system/network.md) (multi-link
resolver law: a static resolver must be pinned on each *connection*) and
[tools/niri/displays.md](tools/niri/displays.md) (portrait transforms, the
three ways a multi-monitor rig breaks). Hostname stays only where it is a
lookup key (install.sh `--host`, `packages/<hostname>.pkgs`).
Sold box: hostname, both LAN addresses, Wi-Fi SSID gone bundle-wide; the knowledge distilled
into [system/network.md](system/network.md) (multi-link resolver law: a static resolver must be
pinned on each *connection*) and [tools/niri/displays.md](tools/niri/displays.md) (portrait
transforms, the three ways a multi-monitor rig breaks). Hostname stays only where it is a lookup
key (install.sh `--host`, `packages/<hostname>.pkgs`).
## 2026-09-01 — Retry backoff and thinking default (pi era)
Pi `settings.json`: 10 s base retry backoff (10/20/40 s), `defaultThinkingLevel:
medium` (no per-model thinking field exists in `models.json`).
Pi `settings.json`: 10 s base retry backoff (10/20/40 s), `defaultThinkingLevel: medium` (no
per-model thinking field exists in `models.json`).
## 2026-08-28 — KB rebuilt to OKF v0.1; live-verified fleet facts
`omp-docs/` copied from legacy `~/.pi/agent/docs`, rebuilt as git-tracked OKF
v0.1 (checkout *is* the deployment); unverifiable claims tagged `unverified`.
Conformance pass promoted concept-in-disguise indexes, stripped index
frontmatter, resolved all links; `scripts/kb-lint.sh` exits 0. Hardware facts
were corrected *then* (3090-on-dankserver since moved — see 2026-10-18).
`omp-docs/` copied from legacy `~/.pi/agent/docs`, rebuilt as git-tracked OKF v0.1 (checkout *is*
the deployment); unverifiable claims tagged `unverified`. Conformance pass promoted
concept-in-disguise indexes, stripped index frontmatter, resolved all links; `scripts/kb-lint.sh`
exits 0. Hardware facts were corrected *then* (3090-on-dankserver since moved — see 2026-10-18).
Inventory census recorded under `inventory/` for the pruning session.
## 2026-08-23 — Homepage deployed to production; Caddy KB doc
Homepage live on Dankclaw via `scripts/deploy.sh` (status checker timer,
deployed.js, rendered basicauth hashes, health check). Hard-won Caddy lesson:
top-level directives are *sorted*, so `rewrite` runs before `basicauth` and
auth matchers saw rewritten paths — serve each page from its own `handle`
block to preserve order; a `handle` inline matcher is a single token; a
leading `@name <directive>` parses as a matcher definition. Now documented on
[caddy/caddyfile.md](caddy/caddyfile.md). A compose mount change needs a
container *recreate*, not a reload. 116 tests green.
Homepage live on Dankclaw via `scripts/deploy.sh` (status checker timer, `deployed.js`, rendered
basicauth hashes, health check). Hard-won Caddy law, now on
[caddy/caddyfile.md](caddy/caddyfile.md): top-level directives are *sorted*, so `rewrite` runs
before `basicauth` and auth matchers saw rewritten paths — serve each page from its own `handle`
block; a `handle` inline matcher is a single token; a leading `@name <directive>` parses as a
matcher definition. A compose mount change needs a container *recreate*, not a reload.
## 2026-08-23 — Homepage design settled (grilling complete)
All 38 open questions answered; full record lives in serverAppConfigs
(`docs/Infrastructure/Homepage Design Grilling.md`). Key deltas: per-service
tcp/http probes, 5 s timeout, TUI-style panel grid, no client auto-refresh,
git-driven deploy, full TDD. Correction recorded: the Dankserver media stack
runs as bare processes, not containers — never probe it as a compose stack.
(`docs/Infrastructure/Homepage Design Grilling.md`). Key deltas: per-service tcp/http probes, 5 s
timeout, TUI-style panel grid, no client auto-refresh, git-driven deploy, full TDD. Correction
recorded: the Dankserver media stack runs as bare processes, not containers — never probe it as a
compose stack.
## 2026-08-22 — vllmScripts merged into serverAppConfigs
Full-history two-parent import; one repo, one checkout per host, one
`deploy.sh`. The plaintext sudo password from old docs was NOT carried over
(a bats leak-check guards it); it remains in both repos' history on the LAN
Gitea — rotation recommended.
Full-history two-parent import; one repo, one checkout per host, one `deploy.sh`. The plaintext
sudo password from old docs was NOT carried over (a bats leak-check guards it); it remains in
both repos' history on the LAN Gitea — rotation recommended.
## 2026-08-21 — Fleet ops: passwordless sudo, Hermes SSH keys
`/etc/sudoers.d/dank-nopasswd` on both servers (per-command drop-ins remain);
dedicated ed25519 key for the Hermes container, verified into all three
machines.
`/etc/sudoers.d/dank-nopasswd` on both servers (per-command drop-ins remain); dedicated ed25519
key for the Hermes container, verified into all three machines.
## 2026-08-19 — pi-web-access: SearXNG endpoint re-pointed to dankclaw (pi era)
## 2026-08-19 — SearXNG + Gluetun bring-up; pi endpoint re-pointed
`searxngBaseUrl` moved off dead `.235:8081` to `.143:8081`; pi caches
`web-search.json` per process — new endpoint needs a new session. `yt-dlp`
installed (2026.07.04).
## 2026-08-19 — SearXNG + Gluetun bring-up on dankclaw
Durable field notes (now on concept pages — see
`searxngBaseUrl` moved off the dead `.235:8081` to `.143:8081`; pi caches `web-search.json` per
process, so a new endpoint needs a new session. Durable field notes now on concept pages —
[agentic-web-research/vpn-routing.md](agentic-web-research/vpn-routing.md),
[searxng-tuning.md](agentic-web-research/searxng-tuning.md),
[engineering/infra/containers.md](engineering/infra/containers.md)):
manual-WireGuard Proton needs `VPN_SERVICE_PROVIDER=custom` + the single-peer
`WIREGUARD_*` env names (not `protonvpn`, not `WIREGUARD_PEERS_*`); gluetun
ships no curl and `:8000` is the control server, not health; the limiter's
JSON cap (`API_MAX=4`/hour) is hardcoded Python — `pass_ip` is the only
bypass; rootless podman strips client IPs on published ports, so SearXNG must
sit behind Caddy for XFF; quadlet generator rejects `CapAdd=`/`Device=` and
generated units enable via `[Install] WantedBy=default.target`. ComfyUI on
gfx1151: build on AMD's ROCm base and filter the torch family out of
ComfyUI's requirements or you clobber the gfx1151 kernels.
[engineering/infra/containers.md](engineering/infra/containers.md): manual-WireGuard Proton needs
`VPN_SERVICE_PROVIDER=custom` + the single-peer `WIREGUARD_*` env names (not `protonvpn`, not
`WIREGUARD_PEERS_*`); gluetun ships no curl and `:8000` is the control server, not health; the
limiter's JSON cap is hardcoded Python and `pass_ip` is the only bypass; rootless podman strips
client IPs on published ports, so SearXNG must sit behind Caddy for XFF; the quadlet generator
rejects `CapAdd=`/`Device=` and generated units enable via `[Install] WantedBy=default.target`.
ComfyUI on gfx1151: build on AMD's ROCm base and filter the torch family out of ComfyUI's
requirements or you clobber the gfx1151 kernels.
## 2026-08-17 — Fleet redo + vLLM tool calling (pi era)
Fleet moved off the pipeline-parallel + NPU topology; Quark W8A8 quant chosen
over the Avesed INT8 for clean Qwen3 XML tool calls.
Fleet moved off the pipeline-parallel + NPU topology; Quark W8A8 quant chosen over the Avesed
INT8 for clean Qwen3 XML tool calls.
## 2026-08-16 — Gitea boot-persistence fix (dankclaw)
`unattended-upgrades` reboot exposed that the compose-era Gitea had no
systemd units (~1 h down). Stack rewritten as quadlet (names, `systemd-gitea`
network, linger + `WantedBy=default.target`, app.ini env-override gotcha, UID
100999 ownership) across `tools/gitea/*`; `podman generate systemd` marked
deprecated in [engineering/infra/containers.md](engineering/infra/containers.md).
`unattended-upgrades` reboot exposed that the compose-era Gitea had no systemd units (~1 h down).
Stack rewritten as quadlet (names, `systemd-gitea` network, linger + `WantedBy=default.target`,
app.ini env-override gotcha, UID 100999 ownership) across `tools/gitea/*`; `podman generate
systemd` marked deprecated in [engineering/infra/containers.md](engineering/infra/containers.md).
## 2026-08-13 — Pi subagent limits recorded (pi era)
Effective caps: globalConcurrencyLimit 2, parallel.maxTasks 8 / concurrency 4,
maxSubagentDepth 2, turnBudget 30; no per-model limit. See
Effective caps: `globalConcurrencyLimit` 2, `parallel.maxTasks` 8 / concurrency 4,
`maxSubagentDepth` 2, `turnBudget` 30; no per-model limit.
[pi/packages/pi-subagents.md](pi/packages/pi-subagents.md).
## 2026-08-11 — Engineering research area; skills leaderboard verified
## 2026-08-11 — Engineering research area; agentic web research section
Created `engineering/` (SDLC, agent instruction files, stacks, infra,
plugins, skills ecosystem, Signal remote) via parallel research fleet.
Skills leaderboard replaced with the ranked list from the skills.sh API
(zero-based pagination, 200-skill pages).
## 2026-08-11 — Agentic web research section; exhaustive bundle audit
Created `agentic-web-research/` (8 pages) and rewrote them on 2025/2026
evidence (SearXNG valkey deployment, real engine defaults, limiter budgets,
Gluetun pattern, open-model claim triage, MCP transports). Audit: 154+
wikilinks resolved, dead external links fixed, `MemorySleepMode=deep`
verified valid (systemd 256+), ASD-STE100 style pass. `AGENTS.md` created as
the system behavior contract.
Created `engineering/` (SDLC, agent instruction files, stacks, infra, plugins, skills ecosystem,
Signal remote) and `agentic-web-research/` (8 pages), the latter rewritten on 2025/2026 evidence
(SearXNG valkey deployment, real engine defaults, limiter budgets, Gluetun pattern, open-model
claim triage, MCP transports). Skills leaderboard replaced with the ranked list from the skills.sh
API (zero-based pagination, 200-skill pages). Same pass: 154+ wikilinks resolved, dead external
links fixed, `MemorySleepMode=deep` verified valid (systemd 256+), ASD-STE100 style pass,
`AGENTS.md` created as the system behavior contract.
## 2026-08-06 — Link fixes; ComfyUI docs; major documentation overhaul
Fixed 15 broken wikilinks (missing frontmatter) + title mismatches → zero
broken. Added ComfyUI pages, per-host system docs, pi config tree, tools
entries; orphaned root duplicates moved to `.AGENTTRASH/`.
Fixed 15 broken wikilinks (missing frontmatter) + title mismatches → zero broken. Added ComfyUI
pages, per-host system docs, pi config tree, tools entries; orphaned root duplicates moved to
`.AGENTTRASH/`.
## 2026-07-18 — Maintenance tooling
Expanded `references/okf-maintenance.md` (cross-links, health metrics,
troubleshooting); created `core/log.md` and `.okf/config.yaml`.
Expanded `references/okf-maintenance.md` (cross-links, health metrics, troubleshooting); created
`core/log.md` (retired 2026-10-20, above) and `.okf/config.yaml`.
## 2026-01-15 — OKF conversion
Docs bundle converted to OKF v0.1; files migrated into `core/`, `tools/`,
`packages/`, `references/` with frontmatter on all files.
Docs bundle converted to OKF v0.1; files migrated into `core/`, `tools/`, `packages/`,
`references/` with frontmatter on all files.
## 2025-08-05 — Gitea documentation
Added [tools/gitea/index.md](tools/gitea/index.md) (topology, management,
data persistence) to the tools index.
Added [tools/gitea/index.md](tools/gitea/index.md) (topology, management, data persistence) to
the tools index — legacy tree, predating the OKF conversion above.