Files
serverAppConfigs/AGENTS.md
T

477 lines
24 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. It
tracks both the Podman container stacks and the LLM inference fleet. One repo,
one checkout per host, one deploy script.
- **Dankserver** (192.168.0.235) — media/library stack (RomM, Copyparty,
Calibre-Web, the *arr stack, Jellyfin) as bare containers, plus the
`frank-default-small` LLM service.
- **Dankclaw** (192.168.0.143) — AI/search/development/observability stack
(SearXNG via Gluetun, ComfyUI, OpenWebUI, Hermes, Gitea, Grafana, Prometheus,
Loki, Alloy, Pi-hole) plus the `frank-default-large` LLM service. Caddy on
Dankclaw reverse-proxies both hosts.
**2026-08-22 merge:** the former `vllmScripts` repository (LLM fleet scripts,
units, env tunables, and runbook) is merged into this repo. Its full git
history is preserved (two-parent import commit under the retired `llm/`
prefix, reorganized in follow-up commits). The retired target checkouts
(`/home/dank/vLLM` on Dankserver, `/home/dank/vllmScripts` on Dankclaw) are
archived. All services now run from `/home/dank/serverAppConfigs` on each
host.
### LLM fleet topology (2026-08-22)
| Target | Runtime | Service | Model | Port |
|--------|---------|---------|-------|------|
| Dankserver | llama.cpp (upstream b10121) | frank-default-small | HauhauCS Qwen3.6-35B-A3B-Uncensored-Aggressive Q6_K_P + ngram-mod, 36 MoE experts on CPU | 7770 |
| Dankclaw | charlie12345/ROCmFPX llama.cpp | frank-default-large | kingjones Qwen3.8 Uncensored Q6_0_ROCmFPX_AGENT (standalone MTP draft head, Turbo4 V cache, 262K context / 1 slot), reasoning on | 7770 |
History: the 2026-08-17 redo retired the pipeline-parallel split and the
FastFlowLM NPU endpoint. The 2026-08-18 swap retired the vLLM service on
Dankclaw. On 2026-08-21 a DFlash2 Vulkan deployment briefly replaced CIRU,
then was reverted the same day for speed. On 2026-08-22 the service briefly
swapped to the Kairic Edge IU4 runtime, then was reverted to v3 the same day
(operator decision). On 2026-08-25 the service switched to the kingjones
Qwen3.8-27B-Uncensored-Q6_0-ROCmFPX_AGENT model (standalone MTP draft,
262144 context, reasoning on). On 2026-09-01 both services were swapped:
Dankserver switched from ik_llama.cpp Ornith CRACK Q8_0 to stock llama.cpp
Tiel-Coder-35B (qwen35moe hybrid MoE), and Dankclaw switched from the Q6_0
AGENT model to the Flash-Next 56B MoE (qwen4exp, PR #27742 merged into
charlie's ROCmFPX fork, --fit off, 128k context). Runbooks:
[docs/LLM/index.md](docs/LLM/index.md).
Retired scripts live in `old/` (reference only) and `.AGENTTRASH/`.
## 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`, and `deploy-remote.sh`
(service-scoped deployer), LLM serve scripts, systemd units, env tunables,
and bench harnesses
- `old/` — retired PP/NPU-era LLM scripts (reference only)
- `docs/` — knowledge base (Obsidian vault). `docs/LLM/` is the LLM fleet
- `tests/` — bats test suite
## Documentation Authoring
The `docs/` vault is the project knowledge base (Obsidian, wikilinks) and is an
OKF (Open Knowledge Format) knowledge bundle. Follow the OKF spec for every doc
you create or edit.
- **Canonical spec:** `~/.pi/agent/docs/references/okf-spec.md` — the OKF v0.1
spec in the **system-config knowledge base** at `~/.pi/agent/docs`. The repo's
own `docs/references/okf-spec.md` is deprecated/removed; use the system one. The
spec index and maintenance guide are
`~/.pi/agent/docs/references/index.md` and
`~/.pi/agent/docs/references/okf-maintenance.md`.
- **Read before you touch:** open the relevant `index.md` first
(`docs/index.md` → `docs/LLM/index.md`). Read the whole target file before
editing it.
- **One concept per file**, with YAML frontmatter that has a non-empty `type:`
field (`reference` / `guide` / `configuration` / `research`) plus `description`,
`tags`, and `timestamp`. Split broad topics; keep pages ~1–500 lines.
- **Progressive disclosure:** an `index.md` at each level links its children.
Every `docs/**/*.md` (except `index.md`) must be linked from
`docs/index.md` with `[[Title]]` — the docs-completeness test checks this.
- **Cross-link** siblings as `[[Title]]` wikilinks; cite external claims under a
`# Citations` section.
- **Always update docs** when a task changes behavior, then run
`./tests/run.sh` (the docs-completeness seam is in there) before commit.
## Git Workflow
All edits flow through git. Do not edit files on the remote machine directly.
### Protocol
1. **Source the config from the target machine** — fetch the live
compose/Caddyfile/serve script from the server where it runs
2. **Place it in this repo** — commit the fetched version (redact secrets
into `.env`)
3. **Edit here** — make all changes locally in the working tree
4. **Run the tests** — `./tests/run.sh` must pass
5. **Commit and push** — push to origin (Gitea on Dankclaw)
6. **Pull on the target** — `git pull` on the remote machine
7. **Deploy** — `scripts/deploy.sh` pulls, plans, and dispatches only the
changed service(s), then checks the selected service health
## Deployment
Use `scripts/deploy.sh` to deploy committed changes to a target. The planner
maps changed paths to individual services; the remote runner restarts only the
selected service and records its successful revision outside the checkout.
```
./scripts/deploy.sh dankclaw
./scripts/deploy.sh dankserver
./scripts/deploy.sh dankclaw --service searxng
./scripts/deploy.sh dankserver --service frank-default-small
./scripts/deploy.sh dankclaw --dry-run
```
A documentation-only change is a no-op. The LLM services are selected only by
their own unit/env/serve files or the shared chat template, so unrelated
container changes cannot interrupt inference. `frank-default-npu` is relink-only
when explicitly selected.
Set `FRANK_SUDO_PW` for headless runs on a host without passwordless sudo
(remote sudo reads it from stdin via `sudo -S`). The value is never stored
in this repo.
```mermaid
graph LR
A[Edit locally] --> B[Tests pass]
B --> C[Commit]
C --> D[Push to origin]
D --> E[deploy.sh computes service plan]
E --> F[Target pulls with ff-only]
F --> G[deploy-remote.sh dispatches selected service]
G --> H[Per-service health check]
```
## Boot Persistence & Self-Healing (MANDATORY)
Every Podman container governed by this project that is successfully deployed
and proven working 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. Do not report such a service as "up" without its unit.
Each container's unit must provide:
- **Boot persistence** — `[Install] WantedBy=default.target`, installed to
`~/.config/containers/systemd/<name>.container` (generating
`<name>.service`) and enabled in the user manager
(`systemctl --user enable <name>.service`). Linger must be on for the deploy
user (`loginctl show-user dank -p Linger` → `Linger=yes`) so user units start
at boot with no login session.
- **Self-healing** — `[Service] Restart=always` (or `Restart=on-failure`) with
a short `RestartSec`, so a crash or OOM brings the container back without
operator action. Add a quadlet health check where the service has one.
- **Repo provenance** — the `.container` file lives in this repo
(`dankclaw/<svc>/quadlet/` or `dankserver/<svc>/quadlet/`), is installed by
the service-scoped deployer, and is covered by the quadlet test seam. The
host-side unit is generated state; the repo file is the source of truth.
Verification before a service is declared deployed:
`systemctl --user is-enabled <name>.service` → `enabled`,
`systemctl --user is-active <name>.service` → `active`, and the container
answers its health check. A running container with no unit is a gap: write the
unit, test it, deploy it.
Incident that established this rule (2026-08-26, Dankclaw reboot): Caddy ran
for weeks as a bare compose container and never came back after reboot; the
Pi-hole unit existed but crash-looped on a sysctl conflict (a later-sorted
drop-in reset `net.ipv4.ip_unprivileged_port_start` to 80), taking LAN DNS
down with it.
## Testing
Tests are mandatory before commit. `bats` must be installed on the
development machine.
```
./tests/run.sh
```
Test seams: compose validity, env consistency (no leaked secrets), Caddyfile
validity, quadlet units, docs completeness, script syntax (all of `scripts/`,
including the LLM serve scripts), LLM unit integrity (units point at files in
this repo), LLM secret hygiene, deployment flow, and service-scoped deployment
planning/dispatch. See [docs/Architecture/Testing Policy.md](docs/Architecture/Testing%20Policy.md).
## Secret Rules
- Never commit real secrets. The live Gitea database password and the live
sudo password are test-enforced as leak checks.
- `.env` files are git-ignored. Commit only `.env.example` with placeholders.
The LLM env tunables in `scripts/` are tracked by exception (`!scripts/*.env`).
They hold paths and numbers, never credentials.
- Do not document passwords in this repo, including sudo passwords. SSH keys
are the auth mechanism. Headless sudo passes the password via the
`FRANK_SUDO_PW` environment variable. The value never enters the repo.
## Deletion & Trash Policy (MANDATORY)
Never run `rm` to delete files. The command deletes permanently. Move every
item you delete into trash instead. The trash keeps the deletion recoverable
and reviewable.
1. In-repo files: move the item to `.AGENTTRASH/` beside its directory.
2. Home and other paths: create a dated staging directory
`~/.AGENTTRASH/YYYYMMDD-topic` and move each item there.
3. List the staged items and their sizes for the user.
4. Wait for explicit user permission before you empty the trash.
Rules:
- `rm`, `rm -rf`, and `git clean -f` are forbidden without explicit user
permission.
- `git * --no-verify` is forbidden.
- A moved item stays in the trash until the user approves its purge.
- The trash is a temporary hold, not a long-term archive.
## Benchmarking Policy (MANDATORY)
**STOP the production service BEFORE running any benchmark on a target.**
These servers serve real users (about 3 people). Running a bench while the
service is up starves the GPU and corrupts results. Restart the service when
the bench finishes.
ALWAYS bench using llama-bench for llama.cpp or vllm bench for vllm runtimes.
DO NOT ROLL YOUR OWN BENCH UNLESS YOU CANNOT TEST THE DESIRED PARAMETER WITH THE OFFICIAL BENCHES. ASK THE USER BEFORE DEVIATING FROM THIS DIRECTIVE.
1. Stop the service before any benchmark:
`sudo systemctl stop frank-default-small` (Dankserver) or
`sudo systemctl stop frank-default-large` (Dankclaw).
2. Run the bench (`llama-bench` for llama.cpp / vllm bench for vLLM).
Scratch ports are not used for tests or benches on the LLM hosts — a
test process cannot share the single GPU with the running service anyway
(the model + KV fill the card), so changes are validated directly against
the service itself.
3. Restart the service when the bench completes: `sudo systemctl start <svc>`.
4. Verify health before reporting:
`curl http://192.168.0.235:7770/health` or
`curl http://192.168.0.143:7770/health`.
## Pi Model Config (`~/.pi/agent/models.json`)
The Pi agent reaches the fleet through `/home/dank/.pi/agent/models.json`.
This file lives outside the repository. Each provider is one host endpoint.
Each model `id` is the served alias. The two aliases are
`frank-default-small` (Dankserver `:7770`) and `frank-default-large`
(Dankclaw `:7770`).
The model `id` values are stable. The file
`/home/dank/.pi/agent/settings.json` references them. Two resolution rules
apply (verified in `@earendil-works/pi-coding-agent` `model-resolver.js`):
- The default model resolves as a `defaultProvider` + `defaultModel` pair.
The `defaultProvider` must name the provider that serves the `defaultModel`.
- A subagent override uses a bare model `id`. A bare `id` resolves only when
it is unique across all providers.
When you rename a service or move a model to a new host, update
`models.json`. Update the provider key, the model `id`, `contextWindow`, and
`input`. If the default model moved hosts, also update `defaultProvider` in
`settings.json`. Set `contextWindow` to the per-slot context (both hosts
`262144`). The full schema is in the agent knowledge base page
`~/.pi/agent/docs/pi/models.md`.
## Runtime & Troubleshooting
### Qwen3.8-Flash-Next ROCmFP4 STRIX_LEAN llama.cpp (Dankclaw, ACTIVE)
The large service serves
`kingjones777/Qwen3.8-Flash-Next-ROCmFP4-STRIX_LEAN-GGUF` (public, 98.5 GiB)
from `/home/dank/models/kingjones-Qwen3.8-Flash-Next-ROCmFP4-STRIX_LEAN/`
with a charlie12345/ROCmFPX `llama-server` built at
`/home/dank/GitPackages/ROCmFPX/build-qwen38` (PR #27742 qwen4exp architecture
merged). The model is a 56B MoE (512 experts, 10 used), 48 layers (12 attn +
36 SSM, full_attention_interval=4), QSA indexer (4 heads, top_k=2048),
hyper-connections (count=4, low_rank=320), PLE (layer 1). Q4_0_ROCMFP4 quant.
It runs f16 KV cache, 131072 context / 1 slot, `--fit off` (auto-fit segfaults
on qwen4exp hyper-connection config), and reasoning on (Sharp template, budget
-1, `--reasoning-format deepseek`; chat kwargs keep `preserve_thinking` +
`preserve_reasoning`). Env `scripts/frank-default-large.env`, serve
`scripts/serve-frank-large.sh`.
**Serving status (2026-09-01):** deployed the Flash-Next 56B MoE model. The
previous kingjones Q6_0 AGENT model (27B, standalone MTP, 262144 context) is
retired. The Q8_0 AGENT experiment is documented in
`docs/LLM/kingjones-q8agent-rocmfpx.md`; it was retired because it crashed in
`ggml_compute_forward_set_rows` and did not provide a working service.
### Tiel-Coder-35B-A3B llama.cpp (Dankserver, ACTIVE)
The small service serves `peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF` (public,
30 GB, UD-Q6_K_XL quant) from `/home/dank/models/tiel-coder-35b/` with
upstream llama.cpp (latest master, CUDA build with
`-DGGML_CUDA_FA_ALL_QUANTS=ON -DCMAKE_CUDA_ARCHITECTURES=86`). The model is a
qwen35moe hybrid MoE: 40 layers (10 attention + 30 SSM/Mamba,
full_attention_interval=4), 256 experts / 8 used, 2 KV heads × 256 dim. It
runs Q8_0 KV cache, 200700 total context (66900/slot × 3 parallel), `--cpu-moe`
for expert offload, `-ngl 999` (non-expert on GPU), and mmap. Env
`scripts/frank-default-small.env`, serve `scripts/serve-frank-small.sh`.
**Serving status (2026-09-01):** deployed the Tiel-Coder-35B model. The
previous ik_llama.cpp Ornith CRACK Q8_0 deployment is retired.
### charlie12345/ROCmFPX ROCmI4 W4A4 llama.cpp (Dankclaw, prior v3, 2026-08-18 to 2026-08-25)
The v4 runtime serves `cafonz/Qwen3.8-27B-ROCmI4-MTP-GGUF` (`eemin` namespace,
public) from `/home/dank/rocmi4-qwen38/ROCmFPX/build-rocmi4-w4a4/bin/llama-server`
(charlie12345/ROCmFPX commit `c49ebdbd` built with `-DGGML_HIP_ROCMI4_W4A4=ON`
against system ROCm 7.2.4 / amdclang 22). The model is `Q4_0_ROCMI4` with an
embedded NextN/MTP head (no separate draft file); the gfx1151 native IU4 WMMA
instruction batch-verifies proposals. Profile: strict Qwen MTP
(`--spec-mtp-strict-qwen`, n_max 16, p_min 0.60, backend sampling, all layers
on ROCm0), `-t 16 -tb 32`, batch 512 / ubatch 256 (ubatch swappable via the
ubatch bench on 8k context), f16 KV, full 262144 context / 1 slot, TEMPERATURE
0.7 / TOP_P 0.95, reasoning on (deepseek, budget -1), `--no-mmap`. Two-file
payload (~14.4 GiB) in `/home/dank/models/eemin-Qwen3.8-27B-ROCMI4-MTP/`. Full
runbook:
[docs/LLM/eemin-rocmi4-mtp.md](docs/LLM/eemin-rocmi4-mtp.md).
### Kairic Edge llama.cpp (Dankclaw, RETIRED experiment 2026-08-22)
The Kairic Edge llama-server serves `jcbtc/Qwen3.8-27B-IU4-Kairic-Edge`
(pinned tag `kairic-edge-qwen38-27b-v1`). It was deployed and reverted to v3
the same day (operator decision). The runtime routes qualified prompt and
verification shapes through the RDNA 3.5 native IU4 matrix instruction.
**Release gap — CK `config.h`:** the pinned CK commit ships only
`config.h.in`. The release script and guide never generate
`third_party/composable_kernel/include/ck/config.h`. Generate it via a CK
CMake configure and copy it in before building (full runbook in
[docs/LLM/kairic-edge.md](docs/LLM/kairic-edge.md)). The file is untracked
in CK git. A `git clean` removes it. Local rebuilds are byte-identical
(`d7625a0c...`).
**Target greedy argmax fast path:** the pinned runner sets
`LLAMA_TARGET_GREEDY_ARGMAX_FASTPATH=1`. The fast path rejects non-greedy
requests (400) and the card's own chat sampling is non-greedy, so the
production attempt ran it at 0. It still rejects reasoning budgets unless
compatibility mode is used. Do not re-deploy Kairic without re-testing chat
first.
**Rollback target:** the v3 model files remain in
`/home/dank/models/CIRU-Qwen3.8-27B-ActiveFPX-v3/` and the v3 binary in
`/home/dank/ciru-qwen38/ROCmFPX/build-promptforge/bin/llama-server`.
Restored to v3 on 2026-08-22 (commit `c60156f`).
### DFlash2 llama.cpp (Dankclaw, retired 2026-08-21)
A DFlash2 Vulkan deployment briefly replaced CIRU on 2026-08-21, then was
reverted the same day for speed. Its serve script, env, and the build
worktree are archived in `.AGENTTRASH/` and documented in
[docs/LLM/dflash2-large-vulkan.md](docs/LLM/dflash2-large-vulkan.md).
### FastMTP patched llama.cpp (Dankserver, retired 2026-08-18)
Historical: the Qwen3.8-27B IQ4_XS FastMTP service was retired on 2026-08-18
(files in `.AGENTTRASH/`). The build notes are kept for reference. The
HauhauCS FastMTP draft sidecar required a llama.cpp build from commit
`4df29be4f4c3673f428170fda944a5b19f743bb8` plus the
`HauhauCS-FastMTP-llama.cpp.patch` (from the HF repo). The build lives at
`/home/dank/llama.cpp-fastmtp/build` on Dankserver. Loading the draft with a
stock binary fails with `expected 5120, 248320, got 5120, 32768`. Do not
rebuild from another commit without re-applying the patch.
**REQUIRED build flags:** `-DGGML_CUDA_FA=ON -DGGML_CUDA_FA_ALL_QUANTS=ON`.
Without `GGML_CUDA_FA_ALL_QUANTS` the q8_0 KV cache + flash attention falls
back to a slow path and prefill collapses ~12x (measured 2026-08-17: 95 t/s
vs 1300+ t/s at an 8K prompt on the 3090). See the HauhauCS HF repo README
for the base recipe.
### Env var naming rule — HIP runtime collision
The ROCm HIP runtime reads the bare environment variables `TEMP`, `HOST`,
and `GPU_MAX_HEAP_SIZE`. The serve scripts export the env file with
`set -a`. A tunable named `TEMP` leaks into HIP. On 2026-08-13 `TEMP=1.0`
made HIP use the path `1.0` for its JIT kernel cache, then segfaulted every
model load. Commit `227ce01` fixed it by renaming the sampling variable to
`TEMPERATURE`.
Never name a tunable `TEMP`, `HOST`, or `GPU_MAX_HEAP_SIZE`. Check for
collisions with this command:
```
strings /opt/rocm-*/lib/libamdhip64.so.* | grep -xE "TEMP|HOST|GPU_MAX_HEAP_SIZE"
```
### Crash-loop diagnostic procedure
When a llama service segfaults during model load, follow these steps.
```mermaid
graph TD
A[Service SEGVs at load] --> B[Read journal + kernel segfault line]
B --> C{Segfault in libamdhip64?}
C -- yes --> D[Bisect script vs raw binary]
C -- no --> E[Check config, memory, model files]
D --> F[Run gdb for backtrace]
F --> G[Find root cause]
```
1. Read the service journal with `journalctl -u <service> -n 100`. Then read
the kernel segfault line with `journalctl -k`. A segfault at address 8 in
`libamdhip64` means a NULL unref in the HIP runtime.
2. Run the serve script directly. Then run the raw binary with the same
flags. A different outcome isolates the env file as the cause.
3. Run gdb for the backtrace with this command:
```
gdb -batch -ex run -ex "bt 25" --args bash scripts/serve-frank-large.sh
```
`llama-server` buffers its timed stdout. On SIGSEGV the log tail is lost.
Trust gdb, not the log.
4. Kernel spam about `amdgpu: SVM mapping failed` is a symptom, not the root
cause. Investigate other causes before assuming memory pressure.
### Operational quirks
- Both hosts allow passwordless sudo for the deploy user (`(ALL) NOPASSWD:
ALL`, verified 2026-08-22). Use plain `sudo` in committed scripts. Do not
use `echo password | sudo -S` in committed scripts. The `FRANK_SUDO_PW`
env var covers headless runs on a host without that.
- Never run `pkill -f` with a pattern that appears in your own command line.
It kills your ssh session. Use `pkill -x <exact-name>`.
- Model load is slow: Dankserver loads the 30.6 GB Q6_K_P through RAM
(`--no-mmap`, MoE experts on CPU) in a few minutes cold. Dankclaw loads the
~30 GiB Q8_0 AGENT payload (main + MTP draft + mmproj) a few minutes cold
with a fresh KV cache; ~18 s with a warm page cache. Health returns 503 while loading. Do not kill the
process. `deploy.sh` waits up to 20 minutes on both targets (see
`LLM_WAIT`).
- A client abort and a server release look different in the journal. A
client abort logs `W srv stop: cancel task, id_task = N`. A natural stop
(EOS or limit) logs only `slot release` with `truncated = 0`. The server
never preempts a running generation. When all slots are busy, new tasks
defer into a queue. See the 2026-08-13 incident in
[docs/LLM/deployment.md](docs/LLM/deployment.md).
- ComfyUI on Dankclaw runs CPU-only (`--cpu`). ROCm wheels do not support
gfx1151. Do not use CUDA images.
- SearXNG outbound traffic must stay on the Gluetun tunnel. Do not move
its ports off the gluetun service.
- Retired hardware history: the Dankserver 35B cuBLAS crash and the DFlash
premature-EOS bug are historical. See
[docs/LLM/dankserver-vram-growth.md](docs/LLM/dankserver-vram-growth.md)
and [docs/LLM/qwen3.8-spec-matrix.md](docs/LLM/qwen3.8-spec-matrix.md).
The FastFlowLM NPU endpoint is retired. See
[docs/LLM/FastFlowLM NPU Inference.md](docs/LLM/FastFlowLM%20NPU%20Inference.md).
### Dankclaw memory model
The GPU budget on Dankclaw is the rocm-smi figure: 64 GiB VRAM reported by
rocm-smi (SVM-backed). The docs' older 96 GiB figure was the addressable
SVM pool. The GPU node reports `local_mem_size 0`, so all allocations are
GTT/SVM-backed. Trust rocm-smi and the runtime. Do not use `free` or
`dmidecode` for the GPU budget. They show the CPU-side carve-out. This is
intentional. Do not change it.
## Why
Direct edits on the remote drift from version control. Future agents will not
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.
> User request → Fetch from remote → Commit here → Edit → Test → Commit → Push → Pull on remote → Dispatch changed service(s) → Health
## Summary
| Context | Action |
|---------|--------|
| Editing a compose/Caddyfile/serve script | **Work through git** — fetch, commit, edit, test, push, pull, deploy. Never edit on the remote directly. |
| Config exists only on remote | **Pull it into the repo first** — commit the fetched version (redacted) before making changes. |
| Unstaged local changes | **Commit or discard** — check `git status` before starting work. Ask the user if unsure. |
| Deploying a committed change | **Run `scripts/deploy.sh <target>`** — it pushes, pulls with fast-forward-only, plans changed paths, dispatches selected services, and checks their health. See [docs/LLM/deployment.md](docs/LLM/deployment.md). | | Container without a Quadlet unit | **Give it one** — self-healing, boot-persistent `.container` unit via git (see Boot Persistence & Self-Healing). |
| Benchmarking | **Stop the service first**, restart, verify health. See the Benchmarking Policy above. |
| 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** — use host `.env` files. `.env.example` holds placeholders only. `FRANK_SUDO_PW` covers headless sudo. |