Files
serverAppConfigs/AGENTS.md
T
Frank 83625e84a7 frank-medium: route by workload — spread batches, pin agent loops
The two replicas behind :7770 were on plain round-robin, which throws away
prefix-cache hits: an agent loop re-sends its system prompt and file context
every turn, and alternating replicas means every other turn pays a cold
prefill. Measured with 90k-token contexts, warm-turn TTFT was 2676 ms.

Pinning everything to one replica is not the answer either — a cold batch
(the shape a benchmark or a burst of parallel subagents produces) then loses a
card's worth of prefill: 16 requests took 30.1 s pinned against 18.6 s spread.

So the policy routes by request shape. A continuation (more than one message —
the agent-loop signature) is pinned to the replica that served its prefix;
anything else spreads across the cards by counter. Measured: 19.97 s for the
burst and 1095 ms warm-turn TTFT — the best of both, without a regression on
either workload.

The policy lives in proxy/routing.py, separate from the HTTP plumbing in
proxy/affinity-proxy.py, so it can be tested without aiohttp, a network or a
GPU. tests/frank_medium_proxy.bats covers it, and it earned its keep: the
first version hashed fresh prefixes to pick a card, which sends every member of
a same-prefix burst to one card (29.8 s, measured), and a stale index in the
pin path returned 500 on every continuation. Both are now assertions.

ROUTING=roundrobin and ROUTING=affinity stay available to re-measure the
choice on this host; the default is adaptive.
2026-09-16 04:45:21 -07:00

442 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md — serverAppConfigs Agent Rules
## Overview
This repository is the single source of truth for the two homelab servers and
the FrankBook-Pro Mac mini. It tracks the Podman container stacks and the LLM
inference fleet. One repo, one checkout per host, one deploy script. All
services run from `/home/dank/serverAppConfigs` on each host (the 2026-08-22
merge absorbed the former `vllmScripts` repo, history preserved).
- **Dankserver** (192.168.0.235) — media/library stack (RomM, Copyparty,
Calibre-Web, Jellyfin, Plex) plus the `frank-medium` LLM service.
- **Dankclaw** (192.168.0.143) — AI/search/dev/observability stack (SearXNG
via Gluetun, ComfyUI, OpenWebUI, Hermes, Gitea, Grafana, Prometheus, Loki,
Alloy, Pi-hole) plus the `frank-large` LLM service. Caddy on Dankclaw
reverse-proxies both hosts.
- **FrankBook-Pro** (192.168.0.112) — macOS mini running the *arr stack
(Prowlarr, Sonarr, Radarr, Lidarr, Readarr, qBittorrent) as quadlets inside
its podman machine; all egress rides the Gluetun WireGuard netns. Deployed
via the `frankbook-pro` handler.
## Fleet topology (ACTIVE)
| Target | Service | Runtime | Model | Port |
|--------|---------|---------|-------|------|
| Dankserver | `frank-medium` | vllm-radiance 0.9.3 (ggz14/radiance-vllm-mxfp4 patches), 2x single-GPU rootless Quadlets behind an adaptive-routing proxy | just1moremodel Qwen3.8-27B-Uncensored-MXFP4-awq + Qwen3.8-27B-DFlash2-FP8 drafter, 2x Radeon AI PRO R9700 (one card per replica, TP=1), 114688 ctx / 4 seqs each | 7770 |
| Dankclaw | `frank-large` | ik_llama.cpp master d85a1d7 (root systemd unit) | mradermacher Qwen3.8-Flash-Next-Uncensored i1-Q4_K_M GGUF, RTX 3090 (dense/attn/KV) + LPDDR (experts/PLE) hybrid, 262144 ctx / 1 slot, vision via mmproj | 7770 |
Retired endpoints (`frank-npu`, `frank-small`, and every prior runtime) are in
the [runbook index](#runbook-index). The full swap history lives in
`serverAppConfigs-docs/LLM/index.md` — do not grow it here.
## Repository Layout
- `dankserver/` — compose.yaml + Caddyfile + .env.example for the media stack
- `dankclaw/` — compose.yaml + Caddyfile + .env.example for the AI stack
- `shared/` — homepage, Prometheus, Loki, Alloy, SearXNG configs
- `scripts/` — `deploy.sh` / `deploy-plan.py` / `deploy-remote.sh`, LLM serve
scripts, systemd units, env tunables, bench harnesses
- `old/` — retired LLM scripts (reference only)
- `serverAppConfigs-docs/` — knowledge base (Obsidian vault, OKF format)
- `tests/` — bats suite + Playwright e2e
## Git Workflow
All edits flow through git. Never edit files on a remote machine directly.
1. **Source** the live config from the target machine
2. **Commit** the fetched version here (redact secrets into `.env`)
3. **Edit** locally
4. **Test** — `./tests/run.sh` must pass
5. **Commit and push** — origin is Gitea on Dankclaw
6. **Pull** on the target (`deploy.sh` does this)
7. **Deploy** — `scripts/deploy.sh` plans and dispatches only changed services
## Deployment
```
./scripts/deploy.sh dankclaw
./scripts/deploy.sh dankserver
./scripts/deploy.sh dankclaw --service searxng
./scripts/deploy.sh dankserver --service frank-medium
./scripts/deploy.sh dankclaw --dry-run
```
The planner maps changed paths to services; the remote runner restarts only
the selected service and records its revision outside the checkout.
Documentation-only changes are a no-op. LLM services are selected only by
their own unit/env/serve files or the shared chat template — unrelated
container changes cannot interrupt inference. Set `FRANK_SUDO_PW` for headless
runs on a host without passwordless sudo; the value never enters the repo.
### Deployment completeness (MANDATORY)
A deploy is **not complete** when `deploy.sh` exits green. It requires:
1. `./tests/run.sh` — all seams green, and
2. the Playwright e2e suite (`tests/e2e/run.sh`) — runs **automatically** as
the `[3/3]` gate at the end of `deploy.sh` against the LIVE fleet; a
behavior-affecting deploy that fails it exits `DEPLOY INCOMPLETE`. Bypass
only with `--skip-e2e` in an emergency.
```mermaid
graph LR
A[Edit locally] --> B[Tests pass] --> C[Commit + push]
C --> D[deploy.sh plan] --> E[Target pulls ff-only]
E --> F[Dispatch selected service] --> G[Per-service health]
G --> H[Playwright e2e gate] --> I{green?}
I -->|yes| J[Deploy complete]
I -->|no| K[DEPLOY INCOMPLETE]
```
## Boot Persistence & Self-Healing (MANDATORY)
Every deployed, working Podman container MUST have a self-healing,
boot-persistent Quadlet unit. A container that runs only from `compose.yaml`
or a manual `podman run` dies silently at reboot — rootless Podman does not
auto-start bare containers, and `restart: unless-stopped` does not survive the
explicit stop that shutdown performs. Never report such a service as "up".
Unit requirements:
- **Boot persistence** — `[Install] WantedBy=default.target`, installed to
`~/.config/containers/systemd/<name>.container`, enabled in the user
manager, with linger on for the deploy user (`loginctl show-user dank -p
Linger` → `Linger=yes`).
- **Self-healing** — `Restart=always` (or `on-failure`) with a short
`RestartSec`; quadlet health check where the service has one.
- **Repo provenance** — the `.container` file lives in this repo
(`<host>/<svc>/quadlet/`), is installed by the deployer, and is covered by
the quadlet test seam. The host-side unit is generated state; the repo file
is the source of truth.
Before declaring a service deployed: `systemctl --user is-enabled <name>` →
`enabled`, `is-active` → `active`, and the container answers its health check.
A running container with no unit is a gap: write the unit, test it, deploy it.
This rule exists because reboots found gaps twice (2026-08-26: Caddy as a
bare compose container never returned; 2026-09-11: five observability
services had no units). The observability services now have units under
`dankclaw/observability/quadlet/` and their compose definitions were removed —
one owner per container name. The stack is RUNNING BUT UNCONFIGURED (no
Grafana datasources/dashboards, no Prometheus rules); see
`serverAppConfigs-docs/Infrastructure/Observability.md`.
## Testing
Tests are mandatory before commit (`bats` required on the dev machine):
```
./tests/run.sh
```
Seams: compose validity, env consistency (no leaked secrets), Caddyfile
validity, quadlet units, docs completeness, script syntax, LLM unit
integrity, LLM secret hygiene, deployment flow, service-scoped planning.
See `serverAppConfigs-docs/Architecture/Testing Policy.md`.
## Secret Rules
- Never commit real secrets. The live Gitea DB password and sudo password are
test-enforced leak checks.
- `.env` files are git-ignored; commit only `.env.example` with placeholders.
Exception: `scripts/*.env` LLM tunables (paths and numbers, never
credentials).
- Do not document passwords anywhere in the repo. SSH keys are the auth
mechanism; `FRANK_SUDO_PW` covers headless sudo and never enters the repo.
## External Exposure Policy (MANDATORY)
Every Caddy-proxied service is either behind the auth chain (Anubis :8923 →
Authentik embedded outpost) or explicitly internal-only — no public DNS name,
no vhost. New services default to internal-only until guarded.
When guarding a new vhost — provider first, then route:
1. Create the outpost proxy provider FIRST (`external_host` = exact vhost URL
with trailing slash; `internal_host = http://caddy:8017`). If the vhost
flips first, authenticated traffic hits the outpost error page.
2. Flip the vhost to `reverse_proxy anubis:8923` with
`header_up X-Real-Ip {remote_host}`, and add the post-auth
`@<svc> host <vhost>` handler on the `http://:8017` listener routing to
the real upstream.
3. Anubis guard set, Caddy TLS edge, and outpost provider list change
together in one deploy.
4. Service-to-service traffic targets internal addresses (`loki:3100`), never
the public hostname — non-browser clients cannot solve challenges.
Exempt categories (document the reason in the vhost comment): the IdP itself
(`auth.`), git smart-HTTP endpoints, services with their own complete auth.
## Deletion & Trash Policy (MANDATORY)
Never `rm`. Move deletions into trash so they stay recoverable:
1. In-repo files → `.AGENTTRASH/` beside the item's directory.
2. Elsewhere → dated staging dir `~/.AGENTTRASH/YYYYMMDD-topic`.
3. List staged items and sizes; wait for explicit user approval to purge.
`rm`, `rm -rf`, and `git clean -f` are forbidden without explicit permission.
`git * --no-verify` is forbidden. The trash is a temporary hold, not an
archive.
## Benchmarking Policy (MANDATORY)
**STOP the production service BEFORE benchmarking** — these servers serve
real users, and a bench beside a live service starves the GPU and corrupts
results. Use the runtime's own harness: `llama-bench` (llama.cpp), `vllm
bench` (vLLM), halogen's `bench`/`sweep` modes for frank-large. Rolling your
own bench requires asking first. Scratch ports are not used on LLM hosts — a
test process cannot share the single GPU anyway; validate against the service
itself.
1. Stop and mask (self-healing would restart it):
- Dankclaw `frank-large` (root unit since the 2026-09-15 ik cutover):
`sudo systemctl stop frank-large && sudo systemctl mask frank-large`
- Dankserver `frank-medium` (system unit):
`sudo systemctl stop frank-medium && sudo systemctl mask frank-medium`
2. Bench.
3. Unmask and start; verify `curl http://192.168.0.<host>:7770/health`.
**CRITICAL:** never `systemctl disable` a system unit on these hosts — it
deletes the unit file from `/etc/systemd/system/`, not just the symlink.
`mask` symlinks to `/dev/null` and is safe/reversible.
## Model Config (harness)
The harness reaches the fleet through `/home/dank/.omp/agent/models.yml`
(outside the repo). Each provider is one host endpoint; each model `id` is
the served alias: `frank-medium` → `192.168.0.235:7770`, `frank-large` →
`192.168.0.143:7770`. Per-role routing lives in
`/home/dank/.omp/agent/config.yml` under `modelRoles`.
Resolution rules: the default model resolves as a `defaultProvider` +
`defaultModel` pair (the provider must serve the model); a subagent override
uses a bare model `id`, which resolves only when unique across providers.
When renaming a service, retiring an endpoint, or moving a model: update the
provider block, model `id`, `contextWindow`, `maxTokens`, `input` in
`models.yml`, then fix every `modelRoles` entry that named the old endpoint —
a role pointing at a removed provider fails at call time. `frank-large`
declares `input: [text, image]` (halogen vision tower). There is **no audio
input anywhere in the fleet** (left with `frank-npu`).
## Active Services
### frank-large — ik_llama.cpp (Dankclaw)
ik_llama.cpp master `d85a1d7` as a **root systemd unit** (repo
`scripts/frank-large.service` symlinked into `/etc/systemd/system/`;
`Restart=always`), serving `mradermacher/Qwen3.8-Flash-Next-Uncensored-i1-Q4_K_M`
(111 GiB GGUF + mmproj-f16) on port 7770 with 262144 ctx / 1 slot, greedy
default, reasoning on (deepseek), vision via mmproj. Topology: the RTX 3090
(PCIe x4 in the M.2 dock) carries dense/attention/KV; ALL MoE expert tensors
and the ~33 GiB PLE table stay in LPDDR via CPU buffers. Deployed flags in
`scripts/frank-large.env` / `scripts/serve-frank-large.sh`; the serve script
refuses to start without `GGML_CUDA_NO_PINNED_WEIGHTS=1` (the pinned-host
buffer incident starved co-tenants into unreachability until a power cycle).
Measured envelope (2026-09-15/16 campaign): prefill ~192 tok/s (1.8k-32k
prompts, CPU VNNI compute ceiling), decode ~3.8-4.3 tok/s — gated by ~100
CPU↔GPU backend splits per token (GPU 95% idle, launch/sync-bound; thread
count irrelevant). The halogen-era 30 tok/s decode bar is out of reach on
this model+hardware; the runbook carries the full evidence chain, the
mainline-llama.cpp comparison (1.0 tok/s — ruled out), and the tuning levers.
Runbook: `serverAppConfigs-docs/LLM/frank-large-ik-uncensored.md`.
Rollback: [[halogen-flash-server]] (quadlet archived in
`~/.AGENTTRASH/20260915-ik-swap/` on dankclaw).
### frank-medium — vllm-radiance MXFP4 (Dankserver, ACTIVE)
`stilldeadcode/vllm-radiance:0.9.3` plus the `ggz14/radiance-vllm-mxfp4`
patches (pinned clone at `/home/dank/GitPackages/radiance-vllm-mxfp4`; its
patches are applied at every container start), serving
`just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq` with the
`Qwen3.8-27B-DFlash2-FP8` block-diffusion drafter (`SPEC=7`).
The deployment is **two single-GPU replicas behind an adaptive-routing proxy** —
`frank-medium-a` (:7771, R9700 #1), `frank-medium-b` (:7772, R9700 #2) and
`frank-medium-proxy` (:7770, the fleet endpoint), three rootless Quadlets
under `dankserver/frank-medium/quadlet/`. Each replica is TP=1 at
114688 context / 4 sequences with vLLM-profiled KV (`KV_MEM=0`).
**TP=2 across both cards cannot run on this host.** RCCL's collective kernels
carry a hidden hostcall-buffer parameter, and ROCr refuses to dispatch them on
a device without PCIe atomics; the second R9700 sits behind the chipset
bridges `05:00.0`/`06:09.0`, which advertise `AtomicOpsCap: Routing-` — a
read-only capability, so no software can enable atomics on that path. Not the
kernel version (7.0.0-28/-30/-31 identical), not the image, not the RCCL
build (a from-source rebuild fails identically). A slot on a CPU root port —
there is none free on this AM5 platform for a second x16 card — or a platform
with more CPU lanes restores TP=2. `frank-medium.container` stays in the repo
as the TP=2 target; the deploy handler does not install it.
Runbook: `serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md`.
Prior runtime: [[frank-medium-rocmfpx]] (llama.cpp era, same day).
## Key Lessons Learned
Durable operational lessons from the fleet's history. Full detail in the
linked runbooks.
**PCIe / multi-GPU**
- **RCCL collectives need PCIe atomics on every participating GPU.** RCCL's
kernels carry a hidden hostcall-buffer parameter; ROCr refuses to dispatch
them when the device has no atomics, and the failure surfaces as
`hipErrorIllegalState` ("the operation cannot be performed in the present
state") from `hipLaunchKernel`, not as anything that names atomics. The
value is consumed at driver probe time, so writing the AtomicOp Requester
bit with `setpci` at runtime changes nothing. Check the *chain*:
`lspci -vv -s <gpu>` and every upstream bridge must show
`AtomicOpsCap: Routing+` (a `Routing-` bridge is a hardware wall), and the
endpoint's Device Control 2 (`setpci -s <gpu> CAP_EXP+28.w`) must have bit
6 set. AMD desktop platforms put one card on CPU root ports (atomics work)
and the rest behind the chipset (usually not) — that asymmetry is what
blocked frank-medium's TP=2. A one-card collective proves nothing: NCCL
short-circuits at `world_size=1`.
- **When a GPU collective dies, read the runtime, not the framework.** The
message that identified the cause came from `AMD_LOG_LEVEL=3`
(`Pcie atomics not enabled, hostcall not supported` → `AQL dispatch
failed!`), not from vLLM or RCCL. `AMD_LOG_LEVEL` costs one env var and
prints the failing HIP call with its return code.
**ROCm / GPU**
- **Layer split, never tensor, on the RCCL-less ROCmFPX builds.** Tensor
parallel falls back to a butterfly allreduce and collapses prefill ~7x
(measured 271.8 vs 1991 t/s). Pin the MTP draft head to ROCm1 — an
unpinned draft under tensor mode aborts the KV build.
- **mmap must stay on** for the big-PLE models: `--no-mmap` forces the
~50 GiB PLE table into anonymous memory → silent cgroup kill, dmesg is the
only evidence. mmap pages ARE page cache. Never pin the PLE tensor with
`-ot` (23 → 13.4 t/s).
- **HIP reads bare env vars** `TEMP`, `HOST`, `GPU_MAX_HEAP_SIZE`. Serve
scripts export env files with `set -a`, so a tunable named `TEMP` leaks
into HIP (`TEMP=1.0` sent the JIT cache to path `1.0` and segfaulted every
load, fixed 2026-08-13 by renaming to `TEMPERATURE`). Never name a tunable
these; check with
`strings /opt/rocm-*/lib/libamdhip64.so.* | grep -xE "TEMP|HOST|GPU_MAX_HEAP_SIZE"`.
- **`-DGGML_CUDA_FA_ALL_QUANTS=ON` is required** for q8_0 KV + flash
attention on CUDA builds; without it prefill collapses ~12x (95 vs 1300+
t/s, measured 2026-08-17).
- **Flash-Next MTP heads do not load on any current loader** — they need full
per-layer `blk.<il>.hc_attn_norm.weight` and ship only `blk.48` (verified
3x). Don't re-attempt without new loader support.
- **QSA flash-attn assert:** `GGML_ASSERT(a-type == GGML_TYPE_F32)` on
qwen4exp needs `scripts/patches/qwen4exp-qsa-f32-mask.patch` — but only on
the agentionai FAST layer set; the kingjones STRIX set works unpatched.
- **GPU budget on Dankclaw is the rocm-smi figure** (64 GiB, SVM-backed).
`free`/`dmidecode` show the CPU-side carve-out — intentional, do not
change.
**Load / crash diagnosis**
- Model load is slow (minutes cold); health returns 503 while loading; do
not kill the process. `deploy.sh` waits up to 20 minutes (`LLM_WAIT`). On
halogen, the FIRST request after a (re)start can go silent ~45–60 s
(health `engine_unresponsive`, 503s) and then self-recovers — do not
restart over it.
- Crash-loop procedure: (1) `journalctl -u <svc> -n 100` + kernel line via
`journalctl -k` (segfault at address 8 in `libamdhip64` = NULL unref in
HIP); (2) run serve script, then raw binary — different outcome isolates
the env file; (3) `gdb -batch -ex run -ex "bt 25" --args bash
scripts/serve-<svc>.sh` — llama-server buffers stdout and loses the log
tail on SIGSEGV; trust gdb, not the log; (4) `amdgpu: SVM mapping failed`
spam is a symptom, not a root cause.
- Journal signatures: client abort logs `W srv stop: cancel task, id_task =
N`; natural stop logs only `slot release` with `truncated = 0`. The server
never preempts a running generation; excess tasks queue when slots are
busy.
**Host memory / swap**
- Swap resize ordering: create and `swapon` the replacement BEFORE `swapoff`
of the old one, or live pages must land in RAM that `MemAvailable`
overstates. Dankclaw swap (64 GiB at `/swap.img`, fstab-driven) protects
the co-tenants, not frank-large — pinned weights don't swap, and paging
the KV pool is worse than the OOM it prevents.
- Host swap state is invisible to `deploy.sh` (no repo file) — check
`swapon --show` before reasoning about headroom.
**Runtime-specific guards**
- **Kairic Edge** (if ever re-deployed): the pinned CK commit needs
`third_party/composable_kernel/include/ck/config.h` generated manually
(untracked; `git clean` removes it), and its greedy-argmax fastpath rejects
non-greedy requests and reasoning budgets. Re-test chat before production.
- **FastMTP sidecar**: build must be commit `4df29be4` + the HauhauCS patch;
stock binaries fail the draft load with a tensor-shape mismatch.
**Ops**
- Never `pkill -f` a pattern that appears in your own command line — it kills
your ssh session. Use `pkill -x <exact-name>`.
- Both hosts allow passwordless sudo for the deploy user. Use plain `sudo` in
committed scripts (no `echo pw | sudo -S`); `FRANK_SUDO_PW` covers hosts
without it.
- ComfyUI on Dankclaw runs CPU-only (`--cpu`): no ROCm wheels for gfx1151,
and CUDA images are wrong for the hardware.
- SearXNG outbound traffic must stay on the Gluetun tunnel; do not move its
ports off the gluetun service.
- `amd_iommu=off` on Dankclaw (bought 13–16 % prefill for frank-large,
2026-09-11) permanently retired the XDNA2 NPU: the driver's SVA/PASID bind
needs an enabled IOMMU (`/dev/accel/accel0` fails `err=-19`). Revert path
in `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md`.
- Rootless Quadlet containers run with `--replace --rm`: `podman logs` covers
only the CURRENT instance; history across recreations lives in
`journalctl --user -u <name>.service`. Read rootless storage WITHOUT sudo
(per-user storage — `sudo podman logs` finds nothing).
## Runbook Index
Active:
| Service | Runbook |
|---------|---------|
| frank-large (halogen-flash-server, Dankclaw) | `serverAppConfigs-docs/LLM/halogen-flash-server.md` |
| frank-medium (vllm-radiance MXFP4, Dankserver) | `serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md` |
| Dankclaw kernel posture (`amd_iommu=off`) | `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md` |
| Deployment flow + incidents | `serverAppConfigs-docs/LLM/deployment.md` |
Retired (rollback targets and history):
| Runtime | Host | Window | Runbook |
|---------|------|--------|---------|
| kingjones STRIX Qwen3.8-Flash-Next ROCmFP4 | Dankclaw | 2026-08-30 → 09-11 | `kingjones-strix-rocmfp4.md` |
| agentionai ROCmFP4-FAST imatrix | Dankclaw | 2026-08-29 → 08-30 | `agentionai-fast-imatrix.md` |
| eemin Qwen3.8-27B ROCmI4 W4A4 MTP | Dankclaw | 2026-08-18 → 08-25 | `eemin-rocmi4-mtp.md` |
| Kairic Edge IU4 | Dankclaw | 2026-08-22 (same-day revert) | `kairic-edge.md` |
| CIRU ActiveFPX v3 / PromptForge | Dankclaw | through 2026-08-22 | `ciru-activefpx-promptforge.md` |
| DFlash2 Vulkan | Dankclaw | 2026-08-21 (same-day revert) | `dflash2-large-vulkan.md` |
| FastFlowLM NPU (`frank-npu`) | Dankclaw | twice, retired 2026-09-11 | `frank-npu.md`, `FastFlowLM NPU Inference.md` |
| FreeToken (official + Huihui FP8, `frank-small`) | Dankserver | 2026-09-02 → 09-15 | `freetoken-frank-small.md` |
| FastMTP patched llama.cpp | Dankserver | retired 2026-08-18 | `qwen3.8-spec-matrix.md` |
| ik_llama.cpp Ornith CRACK | Dankserver | retired 2026-09-02 | `ikllama-ornith-dankserver.md` |
Retired runbooks are under `serverAppConfigs-docs/LLM/`. Retired scripts live
in `old/` and `.AGENTTRASH/`.
## Why
Direct edits on the remote drift from version control, and future agents
won't know what changed. Git is the single source of truth. The two workloads
share two hosts, one Caddy, and one Gitea; one repo keeps them in one
history.
## Summary
| Context | Action |
|---------|--------|
| Editing a compose/Caddyfile/serve script | **Through git** — fetch, commit, edit, test, push, pull, deploy. Never edit on the remote. |
| Config exists only on remote | **Pull it into the repo first** — commit the fetched version (redacted) before editing. |
| Unstaged local changes | **Commit or discard** — check `git status` before starting work; ask if unsure. |
| Deploying a committed change | **`scripts/deploy.sh <target>`** — pushes, pulls ff-only, plans, dispatches, health-checks. |
| Container without a Quadlet unit | **Give it one** — boot-persistent, self-healing `.container` via git. |
| Benchmarking | **Stop and mask the service first**, bench, unmask, restart, verify health. |
| Deleting a file | **Move it to trash** — `.AGENTTRASH/` (in-repo) or `~/.AGENTTRASH/YYYYMMDD-topic`. Never `rm`. |
| Tests exist | **Run them** — verify tests pass after every change. Do not commit failing tests. |
| Real secrets | **Never commit** — host `.env` files; `.env.example` holds placeholders only. |