Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd6a07bb58 | ||
|
|
1921d76319 | ||
|
|
d302151a8d | ||
|
|
5b6f4381b0 | ||
|
|
cae49fcd09 | ||
|
|
ee292ffd10 | ||
|
|
dd4713fa81 | ||
|
|
a8727ddf0b | ||
|
|
2faeab665a | ||
|
|
62b3b92e69 | ||
|
|
ec6a707fe4 | ||
|
|
96553979fb | ||
|
|
ac5b377750 | ||
|
|
8f5a229cc6 | ||
|
|
ea514c8fd2 | ||
|
|
bc60fa33be | ||
|
|
e4ecc93f29 | ||
|
|
d055eb1e7f | ||
|
|
70346f3d57 | ||
|
|
1251cfbd40 | ||
|
|
e67bbd0de0 | ||
|
|
986081bf1d | ||
|
|
a9b3c495da | ||
|
|
c99eb8e11b | ||
|
|
cd20d6ddfd | ||
|
|
82dd0c7d09 | ||
|
|
99b9a6a17c | ||
|
|
39385636be | ||
|
|
d99c1c3c4f | ||
|
|
651924a4bc | ||
|
|
e86e60af39 | ||
|
|
01d9d3fbd5 | ||
|
|
570b5509d0 | ||
|
|
3579c7ebc5 | ||
|
|
0075ca3bbf | ||
|
|
bcd52bb0f4 | ||
|
|
edd7e0cf68 | ||
|
|
f033639b54 | ||
|
|
6fffd83957 | ||
|
|
dd8f3974c1 | ||
|
|
acf67182e9 | ||
|
|
38bf9c4897 | ||
|
|
54f15e2a58 | ||
|
|
1e7e212e5c | ||
|
|
475211d2e7 | ||
|
|
0258ef70f8 | ||
|
|
0c7fe8b4bf | ||
|
|
765b686859 | ||
|
|
332067a516 | ||
|
|
66e3f220e7 | ||
|
|
6134e1168e | ||
|
|
c379d16858 | ||
|
|
ca8b46241e | ||
|
|
3fa0242f56 | ||
|
|
fe039a0b73 | ||
|
|
d4092f2f65 | ||
|
|
cedef97c99 | ||
|
|
0ee08bc368 |
@@ -100,15 +100,18 @@ secret in every agent's context window on every machine that pulls this repo.
|
||||
|
||||
| box | role | GPU | notes |
|
||||
|-----|------|-----|-------|
|
||||
| Dev workstation | client box, never runs inference | laptop iGPU + dGPU (non-unified) | mainline llama.cpp at `~/GitPackages/llama.cpp/build/bin/`; this repo lives here at `~/.omp` |
|
||||
| Dev workstation (`blankpad`) | client box, never runs inference | Lenovo laptop, AMD Ryzen 5 PRO 5650U / Cezanne APU (**unified memory**, s2idle sleep only, no dGPU) | this repo lives here at `~/.omp`; the earlier "iGPU + dGPU / `~/GitPackages/llama.cpp`" description belonged to the sold `dankware` box |
|
||||
| Dankclaw | bench target, secondary models + service stack | Strix Halo APU (**unified memory**) + RTX 3090 (moved over from Dankserver 2026-09) | prod services `frank-dragoon` (halogen-flash-server, :7770, iGPU) and `frank-zealot` (llama.cpp PrismML fork, :7771, 3090); runbooks in the serverAppConfigs vault (`~/Projects/serverAppConfig/serverAppConfigs-docs/LLM/`); ssh: `ssh dankclaw` |
|
||||
| Dankserver | main-model box | 2× Radeon AI PRO R9700 — non-unified (was RTX 3090 until 2026-09) | runs `frank-archon` (radiance, TP=2, :7770) — **this repo's operator session runs on it**; ssh: `ssh dankserver` |
|
||||
|
||||
## Environment gotchas
|
||||
|
||||
- Gitea API needs a token: `GITEA_TOKEN` env (gitignored `.env`). No `tea` CLI
|
||||
installed anywhere. Remote URLs embed the token locally in `.git/config` — that
|
||||
file is never pushed; it is not a substitute for the secret gate.
|
||||
installed anywhere. Remote URLs carry NO embedded token (2026-10-09
|
||||
credential-store cutover) — git and the API both read
|
||||
`~/.config/secrets/gitea` (`store --file`, URL-line format; see
|
||||
`omp-docs/tools/gitea/client-access.md`). That file is never pushed; it is
|
||||
not a substitute for the secret gate.
|
||||
- Passwordless sudo (`sudo -n`) works on Dankclaw and Dankserver for systemctl
|
||||
verbs only; user units need no sudo at all.
|
||||
- SSH into both servers is BatchMode-capable from here:
|
||||
|
||||
+18
-5
@@ -1,6 +1,10 @@
|
||||
providers:
|
||||
openrouterVariant: online
|
||||
fetch: auto
|
||||
maxInFlightRequests:
|
||||
llamacpp (dankserver): 3
|
||||
llamacpp (dankclaw): 2
|
||||
llamacpp (dankclaw-3090): 3
|
||||
modelRoles:
|
||||
slow: llamacpp (dankclaw)/frank-dragoon:medium
|
||||
vision: llamacpp (dankclaw-3090)/frank-zealot:xhigh
|
||||
@@ -13,7 +17,7 @@ modelRoles:
|
||||
default: llamacpp (dankserver)/frank-archon:medium
|
||||
smol: llamacpp (dankclaw-3090)/frank-zealot:medium
|
||||
web: web/searxng
|
||||
speech: local/kokoro
|
||||
speech: frank-tts (dankclaw)/frank-tts
|
||||
judge: openrouter/~typesafe/jev-latest:auto
|
||||
dictation: local/parakeet-tdt-0.6b-v3
|
||||
memory: llamacpp (dankclaw-3090)/frank-zealot:medium
|
||||
@@ -145,17 +149,25 @@ display:
|
||||
subagentLivePreview: true
|
||||
task:
|
||||
showResolvedModelBadge: true
|
||||
maxConcurrency: 3
|
||||
maxConcurrency: 2
|
||||
agentModelOverrides:
|
||||
designer: llamacpp (dankclaw-3090)/frank-zealot
|
||||
librarian: llamacpp (dankclaw-3090)/frank-zealot
|
||||
reviewer: llamacpp (dankserver)/frank-archon
|
||||
scout: llamacpp (dankserver)/frank-archon
|
||||
scout: llamacpp (dankclaw-3090)/frank-zealot
|
||||
security-reviewer: llamacpp (dankclaw)/frank-dragoon
|
||||
sonic: llamacpp (dankclaw-3090)/frank-zealot
|
||||
task: llamacpp (dankserver)/frank-archon
|
||||
seance: llamacpp (dankclaw-3090)/frank-zealot
|
||||
eager: default
|
||||
enableLsp: true
|
||||
maxRuntimeMs: 1800000
|
||||
softRequestBudget: 120
|
||||
maxRecursionDepth: 1
|
||||
disabledAgents: []
|
||||
isolation:
|
||||
enabled: true
|
||||
completionProbe: false
|
||||
snapcompact:
|
||||
systemPrompt: none
|
||||
toolResults: false
|
||||
@@ -203,8 +215,8 @@ vault:
|
||||
tts:
|
||||
localVoice: bm_george
|
||||
speech:
|
||||
enabled: true
|
||||
mode: yield
|
||||
enabled: false
|
||||
mode: all
|
||||
enhanced: true
|
||||
voice: am_fenrir
|
||||
secrets:
|
||||
@@ -232,6 +244,7 @@ followUpMode: one-at-a-time
|
||||
tools:
|
||||
approvalMode: yolo
|
||||
format: auto
|
||||
maxTimeout: 600
|
||||
marketplace:
|
||||
autoUpdate: notify
|
||||
bash:
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: audit-never-run-container-deploy
|
||||
description: "Audit a locally built podman/quadlet deploy path that has never actually run: prove base-image assumptions, env-file reader divergence, and image-hash coverage before the first deploy."
|
||||
---
|
||||
|
||||
# Audit a never-run container deploy path
|
||||
|
||||
A deployable that has never been built or started has never been *executed*: every
|
||||
fault in it is invisible to tests that stub the runtime. Audit it as static code
|
||||
against the tools it calls, then prove each suspicion cheaply against the real
|
||||
tool. The faults worth hunting are the ones that **report success while doing
|
||||
nothing** — those are the ones that survive a green `/health` and a green e2e run.
|
||||
|
||||
## The three fault classes (all measured 2026-10-18, frank-tts-engine)
|
||||
|
||||
1. **A command the base image does not ship.** e.g. `adduser` in a `RUN` line
|
||||
against a trimmed `ubuntu:24.04` — absent, and not pulled in by
|
||||
`ca-certificates curl libgomp1`. Dies at build time (loud), but only the first
|
||||
time anyone builds it.
|
||||
Prove it in the base, never from memory:
|
||||
`podman run --rm <base> sh -c "command -v adduser || echo absent; useradd --system --uid 10000 --home-dir /app --shell /usr/sbin/nologin tts && id tts"`
|
||||
Note the accepted cosmetic warning (`uid 10000 is greater than SYS_UID_MAX 999`)
|
||||
in a comment, or a future agent "fixes" it by dropping `--system` and the uid
|
||||
stops matching the sibling container.
|
||||
2. **Two readers of one env file disagree.** systemd units that pass
|
||||
`EnvironmentFile=` hand it to podman `--env-file`, which does **no shell
|
||||
parsing** — it takes the literal remainder of the line — while bash `source`
|
||||
strips one quoting layer. So `WARM_TEXT="Warm up, one two three."` reaches the
|
||||
container *with its quotes*, and a script that interpolates it into a JSON body
|
||||
produces invalid JSON: the container boots, `active`, every voice registered,
|
||||
every voice cold. Fix in the consumer: strip exactly one matched pair **only**
|
||||
when both ends are the same quote AND the inner content carries another quote or
|
||||
backslash (never mangle a value that legitimately contains quotes), then escape.
|
||||
3. **A build-skip hash that under-covers the image.** `build_compose_image`-style
|
||||
helpers skip `podman build` when a label hash matches; hashing the `Containerfile`
|
||||
alone means editing a `COPY`'d app/script rebuilds **nothing** — deploy reports
|
||||
success, `/health` answers, container serves old code. Each case arm must name
|
||||
the paths its `Containerfile` copies (`extra=(…)`) and hash Containerfile+extras.
|
||||
Arms that have no extras keep their old hash, so they do not needlessly rebuild.
|
||||
|
||||
## Test it as behaviour, never as a grep of the handler
|
||||
|
||||
- Drive the **real** function: `awk '/^build_compose_image\(\) \{/,/^\}/' deploy-remote.sh > bci.sh`,
|
||||
`source` it, put a stub `podman` first on `PATH`, assert the observable question —
|
||||
*did the deploy ask podman to build, or did it skip?*
|
||||
- Stub traps that cost real time: podman is invoked `podman image inspect <image> --format …`
|
||||
so the tag is **`$3`**, not `$2`; tags contain `/` so a state file named after one
|
||||
needs `${tag//\//_}` or the write silently lands in a nonexistent directory and
|
||||
every image looks permanently unbuilt; `--label cfhash=…` arrives **before** `-t <tag>`,
|
||||
so collect the label and write it after the arg loop.
|
||||
- For an env-file bug, write one test that reads the file **both** ways — `source`
|
||||
and the `--env-file` way (parse `KEY=literal remainder`) — and assert the program
|
||||
receives the same value in both shapes, including a value that carries its own
|
||||
quotes and a backslash. A suite that only sources the file is how the bug shipped.
|
||||
- Prove red: `git show HEAD:scripts/deploy-remote.sh > scripts/deploy-remote.sh`,
|
||||
run the new test (must fail at the exact intended step), restore. Keep each fix in
|
||||
its own commit and run the seam at each one so the branch bisects.
|
||||
|
||||
## Base-image and host facts to check while auditing
|
||||
|
||||
- `useradd` lives in `login(1)` (base carries it); `adduser` does not.
|
||||
- Rootless podman **cannot raise rlimits** — `--ulimit nofile=…` above the host hard
|
||||
limit dies at container create, not at runtime.
|
||||
- A referenced `Network=` `.network` file the generator cannot find means the
|
||||
container unit is **not produced at all**.
|
||||
- Two containers talk by DNS name only if they attach to a **common** network;
|
||||
check both unit files rather than trusting a comment that says they share one.
|
||||
- Container DNS name + the network that owns it, never `host.containers.internal`
|
||||
or a published loopback port, for service-to-service traffic.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not deploy the audited thing because the audit is finished: first build of a
|
||||
heavy image (recursive source clone ~10 min) plus a new unit on a host serving
|
||||
people is the operator's call.
|
||||
- Do not add a Prometheus target for a container with no `/metrics` — say the probe
|
||||
is a decision (blackbox HTTP), do not fake a scrape.
|
||||
@@ -1,82 +1,32 @@
|
||||
---
|
||||
name: bounding-subagent-runtime
|
||||
description: "Cap and diagnose runaway or hung omp subagents: task.maxRuntimeMs, softRequestBudget, agentIdleTtlMs, per-agent model overrides, the cfg save-approval quirk, and the finished-work-but-never-exits nested-fanout deadlock."
|
||||
description: "Cap and diagnose runaway or hung omp subagents: task.maxRuntimeMs, softRequestBudget, maxConcurrency, maxRecursionDepth (the 0-trap), per-provider in-flight ceilings."
|
||||
---
|
||||
|
||||
# Bounding subagent runtime (omp harness)
|
||||
## When
|
||||
Subagent runs long, fan out too wide, or you must (re)declare the fleet's subagent limit policy. Also when asked "what limits can subagents have".
|
||||
|
||||
Use when a `task`/`eval` subagent runs absurdly long (tens of minutes on a small job),
|
||||
when it **finishes its deliverable and still never exits**, when you want a hard cap
|
||||
before spawning, or when you must tell a busy agent to stop.
|
||||
## Read first
|
||||
- Repo KB page `omp-docs/pi/subagent-limits.md` — the live policy of record (values + rationale + deliberately-not-set list). Do not re-derive; read it.
|
||||
- `cfg://task` and `cfg://providers/maxInFlightRequests` — effective values, not the stock defaults.
|
||||
|
||||
## The knobs (`cfg://task/...`)
|
||||
|
||||
| Key | Default | Meaning |
|
||||
## The knobs (cfg:// writes; `/save` suffix persists)
|
||||
| Knob | Effect | Gotcha |
|
||||
|---|---|---|
|
||||
| `maxRuntimeMs` | `0` (unlimited) | **Hard wall-clock limit per subagent (ms).** The only true time cap. |
|
||||
| `softRequestBudget` | `200` | Soft cap on assistant **requests** per run; force-stop at **1.5×**. |
|
||||
| `softRequestBudgetNotice` | `true` | Injects one "wrap up" steering message at the soft mark. |
|
||||
| `agentIdleTtlMs` | `420000` | Parks an **idle** agent after 7 min. Useless against a busy-but-slow agent. |
|
||||
| `completionProbe` | `true` | Asks a working subagent to self-report completeness at 2/5/10 requests. |
|
||||
| `maxConcurrency` | `32` (often lowered) | Concurrent subagents; sizes GPU/endpoint load. |
|
||||
| `agentModelOverrides` | `{}` | Per-agent model routing, e.g. `{"scout":"provider/model-id"}`. |
|
||||
| `tools.maxTimeout` | `0` | Caps the timeout the agent may set on a **tool**, not on the agent. |
|
||||
| `task.maxConcurrency` | session semaphore; excess queues | stock default 32 — "2" is not a default |
|
||||
| `task.maxRecursionDepth` | nesting cap | predicate is `depth < n`; **1 = children cannot spawn (correct for "no nesting"); 0 disables spawning for the MAIN session too — never reach for 0**; negative = unlimited |
|
||||
| `task.maxRuntimeMs` | hard wall clock, overrun = `aborted` | default 0 = off |
|
||||
| `task.softRequestBudget` | wrap-up notice at N, force-yield at 1.5N | scout/sonic may impose lower built-ins |
|
||||
| `task.agentIdleTtlMs` | park idle finished agents | `<= 0` disables parking |
|
||||
| `providers.maxInFlightRequests` | concurrent LLM requests per provider id | keys are the quoted block labels from `agent/models.yml` (e.g. `"llamacpp (dankclaw-3090)"`), flat map lookup; absent/≤0 = unlimited; scope = this machine's config root only — other workstations and non-omp clients are NOT counted; excess queues |
|
||||
| `tools.maxTimeout` | ceiling on any agent-requested tool timeout | kills `timeout: 0` runaways |
|
||||
| `task.isolation.enabled` | makes per-spawn `isolated` flag available | opt-in per spawn, not a blanket sandbox; needs git repo; off in plan mode |
|
||||
| `task.completionProbe` | periodic completeness side-requests | token sink on shared endpoints |
|
||||
|
||||
Reasonable fleet defaults: `maxRuntimeMs = 900000` (15 min), leave the request budget
|
||||
alone unless the failure mode is a looping search agent (then lower it — request count,
|
||||
not the clock, is what bounds a 200-call research loop).
|
||||
## Verify semantics from the bundle, not the label
|
||||
Setting descriptions lie by omission. `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js`:
|
||||
- depth gate: `grep -o '.\{130\}xC\b.\{130\}'` finds `U_(n,s)`; `grep -o 'function U_(e,t){.\{0,60\}'` → `e<0||t<e` (n=cap, s=taskDepth).
|
||||
- in-flight lookup: `grep -o '.\{150\}axInFlightRequests.\{200\}'` → flat `[providerId]` lookup, non-number/≤0 = unlimited, clamp ≥1.
|
||||
|
||||
**Verify the cap instead of assuming it.** `read cfg://task/maxRuntimeMs` at the start of
|
||||
any session that plans to rely on it. A session-only write is gone next session, and an
|
||||
unanswered `/save` approval means the value never persisted — so the fleet can run hours
|
||||
with the cap at `0` while everyone believes it is 15 minutes. Nothing else bounds a child.
|
||||
|
||||
## Three failure modes, three different fixes
|
||||
|
||||
- **Runaway loop** (hundreds of sequential tool calls): lower `softRequestBudget`; the
|
||||
clock cap only truncates it late.
|
||||
- **Slow model, legitimate work** (dozens of calls on a low-t/s endpoint = an hour of wall
|
||||
clock): fix routing, not the clock — check `cfg://task/agentModelOverrides` and move the
|
||||
agent class onto a faster/shared endpoint. Watch the slot math: subagent endpoints share
|
||||
KV slots with the interactive session, so `maxConcurrency + 1` sequences on an N-slot
|
||||
endpoint degrades everyone's decode.
|
||||
- **Finished work, no exit (nested fan-out deadlock).** A subagent that spawned **its own**
|
||||
children blocks on their barriers. If those children die out-of-band (killed, starved, or
|
||||
waiting on an answer whose subject changed mid-flight), the parent waits forever with its
|
||||
deliverable already written. No instruction text prevents this — not the repo AGENTS.md,
|
||||
not the task prompt; only the clock cap or a kill ends it.
|
||||
|
||||
Signature: parent and children report **the same elapsed wall clock**, and the parent's
|
||||
on-disk output is complete. Census (agent registry + `ps`): group by elapsed time, look
|
||||
for identical `etimes` across a parent/child set, then judge the **artifacts** instead of
|
||||
the process — balanced code fences, no truncated tables, trailing newline, front matter or
|
||||
the page's H1+status convention intact. Complete artifacts + still "running" = deadlock,
|
||||
not work.
|
||||
|
||||
Repair: kill the **subtree** and confirm the children leave (a killed parent does not
|
||||
cancel children that came from a separate `tasks[]` batch). Then **flatten**: the
|
||||
top-level session does the fan-out and children never spawn children. Re-run only the
|
||||
slices whose artifacts are missing — integrate from what is already on disk.
|
||||
|
||||
## cfg write mechanics (bit me once)
|
||||
|
||||
- `write cfg://task/maxRuntimeMs 900000` → applies **session-only**, replies "NOT saved".
|
||||
- Persist = `write cfg://task/maxRuntimeMs/save` with the same content, and that write
|
||||
needs its **own interactive approval**. If the approval prompt is not answered in time
|
||||
the value stays session-only and the tool says *NEVER retry unless the user asks* —
|
||||
report it in the reply instead.
|
||||
- Revert a session value by writing the old value back (e.g. `0`).
|
||||
|
||||
## Order of operations when one is already running late
|
||||
|
||||
1. `read proc://` → registry of running agents; `read proc://<id>` for status/output.
|
||||
2. `read agent://<id>` → live progress ("Latest assistant text") — often shows it is
|
||||
mid-batch, not hung. If the latest text is already a wrap-up/summary, treat it as
|
||||
**done** and collect its output instead of waiting for an exit event.
|
||||
3. `write agent://<id>` an explicit wrap-up order: stop searching, answer what is
|
||||
sourced, mark the rest `NOT VERIFIED — needs <what>`, cap the report length.
|
||||
4. Only then `write proc://<id>/kill` — the cap does **not** retroactively bind agents
|
||||
spawned before the change, so a long runner must be ordered or killed, never just
|
||||
re-configured. After any kill, re-read the registry: unreaped children and orphaned
|
||||
host-side processes (a scratch server holding GPU/RAM) are the expensive residue, and
|
||||
`pkill -f` patterns that also match production services are forbidden — kill by pid.
|
||||
## Landing a policy change
|
||||
cfg `/save` writes persist immediately; `agent/config.yml` is machine-live tier (heartbeat commits it). Human-facing policy changes land as an `omp-docs` page update via branch → PR (docs tier), `git worktree add` from `origin/main` if the current checkout is branch-locked by workspace policy.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: corpus-voice-skill-rewrite
|
||||
description: "Rewrite a skill to emulate an author corpus's voice: full read, style-DNA distillation from writing-for-agents levers, positive-phrasing pass, sharpened completion criteria"
|
||||
---
|
||||
|
||||
# Corpus-voice skill rewrite
|
||||
|
||||
Rewrite a skill so it reads as if a chosen author wrote it. Done for `grilling` against the `mattpocock/skills` corpus (2026-10-18, command-center PR #20); behaviour preserved, voice swapped.
|
||||
|
||||
## Where the corpus lives
|
||||
|
||||
Installed third-party skills: `~/.agents/skills/<name>/SKILL.md` (the skills-CLI global store; `~/.agents/.skill-lock.json` names sources). Whole mattpocock corpus ≈ 2,800 lines — small enough to read inline in ~4 `cat` batches via bash; do NOT delegate the reading, style lives in the exact prose.
|
||||
|
||||
Read the author's own meta-skill FIRST if one exists (`writing-for-agents` for mattpocock — it states his levers explicitly), then the corpus practicing them.
|
||||
|
||||
## Style DNA checklist (mattpocock; generalise per author)
|
||||
|
||||
- **Leading words**: compact pretrained concepts repeated as tokens, never sentences (_red_, _seam_, _fog of war_, _brief_, _frontier_). Define each once, reuse everywhere. Prefer words already in the author's other skills so the corpus shares one language.
|
||||
- **Vocabulary-first**: bolded glossary terms, then used exactly; explicit "_Avoid_: component, service".
|
||||
- **Positive phrasing**: prohibitions rewritten as target behaviours; keep a prohibition only when no positive form exists (hard guardrails).
|
||||
- **Sharpened completion criteria**: fuzzy bounds ("understanding reached") → checkable + exhaustive ("frontier empty and nothing sits blocked", "one artefact per question, counted before sending").
|
||||
- **Zero history**: no provenance, attribution, or absorbed-from lineage in the body.
|
||||
- **No-op hunting**: delete harness defaults and anything the environment already confesses (formats, rendering, caps). Gotchas the environment CANNOT confess (exclusive dialogs, Esc-abort semantics) stay inline — they're the cache worth keeping.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. Read entire corpus; tabulate levers with per-skill evidence before writing.
|
||||
2. Rewrite preserving settled behaviour 1:1 — voice only; diff the two versions for semantic drift.
|
||||
3. Audit pass: grep the new text for history words (extracted/absorbed/from/upstream), machine-specific constants, repo-specific paths.
|
||||
4. Run repo checks, commit as `style(<scope>)`, note the lever→instance mapping in the PR comment so the reviewer can grade the emulation.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: dankvoice-bats-serialization
|
||||
description: "Run dankvoice bats suites without hangs: serialize builds on shared dist/, DV_HOLD is seconds, stub needs sleep&wait trap before hold, timing tests assert CALLS/at not tail PLAYS."
|
||||
---
|
||||
|
||||
# Dankvoice bats: serialization and hang-proofing
|
||||
|
||||
## Serialize builds on the shared artifact
|
||||
`tests/behavior.bats` setup runs `bun run build` into the repo's shared `dist/index.js`
|
||||
before EVERY test (`DV_EXT="$REPO/dist/index.js"`). Any second concurrent bats run,
|
||||
`bun run build`, or `bun run typecheck` racing the same repo contends on that file and
|
||||
can stall single tests past 60 s. NEVER run two bats processes (or bats + build) against
|
||||
`~/Projects/dankvoice` at the same time — including harness background jobs. If a suite
|
||||
gets backgrounded, kill it before launching any other repo command; re-run serially.
|
||||
|
||||
## The stub player's units and traps
|
||||
- `DV_HOLD` is in **seconds** (`sleep "${DV_HOLD:-0.1}"`), not ms. `DV_HOLD=1500`
|
||||
means a 25-minute "hung" test. Use `DV_HOLD=2` for multi-second holds.
|
||||
- The stub must sleep as `sleep "${DV_HOLD:-0.1}" &` + `wait $!`, with
|
||||
`trap 'printf "KILLED\n" >> "$DV_STUB_LOG"; exit 143' TERM` installed **before** the
|
||||
sleep: a foreground `sleep` defers the trap until the sleep finishes, so SIGTERM-kill
|
||||
tests (shutdown/interrupt pins) stay red and every kill test eats the full hold.
|
||||
- Keep the `cp *.wav "$DV_COPY"` loop when editing the stub — the byte-equality test
|
||||
depends on it.
|
||||
|
||||
## Timing-sensitive assertions
|
||||
The driver prints `PLAYS` from the stub log at scenario end (~800 ms settle), so with a
|
||||
long `DV_HOLD` in-flight playback is UNDERCOUNTED — assert `CALLS`/arrival timestamps,
|
||||
not tail `PLAYS`. The fixture stamps every request with `at=` (ms since driver start,
|
||||
comparable between CALL lines); prefetch overlap = `t2 - t1 < 1500` with `DV_HOLD=2`
|
||||
(sequential chain cannot answer inside 2000 ms).
|
||||
|
||||
## Diagnosing a hung bats test (deterministic recipe)
|
||||
1. `timeout 45 bats -f "<name>" tests/behavior.bats; echo rc=$?` — rc=124 confirms hang.
|
||||
2. Launch the same test as a background job, then probe live:
|
||||
`pgrep -af "driver.ts|bats"` and `ps afx | grep -E "sleep|stub"` — a `sleep <N>` child
|
||||
with N seconds in the stub log names the culprit (usually DV_HOLD misuse).
|
||||
3. Detached `(cmd &)` inside a temp shell is KILLED when the shell exits — use the
|
||||
harness background jobs for anything you must probe.
|
||||
4. Direct driver repro: extract the heredoc with
|
||||
`sed -n '/cat > "$WORK\/driver.ts" <<.TS./,/^TS$/p' tests/behavior.bats | sed '1d;$d' > $W/driver.ts`,
|
||||
build a fresh ext, then run `bun $W/driver.ts <scenario>` with the env the setup block
|
||||
exports. Caveat: hand-made fixture env can yield different segment counts than bats —
|
||||
trust in-bats numbers for assertions.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: docs-dedupe-verify-campaign
|
||||
description: "Run an exhaustive docs dedupe + fact-verification campaign in dank/command-center: scout cluster ledgers, override scouts with evidence, gate every commit, and sweep scaffolding before PR."
|
||||
---
|
||||
|
||||
# Exhaustive docs dedupe + verification campaign (dank/command-center)
|
||||
|
||||
## Frame
|
||||
- Read `omp-docs/references/okf-spec.md` + root `omp-docs/index.md` first; inventory with `find omp-docs -name '*.md' | wc -l`.
|
||||
- Run `scripts/kb-lint.sh omp-docs` baseline; index-coverage gate is `scripts/kb-coverage.py omp-docs` (each dir's `index.md` must link every sibling file/subdir).
|
||||
- Branch off fresh `origin/main` in a NEW worktree (`git -C ~/.omp worktree add ~/.omp/wt/<name> -b docs/<issue>-slug origin/main`) — workspace policy blocks `git checkout -b` in session worktrees.
|
||||
- Open the Gitea issue first (multi-moving-parts law); branch name `docs/<issue#>-<slug>`.
|
||||
|
||||
## Scout fleet (overlap detection)
|
||||
- One `task` call, `tasks[]` batch, agent `scout`, read-only; one cluster per task (skills trees, llama trees, pi vs packages, niri, sleep/power, core, gitea, gitea+cross-cutting sweep). Cap 2 concurrent; queue rest.
|
||||
- Ledger format per cluster → `local://dedupe/<cluster>.md`: inventory table (file | lines | verdict KEEP|MERGE|DELETE|VENDOR|UPDATE), overlaps, facts-to-keep, inbound links, staleness.
|
||||
- **Scouts lie.** Verified misses in the 2026-10-19 campaign: fabricated ledger entries ("peer-reviewed BrowseComp" claim that exists in no file), a DELETE verdict for a generated artifact that is checked in by design AND under test (`tools/niri/keybindings.md`, see `scripts/gen-keybinds.sh` + `tests/gen-keybinds.bats`), and a KEEP for a leftover the scout misread its own changelog about. NEVER apply a scout verdict without re-checking the file, the tests, and the changelog yourself.
|
||||
|
||||
## Fact verification
|
||||
- Fleet facts: read-only probes over `ssh -o BatchMode=yes dankclaw|dankserver` (podman ps/inspect/top, stat, /etc/os-release, curl localhost APIs, `podman exec <c> <tool> --version`). Live state beats docs even when docs claim recent verification — the 2026-10-18 "audit" still drifted (zealot 27B→9B, build 10754→10777, container census 14→32).
|
||||
- External claims: web_search/web read yourself (no subagents per Frank's rule). Kill fabrications outright (invented distro `FreeToken`, nonexistent `ornith-llama-cpp-setup` tree, unattributable "44.5% peer-reviewed CUA") — delete/drop with an explicit note, never keep "plausible" numbers.
|
||||
- Policy drift (live ≠ tuned docs): flag in the doc + PR body as an operator decision; NEVER silently re-apply policy.
|
||||
|
||||
## Log compaction (log.md)
|
||||
- Entry law: dated entry = what changed + durable lesson + concept-page pointer; narratives recoverable via `git log --follow`. Record the law at the top of the log. Tag historical-era entries.
|
||||
|
||||
## Gates + landing
|
||||
- Gate every commit: `bash scripts/kb-lint.sh omp-docs && python3 scripts/kb-coverage.py omp-docs && bats tests/`. Pre-commit hook prints `error: command not found: caddy` on Caddyfile-touching commits — warning only, commit still lands.
|
||||
- Before opening the PR: `git diff --stat origin/main..HEAD | grep -i 'tmp\|scratch\|\.bak'` — scaffolding merged under a `.tmp-` name in the 2026-10-19 campaign and needed a follow-up PR to promote.
|
||||
- PR via API (skill `gitea-api-pr-operations` for token-extraction + merge payload); `xdg-open` the PR the moment it opens; comment the campaign ledger on the issue.
|
||||
- After Frank's merge OK: propagate deployment `git -C ~/.omp fetch && git merge --ff-only origin/main`; reap campaign worktree (`git worktree remove --force`) + delete branch local/remote; close issue with landing comment.
|
||||
|
||||
## Line-edit op syntax trap (bit three times)
|
||||
- Valid ops: `PUT N.=M:` (body `+text` lines), `CUT N.=M` / `CUT N.=M @reg`, `PUT <N @reg`, `PUT >N:` / `PUT <N:`. There is NO `PUT N+=:` form — it lands in the file as a literal garbage line and shifts the insert to the wrong position. For gap-insert use a lone `PUT >N:` or `PUT <N:` per call; pre-compute line numbers against the file map (ops reference original lines within one call, but a failed op poisons positions).
|
||||
- Recovery recipe when a literal op line lands: read exact bounds, `CUT N.=N` (junk), `CUT A.=B @block` (misplaced section), then `PUT <M @block` at the true anchor, re-read tail, add spacing blank if a `---` now abuts a list line.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: docs-point-of-use-destruction
|
||||
description: "Documentation exists for point of use, not archival. Use before any dedupe/compact/verify-docs job, and whenever a census or \"only copy\" argument is being used to KEEP documentation: apply the purpose test and destroy obsolete measurements. Supersedes the preservation conclusions of vault-one-fact-one-owner-dedupe §3/§12."
|
||||
---
|
||||
|
||||
# Documentation exists for point of use
|
||||
|
||||
**Supersedes** `vault-one-fact-one-owner-dedupe` §3 ("port before cut") and §12
|
||||
("expect docs flat") wherever those conclude a page must be *kept*. Sole custody of a
|
||||
useless fact is still useless.
|
||||
|
||||
## The test, before you write and again before you keep
|
||||
|
||||
- Would the reader, doing a real task tomorrow, **act differently** because of this
|
||||
line? No → delete it.
|
||||
- Is it a measured rate (t/s, tok/s, ms, MiB-of-pool, acceptance %, a bench ladder) for
|
||||
a model / runtime / layout that is **not running now and not being compared now**?
|
||||
Destroy it. Not "file it in an Eras row", not "preserve it for history". AI moves
|
||||
faster than the vault: the number is wrong before anyone reads it, and a wrong number
|
||||
in a runbook gets believed.
|
||||
- Keep the **law** the measurement taught; drop the measurement. Qualitative causal
|
||||
stories survive; numbers do not.
|
||||
- What survives: live config and its exact numbers, procedures, failure signatures,
|
||||
security posture, interface contracts, rollback pointers.
|
||||
- **Losing information is an acceptable, usually correct outcome.** Frank said so
|
||||
explicitly on 2026-10-19 after rejecting a PR that had preserved 53 ledgers.
|
||||
|
||||
## Traps that lead agents to preserve junk
|
||||
|
||||
- **A census cannot decide what to keep.** `port-census.py`-style tools answer "where
|
||||
does this fact live", never "does anyone need it". "14–63 sole-record lines per page"
|
||||
was read as *irreplaceable* when the fact was *obsolete*. Ask usefulness, not custody.
|
||||
- **A census that includes the file under test proves the page is held by itself.**
|
||||
Exclude it, print the corpus size, assert absence; treat `0 unheld` on a long page as
|
||||
a tool bug until proven otherwise.
|
||||
- **"One fact, one owner" is a placement rule, not a preservation rule.** It says where
|
||||
a surviving fact lives; it never says the fact must survive.
|
||||
- **A change diary (`log.md`) is the largest archive in most vaults** (this one: 2,380
|
||||
lines, ~8 % of the vault). Git holds the chronology; a causal law belongs on the page
|
||||
that owns the thing it governs. Retire the diary.
|
||||
|
||||
## Execution that worked (2026-10-19, −13.7k lines)
|
||||
|
||||
1. Write the kill list as a reviewed artifact, one path per line, grouped by *why it is
|
||||
dead* — that list becomes the PR's argument and the trash manifest.
|
||||
2. Destroy whole pages first (`scripts/destroy-vault.py <list>`): move to
|
||||
`.AGENTTRASH/<date>-destruction/`, then delete every reference from survivors, whole
|
||||
line included when the dead link was its only content. Do not compare link targets
|
||||
with `pathlib.Path.stem` — it strips real suffixes (`sd.cpp`).
|
||||
3. Then strip numbers from what remains (`scripts/axe-measurements.py`): delete ledger
|
||||
sections (noun-LEDS the heading, nothing instructional after it), blank any table cell
|
||||
reporting a rate, fold Eras older than the current one into a single row pointing at
|
||||
git. Never touch a shape number or a Laws/Wire/Rollback section.
|
||||
4. Sweep indices (generator must exclude the trash dir, drop rows whose page cell became
|
||||
empty), re-lint, re-check links.
|
||||
5. Rewrite the governing policy in the same PR, or the next agent re-grows the vault:
|
||||
project `AGENTS.md` *Documentation Policy* + the global agent settings.
|
||||
|
||||
## Report honestly, per area
|
||||
|
||||
docs / scripts / operator-doc separately, measured against the merge-base. The tools are
|
||||
a cost that must be named, not hidden inside a headline "net" number — that is what got
|
||||
the previous pass rejected.
|
||||
|
||||
## Verify the repair tools themselves
|
||||
|
||||
One repair pass matched "line ends with a comma" and deleted the trailing comma from 660
|
||||
wrapped prose lines. Restore by matching lines that are byte-identical to `HEAD` except
|
||||
for trailing punctuation (`restore-punctuation.py`), keep the destructive rule out of the
|
||||
tool, and record the incident in its docstring.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: docs-refresh-scout-summaries
|
||||
description: "Refresh a stale documentation cluster without delegating the writing: scout subagents produce per-line verdict tables (holder verified by comma-insensitive grep), the parent makes every edit, and a port-before-cut census proves nothing was lost. Use for dedupe-our-docs, fix-the-stale-runbooks, pointers-instead-of-copied-numbers jobs."
|
||||
---
|
||||
|
||||
# Refresh stale docs with scout summaries, parent-side writes
|
||||
|
||||
Use when a cluster of documentation pages is stale and/or restates facts another page owns, and
|
||||
the fix touches many files. **Scouts read and classify; the parent writes.** Sibling skill
|
||||
`vault-one-fact-one-owner-dedupe` covers collapsing era pages into owner pages - its section 3
|
||||
(orphan census), section 7 (edit-tool hazard) and section 9 (verify/PR) apply unchanged.
|
||||
|
||||
## Why not writer subagents (measured cost)
|
||||
|
||||
Writer agents produce plausible output and cannot be trusted on the one claim that matters - the
|
||||
owner page already holds this fact. In one run they asserted every cut was grep-confirmed while
|
||||
FOUR cuts left pointers to pages that did not contain the fact (118->238 GiB, 16,095 tokens,
|
||||
0.739/0.554/0.427/0.340, PR #23897). Each became a broken promise the next reader trusts.
|
||||
Classification is the only part that parallelizes; writing is where taste (ownership, era
|
||||
naming, what counts as a law) lives. So fan out read-only scouts and keep the pen.
|
||||
|
||||
If writer agents were already spawned and are no longer wanted: `write agent://all` to stop
|
||||
them, then `git status` the target tree - `proc://` does not list agent jobs, and a finished
|
||||
worker may already have edited files.
|
||||
|
||||
## Pipeline
|
||||
|
||||
1. **Cluster** targets into groups of 3-4 pages sharing subject matter (one cluster per scout).
|
||||
Know the owner map first (endpoint/service/host pages, plus the repo documentation law).
|
||||
2. **Wave of 2 scouts** (harness cap is `task.maxConcurrency`, usually 2). Scout contract:
|
||||
|
||||
> READ-ONLY, no writes of any kind. Per file, one table row per candidate fact:
|
||||
> `| lines | verbatim figure + 6 words | holder elsewhere (file:line) or none | VERDICT |`
|
||||
> Verdicts: `CUT -> [[Owner]]` (holder verified), `KEEP(owned here)` (no holder, this page is
|
||||
> the record), `PORT(parent)` (rightful owner lacks it - name page and section), `STALE`
|
||||
> (contradicted by current state; quote the contradicting line), `INDEX` (locator page).
|
||||
> Verify every holder claim with a COMMA-INSENSITIVE grep: a holder writing 1,350,182
|
||||
> satisfies a page writing 1350182 and vice versa; 16,095 does NOT satisfy a check for 16095
|
||||
> unless commas are stripped. Never name a holder line you have not read.
|
||||
> End with a per-file tally: kept / cut / port / stale.
|
||||
|
||||
Put the law, the owner map and the verification rule in the batch-level `context`, never per
|
||||
task.
|
||||
3. **Write while the next wave runs**: edit cluster N from wave N, launch wave N+1 in the same
|
||||
turn. Never launch a wave you have nothing to write.
|
||||
4. **Verify each CUT yourself** before cutting - read the holder line. A scout row is a
|
||||
hypothesis, not evidence.
|
||||
5. **PORT items are your edits**: add the fact to the owner page section (Eras row / Laws / spec
|
||||
table), then cut it from the technique page in the same commit.
|
||||
6. **STALE items: check live state**, not another document - `ssh <host> systemctl is-active
|
||||
<svc>`, the endpoint `/health`, the env file. Then rewrite with a date, or drop transient
|
||||
live state an owner page already records.
|
||||
|
||||
## The ownership test that stops wasted ports
|
||||
|
||||
Two pages carrying one figure is the defect - a figure with exactly one holder is fine wherever
|
||||
it lives. A technique page that is the sole record of a campaign owns that campaign's arm
|
||||
tables; do not port such ledgers into an endpoint Eras table. Write the clause into the repo
|
||||
documentation law so the next pass stops porting single-owner facts. Corollary: a worked example
|
||||
may repeat a live number if it declares itself dated and names the owner (a snapshot, not fleet
|
||||
state - recompute from the live files).
|
||||
|
||||
## Census after each cluster (catches your own slips too)
|
||||
|
||||
Run the sibling skill's section 3 census over `git diff -U0` for the cluster (deleted lines x
|
||||
distinctive numeric keys vs the whole vault **plus AGENTS.md**), comma-insensitively; drive the
|
||||
count to zero or list each accepted loss in the commit message.
|
||||
|
||||
```python
|
||||
flat = corpus.replace(',', '')
|
||||
pres = lambda n: n in corpus or n.replace(',', '') in flat
|
||||
toks = lambda l: [n for n in re.findall(r'\d[\d,]*(?:\.\d+)?', l)
|
||||
if len(n.replace(',', '').split('.')[0]) >= 4
|
||||
or (len(n.split('.')[-1]) >= 2 and len(n.replace(',', '')) >= 5)]
|
||||
orphan = [l for l in deleted if toks(l) and not any(pres(n) for n in toks(l))]
|
||||
```
|
||||
|
||||
## Verify and land
|
||||
|
||||
- Vault link seam must print `0 unresolved links`; hard-wrapped `[[Two Word Page]]` links split
|
||||
across lines are the usual cause - unwrap them.
|
||||
- Full suite once at the end, not per cluster; a docs seam running while scouts are live may
|
||||
read half-written files, so re-run after the last edit.
|
||||
- Commit per cluster (docs-only means **no deploy**); push, open the PR through the API with a
|
||||
600-perm curl config, `xdg-open` the PR URL immediately.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: fleet-service-runtime-census
|
||||
description: "Answer \"what is service running right now?\" for a dank-fleet service: live endpoint probe plus host units, image ids, live-vs-repo env, and drop-ins."
|
||||
---
|
||||
|
||||
# Fleet service runtime census
|
||||
|
||||
Answer the question "what is `<service>` running currently?" with evidence, not from docs. Docs say what *should* run; the host says what *does*. Run all five probes, then report one table.
|
||||
|
||||
## 1. Probe the published port (works from any LAN box, no ssh)
|
||||
|
||||
```bash
|
||||
curl -s -m 10 http://<host-ip>:<port>/health; echo
|
||||
curl -s -m 10 http://<host-ip>:<port>/v1/audio/voices # per-endpoint extras: /v1/models, /metrics
|
||||
```
|
||||
|
||||
The extra route matters more than `/health` for identity claims:
|
||||
- llama.cpp and radiance **echo the requested model name** — a 200 proves nothing; read `/v1/models`.
|
||||
- halogen 404s `/completion`, `/ping`, `/props`, `/tokenize`.
|
||||
- The frank-tts façade answers `/health` **without dialing the engine**, so "façade healthy, engine down" is a real state; the voice list in that same response is the honest census.
|
||||
|
||||
## 2. Units + containers on the host (ssh as the deploy user, no sudo)
|
||||
|
||||
```bash
|
||||
ssh <host> 'systemctl --user status <svc>.service <engine>.service --no-pager \
|
||||
| grep -E "^(●| Active| Loaded)"; \
|
||||
podman images --format "{{.Repository}}:{{.Tag}} {{.ID}} {{.Created}}"; \
|
||||
podman ps -a --format "{{.Names}}\t{{.Status}}\t{{.Image}}"'
|
||||
```
|
||||
|
||||
- `active (running) since …` + `Up N hours (healthy)` is the answer; `activating (auto-restart)` with a ~50 ms exit is a **preflight refusal**, not an engine fault.
|
||||
- **NEVER prefix these podman calls with `sudo`.** Containers live in the deploy user's rootless storage; `sudo podman inspect frank-tts-engine` returns `no such object` while the plain form succeeds. Treating that as "container missing" is a false finding.
|
||||
|
||||
## 3. Drop-ins — the override that no health gate sees
|
||||
|
||||
```bash
|
||||
ssh <host> 'systemctl --user show <svc>.service -p DropInPaths; \
|
||||
systemctl --user show <engine>.service -p DropInPaths'
|
||||
```
|
||||
|
||||
`DropInPaths=` must be empty. A leftover bench drop-in (`Environment=FRANK_<SVC>_ENV_FILE=/tmp/<arm>.env`) makes the next deploy boot a different model at a different shape while `/health` still answers 200 and the e2e gate passes.
|
||||
|
||||
## 4. Live env vs repo env — the only truth test
|
||||
|
||||
```bash
|
||||
# repo side (local checkout)
|
||||
grep -vE '^\s*#|^\s*$' scripts/<svc>.env
|
||||
# host side
|
||||
ssh <host> 'sudo cat /home/dank/serverAppConfigs/scripts/<svc>.env' | grep -vE '^\s*#|^\s*$'
|
||||
```
|
||||
|
||||
Diff them. Per repo law the env file beside the serve script **is** the authority; a doc page that disagrees with it is the bug, and host-side drift means an uncommitted edit. Also check `git log --oneline -3 -- scripts/<svc>.env` to confirm the host isn't ahead of git.
|
||||
|
||||
## 5. Report
|
||||
|
||||
Table of: unit state + since, image repo:tag@short-ID + age, backend/alias actually served (from step 1, not from the name), drop-ins, and whether live env == repo env. State explicitly when a fact came from docs rather than from a probe.
|
||||
|
||||
## Traps that produce false findings
|
||||
|
||||
- Digest-pinned images: the local image ID's *creation date* answers "is this stale" better than the tag; `latest` + `Pull=never` is a locally built image, not a registry pull.
|
||||
- `ssh` user context changes what podman sees (root vs rootless) — see step 2.
|
||||
- Two ports on one host are two providers in `models.yml`; a port mismatch is a rename or drift, not a config typo.
|
||||
@@ -1,125 +1,81 @@
|
||||
---
|
||||
name: frank-tts-add-cloned-voice
|
||||
description: "Add or repair a cloned voice on the dank fleet: /home/dank/voices/<name> pack, name rules, boot-warm registration, and the under-a-second proof"
|
||||
description: "Add or repair a cloned voice on the dank fleet: pack builder, the reference.wav contract, the boot warm hook (argv-cap and env-quote laws), and how to prove a voice really answers."
|
||||
---
|
||||
|
||||
Use for "give frank-tts a new voice", "voice X is missing/unknown", or "the first
|
||||
request of the day takes 5 s". Runbook of record: vault pages [[Frank TTS]] (voice
|
||||
packs, naming) and [[Frank TTS Engine]] (the warm registry). Everything below runs
|
||||
as the deploy user on **Dankclaw** (`ssh -o BatchMode=yes dankclaw '…'`); the
|
||||
containers are **rootless** (`systemctl --user …`, `podman …` without sudo).
|
||||
# Adding (or repairing) a cloned voice on the dank fleet
|
||||
|
||||
## Naming (the question that gets asked first)
|
||||
Repo law lives in `serverAppConfigs-docs/Dankclaw/Voice Packs.md` and
|
||||
`Frank TTS Engine.md` — read those for the pack format. This skill is the
|
||||
**operating sequence plus the traps that cost real time on 2026-10-18**.
|
||||
|
||||
A voice is addressed **by its directory name** — `voice=narrator1` on
|
||||
`POST /v1/audio/speech` (omp's TTS device may send `voice_id`, same value). Use:
|
||||
## Stack, fast
|
||||
|
||||
- `^[a-z0-9][a-z0-9._-]*$`, **lowercase**, ≤ 64 chars. No slash, no `..`, no
|
||||
space, no uppercase; must not start with `.`/`_`/`-`.
|
||||
- Why lowercase rather than just "alphanumeric": the shim accepts
|
||||
`[A-Za-z0-9][A-Za-z0-9._-]{0,63}` (`app.py`, `VOICE_NAME_RE`) but the **engine
|
||||
lowercases names**, so `Narrator1` resolves on one side and not the other. The
|
||||
boot warmer refuses a mixed-case pack loudly instead of half-working.
|
||||
- Names are user-visible: a 400 carries them in `available_voices`. Fleet
|
||||
convention is `narrator1`, `narrator2`, … for the open-notebook podcast
|
||||
speakers — open-notebook is configured with exactly `narrator1/2/3`, so renaming
|
||||
those breaks podcast rendering, not just TTS — and a lowercase personal name for
|
||||
a person's own voice.
|
||||
- `frank-tts-engine` — arm A `qwentts.cpp tts-server`, container-internal `:8880`,
|
||||
publishes **no port**. Reach it with `podman exec frank-tts-engine curl …`.
|
||||
- `frank-tts` — OpenAI façade on host `:7772`; resolves voice **by name**.
|
||||
- Packs: `/home/dank/voices/<name>/` with `reference.wav` (+ manifest, `.spk`,
|
||||
`.rvq`). Builder: `scripts/build-voice-pack.sh` (crop 19.84–25 s, first 10 s of
|
||||
any source is forbidden).
|
||||
- Weights: `/home/dank/models/Qwen3-TTS-GGUF/` —
|
||||
`scripts/download-frank-tts-engine-qwentts.sh [--verify-only]` (1.4 GiB, ~18 s
|
||||
on the LAN; verifies byte count + Hub LFS sha256).
|
||||
|
||||
## Pack layout (host state — never commit it)
|
||||
## Sequence
|
||||
|
||||
`/home/dank/voices/<name>/` is a voice **iff** it holds `reference.wav`:
|
||||
1. Build the pack (dry-run first — sizes are arithmetic and asserted).
|
||||
2. Deploy the engine only if its files changed:
|
||||
`./scripts/deploy.sh dankclaw --service frank-tts-engine`
|
||||
(first run builds the image: ~10 min recursive source clone).
|
||||
3. Register/warm: `podman exec frank-tts-engine /app/warm-voices.sh`
|
||||
— registers **every** pack each run (the engine replaces a name it already
|
||||
holds; `tools/tts-server.cpp:148`), then synthesizes twice per voice.
|
||||
4. Prove from the consumer side, not the config:
|
||||
`curl -s localhost:7772/v1/audio/voices` then a real
|
||||
`POST /v1/audio/speech` and listen to the bytes.
|
||||
|
||||
```
|
||||
reference.wav 24 kHz mono pcm_s16le, 18-25 s of clean single-speaker audio
|
||||
reference.spk engine x-vector: enc_dim 2048 f32 = 8192 B
|
||||
reference.rvq RVQ codes: 22 B per 12.5 Hz frame (5456-6864 B)
|
||||
manifest.json source path + sha256, crop start/end, engine commit, name
|
||||
```
|
||||
## Trap 1 — the kernel's argument cap (this broke the first deploy)
|
||||
|
||||
Build it; do not hand-assemble and never fabricate `.spk`/`.rvq` from a WAV:
|
||||
`/usr/bin/curl: Argument list too long` in the warm log ⇒ a request body was
|
||||
passed as **one argv string**. Linux caps a single argument at
|
||||
`MAX_ARG_STRLEN = 131 072 bytes`; a ~1 MB `reference.wav` base64's to ~1.27 MB.
|
||||
curl never runs, and hooks report it as `HTTP no response` while `/health` is
|
||||
green. Fix = write the body to a file and use `--data-binary @file`; feed the
|
||||
payload through a redirection, not `$(base64 …)`.
|
||||
Regression-test shape: **real** curl + real listener + a fixture >128 KB, so the
|
||||
kernel (not a stub) produces the failure. Any pack over ~96 KB trips it.
|
||||
|
||||
```bash
|
||||
scripts/build-voice-pack.sh --all /home/dank/voices # everything in VOICE_PACKS
|
||||
scripts/build-voice-pack.sh --source SRC --offset SEC --duration SEC --sha256 HEX NAME /home/dank/voices
|
||||
scripts/build-voice-pack.sh --dry-run ... # validate + crop, no extractor
|
||||
```
|
||||
## Trap 2 — two readers of the env file disagree about quotes
|
||||
|
||||
Knobs live in `scripts/voice-packs.env` (one `_SOURCE`/`_OFFSET`/`_DURATION`/
|
||||
`_SHA256` set per voice). Three things it enforces that an operator gets wrong:
|
||||
`bash set -a; source` strips `"…"`. podman's `--env-file` (how the unit really
|
||||
reads it) does **not**. A value interpolated into a JSON body therefore arrives
|
||||
quoted and invalidates it. Test both readers, not just `source`.
|
||||
|
||||
1. **The 10-second gate.** A crop starting inside the first 10 s of the source is
|
||||
refused before anything is written (project source-material law), and the
|
||||
manifest is re-checked against the source's sha256 afterwards.
|
||||
2. **The effective crop window is 19.84-25 s, not 18-25 s.** An 18.0 s crop is a
|
||||
legal `reference.wav` but yields only 22 × 225 = 4 950 B of `.rvq`, under the
|
||||
5 456 B floor the codec config implies — so the builder says so up front.
|
||||
3. **It never deletes.** An existing pack is refused unless `--force`, and
|
||||
`--force` moves the old directory to `$PACKS_DIR/.AGENTTRASH/<voice>-<UTC>/`.
|
||||
## Trap 3 — locally built images skip rebuilds on a stale hash
|
||||
|
||||
Files must be world-readable (the container runs as uid 10000): `644` files,
|
||||
`755` dirs. `mkdir -p /home/dank/voices` as the deploy user if it is absent —
|
||||
rootless podman auto-creates a missing bind source **root-owned**, and the
|
||||
endpoint then sees no voices at all.
|
||||
`build_compose_image` compares a `cfhash` label; it must cover every file the
|
||||
`Containerfile` `COPY`s, or an edited `app.py`/hook deploys "successfully" while
|
||||
the container runs old code.
|
||||
|
||||
## Register and warm (the part people forget)
|
||||
## Measured numbers (2026-10-18, deployed build — retire the trial's ladder)
|
||||
|
||||
The engine keeps its voice registry **in memory**, and the **first synthesis of a
|
||||
registered voice decodes the whole reference clip** to snapshot codec state:
|
||||
6.2-8.0 s, first audio byte at ~5 530 ms. Later requests restore in 0.5 ms.
|
||||
Re-registering does **not** warm — only synthesizing does. So:
|
||||
| thing | value |
|
||||
|---|---|
|
||||
| first synthesis of a freshly registered voice | 1 883 ms |
|
||||
| later short requests | 1 226–1 283 ms |
|
||||
| ~110-word request (repeat) | 9.4 s / 9.8 s — tracks generated audio, not state |
|
||||
| whole boot warm, 3 packs | 15.2 s |
|
||||
| engine resident | ~4.0 GiB |
|
||||
|
||||
```bash
|
||||
# registers every pack that has a reference.wav, then speaks the warm sentence once
|
||||
podman exec frank-tts-engine /app/warm-voices.sh
|
||||
systemctl --user --no-pager --full status frank-tts-engine.service | tail -20
|
||||
```
|
||||
The trial's `6.2–8.0 s cold → 0.5 ms warm` claim does **not** reproduce; warming
|
||||
is for registration (registry is memory-only, re-register ≠ warm) plus ~0.6 s.
|
||||
|
||||
The unit's
|
||||
`ExecStartPost=-/usr/bin/podman exec frank-tts-engine /app/warm-voices.sh` does
|
||||
the same at every boot, so a fresh boot is already warm; the explicit call above
|
||||
is for **adding one voice to a running engine** and needs no restart — a restart
|
||||
drops every registration and re-pays the cold cost for the whole library. The
|
||||
`podman exec` form is not cosmetic: the engine **publishes no port**, so there is
|
||||
no host address to curl.
|
||||
## Ops notes
|
||||
|
||||
## Proof (do not report "added" without it)
|
||||
|
||||
```bash
|
||||
curl -s localhost:7772/v1/audio/voices # name is listed
|
||||
time curl -s -o /tmp/a.wav localhost:7772/v1/audio/speech \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"model":"tts-1","voice":"NAME","input":"Checking the cloned voice.","response_format":"wav"}'
|
||||
time curl -s -o /tmp/b.wav localhost:7772/v1/audio/speech \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"model":"tts-1","voice":"NAME","input":"And the second request.","response_format":"wav"}'
|
||||
file /tmp/a.wav /tmp/b.wav # really audio
|
||||
```
|
||||
|
||||
The warmer's journal lines carry `voice NAME cold=…ms warm=…ms` — quote that pair.
|
||||
Note that streaming and buffered output differ at the same seed (92.2 % of samples,
|
||||
-41.6 dB RMS): never compare two paths or two "identical" renders, only that each
|
||||
is audio and that the second request is fast.
|
||||
|
||||
## Failure modes worth naming
|
||||
|
||||
- `400 unknown voice …; available voices: …` → the name is not a pack: wrong
|
||||
spelling/case, missing `reference.wav`, or `/home/dank/voices` root-owned/empty.
|
||||
Trust the 400's list over any manifest.
|
||||
- 400 with `(none: no voice pack with a reference.wav in /voices)` → mount problem,
|
||||
not a voice problem.
|
||||
- First request slow **after** a boot → the warm pass failed; read the unit's
|
||||
journal (the warmer exits 0 and logs when there is no voice directory, so
|
||||
silence there is not success either).
|
||||
- Endpoint answers but synthesis fails 502 → the façade must dial
|
||||
`http://frank-tts-engine:8880` over the shared bridge
|
||||
(`frank-tts-engine.network`); a `host.containers.internal` / `127.0.0.1:8880`
|
||||
upstream can never work between containers, because netavark DNATs published
|
||||
ports only.
|
||||
|
||||
## Never
|
||||
|
||||
Never write under `/voices` from the endpoint (mounted `:ro` by design), never
|
||||
`git add` a voice pack, never `rm` a pack (repo policy: move to `.AGENTTRASH/`),
|
||||
and never benchmark against this endpoint while it serves — stop and mask the
|
||||
units first.
|
||||
- `systemctl --user stop` **removes** the quadlet container — `podman start` then
|
||||
finds nothing. To run the engine without its `ExecStartPost` hook, use a
|
||||
separate container, not stop+start.
|
||||
- `podman cp` of a fixed script into a running container is a **trial only**: the
|
||||
layer dies with the container. Merge + redeploy to make it durable.
|
||||
- Nothing on the voice pair exposes `/metrics`; Prometheus can't scrape it (a
|
||||
blackbox HTTP probe on 7772 is the only honest option).
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: frank-tts-consumer-voice-triage
|
||||
description: "Triage a consumer (OpenWebUI, omp, open-notebook) rejecting or ignoring a frank-tts voice name: prove whether the server or the browser-side Kokoro.js engine produced the error."
|
||||
---
|
||||
|
||||
# frank-tts consumer voice triage
|
||||
|
||||
A consumer says a voice name is invalid, speaks in the wrong voice, or is
|
||||
silent. `frank-tts` (`192.168.0.143:7772`, shim
|
||||
`dankclaw/voiceservice/app.py`) resolves `voice=<name>` against
|
||||
`/home/dank/voices/<name>/reference.wav` and answers **400 + the list of
|
||||
voices that exist** for a name it cannot resolve — never a silent fallback.
|
||||
Any error naming a *different* voice list did not come from it.
|
||||
|
||||
**Signature that decides the case immediately:** an enum of ~29 names
|
||||
`af_heart, af_alloy, … am_*, bf_*, bm_* … bm_fable` is **Kokoro's fixed pack**,
|
||||
the voice list of **Kokoro.js running in the browser** (OpenWebUI ships it
|
||||
in its own front end as TTS engine value `browser-kokoro`, with a personal
|
||||
"Kokoro.js dtype" setting; model `onnx-community/Kokoro-82M-v1.0-ONNX`).
|
||||
`narrator1/2/3` are frank-tts cloned voices. Kokoro cannot clone.
|
||||
|
||||
## Instrument ladder (stop at the first that answers)
|
||||
|
||||
1. **What the endpoint actually accepts** (never infer from a consumer's text):
|
||||
`curl -s http://127.0.0.1:7772/v1/audio/voices` → JSON array of pack names.
|
||||
2. **Speak with the consumer's exact body, from inside the consumer's
|
||||
container** — same network, same shape. Write the JSON to a file and
|
||||
`--data-binary @file` (nested `sh -c` quoting silently sends an empty
|
||||
body and yields a bogus `400 malformed request body`):
|
||||
```
|
||||
printf '%s' '{"model":"frank-tts","input":"probe","voice":"narrator1","response_format":"mp3","speed":1.0}' > /tmp/p.json
|
||||
podman cp /tmp/p.json <consumer>:/tmp/p.json
|
||||
podman exec <consumer> curl -sS -o /tmp/o.bin -w 'http=%{http_code} type=%{content_type} bytes=%{size_download}\n' \
|
||||
-X POST http://host.containers.internal:7772/v1/audio/speech \
|
||||
-H 'Content-Type: application/json' --data-binary @/tmp/p.json
|
||||
```
|
||||
`200 audio/mpeg|audio/wav` ⇒ the server path is healthy; the failure is elsewhere.
|
||||
3. **Did the request arrive at all?** `podman logs --tail 400 <consumer> | grep -c audio/speech`.
|
||||
A server-path failure leaves a log line **and** a frank-tts error body; a
|
||||
client-side engine leaves **neither**. This contrast is the discriminator.
|
||||
4. **Where the rejection string lives** (proves the emitter without guessing):
|
||||
```
|
||||
podman exec <consumer> sh -c 'grep -ho ".\{200\}Should be one of.\{80\}" /app/build/_app/immutable/chunks/*.js | head'
|
||||
podman exec <consumer> sh -c 'grep -ho ".\{60\}af_heart.\{200\}" /app/build/_app/immutable/chunks/*.js | head -2'
|
||||
```
|
||||
Matched inside the front-end bundle ⇒ browser-side validator, not the API.
|
||||
5. **Config authority sweep** (OpenWebUI): admin config is the `config`
|
||||
table (one key per row, JSON `value`; expected
|
||||
`audio.tts.engine=openai`, `audio.tts.model`, `audio.tts.voice`,
|
||||
`audio.tts.openai.api_base_url`); per-user overrides are the `settings`
|
||||
JSON column of table `user` (singular, no `is_active`; some rows hold the
|
||||
string `null` → guard it). `audio.stt.*` and `audio.tts.*` are separate.
|
||||
No `sqlite3` CLI in the image or on the host: `podman exec -i <c> python -`
|
||||
with the stdlib module. **`podman exec` needs `-i` to accept a heredoc
|
||||
script** — without it the script is silently dropped and you get no output.
|
||||
|
||||
## Known traps
|
||||
|
||||
- OpenWebUI is a **rootless** quadlet: `sudo podman …` reports *no such
|
||||
container*. Read it rootless.
|
||||
- `podman ps` in this podman build has no `.ImageName` field — use `{{.Image}}`.
|
||||
- The in-browser engine choice is **per-browser storage**: empty
|
||||
`user.settings` + correct admin config + zero backend hits is the normal
|
||||
shape of this bug. Fix is UI: Settings → Audio → TTS Engine → **Default**.
|
||||
- `GET /api/config` publishes no `audio` block, and nonexistent `/api/...`
|
||||
paths return the SPA HTML with `200` — never conclude "config missing" from
|
||||
an HTML body.
|
||||
- TTS is memory-registry sensitive: the engine's voice registry is
|
||||
memory-only, so "every name returns 400" after a restart points at the warm
|
||||
pass, not at the caller (see [[frank-tts-add-cloned-voice]]).
|
||||
|
||||
## Local playback side (omp `/say`, manual probes)
|
||||
|
||||
If synthesis succeeds (`200`, sane WAV bytes) but nothing is audible, the
|
||||
fault is the PipeWire graph, not the endpoint:
|
||||
`wpctl status` (default sink marked `*`, and *Default Configured Devices* —
|
||||
a remembered-but-offline `bluez_output.*` there is the classic silent
|
||||
route), `wpctl get-volume @DEFAULT_AUDIO_SINK@`, `pw-play -v <file>` (expect
|
||||
`stream drained` + exit 0), and target a device explicitly with
|
||||
`pw-play --target <node-id>` to identify which hardware is live. A one-word
|
||||
clip (~0.7 s) is easy to miss — probe with a multi-second sentence.
|
||||
@@ -1,105 +1,73 @@
|
||||
---
|
||||
name: gitea-api-pr-operations
|
||||
description: "Open, list, merge, and verify pull requests (and diagnose issue-API scope failures) on the Dankclaw Gitea via its REST API from a workstation, keeping the token out of argv (curl -K config file, mode 600) — includes the credential-store extraction one-liner, the merge endpoint's Do-field law and Content-Type-header trap, the suggested-URL-is-not-a-PR gotcha, and the insufficient-token-scope trap."
|
||||
description: "\"Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea API (192.168.0.143:3000, repo dank/command-center) with the token stored in ~/.config/secrets/gitea — incl. the required merge payload, where review comments actually live on 1.27.3, and how to recover the session id for the mandatory body footer.\""
|
||||
---
|
||||
|
||||
# Gitea API PR operations (Dankclaw)
|
||||
# Gitea API PR operations (Dankclaw `dank/command-center`)
|
||||
|
||||
`git push` on this fleet prints `Create a new pull request for ...` — that is a
|
||||
**suggestion, not a PR**. Verify (`GET /pulls?state=open` → expect `[]`) before
|
||||
claiming a PR "should exist"; create it via the API when asked.
|
||||
## Where
|
||||
- Token: `~/.config/secrets/gitea` (`http://user:TOKEN@192.168.0.143` single line).
|
||||
Extract WITHOUT echoing:
|
||||
`tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)`
|
||||
- Build a 600-perm temp curl config and use `curl -K`:
|
||||
`cfg=$(mktemp) && chmod 600 "$cfg" && printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"`
|
||||
Delete `$cfg` after use. NEVER pass `-H "Authorization: token $tok"` (the literal
|
||||
header lands in transcript + logs). NEVER echo the token.
|
||||
- Base URL: `http://192.168.0.143:3000/api/v1/repos/dank/command-center`.
|
||||
|
||||
## Endpoint + auth
|
||||
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>`
|
||||
- Token lives in the git credential store configured by
|
||||
`git config --get credential.helper` → `store --file ~/.config/secrets/gitea`,
|
||||
line format `http://user:TOKEN@192.168.0.143:3000`.
|
||||
- The repo-local `.env` `GITEA_TOKEN` (main checkout, gitignored) carries
|
||||
**`read:issue` too** (measured 2026-10-18): filing issues needs THAT token,
|
||||
the credential-store one 403s on issues.
|
||||
## Recipes (all verified live 2026-10-18)
|
||||
- **Open PR** (token only needed for private repos; this one reads publicly but
|
||||
POST needs auth): `POST /pulls` JSON `{title, head, base:"main", body}`.
|
||||
- **PATCH PR title/body**: `PATCH /pulls/<n>` with JSON body `{title}` and/or
|
||||
`{body}`. Returns 201 with the full PR. Use `jq -n --arg` to build JSON; for
|
||||
multi-line bodies write the text to a temp file and `--arg b "$(cat b.txt)"`.
|
||||
(jq CLI: a bare `---` argument after the program is parsed as a flag → error;
|
||||
fold footers into the --arg string instead.)
|
||||
- **List open PRs**: `GET /pulls?state=open` → `jq -r '.[] | "#\(.number) \(.head.ref) \(.title)"'`.
|
||||
- **Read PR**: `GET /pulls/<n>` (no token; public repo) → `jq -c '{state,merged,mergeable,head:.head.ref}'`.
|
||||
`mergeable:false` is often Gitea's *not-yet-checked* state, not a conflict — prove a real
|
||||
conflict locally with `git merge-tree --write-tree HEAD origin/main` before believing it.
|
||||
- **MERGE**: `POST /pulls/<n>/merge` with `-H 'Content-Type: application/json'`
|
||||
and body **`{"Do":"merge"}`** — this build REQUIRES the `Do` field:
|
||||
`{}` or `{"DoNotMerge":false,...}` → 422; `{"Do":"merge"}` → 200 +
|
||||
`{"state":"closed","merged":true,"merge_commit_sha":"..."}`. Verify with a plain
|
||||
GET /pulls/<n> afterwards (merge endpoint's own stdout may not parse).
|
||||
Merge is the `--no-ff` merge commit on main; then `git -C ~/.omp fetch && git
|
||||
merge --ff-only origin/main` propagates the live deployment.
|
||||
- **Issues**: same shape at `/issues` (`POST {title, body}`), `/issues/<n>/comments`
|
||||
(`POST {body}`, `PATCH .../comments/<id>`) — both work with the temp-cfg auth.
|
||||
|
||||
Extract the token (the `http://` scheme contains a colon — scheme-stripping
|
||||
awk regexes silently grab the port instead; use this anchored sed):
|
||||
## Review comments: the API lies on 1.27.3 (learned 2026-10-18)
|
||||
- `GET /pulls/<n>/comments` (inline review comments) → **404 `not found` on every
|
||||
route** (token or not, `?pull_request_id=` or not) even when a review with
|
||||
comments exists.
|
||||
- `GET /pulls/<n>/reviews` works (shows `state`, `comments_count`) but bodies are empty.
|
||||
- **The comments are readable from the PR HTML**:
|
||||
`curl -sS http://192.168.0.143:3000/dank/command-center/pulls/<n> -o /tmp/pr.html`
|
||||
then extract `<div class="content comment-content">` blocks
|
||||
(python: `re.finditer(r'comment-content', src)` → widen ±800 chars, strip tags).
|
||||
Duplicates appear (suggested-change UI shows text twice); dedupe mentally.
|
||||
- Per-conversation resolve API also 404s — flag resolutions in an issue comment instead.
|
||||
|
||||
```bash
|
||||
tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
|
||||
[ -n "$tok" ] && echo "token-extracted(len=${#tok})" # length check, never echo the value
|
||||
```
|
||||
## Body-footer law (AGENTS.md)
|
||||
Every issue/PR body ends with `---` line then `cwd: <wd> · session: <id>`. Keep
|
||||
it inside the --arg string (not a `---` CLI arg).
|
||||
|
||||
## Keep the token out of argv (repo secret law)
|
||||
`curl -H "Authorization: token $tok"` is world-readable in `/proc/<pid>/cmdline`.
|
||||
Use a mode-600 config file:
|
||||
`cwd` is THIS session's own working directory (note a build worktree separately if they
|
||||
differ). When `/status` is unreachable (headless/subagent context), recover the session id
|
||||
from the session store instead of inventing one:
|
||||
`ls -t ~/.omp/agent/sessions/<cwd-slug>/ | head -2` → `<timestamp>_<uuid>` (the same uuid as
|
||||
`<uuid>.jsonl`); the slug is the cwd with `/` replaced by `-`. Subagent files live under the
|
||||
**parent** session's directory, so the newest entry there is the parent id.
|
||||
|
||||
```bash
|
||||
cfg=$(mktemp); chmod 600 "$cfg"
|
||||
printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
|
||||
```
|
||||
|
||||
- POST JSON needs a second header line: `header = "Content-Type: application/json"`.
|
||||
There IS NO `content-type` config option — curl rejects the file.
|
||||
- `rm -f "$cfg"` when done (a mode-600 token file left in /tmp is its own leak).
|
||||
|
||||
## Operations
|
||||
```bash
|
||||
# list open PRs
|
||||
curl -sS -K "$cfg" ".../pulls?state=open" | jq -r '.[] | [.number,.head.name,.base.name,.title] | @tsv'
|
||||
# create (build the payload as a FILE, see below)
|
||||
curl -sS -K "$cfg" -o /tmp/pr.json -w '%{http_code}\n' -X POST -d @/tmp/pr-payload.json ".../pulls"
|
||||
# verify: 201 is not enough — check mergeable
|
||||
curl -sS -K "$cfg" ".../pulls/1" | jq '{number,state,merged,mergeable,base:.base.ref,head:.head.ref,url:.html_url}'
|
||||
```
|
||||
|
||||
## Merging: the field is `Do`, and it is a STRING naming the strategy
|
||||
|
||||
Measured 2026-10-18 on Gitea at 192.168.0.143:3000 (`POST .../pulls/<n>/merge`).
|
||||
`MergePullRequestOption` is `{From, To, When, Do}` — **there is no `DoMerge` and
|
||||
no `merge_method`**. Every wrong shape is a 422 with a distinct message, so read it:
|
||||
|
||||
|body sent|http|message|
|
||||
|---|---|---|
|
||||
|`{"DoMerge":true,"merge_method":"merge"}`|422|`[Do]: Required`|
|
||||
|`{"Do":true,"merge_method":"merge"}`|422|`[]: json: cannot unmarshal JSON boolean into Go string within "/Do"`|
|
||||
|`{"Do":"true"}`|422|`[Do]: In…` (validation: not a strategy name)|
|
||||
|form body `Do=true&merge_method=merge`|422|`[Do]: Required` (the value must be a strategy name)|
|
||||
|`{"Do":"merge"}`|**200**|merged|
|
||||
|
||||
```bash
|
||||
curl -sS -K "$cfg" -H 'Content-Type: application/json' -X POST -d '{"Do":"merge"}' ".../pulls/4/merge"
|
||||
# then prove it: .merged true, .state closed, .merge_commit_sha set, and origin/main moved
|
||||
curl -sS ".../pulls/4" | jq -c '{number,state,merged,merge_commit_sha}'
|
||||
```
|
||||
|
||||
Strategy names: `merge`, `rebase`, `rebase-merge`, `squash`, `manually` — a
|
||||
strategy the repo does not allow is rejected too.
|
||||
|
||||
**Merge trap (measured 2026-10-18):** on `POST /merge` the `Content-Type` header
|
||||
must go on the **command line** (`-H 'Content-Type: application/json'`). Same
|
||||
curl that succeeds on `POST /pulls` with Content-Type supplied only via the `-K`
|
||||
config file returns **405 with an EMPTY body** on `/merge`. A 405 there is this
|
||||
trap (or a base-moved mergeability recheck — GET the PR, see `mergeable:true`,
|
||||
then retry with `-H`), NOT a permission problem.
|
||||
|
||||
## Building issue/PR bodies without quote carnage
|
||||
Hand-written `jq -n --arg t "..." '{title:$t, body:"..."}'` one-liners with long
|
||||
markdown bodies WILL break on a stray quote (and the broken quoting can swallow
|
||||
the following commands). Build bodies as files (heredoc or write tool), then:
|
||||
```bash
|
||||
b=$(jq -Rs . body.md)
|
||||
jq -n --arg t "$title" --argjson b "$b" '{title:$t, body:$b, head:"<branch>", base:"main"}' > payload.json
|
||||
curl ... -d @payload.json
|
||||
```
|
||||
|
||||
## Gotchas measured 2026-10-14 / 2026-10-17 / 2026-10-18
|
||||
- Unauthenticated/bad token → HTTP 401 body `{"message":"not found"}`; `jq '.[]|[.number,..]'` then fails with *cannot index "not found"* — that is an auth failure, not a missing repo.
|
||||
- **Insufficient token scope ≠ missing data.** The credential-store token
|
||||
(measured 2026-10-17) carries only `write:repository,write:user`; the issues
|
||||
API answers HTTP 403 `{"message":"token does not have at least one of required
|
||||
scope(s), required=[read:issue], token scope=write:repository,write:user"}`.
|
||||
jq surfaces that JSON as the *cannot index* error — do NOT conclude the issue
|
||||
list is empty, and note POSTs under this token create NOTHING (verify, don't
|
||||
assume retry safety). Filing issues needs a fresh token minted in the Gitea
|
||||
web UI with `read:issue`+`write:issue` (Frank-class credential action; ask) —
|
||||
OR use the main-checkout `.env` `GITEA_TOKEN`, which already has issue scope.
|
||||
PR endpoints work fine with either token (`pulls` POST/GET/merge all 200/201).
|
||||
- On the created-PR response `.head.name` can be null; verify with the `GET /pulls/<n>` and use `.head.ref`.
|
||||
- **Swagger JSON is disabled on this instance** — `/api/swagger.v1.json`, `/api/swagger.json` and `/api/v1/swagger.json` all 404. Do not go looking for the schema there; the 422 messages name the field and its Go type, which is the faster instrument.
|
||||
## Gotchas
|
||||
- Content-Type: POSTs with `-d @file.json` MUST carry
|
||||
`-H 'Content-Type: application/json'` on the command line. `-H` entries put in
|
||||
the curl config file are ignored for this (header ordering quirk).
|
||||
- `xdg-open <pr-url>` lands the PR in Frank's Firefox the moment it is opened —
|
||||
law, not courtesy.
|
||||
- `jq: cannot index "not found" with "user"` = endpoint 404'd into an error
|
||||
object; check `-w '%{http_code}'` before blaming jq.
|
||||
- The token in `.git/config`'s remote URL is the same secret; the credential
|
||||
file (`~/.config/secrets/gitea`) is the one git actually consults (global
|
||||
credential.helper) and the one to reuse for API calls.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: gitea-merge-form-encoded
|
||||
description: "Use when a Gitea API merge on Dankclaw (192.168.0.143:3000) returns HTTP 422 \"[Do]: Required\" — the merge endpoint needs form-encoded fields, not the JSON body that PR creation accepts."
|
||||
---
|
||||
|
||||
Gitea on Dankclaw (192.168.0.143:3000, `dank/serverAppConfigs`) — merge quirk, measured 2026-10-19.
|
||||
|
||||
Creating a PR accepts a JSON body. **Merging does not**: `POST /api/v1/repos/{owner}/{repo}/pulls/{n}/merge` with a JSON body (`{"merge_style":"no-ff", "Do-Not-Reply-From":…, "Reply-To":…}`) or with those values as request headers returns `422 {"message":"[Do]: Required"}` — four variants of that all fail identically, so do not "fix" it by changing the style name or moving the headers around.
|
||||
|
||||
Working call (form-encoded):
|
||||
|
||||
```bash
|
||||
tok=$(sed -n 's|http://[^:]*:\([^@]*\)@.*|\1|p' ~/.config/secrets/gitea | head -1) # git credential.helper store file
|
||||
curl -sS -X POST -K <(printf 'header = "Authorization: token %s"\n' "$tok") \
|
||||
--data-urlencode 'do=merge' \
|
||||
--data-urlencode 'merge_style=no-ff' \
|
||||
--data-urlencode 'Do-Not-Reply-From=chief@franksdank.cloud' \
|
||||
--data-urlencode 'Reply-To=chief@franksdank.cloud' \
|
||||
http://192.168.0.143:3000/api/v1/repos/dank/serverAppConfigs/pulls/<N>/merge
|
||||
# 200 with an empty body == merged
|
||||
```
|
||||
|
||||
- `do=merge` is the field the 422 was demanding (the web-UI form field name, not an API doc field).
|
||||
- `merge_style` values are the dashed names: `no-ff`, `merge`, `rebase`, `squash`.
|
||||
- Keep the token out of argv: `-K <(printf …)` feeds the `Authorization` header through stdin (a `-H "Authorization: token …"` lands in `/proc/<pid>/cmdline`, world-readable — the 2026-09-16 HF-token leak class).
|
||||
- Verify by fast-forwarding a checkout (`git fetch && git merge --ff-only origin/main`) rather than re-reading the PR.
|
||||
- Fleet law still applies: never merge without an explicit current OK from Frank, and open the PR URL in his browser (`xdg-open`) the moment it exists.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: gitea-pr-api-cycle
|
||||
description: "Open/update/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000, dank/command-center) with the token-safe curl+jq sequence, including the {\"Do\":\"merge\"} merge payload and post-merge live-checkout fast-forward."
|
||||
---
|
||||
|
||||
# Gitea PR/issue cycle — dank/command-center
|
||||
|
||||
Friction-earned 2026-10-18 across PRs #15–#20 and issue #18: three separate fumbles (jq trailer flag, 422 merge payload, silent -o/-w swallowing) codified once.
|
||||
|
||||
## Where
|
||||
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/command-center/{pulls,issues}`
|
||||
- Token: never inline, never echoed. Extract from the git credential file into a 600-perm temp curl config:
|
||||
`tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)`
|
||||
`cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"` → `curl -K "$cfg"`; `rm -f "$cfg"` when done.
|
||||
- Unauthenticated GET works for state reads (`GET /pulls/N` → `jq -c '{state,merged,merge_commit_sha}'`).
|
||||
|
||||
## Body construction (the two traps)
|
||||
- Build JSON with `jq -n --arg t <title> --arg b "$(cat b.txt)" '{title:$t, head:<branch>, base:"main", body:$b}'`; write the body to a temp file first (`printf '%s' … > b.txt`). NEVER pass `---` as a bare jq argument — jq parses it as a flag (`unknown flag: ---`). The AGENTS.md attribution trailer (`---` + `cwd: … · session: …`) goes INSIDE the body string.
|
||||
- Always capture status explicitly: `-o r.json -w '%{http_code}'` and print it; a bare `-o` hides failures (a 422 looked like success-with-no-output once).
|
||||
|
||||
## Merge (the 422 trap)
|
||||
`POST /pulls/N/merge` requires body `{"Do":"merge"}` — `Do` is the Required field on this Gitea. `{}`, `{"DoNotMerge":false}`, `{"merge_style":…}` all return 422 (`"[Do]: Required"`). Success may return an EMPTY body — verify by GET afterwards (`state=closed, merged=true`), not by the POST response.
|
||||
- Merge only with Frank's explicit per-PR OK (repo law).
|
||||
- Post-merge the live deployment must follow: `git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main` (the checkout IS the deployment; branch is `main`).
|
||||
|
||||
## PR etiquette per AGENTS.md
|
||||
- Immediately after opening: `xdg-open <PR-URL>` lands it in Frank's Firefox.
|
||||
- Redesign mid-review: amend + `git push -f` the branch, then PATCH the PR body (`/pulls/N`, `{"body":…}`) so title/body match the net diff — verify `{number,state,mergeable}` in the response.
|
||||
- Issue creation identical shape: `POST /issues` with `{title, body}`, expect 201; PATCH same endpoint `/issues/N` to fix bodies retroactively, then GET-readback verify.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: gitea-review-comment-html-scrape
|
||||
description: "Read inline review comments on the Dankclaw Gitea when pulls/N/comments 404s (Gitea 1.27.3): scrape comment-content blocks and diff anchors from the PR HTML page"
|
||||
---
|
||||
|
||||
# Gitea review-comment extraction when the API 404s
|
||||
|
||||
On the Dankclaw Gitea (192.168.0.143:3000, Gitea 1.27.3) the review-comment API is dead for inline comments:
|
||||
`/api/v1/repos/{owner}/{repo}/pulls/{N}/comments` returns 404 `not found` **even with a valid token**, and `pulls/comments?pull_id=` / `?pull_request_id=` variants 404 too. `/pulls/{N}/reviews` works but its bodies are empty for inline reviews (read `comments_count` for how many exist); `/issues/{N}/comments` shows only conversation comments, never inline ones.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. Confirm the review exists and how many inline comments it holds:
|
||||
`curl -sS http://192.168.0.143:3000/api/v1/repos/dank/command-center/pulls/{N}/reviews | jq -c '.[] | {id, state, comments_count}'`
|
||||
2. Scrape the PR page instead (no token needed for a public repo):
|
||||
`curl -sS http://192.168.0.143:3000/dank/command-center/pulls/{N} -o /tmp/pr.html`
|
||||
3. Extract each comment's text — every inline comment renders a `comment-content` block; dedupe (each comment appears twice in the HTML):
|
||||
|
||||
```python
|
||||
import re, html
|
||||
src = open('/tmp/pr.html').read()
|
||||
seen = set()
|
||||
for m in re.finditer(r'comment-content', src):
|
||||
t = html.unescape(re.sub(r'<[^>]+>', ' ', src[m.start()-100:m.start()+800]))
|
||||
t = re.sub(r'\s+', ' ', t).strip()[:400]
|
||||
if t not in seen:
|
||||
seen.add(t); print('>>', t)
|
||||
```
|
||||
|
||||
The comment text appears twice back-to-back inside each block ("text text") — read it once. Blocks start after `Copy Link|Copy Source`; ignore those chrome words.
|
||||
|
||||
4. **Locate the file/line anchor** (comment-content blocks carry no `data-path`/`data-line` attrs on this build): search the raw HTML for the comment string, walk *backward* ~4–7 KB, strip tags; the nearby `agent/...` path, `@@ -a,b +c,d @@` hunk header, and surrounding diff context lines identify the anchored file and the quoted source line.
|
||||
|
||||
## Replying / housekeeping
|
||||
|
||||
- Per-conversation resolve API does not exist on this build (404s) — reply to the round as a normal issue comment (`POST /repos/{o}/{r}/issues/{N}/comments`, token via `curl -K` temp cfg, `header = "Authorization: token …"`, 600 perms, `jq -n --arg b "$(cat b.txt)" '{body:$b}'`) listing which note got which fix commit.
|
||||
- Edit a posted comment: `PATCH /repos/{o}/{r}/issues/{N}/comments/{id}` with `{"body": ...}` — use to fix your own typos rather than double-posting.
|
||||
- Token source on this box: `~/.config/secrets/gitea` (grep the remote-URL pattern); never echo it.
|
||||
|
||||
## Why this works
|
||||
|
||||
The PR page is server-rendered with full inline comment bodies; Gitea's "Copy Source" UI element guarantees the raw text is present pre-markdown-render.
|
||||
@@ -22,7 +22,7 @@ description: "Locate, edit, validate, and commit niri compositor config on Frank
|
||||
- Preset list order = cycle order for `Mod+R` (`switch-preset-column-width`); `Mod+Shift+R` cycles backwards. WITH custom preset lists the cycle contains ONLY the configured presets — no implicit full/maximize tail (verified: [0.25, 0.5] cycles period 2). Maximize = separate `Mod+F` `maximize-column`. Built-in defaults without the node: 1/3, 1/2, 2/3.
|
||||
- WITHOUT `preset-window-heights`, `switch-preset-window-height` FALLS BACK to the width presets — user sees width-fractions as heights; define the node explicitly to stop it.
|
||||
- Ground truth for node placement: the binary's embedded template — `grep -a -B4 -A8 "preset-column-widths {" $(which niri)`.
|
||||
- Window outlines: base geometry (`width`, `off`) lives in `cfg/layout.kdl` `focus-ring`/`border`; COLORS effectively live in `noctalia.kdl` (see include-order law). `border { off }` + focus-ring active = the visible outline.
|
||||
- Window outlines: base geometry (`width`, `off`) lives in `cfg/layout.kdl` `focus-ring`/`border`; COLORS effectively live in `noctalia.kdl` (see include-order law). `border { off }` + focus-ring active = the visible outline. Frank's look since 2026-10-18: amber-orange `#ff8c00`, width 4 (`active`+`urgent` in noctalia.kdl; tab-indicator/insert-hint/recent-windows stay red `#f43841` by choice).
|
||||
|
||||
## Workflow
|
||||
1. Edit the live file under `~/.config/niri/cfg/` (symlinked = repo file; except `noctalia.kdl`, edit live then `cp` to mirror).
|
||||
|
||||
@@ -1,50 +1,52 @@
|
||||
---
|
||||
name: noctalia-bar-layout-edit
|
||||
description: "Edit/verify Noctalia v5 bar layout and clock widgets (multi-timezone instances, capsule groups); GUI→repo propagation chain, settings.toml symlink audit, and clobber-recovery via dangling stashes on Frank's dotfiles workstations"
|
||||
description: "Edit/verify Noctalia v5 bar layout and clock widgets (multi-timezone instances, capsule groups); GUI-repo propagation chain, settings.toml symlink audit, and stash-clobber recovery on Frank's dotfiles workstations"
|
||||
---
|
||||
|
||||
# Noctalia v5 bar layout edit
|
||||
|
||||
Bar widget layout on Frank's noctalia v5 workstations is GUI-override state, not config.toml.
|
||||
|
||||
## Where
|
||||
- Layout file: `~/.local/state/noctalia/settings.toml` → `[bar.<name>]` keys `start` / `center` / `end` (arrays of widget ids; `[widget.<id>]` subtables hold per-widget settings).
|
||||
- On Frank's boxes this file MUST be a **symlink into the command-center repo** (e.g. `~/.omp/dotfiles/noctalia/.local/state/noctalia/settings.toml`) — edit the repo file, not the link path (atomic-rename writes through the link path would replace the symlink).
|
||||
- Layout file: `~/.local/state/noctalia/settings.toml` -> `[bar.<name>]` keys `start` / `center` / `end` (arrays of widget ids; `[widget.<id>]` subtables hold per-widget settings).
|
||||
- On Frank's boxes this file MUST be a **symlink into the command-center repo** (e.g. `~/.omp/dotfiles/noctalia/.local/state/noctalia/settings.toml`) - edit the repo file, not the link path (atomic-rename writes through the link path would replace the symlink).
|
||||
- Layer order: built-in defaults < `~/.config/noctalia/config.toml` (+`[include]`) < `settings.toml` (wins). Noctalia writes through symlinks and keeps them.
|
||||
- Defaults when a key is absent: `start = ["launcher","wallpaper","workspaces"]`, `center = ["clock"]`.
|
||||
- **Noctalia rewrites this file itself** (GUI actions + normalizes keys/format, e.g. strips `{:%...}` braces, reorders widget lists). Always re-read the exact lines immediately before editing; stale line anchors WILL drift between reads.
|
||||
- Defaults when a key is absent: `start = ["launcher","wallpaper","workspaces"]`, `center = ["clock"]`, `end = ["notifications","clipboard","network","bluetooth","tray","battery","session","control-center","clock"]`.
|
||||
|
||||
## GUI→repo propagation (checkout-is-deployment)
|
||||
- `~/.config/noctalia/config.toml` is symlinked into the repo → GUI settings writes land in the repo working tree automatically.
|
||||
- `~/.local/state/noctalia/settings.toml` can SILENTLY become a plain file (observed 2026-10-17; hardlink/symlink wiring decays) → GUI bar-layout changes then diverge from the repo with no signal. AUDIT: `readlink ~/.local/state/noctalia/settings.toml` must print the repo path; if it's a plain file and repo copy is content-identical, re-arm: `ln -sfn <repo-path> <live-path>` + `noctalia config validate`.
|
||||
- Landing in the working tree ≠ propagated: other boxes only see it after commit + push to Gitea main. `live-sync.path` (PR #15; DirectoryNotEmpty on `dotfiles/`) commits dotfiles-tier writes within ~2 s once deployed — until it is active on a box, the hourly heartbeat leaves up to 59 min of exposure, and on 2026-10-18 exactly that window let a trunk-sync stash destroy months of GUI layout work. Until verified active (`systemctl --user status live-sync.path`), commit GUI layout edits the same session yourself. Noctalia's runtime churn (wallpaper path, nightlight, panel positions) rides along; normal.
|
||||
- niri has NO settings GUI; niri "GUI changes" don't exist, and `niri msg output ...` runtime overrides are not persisted. niri cfg mirror↔live links also decay (all inodes decoupled once) → audit by CONTENT: `diff` every `~/.config/niri/cfg/*.kdl` against its `dotfiles/zz-blankpad/niri/...` mirror, never trust links.
|
||||
## Clobber window and recovery (2026-10-18 incident law)
|
||||
- GUI-built layout is **working-tree-only until the hourly live-sync heartbeat commits it**. A merge/sync session that "clears the dirty tree" can `git stash` those edits, and later stash operations can destroy the entries - customizations then vanish from the live bar with the repo copy stale.
|
||||
- Never blame the GUI or noctalia first: diff live file vs `HEAD`, check heartbeat commits (`git log -- dotfiles/noctalia/.local/state/noctalia/settings.toml`), then the stash chain.
|
||||
- Destroyed stashes are recoverable: `git fsck --lost-found` lists dangling commits; `git show <dangling>:dotfiles/noctalia/.local/state/noctalia/settings.toml` dumps each candidate; pick by `[bar.default]` richness (capsule groups, extra clock instances). Validate a candidate in a sandboxed `XDG_*_HOME` before touching the live file.
|
||||
- After recovery, commit the file immediately so the window closes.
|
||||
|
||||
## If the bar got wiped (recovery)
|
||||
- The working-tree file is often the ONLY copy of GUI work; a `git checkout`/stash/merge clobber is survivable because git objects outlive stash entries: `git fsck --lost-found`, inspect `git log -1 --format='%ci %s' <dangling>` (newest `WIP on` stash commits hold the last good settings.toml), dump with `git show <c>:dotfiles/noctalia/.local/state/noctalia/settings.toml`.
|
||||
- Before restoring: back up the clobbered file to `.AGENTTRASH/`; validate the candidate in a sandbox (`XDG_CONFIG_HOME=… XDG_STATE_HOME=… noctalia config validate` — plugin-widget "unrecognized type" warnings there are offline-CLI blindness, not blockers); restore via `cp` onto the repo path (cp writes through the symlink; `mv` would replace it); prove visually with `noctalia msg screenshot-fullscreen`; commit immediately.
|
||||
- Cross-check the recovered layout against Frank's old screenshots in `~/Pictures/screenshot_*.png` (crop top ~7% with magick) — they are ground truth for what the bar should look like.
|
||||
## Widgets (v5 census, verified 2026-10-17)
|
||||
- Full roster in `settings.toml` widget_order plus docs: activity, backlinks, battery, bluetooth, brief, bright, cat, cheatsheet, chart, clipby, clipboard, control-center, cpu, controls, cava (now playing), disk, gpu, help, keyboard, language, launcher, mediaplayer, memory, mic, network, night-light, noctalia-palette, notification-history, notifications, niri-colors, power-section, power-profile, screenrecord, scratchpad, screenshot, session, shell (custom shell command), slowouts, slowouts-visible (per-output bar assignment), speedtest, swayosd-*, taskbar, tray, voiceline, volume, wallpaper, weather, workspaces.
|
||||
- `clock` widget: `format` (chrono strftime), `schema` (12/24), `multi_line`, `show_seconds`, `popup`, and **timezone** = IANA name. Multiple clock widget instances are supported: ids like `clock`, `clock-utc`, `clock-sea` each get a `[widget.<id>]` subtable -> one capsule per instance. This is how multi-timezone (Seattle work + UTC dev) lands: add `clock` + `clock-utc` to `end` (or `center`), set `timezone = "Etc/UTC"` on the utc one.
|
||||
- `cat` widget: shows random ASCII art, `rows` setting (4-10).
|
||||
- `shell` widget: `command`, `run interval` (seconds), `icon`, `terminal`; output lands in its capsule popup - can wrap any CLI (hyprstatus-style).
|
||||
- `taskbar` widget: shows window buttons **only on outputs where `niri-bridged` is enabled** (noctalia-launcher enables it). Buttons render **icon-only** (app icon from icon theme; fallback = first letter of title); the window title string is NOT shown in the bar. Hover tooltip carries title + app_name. Per-widget settings: `show_icons`, `show_actions`, `disable_middle_click`, `preferred_app_names` (map app_name -> icon for missing icons).
|
||||
- Widget ids usable in `[bar.*].start|center|end` are restricted to "bar widgets" (`is_bar_widget` allowlist in `bar.rs`); non-bar widget ids are silently dropped at load. `group:<id>` entries render capsule groups.
|
||||
- Capsule groups: `[bar.<name>.capsule_groups]` id = `["widget",...]`.
|
||||
- Niri layout law for bars: window width must stay **odd** (strut = (W-1)/2); bars reserve via layer-shell, niri's reserved area = layer surface size, so bar height changes need no niri edit.
|
||||
|
||||
## Apply/verify loop (no restart, no reload IPC)
|
||||
1. Edit the repo `settings.toml`.
|
||||
2. `noctalia config validate` (checks merged layers; also `noctalia config export [full] | yq -p toml` to inspect effective values).
|
||||
3. Both layers are file-watched → hot-reload. Prove visually: `noctalia msg screenshot-fullscreen` saves a PNG to `~/Pictures`; read the image and inspect the bar.
|
||||
4. **Screenshot race:** the `ok` return precedes the PNG write. Never chain `noctalia msg screenshot-fullscreen && magick "$(ls -t ~/Pictures/screenshot_*.png | head -1)" ...` in one command — you crop the PREVIOUS capture. List `ls -t` in a separate later call and crop the named file.
|
||||
5. Crop for reading: `magick <png> -crop 100%x8%+0+0 +repage /tmp/strip.png`, then read the strip; cross-check clocks against `date -u` / `date`.
|
||||
## Bar styles (GUI-visible, settings.toml keys)
|
||||
- `[bar.*]`: `thickness`, `font_family`, `font_weight`, `font_size`, `opacity`, `border_width`, `border_offset`, `rounded`, `paddings`, `icon_ratio`, `follow_layout`, `output`, `exclusive`, `layer` (`bottom` = below window layer; `overlay` = above windows), `position` (`top`/`bottom`).
|
||||
- Per-monitor bars: add a `[bar.<name>]` with `output = "<monitor>"` and `follow_layout = true` (niri) -> per-workspace strut follows tall windows on that monitor. This is the mechanism for "bar shrinks when a window is tall" (the bar is a layer-shell strut, NOT a niri layer rule).
|
||||
- The GUI "Bar" page is the reference list; anything absent from settings.toml is a built-in default.
|
||||
|
||||
## Clock widgets
|
||||
- `[widget.clock]` settings: `format`, `vertical_format`, `tooltip_format`, `timezone` (IANA id, e.g. `"UTC"`, `""` = system local). Tokens are strftime-compatible (`%H` 24h, `%I:%M %p` 12h, `%F` ISO date); `{:%...}` brace fields optional; `\n` = second line. Docs: docs.noctalia.dev → bar/widgets/clock + configuration/date-format-tokens.
|
||||
- **One timezone per widget; multiple instances allowed:** any `[widget.<id>]` needs `type = "clock"` for custom ids (bare `clock` id infers it). Each instance converts independently — e.g. local 12h + `timezone = "UTC"` 24h side by side.
|
||||
- Placement: add ids to `[bar.<name>]` start/center/end, or to a capsule group: `[[bar.default.capsule_group]]` with `id = "g1"` + `members = [...]`, referenced from `start` as `"group:g1"` (member order = left-to-right inside the capsule).
|
||||
- Local date + local 12h fit in ONE widget (same tz); the UTC 24h clock must be a second widget.
|
||||
- Expect the date widget (local) and UTC clock to disagree on the day after ~5 PM Seattle — that's correct UTC, not a bug; offer `%F` inside the UTC widget if it irks.
|
||||
## Slowouts (window-height-aware bars/panels)
|
||||
- `slowouts` = hidden engine (not a bar capsule): `[slowouts]` keys `enabled`, `duration`, `easing`, `trigger_offset`; per-monitor `[slowouts.<monitor-name>]` overrides (e.g. `DP-1`) with the same keys plus `edge`.
|
||||
- `position = "top"` + `exclusive = false` + slowouts on the monitor -> panel hides when a window touches its edge, re-reveals otherwise. `slowouts-visible` = toggle capsule.
|
||||
- **Slowouts needs niri workspace-autoresize ON** (niri `workspace-autoresize` = "per-workspace" in cfg/layout.kdl `output ... { workspace-autoresize per-workspace }`): noctalia computes hide from workspace height < panel bottom edge; uniform-strut layouts never trigger it.
|
||||
- Per-output bars: slowouts config keys are monitor names, so one `[bar.<x>]` with `output` + `exclusive=false` + `layer=overlay` per slowout monitor is the supported shape.
|
||||
|
||||
## Taskbar recipes
|
||||
- Move window indicator beside workspaces: put `"taskbar"` in `start` after `"workspaces"`.
|
||||
- Combine workspaces+windows into ONE widget: `[widget.taskbar] group_by_workspace = true` — per-workspace capsules with disc labels (focused/occupied/empty/urgent colors); `workspace_group_content = "icons"|"count"|"dots"` (count/dots: left-click activates the workspace); `hide_empty_workspaces`, `show_workspace_label`, `minimal`, `workspace_label_placement = corner|centered|inside`. Requires compositor per-window workspace assignment (niri: yes; LabWC: no → stays flat).
|
||||
## Layout modes and window title decorations
|
||||
- `[layout]`: `mode` (`tiling`|`floating`|`monocle`), `pseudotiled`, `focus_bar` (window-title bar at top, center, layer-shell; style via `[ui.controls...]`/bar-ish settings), `window_follows_cursor`, `gaps`, `border_width`, `rounded`, `blur`, `opacity`, `borderless_csd`, `show_native_decoration`.
|
||||
- `focus_bar` is the noctalia analog of niri's drawn window title, but ONE global bar (no per-window arrows). True per-window title tags = niri `draw-with-border` on window rules; noctalia adds no per-window decoration (it's a bar app + portal, composites nothing).
|
||||
- Floating mode: right-click desktop / `SUPER+Shift+F` / toggle widget; widgets `floating-directories`, `floating-files`, `pins` (sticky notes), `canvas`, `sketchpad`, `post-its`. Float windows snap to screen quarters (`SUPER+Ctrl+Alt+Arrows`) and tile into current layout (`SUPER+Shift+T`). `[layout.floating.files]` = icon/view/sort/preview size.
|
||||
- `[bar.*].window_display` = `disabled`|`icons`|`text`|`combined` - per-window chips in the bar strip (niri only; this is what niri-taskbar could not do). Values: `disabled`|`icons`|`centered`|`inside`. Requires compositor per-window workspace assignment (niri: yes; LabWC: no -> stays flat).
|
||||
- The `workspaces` widget itself has no window display; styles are regular/minimal/focus_hint only.
|
||||
|
||||
## Law
|
||||
- Full widget/setting tables: https://docs.noctalia.dev/noctalia/bar/ and .../bar/widgets/<name>/ . Commit bar changes atomically to the command-center repo (checkout is the deployment).
|
||||
- **Ask before applying.** Frank asks "how do I X" questions about the live bar; answer with the exact snippet + file, apply ONLY on explicit go. A settings.toml edit is a live-deployment change, not a demo. (Exception: restoring Frank's own layout after data loss, on his ask.)
|
||||
- Git `M` on settings.toml usually carries noctalia's own runtime churn (wallpaper path, nightlight, panel positions) alongside your edit — mention it when committing.
|
||||
- **Ask before applying.** Frank asks "how do I X" questions about the live bar; answer with the exact snippet + file, apply ONLY on explicit go. A settings.toml edit is a live-deployment change, not a demo.
|
||||
- Git `M` on settings.toml usually carries noctalia's own runtime churn (wallpaper path, nightlight, panel positions) alongside your edit - mention it when committing.
|
||||
- Right after building layout in the GUI, commit settings.toml (or trigger `scripts/live-sync.sh`) before any branch/merge work: the uncommitted-GUI-edit window is exactly where the 2026-10-18 clobber happened.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: omp-bundle-implementation-port
|
||||
description: "Extract and port a working implementation from omp's minified dist/cli.js bundle (copy-not-invent requests); anchors, python slicing, regex-constant sweep, trace-driven test prediction."
|
||||
---
|
||||
|
||||
# Porting built-in omp behavior out of the minified bundle
|
||||
|
||||
Use when Frank says "copy the omp implementation / work off known-working code" — i.e. port a built-in feature (vocalizer segmentation, retry logic, formatting rules) into our own file instead of hand-rolling a heuristic. For merely *auditing behavior claims*, use `omp-bundle-claim-audit`; this skill is extraction-for-porting.
|
||||
|
||||
Bundle: `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js` (minified; single-letter names, private `#x` fields survive).
|
||||
|
||||
## Extraction recipe
|
||||
|
||||
1. **Find the setting/entry strings first.** `grep -o 'ce({id:"<area>[^"]*"[^}]\{0,200\}' cli.js` — setting descriptions state semantics verbatim (e.g. `speech.enhanced` = "Rewrite assistant output into natural spoken prose with the tiny/smol model"). Descriptions are quotable ground truth.
|
||||
2. **Anchor to code with a distinctive token**, not guesses: a method name (`pushDelta`), a state init, or a string. Then slice generously with python (bash heredoc), NOT grep -o which truncates:
|
||||
```python
|
||||
src = open('cli.js').read()
|
||||
i = src.index('pushDelta'); print(src[i-1500:i+3000])
|
||||
```
|
||||
3. **Follow the constructor**: pipeline classes instantiate collaborators there (`this.#t ??= new fHe` → `fHe` is the real worker; `#g()` returned `{speakable:new fHe, pending:[]…}`). Dump `class fHe{` +4000 chars.
|
||||
4. **Regex/helper sweep**: minified code declares shared constants in a trailing `var a,b,c,…` list and initializes them elsewhere (`Xyr=`, `Pwc=function`). Loop the names with `re.search(rf'{name}=', src)` and print ~400 chars each — the actual boundary/cleanup rules live in these one-liners.
|
||||
5. Write slices to `/tmp/*.txt` and `read` them; don't eyeball megabyte greps.
|
||||
|
||||
## Port rules
|
||||
|
||||
- Rename fields/methods to readable names but keep logic **1:1** — the value is behavioral fidelity (Frank's "known-working set of code").
|
||||
- Private `#x` fields port cleanly: TS classes in Bun extensions support `#`.
|
||||
- Global-regex `.lastIndex=0` before each scan loop — minified code relies on it; keep it.
|
||||
|
||||
## Verification shortcut
|
||||
|
||||
Before running tests, **hand-trace** each test input through the ported rules (boundary indices, state transitions) and predict exact segment counts/strings; then run — predicted-vs-actual mismatch localizes port errors faster than test output alone. Worked example (2026-10-19, dankvoice PR #26): traced `"First stop here. Second one goes!!! Really? Third ... done"` → predicted 3 segments incl. `"Third ... done"` merged via flush (10-char boundary < 24-char started-min) → bats green first run.
|
||||
|
||||
## Known anchors (2026-10-19 bundle)
|
||||
|
||||
- Speak-aloud vocalizer pipeline: class `I6o` (gating on `speech.enabled`, suspend counter, enhancer, `#g()` per-message state).
|
||||
- Utterance segmenter: class `fHe` + helpers `Pwc` (line-head classify), `Iwc`/`_wc`/`b6o`/`w6o` (boundary search), `Cwc`+`y6o` (cleanup: links→label, URL→host, path→basename). Regex consts: `Xyr` sentence-end runs, `pHe` weak breaks, `mwc` abbreviations, `hwc` undecided line-heads, `g6o` horizontal rules.
|
||||
- Port already living in `agent/extensions/dankvoice.ts` (`SpeechSegmenter`) — when the bundle updates, re-extract `fHe` and diff against it before assuming our copy still matches.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: omp-extension-live-tui-verify
|
||||
description: "Verify an omp extension's slash commands and event hooks against a real TUI session: pty submit semantics, observable side-effect instrument, transient-toast trap, agent_end willContinue law, PipeWire output triage."
|
||||
---
|
||||
|
||||
Verify that a `.ts` file in `agent/extensions/` actually works inside the interactive TUI — the black-box bats seam cannot see harness-side wiring (event names, arg passing, mode gating), and it will pass beside a dead hook.
|
||||
|
||||
Distinct from `omp-tui-live-visual-verify` (config/theme rendering proven by screenshot): here the instrument is **observable side effects**, not pixels, because extension notifications are transient toasts that vanish before the pty buffer is read.
|
||||
|
||||
## 1. Launch the exact build under test
|
||||
|
||||
```bash
|
||||
omp --no-extensions --no-session -e /abs/path/agent/extensions/<name>.ts
|
||||
```
|
||||
as a **pty service** (`pty: true`, `ready: {"log": "╭|❯|Tip:|─", "timeout": 60}`).
|
||||
`--no-extensions` + explicit `-e` proves the file loads by path and nothing else interferes. Neutral cwd (`/tmp`) so repo state cannot color the run.
|
||||
|
||||
## 2. Submit input correctly (the costly gotcha)
|
||||
|
||||
`write proc://<id>` with a trailing `\n` does **not** submit — the text just sits in the composer.
|
||||
|
||||
- **Works:** two writes — first `"/cmd args"` (no terminator), then a **separate** write whose whole content is `\r`.
|
||||
- **Broken:** `"/cmd args\r"` in one write. It still submits, but the trailing `\r` arrives inside the command args. Symptom: `unknown voice 'narrator1\r'`, an unexpected extra utterance, or a chat message whose transcript line ends in a literal `\r`. That is a harness artifact — never "fix" it by stripping control characters in the extension (`.trim()` already covers what a keyboard can produce).
|
||||
|
||||
## 3. Instrument with a stub, never with ears or eyes
|
||||
|
||||
Point the extension's player/handler at a script that appends evidence:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
echo "$1" >> /tmp/stub.log; cp "$1" "/tmp/out-$$.wav"; exit 0
|
||||
```
|
||||
Run the TUI with that stub in the env the extension reads (`DANKVOICE_PLAYER=…` style). Then every assertion is a file: `wc -l` of the log (how many times it fired), `ffprobe -show_entries format=duration` (what it produced), `ls -d <tmp-glob>` (that cleanup ran).
|
||||
|
||||
Sequence per behavior: command → CR → short `sleep` → count. **Count before and after** each command; the delta is the only honest proof a subcommand stayed silent.
|
||||
|
||||
## 4. Discriminate "fired twice" from "fired once per turn"
|
||||
|
||||
Stray CR writes can create extra empty turns, each with its own `agent_end` — two utterances for one intended prompt is usually **two turns**, not a duplicate speak. Confirm by counting assistant turns in the transcript (`read proc://<id>`), and by matching each clip's duration to its own answer's length.
|
||||
|
||||
## 5. Event-hook contract traps
|
||||
|
||||
- `agent_end` payload (`dist/types/extensibility/shared-events.d.ts`): `{ type, messages: AgentMessage[], willContinue? }`. **`willContinue: true` means the harness already scheduled a continuation** (auto-retry / empty-stop retry) — a subscriber that narrates or notifies there speaks an answer the model is about to redo. Gate on it.
|
||||
- Read the real event/payload shape from `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/types/…`; grep `dist/cli.js` for the literal event names to confirm they exist.
|
||||
- A driver test that *calls* the handler by hand proves nothing about emission — that is exactly the gap this live check closes.
|
||||
|
||||
## 6. Audio output goes silent while every command reports success
|
||||
|
||||
`pw-play` exits 0 and prints `stream drained` even when nobody hears anything. Triage order:
|
||||
|
||||
```bash
|
||||
wpctl status # Sinks, the `*` default, and live Streams
|
||||
wpctl get-volume @DEFAULT_AUDIO_SINK@
|
||||
```
|
||||
- A **`Default Configured Devices` entry naming a `bluez_output.*` node that is not in the Sinks list** = remembered-but-offline Bluetooth: the classic silent-route cause.
|
||||
- Confirm the stream really links: a short clip can vanish before inspection, so use a multi-second clip and read `wpctl status` mid-play, or target explicitly: `pw-play --target <node-id> <file>` (node ids from `wpctl status`; `wpctl inspect <id>` gives `node.name`).
|
||||
- Prove the synth path independently of the player: `curl -o x.wav …` then `file x.wav` (expect `RIFF … WAVE … PCM`).
|
||||
|
||||
## 7. Land it
|
||||
|
||||
Live result belongs in the commit message, the reference page, and the work log — and the guard the live check uncovered gets its own behavior test (assert observable output only: fired count, payload content, silence).
|
||||
|
||||
## Cleanup
|
||||
|
||||
`/exit` or `proc://<id>/kill` the service; move stub scripts, logs, and clips to `~/.AGENTTRASH/<date>-<topic>/` — never `rm`. Kill background jobs that keep playing to the operator's speakers.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: omp-extension-stale-module-audit
|
||||
description: "Diagnose recurring omp extension handler timeouts/silence when the on-disk fix is already deployed: harness has no extension hot-reload, so running sessions freeze the module at session start (zombie modules)."
|
||||
---
|
||||
|
||||
Use when a fix for an omp extension is merged/deployed yet the SAME errors persist ("handler timed out after 30000ms", silence, wrong behavior) in running sessions.
|
||||
|
||||
**Core fact:** the harness loads extension `.ts` modules at session start and never reloads them (no `/reload` for extensions; bundle `reload` hits are LSP/MCP/browser only). A session older than the fix keeps executing stale code regardless of the file on disk.
|
||||
|
||||
**Procedure:**
|
||||
1. Census the errors from logs, not guesses — timeout lines carry the culprit pids:
|
||||
`grep -h "handler timed out" ~/.omp/logs/omp.<today>*.log | python3 -c 'import sys,json; rows=[json.loads(l) for l in sys.stdin]; rows.sort(key=lambda r:r["timestamp"]); [print(r["timestamp"], r["event"], r["pid"]) for r in rows]'`
|
||||
(`~/.omp/logs/omp.<date>.<pid>.log` — the pid in the filename is the session.)
|
||||
2. Session start times vs deployed file versions:
|
||||
`ps -eo pid,lstart,args | grep '[o]mp'`
|
||||
`git log --format='%h %ad %s' --date=format:'%m-%d %H:%M' -- <extension path>`
|
||||
`git reflog --format='%gd %ad %gs' --date=format:'%m-%d %H:%M' | grep merge` (live-sync pull times = when the file content changed).
|
||||
Any session whose start time precedes the fix's arrival has the stale module.
|
||||
3. Confirm the stale revision actually contains the bad pattern — read what that session loaded, not HEAD:
|
||||
`git show <old-rev>:<path> | grep -n 'await speak\|async (event'`
|
||||
(e.g. an `async agent_end` awaiting a >30 s chain is exactly what the harness's per-handler `Kzc` 30 s race logs; fire-and-forget `void` is fine — detached promises are NOT tracked by the dispatch window.)
|
||||
4. Optional empirical proof when static analysis says "impossible": probe extensions through the real harness:
|
||||
`omp -p --no-extensions -e /tmp/probe.ts "Reply OK"` with a probe that logs handler start/return and `void`s a 40 s detached promise + spawns a long child. Fast exit + no timeout log ⇒ detachment is untracked ⇒ blame stale modules, not the harness.
|
||||
5. Remediation: restart the affected sessions (kill the listed pids or their terminals — the operator session itself may be one of the zombies; say so explicitly). There is no other fix.
|
||||
|
||||
**Gotchas:**
|
||||
- Per-handler attribution in the log is accurate per dispatch; don't blame other extensions before checking the pid's start time.
|
||||
- Endpoints healthy + silence persisting usually means TWO stacked bugs (e.g. stale awaiting module in old sessions AND a mute bug in current code) — diagnose each pid cohort separately.
|
||||
- Extension chain code must use self-owned `AbortSignal.timeout(...)`, never a ctx-scoped signal: harness dispatch signals die when the dispatch window closes.
|
||||
@@ -1,12 +1,14 @@
|
||||
---
|
||||
name: omp-models-yml-runner-api-placement
|
||||
description: Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml without disabling every custom provider
|
||||
description: "Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml, wire the speech role, and diagnose a failing speech device without blaming a healthy endpoint"
|
||||
---
|
||||
|
||||
# Scope
|
||||
|
||||
`~/.omp/agent/models.yml` (or `models.yaml`) on a workstation: adding a **runner**
|
||||
model row (TTS / image / embedding / rerank / video) alongside chat providers.
|
||||
model row (TTS / image / embedding / rerank / video) alongside chat providers,
|
||||
flipping `config.yml` `modelRoles` at it, and diagnosing a speech device that
|
||||
refuses to speak.
|
||||
|
||||
# The landmine
|
||||
|
||||
@@ -63,9 +65,52 @@ yielding.
|
||||
# Speech transport contract (`openai-speech`)
|
||||
|
||||
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
|
||||
only when explicitly set; `sample_rate`/`bit_rate` **throw**
|
||||
**only** when the caller passed `voice_id`; `sample_rate`/`bit_rate` **throw**
|
||||
(`openai-speech does not support sampleRate`). Codec comes from the
|
||||
`output_path` suffix; 15 000-character cap. An unknown `voice` name is a server
|
||||
error, not a fallback — and `auth: none` makes a dead endpoint read as
|
||||
*available*, so point `modelRoles.speech` at such a provider only after it has
|
||||
answered a real synthesis.
|
||||
|
||||
# Three speech-role laws (bundle-read + measured 2026-10-18)
|
||||
|
||||
1. **No setting carries a voice for `openai-speech`.** The transport builds the
|
||||
body with `voice` from the call's `voice_id` and nothing else.
|
||||
`tts.localVoice` is the *local Kokoro backend's* key; `speech.voice` is the
|
||||
TUI's **speak-aloud** voice, an enum of Kokoro names that never reaches an
|
||||
HTTP request (setting it to a pack name breaks the vocalizer and does nothing
|
||||
for synthesis — read `cfgSpeechVoice` in `dist/cli.js` before believing
|
||||
otherwise). So a voice-less call is the harness's **normal** shape: a
|
||||
voice-required endpoint needs a server-side default (fleet answer:
|
||||
`DEFAULT_VOICE` in `scripts/frank-tts.env`, journal marks the line
|
||||
`(default)`, unknown names still 400 with the list).
|
||||
2. **An explicit role has no fallback candidate.** `modelRoles.speech` set to one
|
||||
model yields exactly one candidate, so a dead endpoint or an unresolvable
|
||||
selector fails with *"No available speech model matches the speech role"*
|
||||
instead of degrading to Kokoro. Loud-by-design; do not "fix" it by adding a
|
||||
fallback in the client.
|
||||
3. **`models.yml` loads at process start; `config.yml` is read live.** A session
|
||||
older than the new row fails exactly like a dead endpoint while a fresh
|
||||
process succeeds in the same minute.
|
||||
|
||||
# Diagnosing "speech device won't speak" — in this order
|
||||
|
||||
1. `curl` the endpoint directly with a real voice name. 200 + audio ⇒ the service
|
||||
is fine and the fault is harness-side; stop touching the server.
|
||||
2. Control test in the *same* session: point `modelRoles.speech` at
|
||||
`local/kokoro` and call the device. It works ⇒ `config.yml` is live-read and
|
||||
the runner selector or its voice argument is the problem, not the endpoint.
|
||||
Revert the role immediately after.
|
||||
3. Distinguish stale registry from dead endpoint with a fresh process:
|
||||
`omp -p --no-session --no-extensions --no-skills --no-rules --tools tts --cwd /tmp
|
||||
'Call the tts tool exactly once with text="…" output_path=/tmp/x.wav
|
||||
voice_id=<name> — then reply with the tool result line.'`
|
||||
Succeeds ⇒ the long-lived session predates the `models.yml` row; restart it.
|
||||
4. Cross-check server-side, never trust the client's echo: the façade/access log
|
||||
line and the engine's own ledger must carry the same byte count the tool
|
||||
printed. (`model=…` in a tool result is what the client *requested*; a server
|
||||
line is the fact.)
|
||||
5. For harness-behaviour claims, read the bundle rather than reasoning from the
|
||||
tool description: `/home/blank/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js`,
|
||||
e.g. `grep -o -E '.{300}response_format:[a-zA-Z$]+.{250}'` for the speech body
|
||||
builder, `grep -o -E 'cfgSpeechVoice:.{0,400}'` for what a settings key feeds.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: one-decision-per-ask-question
|
||||
description: "Use when putting decisions to Frank via the ask tool or a grilling round — exactly one work item per question, rounds may group many single-item questions."
|
||||
---
|
||||
|
||||
# One Decision, One Work Item, One Question
|
||||
|
||||
Friction-earned rule (corrected twice on 2026-10-18, during the voice/TTS planning).
|
||||
|
||||
## Rules
|
||||
|
||||
- Each question addresses **exactly one work item**. A round may contain many questions covering different work items; a question may not.
|
||||
- Never merge two artefacts into one option because they share an implementation dependency. Sharing an endpoint, a service, or a build step is NOT grounds for one question. (`frank-tts` serving both a podcast and an audiobook generator produced exactly this bug.)
|
||||
- Never offer something Frank did not ask for inside another question's option text. If an idea is mine, it gets its own question or stays out of the plan; it never rides along as "…and also X" in an option label or preview.
|
||||
- Do not derive option wording from my own scaffolding (todo text, my table row labels, my runbook headings). Derive it from his stated goal.
|
||||
- Mark the recommended option; a bare "yes" must accept it.
|
||||
- Facts are mine to fetch (subagent/scout), never a question for him. Only decisions go to him.
|
||||
- A question whose answer depends on an unsettled fact or another open question belongs to a **later round**, not this one.
|
||||
|
||||
## Anti-patterns that triggered this
|
||||
|
||||
- One option: "Audiobook/podcast factory — open-notebook + EPUB→narration builder." Two products, one shared dependency, zero requests for the second one. He answered "I never asked for audiobook generation."
|
||||
- One question: "Is the deliverable the podcast, or the platform (harness voice + OpenWebUI + alerts)?" Four work items in one question. Rejected: "You're lumping multiple asks into one question. Ask me per item."
|
||||
|
||||
## Check before sending
|
||||
|
||||
For each question: can I name the single artefact it decides? If two names come to mind, split it.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: openwebui-tts-chain-audit
|
||||
description: Audit/repair OpenWebUI TTS on Dankclaw when cloned voices get rejected by a 29-name Kokoro enum or synthesis is silent; rootless DB + endpoint probes
|
||||
---
|
||||
|
||||
Audit OpenWebUI's TTS/STT wiring on Dankclaw when a voice is rejected, synthesis is silent, or someone asks "is the cloned voice wired correctly". Not for model-picker/ACL issues (openwebui-picker-acl-repair owns those).
|
||||
|
||||
## Shape of the stack
|
||||
- Container `open-webui` runs **rootless as `dank`** on Dankclaw: `ssh dankclaw 'podman exec open-webui ...'` (NO sudo: `sudo podman` sees an empty world). Host port 3005.
|
||||
- Server-side config = `webui.db` `config` table (admin), per-user overrides = `user.settings` JSON column. Settings changed via UI persist per-user; admin page persists to `config`.
|
||||
- Cloned voices live behind the frank-tts façade `:7772` (`/v1/audio/speech`, `/v1/audio/voices`); voices resolve to `/home/dank/voices/<name>/reference.wav`.
|
||||
|
||||
## The trap (2026-10-18 law)
|
||||
`Error: Voice "narrator1" not found. Should be one of: af_heart, ...` (29 names) is **OpenWebUI's bundled in-browser Kokoro** (`browser-kokoro`) rejecting client-side — frank-tts never sees the request. Signature: container logs show zero `audio/speech` hits. Fix = the personal dropdown Settings -> Audio -> TTS Engine -> Default (empty string), not any deploy.
|
||||
|
||||
## Probe sequence (read-only, safe)
|
||||
1. Admin truth:
|
||||
`podman exec open-webui python -c "import sqlite3; c=sqlite3.connect('/app/backend/data/webui.db'); [print(k,'=',v) for k,v in sorted(dict(c.execute('SELECT key,value FROM config')).items()) if 'audio' in k or 'tts' in k.lower()]"`
|
||||
Expect: `audio.tts.engine=openai`, `audio.tts.openai.api_base_url=http://host.containers.internal:7772/v1`, `audio.tts.model=frank-tts`, `audio.tts.voice=<cloned name>`.
|
||||
2. User overrides — a user pinned to `browser-kokoro` beats admin config. CAUTION: `user.settings` can be SQL NULL or the JSON string `"null"`:
|
||||
`... for uid,s in c.execute('SELECT id,settings FROM user'): d=json.loads(s) if s else None; d=d if isinstance(d,dict) else {}; hits={k:v for k,v in d.items() if 'audio' in k or 'tts' in k.lower()} ...`
|
||||
3. Endpoint truth: `curl -s http://127.0.0.1:7772/v1/audio/voices` (roster), then POST the exact OpenWebUI body `{"model":"frank-tts","input":"probe","voice":"narrator1","response_format":"mp3","speed":1.0}` to `/v1/audio/speech` — expect `200 audio/mpeg`.
|
||||
4. Contrast logic: server-path failure leaves a container log line AND a façade error; browser-kokoro failure leaves neither.
|
||||
|
||||
## Law
|
||||
- Full contract + diagnosis live in serverAppConfigs docs (`Dankclaw/OpenWebUI.md` § Text-to-speech, `Dankclaw/Frank TTS.md` troubleshooting). Docs are the source; this skill is just the probe route.
|
||||
- Rootless gotcha generalizes: any Dankclaw service probe through podman goes WITHOUT sudo unless the unit is a system quadlet.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: plugin-git-install-committed-artifact
|
||||
description: "Use when shipping an omp/pi plugin for omp plugin install git: — the installer clone fails validation (missing build artifact) or silently reuses a stale bun git-mirror cache; recipes for committing dist + drift pins + cache purge."
|
||||
---
|
||||
|
||||
# Plugin install from git: committed artifact + fresh mirror
|
||||
|
||||
`omp plugin install git:<url>` (and `pi install git:`) **clone the repo and validate the manifest's declared extension entry WITHOUT running a build step**. Two failure classes seen live (dank/dankvoice, 2026-10-19):
|
||||
|
||||
## 1. Declared entry missing from the clone
|
||||
|
||||
Symptom: `Plugin <name> extension validation failed: ./dist/index.js: declared extension entry not found on disk`.
|
||||
|
||||
Cause: build output (`dist/`) is gitignored; the manifest points at it; the clone has only `src/`.
|
||||
|
||||
Fix — ship the artifact:
|
||||
- Remove `dist/` from `.gitignore`, `bun run build`, commit `dist/`.
|
||||
- Prove build determinism first: build twice, `cmp` the outputs (bun esm builds are byte-reproducible without sourcemaps).
|
||||
- Pin the contract in a small bats suite so it can't rot:
|
||||
1. the manifest's declared entry is tracked in git (`git ls-files -- "${entry#./}"` non-empty — this is the installer's exact contract, not a config assertion);
|
||||
2. committed bytes equal a fresh build (`bun run build` then `git diff --name-only -- dist` is empty → red on drift).
|
||||
|
||||
## 2. Stale bun git-mirror cache
|
||||
|
||||
Symptom: fix pushed, retry **still** fails identically, and the retry is suspiciously fast (seconds).
|
||||
|
||||
Cause: bun keeps bare mirrors in `~/.bun/install/cache/<hash>.git` and may reuse them at an old commit — even after the fix is pushed and the default branch is correct.
|
||||
|
||||
Diagnose + fix:
|
||||
```bash
|
||||
find ~/.bun/install/cache -maxdepth 2 -iname "*<repo>*"
|
||||
git --git-dir ~/.bun/install/cache/<hash>.git log --oneline -3 --all # stale HEAD?
|
||||
rm -rf ~/.bun/install/cache/<hash>.git # it is a cache; safe
|
||||
```
|
||||
Prove the fetch path independently first (also validates credentials for private Gitea repos):
|
||||
```bash
|
||||
GIT_TERMINAL_PROMPT=0 git clone -q <url> /tmp/x && git -C /tmp/x log --oneline -1 && ls /tmp/x/dist
|
||||
```
|
||||
|
||||
## Order of investigation (don't skip)
|
||||
1. Gitea default branch correct? (`GET /api/v1/repos/<o>/<r>` with token — private repos 404/None unauthenticated.)
|
||||
2. Plain unattended clone contains the entry? If yes, the installer's view is cached → purge mirror.
|
||||
3. Only then suspect the installer itself (validation code lives in the minified bundle; `grep -oE '.{600}<error string>' "$bunfile"` on `readlink -f $(which omp)` shows the exact check).
|
||||
|
||||
## After install — proof without a TUI
|
||||
- `omp plugin doctor` → per-plugin ✔ line.
|
||||
- Audible/behavioral chain when TUI input is unusable (update-banner modal freezes pty submits): run `omp --mode=rpc --no-session` with the feature env armed (dankvoice: `DANKVOICE_AUTO=1`, gate is print/json-only) and watch for objective side effects, e.g. PipeWire: `pw-dump | grep -q "say-<ms>.wav"` proves synth+playback end-to-end.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: red-verify-without-stash
|
||||
description: "Prove a regression test is red-capable against pre-fix code in a shared worktree where git stash/checkout are policy-blocked, via git-show blob swap with md5-verified restore."
|
||||
---
|
||||
|
||||
# Red-verifying tests without `git stash`
|
||||
|
||||
## When
|
||||
You fixed a file and added tests that should have failed before the fix, but workspace policy blocks `git stash` (and `git checkout -- file`) in shared worktrees (e.g. `~/.omp` shared-workspace policy). Needed to prove red-capture instead of asserting it.
|
||||
|
||||
## Recipe
|
||||
One `bash` call — swap the fixed file for the HEAD (pre-fix) blob, run only the fix-relevant tests, restore, verify byte-identity:
|
||||
|
||||
```bash
|
||||
cd "$WT"
|
||||
md5sum path/to/fixed.file > /tmp/file-hash
|
||||
cp path/to/fixed.file /tmp/fixed.file.bak
|
||||
git show HEAD:path/to/fixed.file > path/to/fixed.file # pre-fix content; new tests now exercise old code
|
||||
bats -f "test name A|test name B" tests/suite.bats # expect: not ok for every fix-relevant test
|
||||
cp /tmp/fixed.file.bak path/to/fixed.file
|
||||
md5sum -c /tmp/file-hash && echo RESTORED
|
||||
```
|
||||
|
||||
## Notes
|
||||
- `git show HEAD:path > path` truncates-and-refills the file in place; safe because you hold the md5 and a backup copy, and the restore check proves the fixed bytes are back.
|
||||
- Run only the fix-relevant tests via the runner's name filter (`bats -f` regex); a full suite wastes time and old tests pass on both versions.
|
||||
- Expect exactly the tests that pin the fix's behavior to fail pre-fix; tests unrelated to the change (pure coverage of old behavior) pass on both versions — don't count them as red evidence.
|
||||
- `bats` pipelines mask exit codes without `pipefail`; judge by the `ok`/`not ok` lines, not the shell status.
|
||||
- For non-test proof of endpoint behavior (live A/B of an HTTP request body), replay the exact body with `curl` against the live endpoint instead of speculating; keep both timings.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: ryzenai-generic-failure-loader-order
|
||||
description: "Debug AMD Ryzen AI OGA \"Exception during initialization: Generic Failure\" on Linux XDNA2 — the LD_LIBRARY_PATH-vs-activate ordering trap, its proof instruments, and the OGA 0.14 run contract."
|
||||
---
|
||||
|
||||
Symptom (any `onnxruntime_genai` run on a Ryzen AI NPU, container or bare):
|
||||
|
||||
```
|
||||
[E:onnxruntime:, inference_session.cc:2811 operator()] Exception during initialization: Generic Failure
|
||||
```
|
||||
|
||||
Fails ~2 ms after `og.Model()`. No `openat` of `/dev/accel/accel0`, **nothing in the kernel log**, no XRT log (the INI logger is not built into AMD's shipped XRT), no core dump. It is NOT a platform, model-export, or driver-ABI failure.
|
||||
|
||||
## Root cause
|
||||
|
||||
`source <venv>/bin/activate` from `install_ryzen_ai.sh` **prepends** to `LD_LIBRARY_PATH`:
|
||||
|
||||
```
|
||||
site-packages/flexml/flexml_extras/lib : site-packages/onnxruntime/capi : site-packages/voe/lib
|
||||
```
|
||||
|
||||
`voe/lib` is the **compiler-side** bundle: it ships its own `libxrt_coreutil.so.2` and `libryzen_mm.so.1`. Set the path *before* activating and those copies win, XRT's XDNA shim binds against the wrong coreutil, `xrt_core::shim_loader::shim_loader()` → `get_userpf_device()` → `xrt::device(0)` throws, `ryzenai::Allocator::Alloc` throws next, and ORT flattens both into the bare string above.
|
||||
|
||||
**Rule: activate first, THEN set `LD_LIBRARY_PATH`, with `/opt/xilinx/xrt/lib` first.** Passing the var via `podman -e` and sourcing activate afterwards in the same `bash -lc` is the broken shape — that is exactly how a runner script differs from a hand-typed probe, and it is why diagnostics pass while the runner fails. Mounting XRT is not enough either: the tree must be **on** the path.
|
||||
|
||||
## Proof instruments, cheapest first
|
||||
|
||||
1. `ldd <deployment>/lib/libonnxruntime_providers_ryzenai.so | grep -E "libxrt_coreutil|libryzen_mm"` — the resolved path settles it alone: `/opt/xilinx/xrt/lib` = good, `.../voe/lib` = this bug.
|
||||
2. `XILINX_XRT=/opt/xilinx/xrt /opt/xilinx/xrt/bin/xrt-smi examine` → `RyzenAI-npu5 | aie2p | 6x8` + `NPU Firmware Version`. Needs `libboost_filesystem1.83.0`, `libboost_program_options1.83.0`, `libuuid1` (26.04 names; the 24.04 docs' 1.74.0 does not exist). A "No such file or directory" for a file that exists = missing interpreter/dep, not a missing binary.
|
||||
3. `strace -f -e trace=openat` — `/dev/accel/accel0` never opened ⇒ failure is pre-device ⇒ loader-level.
|
||||
4. `gdb -q -batch -x <(echo 'catch throw'; echo run; echo 'bt 10')` style catch-throw loop → names `shim_loader` / `Allocator::Alloc`.
|
||||
|
||||
Known dead ends (each costs one run to rule out, then stop): `--ulimit memlock`, XRT INI verbosity, `DD_LOG_LEVEL` / `RYZENAI_TRACE` / `RYZENAI_EP_TRACING` (print nothing before the throw), byte-scanning XRT `.so` for PCI IDs (ambiguous), libboost presence for the OGA path (only `xrt-smi` needs it), flag-by-flag container matrices (`--memory`, `--security-opt`, `--name`, extra mounts — all innocent).
|
||||
|
||||
## Verified facts on this stack
|
||||
|
||||
- Vendor XRT 2.25.37 userspace (`dpkg-deb -x`, never dpkg-installed) works against Ubuntu's **in-tree `amdxdna` 0.7.0`; no DKMS module needed. `1022:17f0` rev 11 = XDNA2, handled as `aie2p 6x8`.
|
||||
- AMD's `quicktest` passing does **not** prove NPU use (it can silently run on CPU); trust `xrt-smi` + logits, not "it didn't crash".
|
||||
- `/dev/accel/accel0` is `root:render 0660`; rootless podman cannot read it (userns drops the gid; `keep-groups`, `keep-id:gid=990`, `--group-add render` all fail) → rootful container, or a udev rule like the GPU nodes'.
|
||||
- OGA 0.14 (RAI 1.8) contract: `GeneratorParams` has only `set_search_options` (no `input_ids`, no `model.generate`); feed with `Generator.append_tokens(ids)`, loop `generate_next_token()`, read `get_sequence(0)`; `max_length` counts **prompt + generated**. Full Fusion (`*_npu_4K`) caps input+output at 4096.
|
||||
- Validate the *content*, not just the exit code: `np.argsort(logits[0])[-8:]`. `append_tokens` returning a real logits tensor proves the graph ran; if argmax is the sequence terminator (`
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: ryzenai-npu-oga-linux-run
|
||||
description: "Run an AMD vendor-compiled Ryzen AI (XDNA2) model — OGA/onnxruntime-genai — on a Linux Ryzen AI host with the in-tree amdxdna driver, without installing the vendor driver or touching the host driver; includes the triage for ORT \"Exception during initialization: Generic Failure\"."
|
||||
---
|
||||
|
||||
Run a vendor-compiled Ryzen AI (XDNA2) model on Linux with the **in-tree** amdxdna driver: no vendor kernel module, no `dpkg -i`, host driver untouched.
|
||||
|
||||
## Preconditions
|
||||
- Host: Strix Point / Strix Halo / Krackan (PCI `1022:17f0`), in-tree `amdxdna` bound to the NPU function (`readlink /sys/bus/pci/devices/<bdf>/driver` → `amdxdna`), `/dev/accel/accel0` present.
|
||||
- `ryzen_ai-<ver>.tgz` installed into a venv (`install_ryzen_ai.sh -a yes -p <venv>`; it hard-codes `python3.12` — put a uv-managed 3.12 **bin dir** on PATH, never a symlink: a symlinked shim breaks venv creation with `No module named 'encodings'`).
|
||||
- Model artifact MUST match the RAI version: `<name>_rai_<ver>_npu_<ctx>`. Other versions load and emit garbage.
|
||||
- Vendor XRT userspace: `dpkg-deb -x` the debs from `RAI_<ver>_Linux_NPU_XRT.zip` (base + npu + `xrt_plugin…amdxdna.deb` — the plugin is what ships `libxrt_driver_xdna.so.*`) into a host dir, bind-mount it at `/opt/xilinx/xrt`.
|
||||
|
||||
## Container run line (the part that is easy to get wrong)
|
||||
```
|
||||
--device /dev/accel/accel0 # root:render 0660 → rootful, or a world-writable udev rule
|
||||
-v <xrt-root>/opt/xilinx/xrt:/opt/xilinx/xrt:ro
|
||||
-e RYZEN_AI_INSTALLATION_PATH=<venv> -e XILINX_XRT=/opt/xilinx/xrt
|
||||
-e LD_LIBRARY_PATH=/opt/xilinx/xrt/lib:<venv>/onnxruntime/lib:<venv>/deployment/lib
|
||||
# cwd MUST be <venv>/deployment/lib — the RyzenAI EP is dlopen'd by bare name and the
|
||||
# only place ORT looks is the working directory.
|
||||
# Activate the venv explicitly; a login shell (`bash -lc`) re-sources the profile and
|
||||
# can put a numpy-less base python first on PATH.
|
||||
```
|
||||
|
||||
## LAW: mounting XRT is not enough — `/opt/xilinx/xrt/lib` must be ON `LD_LIBRARY_PATH`
|
||||
The RyzenAI EP reaches the XDNA shim through XRT's own loader. With the tree bind-mounted
|
||||
but off the loader path, `xrt::device(0)` throws in `xrt_core::shim_loader`, the RMM
|
||||
allocator fails next, and ORT flattens all of it into
|
||||
|
||||
```
|
||||
[E:onnxruntime:, inference_session.cc:2811] Exception during initialization: Generic Failure
|
||||
```
|
||||
|
||||
Signature that distinguishes this from a real device/driver fault: fails in ~2 ms, **never
|
||||
opens `/dev/accel/accel0`**, nothing in `dmesg`/`journalctl -k`, no XRT log (the INI logger
|
||||
is not built into the vendor build — `XRT_INI_PATH` + `[Common] verbosity` writes nothing).
|
||||
|
||||
## Triage recipe for "Generic Failure" (in this order — each step is one call)
|
||||
1. `strace -f -e trace=openat` → does it open `/dev/accel/accel0`? If **no**, the failure is pre-device: loader/shim, not the driver. Also note the last file it opens (`genai_config.json` = config parsed, then provider setup fails).
|
||||
2. `gdb -q -batch -x cmds --args python driver.py` with `catch throw` + `bt 10` + `continue`. Frames `xrt_core::shim_loader::shim_loader` / `get_userpf_device` / `xrt::device::device` then `ryzenai::Allocator::Alloc` ⇒ the shim never loaded ⇒ fix `LD_LIBRARY_PATH`.
|
||||
3. Prove it with a two-arm run in ONE container (toggle only the suspect) before writing the law down. Both arms passing ⇒ that variable was never the cause.
|
||||
4. Ground truth on device visibility: `xrt-smi examine` → `RyzenAI-npu5 | aie2p | 6x8` + `NPU Firmware Version`. If xrt-smi dies on `libboost_filesystem.so.1.83.0`, install `libboost_filesystem1.83.0`+`libboost_program_options1.83.0` — **boost is only needed by xrt-smi, not by the OGA/EP path** (verified by purging it and still loading the model).
|
||||
5. `voe` in site-packages is a *compiler* toolkit (passes/patterns), not a runtime — swapping `LD_LIBRARY_PATH` to `voe/lib` fixes nothing.
|
||||
6. Byte-scanning XRT `.so` files for supported PCI IDs is inconclusive (2-byte ID patterns collide constantly) — don't bother.
|
||||
|
||||
## Distrust "it ran" on this stack
|
||||
- Vendor `quicktest` can print success while computing on CPU (missing peano lib ⇒ silent CPU fallback). Prove NPU residency per run: sample `/sys/class/accel/accel0/device/mem_usage` (needs root) during generation, or read the EP's `Actually running on NPU` line.
|
||||
- Never report request #1 after a boot as a rate — first pass includes graph/init cost.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: ryzenai-npu-oga-linux-spike
|
||||
description: "Run a vendor-compiled AMD Ryzen AI (XDNA2) ONNX-GenAI model on a Linux Strix Halo host from public artifacts: staging, rootless NPU device-access wall, container recipe, and the NuTTS-air speech-token contract."
|
||||
---
|
||||
|
||||
Run an AMD-precompiled XDNA2 model (OGA / onnxruntime-genai) on a Linux Strix Halo host (Dankclaw-class) without installing the vendor stack on the host.
|
||||
|
||||
## Where the bits actually live (both public, no AMD account)
|
||||
|
||||
- Runtime tarball: `https://download.amd.com/opendownload/RyzenAI/1.8.0/ryzen_ai-1.8.0.tgz` (~7.28 GiB). Verify with a ranged GET — first bytes `1f 8b 08`.
|
||||
- NPU driver/XRT zip: `https://download.amd.com/opendownload/RyzenAI/Driver/RAI_1.8_Linux_NPU_XRT.zip` (90 MB).
|
||||
- Dead ends (404, do not re-probe): `download.amd.com/driver/linux/RAI_1.8_Linux_NPU_XRT.zip`, `.../opendownload/RyzenAI/ryzen_ai-1.8.0.tgz`, `www.xilinx.com/bin/public/openDownload?filename=...` (301 → 404). The login-walled EULA *form* is not the gate; the `opendownload` tree is the real file host.
|
||||
|
||||
Tarball has 30 top-level entries: cp312/linux_x86_64 wheels (`onnxruntime_genai_ryzenai-0.14.0`, `onnxruntime_vitisai-1.27.0`, `vitis_mllib`, `flexml*`, `device_essentials_strx`, `model_generate`, `voe-1.8.0`), plus `deployment.tar.gz` (contains `lib/libonnxruntime-genai.so`, `lib/libonnxruntime_providers_ryzenai.so`, AIE compiler libs), `onnxruntime.tar.gz`, `quicktest.tar.gz` (VitisAI-EP install validator), `LLM.tar.gz` (text-only OGA examples: `run_model.py`, `tokenizer_factory.py`), `install_ryzen_ai.sh`. `voe-4.0-linux_x86_64.tar.gz` is only `vaip_config.json` — no TTS xclbins. **AMD ships no TTS runner and no TTS example.**
|
||||
|
||||
## Host rules
|
||||
|
||||
- **Never `dpkg -i` the vendor XRT.** `dpkg-deb -x <deb> <dir>` and bind-mount `<dir>/opt/xilinx/xrt` at the same path in the container. The plugin deb would install a DKMS `amdxdna` that replaces the distro in-tree driver; the in-tree driver on Ubuntu 26.04 is `amdxdna 0.7.0` and already loads `amdnpu/17f0_11/npu_7.sbin` at boot (`dmesg | grep amdxdna`).
|
||||
- **Rootless containers cannot read `/dev/accel/accel0`** (`root:render 0660`): the host gid never enters the userns gid map, so `--device` yields `nobody:nogroup`. Verified failing 2026-10-19: `--group-add keep-groups`, `--userns=keep-id`, `--userns=keep-id:gid=990` (process gets gid 990, node still unreadable), `--group-add render`, `--group-add gid:990` (both error). Options: run rootful (`sudo podman`, separate storage → rebuild the image rootful, ~1 min), or add a world-writable udev rule for `accel0` (the fleet already does exactly this for `kfd`/`renderD*` in `99-comfyui-gpu-access.rules`, whose header explains why: systemd user-unit spawns carry no supplementary groups).
|
||||
- **Rootless image build trap:** the vendor installer's `tar -xf` preserves recorded uids (95777 etc.) and aborts under `set -e` with `Cannot change ownership ... Invalid argument`, silently skipping the `deployment`/`onnxruntime` extraction. Patch it in the Containerfile: `sed -i 's/tar -xf/tar --no-same-owner -xf/g' install_ryzen_ai.sh`.
|
||||
- Installer usage: `./install_ryzen_ai.sh -a yes -p /opt/rai -c /opt/rai` (python **3.12** required; base `python:3.12-slim`; needs `linux-libc-dev zip pciutils patch libgomp1`). It builds a venv from the local wheels, reinstalls `onnxruntime-vitisai` then `--no-deps onnxruntime-genai-ryzenai`, installs `requirements_extras.txt` (transformers 4.57.6), patches `bin/activate` (sets `RYZEN_AI_INSTALLATION_PATH=$VIRTUAL_ENV` + `LD_LIBRARY_PATH` into site-packages), and untars `LLM/deployment/onnxruntime/voe/quicktest` into the cpp path.
|
||||
|
||||
## HF artifact staging
|
||||
|
||||
- Gated (401 on API, 403 on git) for a plain account: `neuphonic/neucodec` (the **only** NeuCodec encoder), `neuphonic/neucodec-onnx-decoder`, `neuphonic/neucodec-onnx-decoder-int8`, `neuphonic/neutts-air`. `amd/neutts-air_rai_1.8.0_npu_4K` is public. Public mirrors of the codec exist (`eugenehp/neucodec` has the full 1.16 GB `pytorch_model.bin`) — treat a mirror of a gated release as a licensing decision for the operator, not a workaround to take silently.
|
||||
- `github.com/neuphonic/neutts` is public and ships `samples/{dave,emily,greta,jo}.{pt,txt,wav}` = **pre-encoded reference codes + transcripts** → a spike needs zero gated bytes.
|
||||
- Auth without leaking the token into `ps`: build a 600-perm credential file with a shell builtin and point git at it —
|
||||
`umask 077; printf 'https://oauth2:%s@huggingface.co\n' "$(cat ~/.cache/huggingface/token)" > ~/.git-credentials-hf`
|
||||
then `git -c credential.helper="store --file ~/.git-credentials-hf" clone ...`. Never `git clone https://oauth2:$TOKEN@...`.
|
||||
|
||||
## NuTTS-air on XDNA2 (the model-side contract)
|
||||
|
||||
- The package is an OGA model dir: `genai_config.json` (`type: qwen2`, 24 layers, hidden 896, 14 heads / 2 KV, ctx 32768, `provider_options: [{"RyzenAI": {"hybrid_opt_token_backend": "npu"}}]`), plus `dd_metastate_*` AIE artifacts and `model.onnx` + `model.onnx.data`.
|
||||
- **Speech tokens live in the text vocab**: `<|speech_0|>` = id 151671 (10 001 codes), with `<|TEXT_PROMPT_START|>` 151666, `<|TEXT_PROMPT_END|>` 151667, `<|TEXT_REPLACE|>` 151665, `<|SPEECH_REPLACE|>` 151668, `<|SPEECH_GENERATION_START|>` 151669, `<|SPEECH_GENERATION_END|>` 151670. Generation is a plain `og.Generator` loop; the graph's own `eos_token_id` is `[151645, 151643]`, so pass 151670 as an extra eos or stop on it manually.
|
||||
- **Real-time line = 50 speech tokens/s** (`sample_rate=24000`, `hop_length=480`). Judge any NPU/iGPU TTS number against 50.
|
||||
- Build the prompt by *importing* the vendor function, never by copying the template:
|
||||
`sys.path.insert(0, <neutts repo>); from neutts.neutts import NeuTTS`, then
|
||||
`NeuTTS._apply_chat_template(SimpleNamespace(tokenizer=hf_tok, input_format="text"), ref_codes, ref_text, text, None)` — works on a duck-typed `self`, so no backbone/codec load. Template: `user\n<|TEXT_REPLACE|>\nassistant\n<|SPEECH_REPLACE|>`; splice `TEXT_PROMPT_START + encode(f"{ref_text} {text}") + TEXT_PROMPT_END` at TEXT_REPLACE, then truncate at SPEECH_REPLACE and append `[SPEECH_GENERATION_START] + encode("".join(f"<|speech_{i}|>"))`.
|
||||
- Voice identity = reference codes + transcript (request-time conditioning). No trained artifact exists to preserve; packs holding only `reference.wav` require the gated encoder once, offline.
|
||||
|
||||
## Status caveat
|
||||
|
||||
Measured on 2026-10-19 up to and including: staged artifacts, image build, device-access matrix, prompt contract. The token-rate measurement (does the vendor userspace drive the in-tree 0.7.0 driver, and does it clear 50 tok/s) is **not** captured here — re-measure, never trust a remembered rate.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: ryzenai-venv-dependency-triangle
|
||||
description: "Debug the torchao/torchtune/transformers import triangle inside an AMD Ryzen AI (ryzen_ai-*.tgz) venv under torch 2.6, including the masked \"cannot import name\" failures and the port-the-one-symbol escape."
|
||||
---
|
||||
|
||||
# Ryzen AI venv dependency triangle (torch 2.6 era)
|
||||
|
||||
Symptom that starts it all — and it never names the culprit:
|
||||
|
||||
```
|
||||
ImportError: cannot import name 'HubertModel' from 'transformers'
|
||||
```
|
||||
|
||||
The named symbol is healthy. `import torchao` alone reproduces the failure; `import transformers` alone does not. transformers imports its **torchao integration** whenever the real modelling path loads, and the failure is re-raised two frames deep by the lazy loader as a missing name.
|
||||
|
||||
## Instruments
|
||||
|
||||
Never conclude from `grep -c Symbol transformers/__init__.py` — 4.5x generates its lazy `__init__`, so an absent literal proves nothing. Get the real exception:
|
||||
|
||||
```python
|
||||
import traceback, torch, transformers
|
||||
try:
|
||||
__import__("transformers.models.hubert.modeling_hubert")
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
```
|
||||
|
||||
That prints the truth, e.g. `AttributeError: module 'torchao.quantization' has no attribute 'Float8WeightOnlyConfig'` from `transformers/modeling_layers.py`.
|
||||
|
||||
Also probe the order matrix (`import X; from transformers import Y` for each suspect): only the poisoned order fails, which isolates the culprit without reading vendor code.
|
||||
|
||||
## The triangle (measured 2026-10-19, ryzen_ai 1.8.0, torch 2.6.0+cpu, transformers 4.57.6)
|
||||
|
||||
| Option | Result |
|
||||
|---|---|
|
||||
| torchao absent | `from transformers import HubertModel` -> OK |
|
||||
| torchao 0.7.0+cpu | `ImportError: Float8WeightOnlyConfig` (masked as missing symbol) |
|
||||
| torchao 0.8.0+cpu | imports (warns `No module named 'triton'`), still lacks `Float8WeightOnlyConfig` |
|
||||
| torchao >= 0.9 | has the symbol, requires torch >= 2.7 -> impossible in this venv |
|
||||
| torchtune, ANY version (0.6.1 … 0.1.1) | guard `torchao not installed`, or `pyarrow requires NumPy 2.0 or newer, found 1.26.4` |
|
||||
|
||||
Two independent dead ends: torchtune's package init pulls `datasets` -> **the pyarrow AMD ships in its own venv refuses that venv's own numpy 1.26.4**. So `import torchtune` is structurally dead, and torchao (the only thing that satisfies its guard) breaks transformers. **Never "just upgrade" torch/numpy: the vendor's ONNX runtime and torch build are what the NPU EP is loaded against.**
|
||||
|
||||
## Escape: port the one symbol, pin both absent
|
||||
|
||||
Ask what the third-party package is actually used for before fighting the resolver. In `neucodec` it was one symbol (`RotaryPositionalEmbeddings`, ~40 lines, copied verbatim from `torchtune/modules/position_embeddings.py`, BSD-3) used by two files. Apply it in the image with a transform that **asserts** each import line exists, so a vendor update fails the build instead of shipping a half-patched package:
|
||||
|
||||
```dockerfile
|
||||
COPY patches/rope_local.py /opt/patch/rope_local.py
|
||||
RUN python - <<'PY'
|
||||
import pathlib
|
||||
from importlib.util import find_spec
|
||||
pkg = pathlib.Path(find_spec("neucodec").origin).parent # find_spec: importing it is what is impossible yet
|
||||
(pkg / "rope_local.py").write_text(pathlib.Path("/opt/patch/rope_local.py").read_text())
|
||||
old, new = "from torchtune.modules import RotaryPositionalEmbeddings", "from .rope_local import RotaryPositionalEmbeddings"
|
||||
for name in ("bs_roformer5.py", "codec_decoder_vocos.py"):
|
||||
p = pkg / name; s = p.read_text()
|
||||
assert old in s, name + ": import line moved - re-read the vendor source"
|
||||
p.write_text(s.replace(old, new))
|
||||
PY
|
||||
```
|
||||
|
||||
Gate both absences at build time so nobody re-adds them and inherits a three-week-later mystery:
|
||||
|
||||
```dockerfile
|
||||
RUN ... import-check \
|
||||
&& python -c "import importlib.util as u; assert u.find_spec('torchao') is None; assert u.find_spec('torchtune') is None"
|
||||
```
|
||||
|
||||
Prove a ported numeric kernel with the vendor's own fixtures, not with faith: `neucodec` ships `samples/jo.wav` *and* its own encoding `samples/jo.pt`, so re-encoding the WAV and comparing integer codes is a pass/fail correctness test of the RoPE port.
|
||||
|
||||
## Rules of thumb
|
||||
|
||||
- `--no-deps` for anything adjacent to torch/onnxruntime/numpy; check the vendor pins by reading them out of the venv (`python -c "import torch; print(torch.__version__)"`), never from docs.
|
||||
- A `--dry-run` install that resolves without touching torch/numpy/onnxruntime is the licence to install *with* deps.
|
||||
- `uv pip install --target DIR` / a nested venv does **not** layer over a parent venv: a venv created from `/opt/rai/bin/python` resolves "system site-packages" to the base interpreter, and uv's resolver ignores `.pth` paths and will pull a second numpy that breaks the vendor C extensions.
|
||||
- Ephemeral `podman run --rm` probes discard everything you installed in them — build a throwaway tag (truncate the Containerfile before the failing step) instead of re-hypothesising.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: sigterm-kill-test-stub
|
||||
description: "Pin \"code kills its child process\" behavior with a bash stub player: make the stub SIGTERM-reactive via sleep&wait, log KILLED via trap; diagnose deferred-trap failures."
|
||||
---
|
||||
|
||||
Use when a test must prove production code sends SIGTERM to a spawned child (player, worker, server stub) and the test asserts the kill happened — or when such a test mysteriously fails with `KILLED` missing and wall time equal to the stub's sleep duration.
|
||||
|
||||
## The trap-during-sleep gotcha
|
||||
|
||||
A bash stub that sleeps in the **foreground** does NOT run its TERM trap when signalled: bash defers traps until the current foreground command completes. The stub dies (or logs) only after the sleep finishes — long after the test read the evidence file.
|
||||
|
||||
Symptoms:
|
||||
- Assertion on a `KILLED` marker line fails although production code demonstrably calls `child.kill("SIGTERM")`.
|
||||
- Test wall time equals the stub's full sleep (the kill "had no effect").
|
||||
|
||||
## Fix: reactive stub skeleton
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >> "$STUB_LOG" # call record
|
||||
trap 'printf "KILLED\n" >> "$STUB_LOG"; exit 143' TERM # BEFORE the sleep
|
||||
for arg in "$@"; do case "$arg" in *.wav) cp "$arg" "$COPY" ;; esac; done
|
||||
sleep "${STUB_HOLD:-0.1}" & # background sleep…
|
||||
wait $! # …wait is interruptible
|
||||
printf 'DONE\n' >> "$STUB_LOG"
|
||||
```
|
||||
|
||||
- `sleep N & wait $!` is the idiom: a signal makes builtin `wait` return immediately, the trap runs at once, `exit` inside the trap prevents fallthrough to `DONE`.
|
||||
- Tune duration per test via the env knob (`STUB_HOLD=5`) so the child is provably still alive when the kill is triggered.
|
||||
- Put the trap BEFORE any long command; a trap registered after the sleep never guards it.
|
||||
|
||||
## Harness wiring (black-box driver pattern)
|
||||
|
||||
- Scenario: start the long work → settle (>spawn latency, <hold) → trigger the killer (shutdown event, second invocation, `/stop`) → settle → read log.
|
||||
- Expose the stub log lines on the driver's stdout (`LOG <line>` per line) so tests grep output, not files: black-box seam stays output-only.
|
||||
- Assert `KILLED` present; for interrupt-semantics also count (`kills -eq 1`) and total call lines (`PLAYS 3` = call, KILLED, call).
|
||||
|
||||
## Orphan hygiene
|
||||
|
||||
Second child still sleeping when the driver exits: spawn with `stdio: "ignore"` so the orphan holds no capture pipe — it SIGPIPEs/quiets at teardown; do not lengthen tests waiting for it.
|
||||
|
||||
## Verify before trusting
|
||||
|
||||
Run the pair separately: kill-tests green while a byte-equality test (`cp` of the payload before sleep) stays green — the reactive rewrite must not drop the copy step (it sits between trap and sleep).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: skills-cli-retroactive-store-wiring
|
||||
description: "Retroactively register hand-copied skills with the vercel-labs skills CLI (global store + lockfile), wire omp via the agents provider (no repo symlinks), and extract authored overrides without twins"
|
||||
---
|
||||
|
||||
# Skills CLI retroactive store wiring (omp tiers)
|
||||
|
||||
Retrofit provenance for hand-copied skills and wire them for omp on Dank workstations. Executed 2026-10-18 on blankpad; doctrine doc: `omp-docs/tools/vercel-labs-skills/skill-store-wiring.md` (repo).
|
||||
|
||||
## Tier law (final design — do NOT reintroduce per-skill symlinks in the repo)
|
||||
| Tier | Path | Owner |
|
||||
|---|---|---|
|
||||
| Store | `~/.agents/skills/` + lockfile `~/.agents/.skill-lock.json` (v3) | `skills` CLI |
|
||||
| Authored | `<repo>/agent/skills/` real dirs | Frank, branch → PR |
|
||||
| Managed | `~/.omp/agent/managed-skills/` | `manage_skill` only |
|
||||
|
||||
omp has NO agent id in the skills CLI (79-agent registry in `dist/cli.mjs`; `-a pi` writes `~/.agents/skills` too — pi detector keys on `~/.pi/agent`). omp reads the store DIRECTLY via its `agents` provider (`skills.enableAgentsUser`, default true; check with `read cfg://skills`). Precedence: authored (native, 100) > skillshare > ... > agents provider > managed (5). Identical copies collapse by realpath; differing same-named copies survive namespaced `agents/<name>` with a startup warning — so when authoring an override, REMOVE the store twin (`npx -y skills remove <name> -g`; prunes lockfile too) or `skills update -g` resurrects a twin.
|
||||
|
||||
## Procedure (retroactive registration)
|
||||
1. List the repo/live skill dirs to register; identify true sources (per-skill — e.g. `find-skills` is `vercel-labs/skills`, not `mattpocock/skills`).
|
||||
2. Drift check FIRST, per skill, before anything moves: `diff -rq <live>/<name> ~/.agents/skills/<name>` after canonical install; abort-and-report on drift (byte-identical = clean cutover).
|
||||
3. Canonical install (scratch dir with a package.json; global scope): `npx -y skills add <owner>/<repo> --all -g` (+ per extra source `npx -y skills add vercel-labs/skills --skill find-skills --agent universal -g`). Artifacts: store dirs + `~/.agents/.skill-lock.json`; verify `jq '.skills|length'`.
|
||||
4. Authored extraction (only for skills the user will customize): copy dir into repo `agent/skills/<name>/`, add an "Authored override" note after frontmatter, then `npx -y skills remove <name> -g` and confirm `jq '.skills|has("<name>")' == false`.
|
||||
5. Clear repo copies of store-managed skills (repo records authored overrides ONLY). Live cutover after merge: move old dirs to /tmp snapshot, `git merge --ff-only origin/main` in the live checkout.
|
||||
6. `skills ls` from a repo containing `agent/skills` mislabels skills as Eve project skills (path collision) — attribution noise, ignore.
|
||||
7. Fresh-box recipe (three commands, no wiring): add `--all -g`, add find-skills, `remove grilling -g`.
|
||||
|
||||
## Gotchas
|
||||
- Eve/Codex `agents/openai.yaml` files ride along from upstream; omp ignores them — prune from authored dirs (they get swept off disk elsewhere, leaving phantom ` D` in git status).
|
||||
- `skills remove` prunes only global scope if `-g` given; check lockfile after.
|
||||
- Mid-run doc readers (subagents) may see pre-cutover trees — send them a correction message, don't let stale advice ship in issue bodies.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: strix-halo-npu-readiness-census
|
||||
description: "Engine-agnostic census of whether an AMD Strix Halo / Ryzen AI host can run anything on its XDNA2 NPU: driver bind, firmware, device perms, IOMMU, distro XRT, missing AIE compiler, fabric clock, RAM headroom."
|
||||
---
|
||||
|
||||
Use when asked "can we run <engine/model> on the Strix Halo / Ryzen AI NPU?" for a runtime that is **not** halogen (halogen has its own gate + vendor catalog: use `halogen-npu-feasibility-census` for that). This answers only: is the *host* able, and what is missing.
|
||||
|
||||
## Why it is usually the wrong door first
|
||||
|
||||
XDNA2 executes **precompiled AIE partition programs** loaded through XRT → `amdxdna`. There is no generic tensor dispatch: an engine built on ggml/llama.cpp (or any GGUF loader) cannot "enable" the NPU — the port must be a re-export + vendor compile, not a flag. So pair this census with the accelerator the same engine *can* select by build flag (Vulkan/HIP on the gfx1151 iGPU) before proposing NPU work.
|
||||
|
||||
## Census (all read-only, over ssh from the operator box)
|
||||
|
||||
```bash
|
||||
ssh <host> '
|
||||
cat /proc/cmdline; ls /sys/kernel/iommu_groups | wc -l # iommu=pt required; groups==0 => IOMMU off => NPU dead
|
||||
lsmod | grep -i amdxdna # in-tree driver
|
||||
sudo dmesg | grep -iE "xdna|npu" | tail # "Initialized amdxdna_accel_driver <ver> for 0000:c6:00.1"
|
||||
lspci -nn | grep -iE "npu|processing" # Strix Halo = 1022:17f0
|
||||
ls /sys/bus/pci/drivers/amdxdna # must list the BDF
|
||||
ls -l /dev/accel # accel0, root:render 660
|
||||
id -nG <deploy-user> | tr " " "\n" | grep -E "render|video" # rootless podman needs render, else --device fails at create
|
||||
find /usr/lib/firmware -iname "*npu*" | head # firmware is /usr/lib/firmware/amdnpu/<devid>_<rev>/npu.sbin*.zst — match the PCI rev
|
||||
dpkg -l | grep -iE "xrt|xdna"; ls /opt/xilinx/xrt/lib 2>&1 # distro libxrt-npu2 = XDNA2 runtime; vendor /opt/xilinx/xrt usually absent
|
||||
apt-cache policy libxrt2 libxrt-npu2 | grep Candidate
|
||||
apt-cache search onnxruntime | head # distro ONNX RT exists; the Ryzen AI EP does not
|
||||
python3 -c "import onnxruntime" 2>&1 | tail -1 # absence of the EP + of ryzenai-compile is the real blocker
|
||||
for c in /sys/class/drm/card*/device; do echo "-- $c"; cat $c/power_dpm_force_performance_level 2>&1; cat $c/pp_dpm_fclk 2>&1 | tr "\n" " "; echo; done
|
||||
nproc; free -g | head -2 # RAM headroom + thread count the engine will pick
|
||||
'
|
||||
```
|
||||
|
||||
## Reading it
|
||||
|
||||
- Driver bound + `accel0` + firmware dir + `iommu=pt` ⇒ **kernel side is ready**; do not re-litigate "is the NPU disabled". A missing device node is *not* the modern failure mode.
|
||||
- `auto` on `pp_dpm_fclk` / `power_dpm_force_performance_level` ⇒ fabric clock **unheld**. GPU+NPU concurrently with a moving fclk hangs these hosts and corrupts NPU results; any NPU plan must pin the top fclk step (or run in a container holding it) first.
|
||||
- No `ryzenai-compile`/ONNX Ryzen AI EP ⇒ nothing on the host can produce a partition program. That is the enablement delta, and it is a toolchain install + a model-specific export/quantize/compile project, not a config change.
|
||||
- `free` headroom matters: NPU partitions want a few GB of host RAM each and can log benign `Failed to alloc dev bo memory, ret -28` under pressure.
|
||||
|
||||
## Laws / traps
|
||||
|
||||
- Read state only. Never probe or load the NPU beside a live inference service — stop + mask the service first (fleet benching policy); this host class has a precedent of a concurrent GPU/NPU experiment needing a power cycle.
|
||||
- `sudo podman ...` finds nothing (per-user rootless storage); run podman as the deploy user.
|
||||
- `/dev/accel/accel0` existing does not mean a context/PASID can be created; with IOMMU off the failure is at context creation, not at the node.
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
name: vault-one-fact-one-owner-dedupe
|
||||
description: "\"Collapse per-payload/per-attempt vault pages into one owner page per endpoint without losing a fact: port-before-cut orphan census (corpus must exclude the file under test), doubled-page repair, trash-aware tools, delegated-write verification, honest per-area line accounting, index regeneration, and the merge-resolution order.\""
|
||||
---
|
||||
|
||||
# One fact, one owner: dedupe the vault without losing a fact
|
||||
|
||||
Use when a repo has accumulated one page per attempt/era (runbooks, bench logs, swap
|
||||
narratives) and the law is **one page owns one thing**. Applies to
|
||||
`serverAppConfigs-docs/` (Obsidian vault, `AGENTS.md` → *Documentation Policy*), but the
|
||||
mechanics are general.
|
||||
|
||||
## 1. Decide ownership before touching files
|
||||
|
||||
- Endpoint/service page owns: current shape, **Eras** table (window | payload | runtime |
|
||||
shape | replaced because | rollback), **Laws**, wire contract, files.
|
||||
- Technique page owns the mechanism and the arithmetic — **no per-endpoint throughput
|
||||
table or boot ledger**.
|
||||
- `log.md` owns incidents and laws only. A swap is an Eras row, never a log entry.
|
||||
- `AGENTS.md` carries operator-critical numbers + cross-border laws; everything else links.
|
||||
- A **retired** endpoint still gets one locator page: era windows quoted verbatim from each
|
||||
era page's own banner, no numbers restated, and any date conflict recorded as an open
|
||||
question instead of silently resolved. Never pick a winner between two primary sources.
|
||||
|
||||
## 2. Single-writer rule (the mistake that costs the most)
|
||||
|
||||
Facts get **ported into the owner page before they are cut** anywhere else. So while any
|
||||
subagent may port facts, only ONE writer may touch the owner pages. Spawn children with an
|
||||
explicit `DO NOT EDIT <owner pages>` and require a `needsPort` list in their report instead
|
||||
of letting them append. Send the narrowing message early — a child already editing them has
|
||||
to be reconciled by hand.
|
||||
|
||||
## 3. Port-before-cut census (proves nothing was lost)
|
||||
|
||||
For every deleted line, extract distinctive keys and check them against the vault minus the
|
||||
file being rewritten. Any line with **no** surviving key is an orphan → port, or accept
|
||||
deliberately and say so. Use `scripts/port-census.py <new-dir> <git-HEAD:path|file>` (numbers
|
||||
with ≥ 4 int digits, decimals, flags, paths, backticked spans, `--`, ALL-CAPS, or ≥ 2 hyphens).
|
||||
|
||||
Three calibration traps:
|
||||
- **Prose words are not facts.** A hyphen or trailing period inside a word (`long-standing`,
|
||||
`deployed:`) was counted as a tracked token while real identifiers hid behind it; that noise
|
||||
buried 300 real lines under 979 fake ones. Strip punctuation, require fact shape.
|
||||
- **The corpus decides the question.** Corpus = the new file only → "what did this rewrite
|
||||
drop?" Corpus = whole vault → "is the dropped fact orphaned repo-wide?" Ask one at a time.
|
||||
- **The corpus MUST exclude the file under test, and you must assert it.** A census pointed at
|
||||
the directory that *contains* the page proves the page is held by itself: ten era pages
|
||||
reported `0 sole-record lines`, which reads as "pure duplication, trash it". With the file
|
||||
excluded, the same pages held **14–63 sole-record lines each** — campaign ledgers that exist
|
||||
nowhere else. A tool's own docstring promise ("excluding it") is not a check: print the
|
||||
corpus size, assert the target is absent, and treat `0 unheld` on a long page as a tool bug
|
||||
until proven otherwise. A mechanical census is a scout: its verdict is a lead, not a finding.
|
||||
|
||||
Also grep `AGENTS.md` separately — a fact it carries is not an orphan.
|
||||
|
||||
## 4. When a page is the same page twice
|
||||
|
||||
Signature: a page whose `wc -l` is ~2× its `HEAD` size, with a **second frontmatter block in
|
||||
the body** whose closing `---` is glued onto the end of a body/timestamp line. Cause chain: a
|
||||
frontmatter rewrite that re-reads the file it just wrote (so its own output becomes the body),
|
||||
then a fence-repair pass keyed on `line == "---"` that cannot see a glued fence and inserts a
|
||||
fence mid-page, so the next page-tail becomes a new document.
|
||||
|
||||
Un-double rule that worked: find the first frontmatter's close (accepting a glued fence), then
|
||||
the first `type:` line after it that has more keys (`title|description|tags|timestamp`) behind
|
||||
it — that is the second copy, and a stray `type:` in a code block has no key run. Keep the copy
|
||||
whose body is a **superset** (compare after stripping the dropped copy's own frontmatter),
|
||||
un-glue the fence, and prove `0` census losses before writing. Refuse to guess: if the halves
|
||||
differ materially, stop.
|
||||
|
||||
## 5. `.AGENTTRASH` is not vault content
|
||||
|
||||
Every tool that walks the vault must exclude it — **both the scan set and the resolution
|
||||
index**. The link checker resolving links *into* the trash turns "live page points at a
|
||||
deleted page" into a false PASS, which is worse than a false alarm. The `docs_completeness`
|
||||
seam's `find` must carry `-not -path '*/.AGENTTRASH/*'` in both walks, or a trashed page
|
||||
demands an index row and a trashed page can satisfy the docs requirement for a live service.
|
||||
|
||||
Trash is gitignored; move with plain `mv` into the dated dir. `git mv` into an ignored path
|
||||
stages a rename the next agent cannot see (`git restore --staged <src> <dst>`). Never `rm`,
|
||||
never `git clean -f`.
|
||||
|
||||
## 6. Rewire + regenerate indexes mechanically
|
||||
|
||||
```bash
|
||||
grep -rnoE "\[\[(page-a|page-b|...)\]\]" --include="*.md" serverAppConfigs-docs | grep -v AGENTTRASH
|
||||
grep -rnoE "\((page-a|page-b|...)\.md\)" --include="*.md" serverAppConfigs-docs | grep -v AGENTTRASH
|
||||
```
|
||||
|
||||
Both wikilinks and markdown path refs must be repointed (Research `See Also` lists and bench
|
||||
pages are where path refs hide). Then **regenerate** index rows with a script, never by hand.
|
||||
Generator bugs to check for: presence tested as a **bare stem substring** — a page whose name
|
||||
prefixes another's (`frank-medium` inside `frank-medium-rocmfpx`) then silently never gets a
|
||||
row (test the wikilink form `[[<stem>]]`); and **no dead-row sweep**, so deleting a page leaves
|
||||
an index row that fails the link check — the generator must drop rows for absent pages too.
|
||||
|
||||
## 7. Enforce the law with a seam
|
||||
|
||||
`tests/docs_completeness.bats`: derive the live set instead of hardcoding it — an endpoint is
|
||||
live when `scripts/` holds **both** `<name>.env` and `<name>.service`; fail if any
|
||||
`<name>-*.md` page exists, and guard against a vacuous derivation (`derived -ge 3`). A
|
||||
re-created era page then cannot pass tests. For the length hint, prefer **derived** exemptions
|
||||
(live endpoint, or a `retired-endpoint` tag: a campaign ledger that exists nowhere else is
|
||||
owned by the page that measured it) plus one documented per-page opt-out; never let a size
|
||||
ceiling force a ledger to be split across two owners.
|
||||
|
||||
Splitting an over-long page to satisfy a ceiling is usually line-NEGATIVE: one measured split
|
||||
turned a 700-line page into 488 + 306 across five files (+23 for the folder net of stub
|
||||
deletions), because frontmatter blocks, pointer sections and connective prose are charged per
|
||||
file while the facts move for free. Roll the split back, carry `okf-exempt: length` with the
|
||||
reason, and keep the stub pages deleted.
|
||||
|
||||
## 8. Edit-tool hazard (costs the most time)
|
||||
|
||||
Body rows beginning with `-` or `+` are op markers. The documented `+-` escape **writes a
|
||||
literal `+`** — do not use it. Safe rewrites inside an existing bullet: keep the `- ` opener
|
||||
line untouched and PUT only the indented continuation lines, or convert the note to a nested
|
||||
bullet whose body lines start with two/four spaces. If a wrapped `[[Two Word Page]]` shows up
|
||||
as an unresolved link, the target was hard-wrapped across lines — unwrap it.
|
||||
|
||||
A `PUT` that spans a multi-line statement can silently drop neighbouring assignments
|
||||
(auto-repaired boundary echo); after editing a constants block, `grep -n "^[A-Z_]* = "` and
|
||||
run a syntax probe before executing.
|
||||
|
||||
## 9. Delegated writes: verify bytes, never narrative
|
||||
|
||||
Measured in one campaign, all four failure modes live:
|
||||
- A subagent killed by the runtime limit yielded a 495-line owner page, a verified era table
|
||||
and a byte census — **the file did not exist** and its table was self-referential.
|
||||
- Another wrote its output to disk and died without reporting, leaving a rewritten `log.md`;
|
||||
the census showed it had dropped laws held nowhere else.
|
||||
- Verdict tables claimed "0 sole records / no contradictions" while missing the contradiction
|
||||
sitting in their own evidence row, and invented bullet IDs — one renamed a bullet to the
|
||||
**opposite** claim.
|
||||
- A scout called accurate docs "WRONG" because the docs said `wip: "down"`.
|
||||
|
||||
Rules: after ANY delegated write run `wc`/`ls` + the orphan census yourself; re-grep every
|
||||
claimed holder before cutting a line; treat `cancelled`/timed-out yields as unverified no
|
||||
matter how concrete they read; and re-read the source bullet before trusting a table's ID.
|
||||
|
||||
## 10. Merge order (a long-lived branch under a moving main)
|
||||
|
||||
1. Commit your work in atomic groups **before** merging (a dirty tree cannot merge cleanly).
|
||||
2. Inspect the incoming commits: `git log --oneline --stat HEAD..origin/main`. If they modify
|
||||
a page you deleted, **port their additions into the owner page**, then merge and resolve
|
||||
the modify/delete as `git rm`.
|
||||
3. Resolve a content block with `write conflict://<id>` (`@ours` / `@theirs` / explicit text),
|
||||
then **`git add` the file** — resolving content does not stage it.
|
||||
4. **Re-probe live state after the merge, before you write any number.** A campaign that
|
||||
reported fleet shapes at the start was made stale by a mid-campaign reslice on main: probe
|
||||
`/slots` and `tr '\0' '\n' < /proc/$(pidof <engine>)/cmdline` again at the end, and let the
|
||||
live answer beat both your earlier probe and the docs.
|
||||
5. `git * --no-verify` is forbidden here. Sweep dead paths in `AGENTS.md`
|
||||
(`grep -oE "[A-Za-z0-9._-]+\.md" AGENTS.md | sort -u`, then `find` each; hard-wrapped names
|
||||
are false positives — confirm with `ls`).
|
||||
|
||||
## 11. Verify, open, land
|
||||
|
||||
- `./tests/run.sh` (full suite) + `python3 scripts/check-vault-links.py serverAppConfigs-docs`
|
||||
→ must print `0 unresolved links`, and the OKF linter `0 violations`.
|
||||
- Env trims: prove value-neutrality mechanically (`git show HEAD:scripts/<svc>.env | grep -E
|
||||
'^[A-Z_]+=' | sort` vs working copy, diff empty). **`scripts/<svc>.env` is a deploy-planner
|
||||
path**: a comment-only trim makes the next service-scoped deploy restart a healthy endpoint.
|
||||
- Open the PR through the API with a 600-perm curl config (managed skill
|
||||
`gitea-api-pr-operations`), end the body with the `cwd:` / `session:` footer, then
|
||||
`xdg-open <PR-URL>` the instant it exists. Never merge without Frank's current explicit OK.
|
||||
Push with an explicit refspec (`HEAD:refs/heads/<pr-branch>`) — a worktree's own branch name
|
||||
is not the PR branch, and `git push origin HEAD` opens a stray branch.
|
||||
- Docs-only change ⇒ **no deploy**. Fast-forward the deploy checkout only
|
||||
(`git -C ~/Projects/serverAppConfig merge --ff-only origin/main`).
|
||||
- Say what was NOT done, in the PR body: soft lint tallies left (missing `## Citations`),
|
||||
conflicts left standing, repo config that was never installed on a host.
|
||||
|
||||
## 12. Report line counts per area, never one net
|
||||
|
||||
"Compaction" measured against the merge-base **per area**: docs / `scripts/` / `AGENTS.md`.
|
||||
A single headline number hides that tooling and prose move in opposite directions — one
|
||||
campaign reported "−1,849 vault lines" while the vault was flat and the growth was the two
|
||||
enforcement scripts that made the law mechanical. Expect a mature one-owner vault to land
|
||||
**docs flat**: the reductions available are deletions of *restatement* (duplicated pages,
|
||||
transient deploy chatter in `log.md`, `AGENTS.md` bullets whose numbers an endpoint page owns)
|
||||
and they are roughly cancelled by the cost of the citations the verification pass adds. When
|
||||
asked to shrink further, show the menu — drop the tools, drop the citations, or accept content
|
||||
loss — instead of inventing cuts. `log.md` compaction rule that survived census: drop a line
|
||||
only when it matches a transient shape (`/health`, `deployed`, `restart`, `systemctl start`)
|
||||
**and** every fact token in it is held by an owner page; a token-held line can still be the
|
||||
only record of *when* an incident happened.
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792440454430
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792368375152,
|
||||
"lastConsolidatedAt": 1792439707623,
|
||||
"lastResult": {
|
||||
"at": 1791599865775,
|
||||
"sessions": 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792359704968
|
||||
"lastConsolidatedAt": 1792401025778
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792359739617
|
||||
"lastConsolidatedAt": 1792439491829
|
||||
}
|
||||
|
||||
+59
-107
@@ -1,118 +1,70 @@
|
||||
## Runtime Stack
|
||||
## Subagent routing (harness law, 2026-10-18)
|
||||
|
||||
**llama.cpp** is the inference runtime for all `frank-zealot`-class endpoints. The `frank-zealot` service runs llama.cpp master (0.5.0-dev, commit `5fc4f3c`), CUDA sm_86 build, under a root systemd unit.
|
||||
**No subagent class may route to `frank-archon`** — the operator session lives there and
|
||||
the endpoint runs 3 sequences (`--max-num-seqs 3`), so `task.maxConcurrency + 1` children
|
||||
eat the parent's own KV slots. Violation cost: three doc children + parent = 4 sequences
|
||||
on 3 slots, children ran 2 h 27 m / 2 h 45 m, the interactive session starved.
|
||||
Routing and bounds live in `~/.omp/agent/config.yml` (`task:` block — the owner; verified
|
||||
with `read cfg://task/agentModelOverrides`): heavy writers (`task`, `scout`) → frank-dragoon
|
||||
(2 slots, idle, slow card ⇒ slice work smaller), bursty readers (`reviewer`, `sonic`,
|
||||
`designer`, `librarian`) → frank-zealot (2 slots, also the compaction hub and every
|
||||
tiny/smol/plan/commit/memory role). Caps: `maxConcurrency: 2`, `maxRuntimeMs: 1800000`,
|
||||
`softRequestBudget: 120`, `maxRecursionDepth: 1` (children never spawn children).
|
||||
`cfg://.../save` approvals auto-deny after a hardcoded 10 s dialog — after Frank has
|
||||
approved a change, edit `config.yml` directly and validate the YAML; a running session
|
||||
picks global-config edits up at once. Full procedure: managed skill
|
||||
`bounding-subagent-runtime`.
|
||||
|
||||
## Model Inventory
|
||||
## Documentation law (2026-10-18)
|
||||
|
||||
### frank-zealot (Dankclaw: 192.168.0.143:7771)
|
||||
**One fact, one owner.** The `serverAppConfigs` repo owns every fleet fact;
|
||||
`AGENTS.md` restates only what a deploy gate or the harness needs and otherwise
|
||||
links. `serverAppConfigs-docs/` (Obsidian vault) carries the lore: one page per
|
||||
endpoint (`[[frank-archon]]`, `[[frank-dragoon]]`, `[[frank-zealot]]`) holding
|
||||
Current shape, **Eras** (one row per payload swap: dates, payload, runtime,
|
||||
shape, why replaced, rollback), Laws, wire contract, files. `log.md` records
|
||||
incidents and laws only — no per-swap ledgers, no bench tables. A retired
|
||||
payload is an Eras row, never a page of its own. `scripts/<service>.env` beats
|
||||
any page: where they disagree, the page is the bug.
|
||||
|
||||
**Ternary-Bonsai-2-27B-PQ2_0.gguf** (prism-ml/Ternary-Bonsai-2-27B-gguf)
|
||||
## Model swaps (2026-10-18)
|
||||
|
||||
- Runtime: llama.cpp
|
||||
- Format: PQ2_0 quantized GGUF
|
||||
- 27B parameter class
|
||||
- Vision: native BF16 mmproj projector for image input
|
||||
- MTP: OFF (no `nextn` tensors in checkpoint)
|
||||
- Context: 256k per-slot window (`--ctx-size 401408 --parallel 2` = 200,704 tokens effective)
|
||||
- Batch: 4096 / ubatch 2048 (CUDA optimum)
|
||||
- Log verbosity: 4
|
||||
- Ports: 7771 on Dankclaw
|
||||
Swap the model, don't instrument the swap: edit the trio
|
||||
(`scripts/<svc>.env` + serve script + unit) → `./tests/run.sh` → PR →
|
||||
`deploy.sh <host> --service <svc>` → `/health` + one chat / tool-call / vision
|
||||
probe → done. **No benchmarks, A/Bs, ladders, or acceptance tables unless
|
||||
Frank explicitly asks for data.** If a payload misbehaves, revert the commit and
|
||||
redeploy. When a bench IS ordered, the GPU-safety procedure (stop + mask, no
|
||||
scratch ports, never `systemctl disable`) is `AGENTS.md` → *Model swaps*
|
||||
and *Deployment completeness*.
|
||||
|
||||
### frank-dragoon (Dankclaw: 192.168.0.143:7770)
|
||||
## Where facts live
|
||||
|
||||
**Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated** (`.hgn` format)
|
||||
|Need|Owner|
|
||||
|---|---|
|
||||
|Topology, ports, payload, shape|repo `AGENTS.md` → Fleet topology; `scripts/<svc>.env` is the authority|
|
||||
|Endpoint lore, eras, laws, rollback|vault `serverAppConfigs-docs/LLM/<endpoint>.md`|
|
||||
|Harness model entries (`contextWindow`, `maxTokens`, `reasoningContentField`, `modelRoles`)|`~/.omp/agent/models.yml`|
|
||||
|Incidents and operational laws|vault `serverAppConfigs-docs/log.md`|
|
||||
|Service/infra pages|vault `Dankclaw/`, `Dankserver/`, `Infrastructure/`, `Architecture/`|
|
||||
|
||||
- Runtime: halogen-flash-server 0.16.2 (digest-pinned `ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`)
|
||||
- Format: v2 `.hgn` (flash-server exclusive)
|
||||
- MTP head: INSIDE checkpoint (`HALOGEN_MTP_HEAD` unset)
|
||||
- Vision: ON (v2 tower loads beside checkpoint)
|
||||
- Base: abliterated (27B-era was censored finetune)
|
||||
- Slots: 2 × 262,144 ctx (KV pool 524,288 positions)
|
||||
- Server sampler: 1.0/0.95/20
|
||||
- Prompt cache: ON by default
|
||||
Models.yml resolution rules that are harness-local: a default model resolves as
|
||||
a `defaultProvider` + `defaultModel` pair; a subagent override is a bare model
|
||||
`id` and resolves only when unique across providers; two ports on one host need
|
||||
two provider blocks — the file cannot express "same host, different port".
|
||||
|
||||
### frank-archon (Dankserver: 192.168.0.235:7770)
|
||||
There is **no audio input anywhere in the fleet** (gone with the Gemma4
|
||||
retirement, 2026-10-01). All three chat endpoints take text + images.
|
||||
|
||||
**StillDeadcode/qwen3.8-next-flash-fp8-iq4r-moe** (`.rad` format)
|
||||
## Retired laws
|
||||
|
||||
- Runtime: radiance 1.0.13 (digest-pinned `stilldeadcode/radiance@sha256:4c3f484564…`)
|
||||
- Format: fp8-KV `.rad` (121,969,901,568 B = 116,322 MiB)
|
||||
- MTP head: MTP depth 2, MNBT 2048, Froggeric v22.5 template
|
||||
- Vision: 27-layer Qwen-VL tower native
|
||||
- KV: 3 × 200,000 fp8-KV (`--max-num-seqs 3`)
|
||||
- 30,720 MiB of 30,969 MiB claimable
|
||||
- 44 GiB/rank pinned host pool, PLE pinned to RAM
|
||||
- Base-alignment: censored (abliterated family lives on frank-dragoon)
|
||||
|
||||
## Resolution Rules
|
||||
|
||||
Official model ids are StarCraft unit names, on the wire since 2026-10-09, and since 2026-10-10 they are also the SERVICE names: `frank-archon` → `192.168.0.235:7770`, `frank-dragoon` → `192.168.0.143:7770`, `frank-zealot` → `192.168.0.143:7771`.
|
||||
|
||||
The default model resolves as a `defaultProvider` + `defaultModel` pair (provider must serve the model); a subagent override uses a bare model `id`, which resolves only when unique across providers. Two ports on one host need two providers — the file cannot express "same host, different port".
|
||||
|
||||
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 provider the file does not define fails at call time. As of 2026-10-06 **all three endpoints accept images** (`input: [text, image]`) — `frank-dragoon` returned to vision with the flash-server v2 swap, while `frank-archon` takes them natively and `frank-zealot` through its BF16 mmproj.
|
||||
|
||||
Reasoning channel differs by runtime: llama.cpp AND SGLang (qwen3 parser, live-verified) emit `reasoning_content`, vLLM emits `reasoning` — `models.yml` sets `reasoningContentField` per endpoint to match.
|
||||
|
||||
There is **no audio** input anywhere in the fleet: that capability left with the FLM NPU endpoint on 2026-09-11, returned 2026-09-17 with the Gemma4 projector's audio encoder, and left again with the Gemma4 retirement on 2026-10-01.
|
||||
|
||||
## 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 **configured and delivering as of 2026-09-17**: a Grafana datasource and the `llm-fleet` dashboard are provisioned from `shared/grafana/provisioning/`, `shared/prometheus/rules/llm-fleet.yml` loads 5 alert rules + 1 recording rule, and firing rules are delivered: Prometheus → Alertmanager (`dankclaw/observability/quadlet/alertmanager.container`, loopback :9093) → host-side Signal relay (`scripts/signal-relay.py`, user unit, 192.168.0.143:18093 — the host LAN IP, the one bind a container can reach as `host.containers.internal`; iptables admits loopback, the host and the podman bridge subnet only) → signal-cli account 318311.
|
||||
|
||||
Prometheus targets read down for structural reasons (HTML-scraped "probes", Gitea metrics off); the remedy is a blackbox exporter. See `serverAppConfigs-docs/Infrastructure/Observability.md`.
|
||||
|
||||
## 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` for the llama.cpp endpoints; a live llama.cpp server also reports honest per-request numbers in the `/completion` `timings` object (prefill/decode rates, `draft_n`, `draft_n_accepted`), which is how frank-dragoon and frank-zealot were measured. 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.
|
||||
|
||||
A gufo-era law: **`gufo bench` cannot rank speculative-decode settings, and will invert the ranking.**
|
||||
|
||||
Measured 2026-09-28 on frank-dragoon at depth 32768: its corpus reports `acceptance=0.933 ± 0.000` with an **identical trace sha256** across draft widths, i.e. the same highly-predictable text every run, and 93 % of every proposal accepted. It reported 29–37 t/s decode where real traffic gives ~15 t/s, and it made `--draft-tokens 16` look 6 % faster than 7 when real text made it 14 % *slower*. Use it for shape, not for A/B. For an A/B, replay **real** text through the live server and read the `Server-Timing` header — but note the unit rides in the **name** half (`ttft;dur=402.485`), so splitting on `=` alone keys the dict `ttft;dur` and every lookup silently returns nothing. Generate **at least 128 tokens** for the measurement: at 8 tokens, `inter_token` is first-token noise and the run-to-run spread swamps every real difference.
|
||||
|
||||
This covers **any GPU experiment, not just benchmarks** — including a bare 2-rank RCCL/`torch.distributed` probe. On 2026-09-16 one such probe was run against the live service "because it only needs 200 MB of VRAM"; the host went hard down ~20 minutes later (no ARP reply, 100% ICMP loss from two subnets, no recovery — a power cycle was required). A hung AQL dispatch wedging the GPU driver is the plausible mechanism, and this host has precedent for going unreachable until a power cycle. If an experiment needs GPUs, stop and mask first, or use a host that serves nobody. Telling an off machine from a wedged one: `ip neigh show <ip>` from a peer — FAILED means off/NIC dead, while a merely wedged kernel usually still answers ARP.
|
||||
|
||||
1. Stop and mask (self-healing would restart it):
|
||||
- Dankclaw `frank-dragoon` (root unit, halogen podman on the iGPU):
|
||||
`sudo systemctl stop frank-dragoon && sudo systemctl mask frank-dragoon`
|
||||
- Dankclaw `frank-zealot` (root unit, llama.cpp on the 3090):
|
||||
`sudo systemctl stop frank-zealot && sudo systemctl mask frank-zealot`
|
||||
- Dankserver `frank-archon` (root unit, radiance on both R9700s, TP=2):
|
||||
`sudo systemctl stop frank-archon && sudo systemctl mask frank-archon`
|
||||
2. Bench.
|
||||
NOTE: `systemctl mask` FAILS on a relinked unit (the unit file in `/etc/systemd/system/` is itself a symlink into the checkout — mask refuses to replace it). Stop is enough for the bench window; the deploy restores the unit, so re-deploy after benching instead of unmasking.
|
||||
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.
|
||||
|
||||
## Subagent Lifecycle (MANDATORY)
|
||||
|
||||
A subagent that appears stuck must have an explicit recovery path defined before the subagent is considered deployed. "Stuck" includes: silent completion halt, repeated identical responses, or extended latency without token emission.
|
||||
|
||||
Recovery requirements:
|
||||
|
||||
- **Explicit timeout** — a hard timeout (≤30s) on the subagent's response channel, with a defined action (retry, fallback, or escalate).
|
||||
- **State checkpoint** — the subagent maintains checkpoints at decision boundaries, allowing resumption from a known good state.
|
||||
- **Health signal** — the subagent publishes a heartbeat (≤5s interval) that triggers the recovery path when missed.
|
||||
|
||||
A subagent without these three properties is a gap: write the timeout, add the checkpoint, wire the heartbeat, then deploy.
|
||||
|
||||
This rule exists because subagent hangs were observed in early deployments (2026-09-xx: silent completion halt; 2026-09-xx: repeated identical responses). The deployment script must verify the heartbeat channel is live before reporting "deployed".
|
||||
|
||||
Before declaring a subagent deployed: verify the heartbeat endpoint returns within the timeout window, and confirm the recovery path is wired (retry/fallback/escalation logic exists in the codepath).
|
||||
|
||||
A subagent without a heartbeat or recovery path is a gap: wire the heartbeat, add the recovery path, then deploy.
|
||||
- The old "Benchmarking Policy (MANDATORY)" wording demanded a bench harness per
|
||||
payload; superseded 2026-10-18 by the swap law above.
|
||||
- A "Subagent Lifecycle (MANDATORY)" rule (heartbeat ≤5 s, checkpoints, hard
|
||||
timeout) was recorded here 2026-09 and **never implemented anywhere**; it
|
||||
described nothing that exists and is deleted 2026-10-18. Do not re-add it
|
||||
without a real implementation.
|
||||
- The fleet model inventory that used to live in this memory file drifted out of
|
||||
date within hours (it still claimed Bonsai-2-**27B** at `--ctx-size 401408`
|
||||
and a dragoon 524,288-position pool). Memory files do not get deploys — the
|
||||
repo does. That is why it is pointers now.
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
## Voice Generation
|
||||
|
||||
- Custom voice source material must exclude the first 10 seconds of any audio sample file used to generate voices; this is a project-wide invariant across all source files, not a per-file exception.
|
||||
- Voice selector valid options: af_heart, af_alloy, af_aoede, af_bella, af_jessica, af_kore, af_nicole, af_nova, af_river, af_sarah, af_sky, am_adam, am_echo, am_eric, am_fenrir, am_liam, am_michael, am_onyx, am_puck, am_santa, bf_emma, bf_isabella, bm_george, bm_lewis, bf_alice, bf_lily, bm_daniel, bm_fable.
|
||||
|
||||
## Input Capabilities
|
||||
|
||||
All endpoints accept text input with optional image input: `frank-dragoon` returned to vision with the flash-server v2 swap, while `frank-archon` takes them natively and `frank-zealot` through its BF16 mmproj.
|
||||
|
||||
There is no audio input anywhere in the fleet: that capability left with the FLM NPU endpoint on 2026-09-11, returned 2026-09-17 with the Gemma4 projector's audio encoder, and left again with the Gemma4 retirement on 2026-10-01.
|
||||
|
||||
## Voice Usage Notes
|
||||
|
||||
- Voice generator output does not affect token counting or inference costs — all voices are rendered post-inference.
|
||||
- Voice quality varies by source sample: exclude the first 10 seconds of any audio sample file used to generate voices; this is a project-wide invariant across all source files, not a per-file exception.
|
||||
- Voice selection is client-side only; the server does not generate or return audio data.
|
||||
|
||||
## Load Management
|
||||
|
||||
- Subagent settings must prevent frank-archon from being overwhelmed; concurrent request handling prioritizes endpoint stability over throughput.
|
||||
|
||||
## Podcast
|
||||
|
||||
- Podcast functionality is sourced from open-notebook; this is a project decision established before this document was created.
|
||||
- Podcast functionality is sourced from open-notebook; this is a project decision established before this document was created.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792370846394,
|
||||
"lastConsolidatedAt": 1792400853634,
|
||||
"lastResult": {
|
||||
"at": 1792370515419,
|
||||
"at": 1792393886960,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
## top bar
|
||||
- Preserve top bar customizations; customizations lost or obliterated on subsequent runs
|
||||
- Todo list updates as tasks change, not waiting for entire bill of tasks to finish
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792370776395,
|
||||
"lastConsolidatedAt": 1792381281460,
|
||||
"lastResult": {
|
||||
"at": 1792370420509,
|
||||
"at": 1792381281460,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
## window outlines
|
||||
- Window outline color is amber, and outline thickness is thickened
|
||||
- Window outline color is amber, and outline thickness is thickened
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792381246498
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Output behavior
|
||||
|
||||
Output must not be shaken off during presentation. Output remains anchored until explicitly confirmed.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792439324791,
|
||||
"lastResult": {
|
||||
"at": 1792398531282,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792381487662
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792393561866,
|
||||
"lastResult": {
|
||||
"at": 1792381870670,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792388836682
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"v":1,"kind":"product_decision","statement":"Limit on zealot set to 3, dragoon set to 2, archon set to 3","source":"explicit_user","evidence":"make the limit on zealot 3, dragoon 2, archon 3","friction":{"corrective":false,"regression":false,"subtle":false},"sessionId":"01a15302-f12d-77d8-9a33-be31a1a05528","ts":1792394470419}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792394427081
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792439315327,
|
||||
"lastResult": {
|
||||
"at": 1792439315327,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792439612626,
|
||||
"lastResult": {
|
||||
"at": 1792439612626,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792398333838
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792440990901,
|
||||
"lastResult": {
|
||||
"at": 1792433353188,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792434572397,
|
||||
"lastResult": {
|
||||
"at": 1792432178813,
|
||||
"sessions": 1,
|
||||
"deltas": 2,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792441029706
|
||||
}
|
||||
+43
-89
@@ -24,27 +24,13 @@ providers:
|
||||
# compactionModel candidate keeps the wire model id valid there.
|
||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||
name: "frank-archon"
|
||||
# Dankserver: BOTH Radeon AI PRO R9700s (gfx1201, TP=2 over the
|
||||
# 2026-09-20 board swap's root ports, wht6 wire). Engine:
|
||||
# radiance (StillDeadcode, Apache-2.0) from the digest-pinned
|
||||
# stilldeadcode/radiance image (index digest sha256:4c3f4845..., 287
|
||||
# MiB, kernels prebuilt for gfx1201), plain `podman run` through
|
||||
# scripts/serve-frank-archon.sh, root unit (User=dank, dank's
|
||||
# rootless podman storage). Serving the OFFICIAL
|
||||
# Qwen/Qwen3.8-Flash-Next-FP8 at NATIVE modelopt block-FP8
|
||||
# precision — a rad-convert no-recipe pure copy into
|
||||
# qwen4exp-fp8-native.rad (172.76 GiB measured by plan-only), built by
|
||||
# scripts/build-frank-archon-rad.sh against a staging dir whose
|
||||
# config.json drops quantization_config so the shipped qwen4exp
|
||||
# plugin's (qwen4exp, "") selection key matches; per-weight
|
||||
# encodings are untouched. Experts stream from a 40 GiB/rank
|
||||
# pinned host pool; the 47.7 GiB PLE ngram table is pinned in RAM
|
||||
# (Frank's call); KV fp8; MTP depth 3 (the checkpoint's own mtp.*
|
||||
# head); prefix cache 4 GiB host + 64 GiB disk. Chat template:
|
||||
# froggeric's fixed qwen3.8 v22.5
|
||||
# (scripts/qwen38-froggeric-v22.5.jinja) via
|
||||
# --override-chat-template. Rollback: git tag
|
||||
# frank-archon-freetoken-pr587-20261005 (FreeToken, one card).
|
||||
# Dankserver: BOTH Radeon AI PRO R9700s (gfx1201, TP=2, wht6 wire) on
|
||||
# the digest-pinned radiance image, plain `podman run` under a root
|
||||
# unit. Payload, shape, eras, laws, rollback:
|
||||
# serverAppConfigs-docs/LLM/frank-archon.md — and
|
||||
# scripts/frank-archon.env is the authority for every number there.
|
||||
# contextWindow MUST quote the engine's --max-model-len: radiance
|
||||
# refuses a longer prompt and never truncates.
|
||||
contextWindow: 200000
|
||||
# maxTokens must survive the harness compaction gap AND the chars/4
|
||||
# token-estimate under-run on code-heavy prompts. The clamp is
|
||||
@@ -105,40 +91,22 @@ providers:
|
||||
# compaction model candidate so the wire model id stays valid
|
||||
# there even from a frank-dragoon session.
|
||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||
# halogen-flash-server 0.16.2 (ghcr.io/peonist-ai/
|
||||
# halogen-flash-server@sha256:0c61bf84, digest-pinned) serving the
|
||||
# Quat3rnion NATIVE v2 ABLITERATED conversion of orcarouter's pinned
|
||||
# BF16 Qwen3.8-Flash-Next (qwen38-flash-next-v2-abliterated.hgn,
|
||||
# ~62 GiB resident, + REQUIRED n-gram table + vision tower, both
|
||||
# hardlinked from the swift15 dir) on the Strix Halo iGPU: TWO slots
|
||||
# over ONE 401,408-position KV pool (= 2 x ctx, raised 2026-10-18) at
|
||||
# HALOGEN_CTX=200704 (the ceiling lowered from the card's 262,144 on
|
||||
# 2026-10-17; ~10.8 GiB reserved where 200,704 cost 5.5, 262,144 cost
|
||||
# 7.2 and 524,288 measured 14.6), with the MTP head ALWAYS ON
|
||||
# (HALOGEN_SPEC_ADAPT=0 — the default adaptive policy halves
|
||||
# decode). Measured 2026-10-17: 50 t/s greedy decode, 1,604 t/s
|
||||
# cold prefill at 59k, NIAH 18/18. ABLITERATED base (no refusals:
|
||||
# 0/5 on the hostile census) and VISION ON (OpenAI image_url
|
||||
# parts; >=256 px images or it answers "video"). Runbook:
|
||||
# serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-native.md.
|
||||
# Window the harness may fill — deliberately BELOW the engine
|
||||
# ceiling (HALOGEN_CTX=200704, lowered from 262,144 on 2026-10-17):
|
||||
# the clamp below leaves the 45k gap that absorbs the estimate error.
|
||||
# Both slots SHARE one 401,408-position KV pool — exactly two full
|
||||
# windows — so TWO maximum-length conversations can be resident at the
|
||||
# same time (at the old pool == ctx 200,704 only two of <=100k could).
|
||||
# The per-request ceiling is unchanged, so this window is unchanged.
|
||||
# halogen flash-server on the Strix Halo iGPU. Payload, shape, eras,
|
||||
# laws, rollback: serverAppConfigs-docs/LLM/frank-dragoon.md — and
|
||||
# scripts/frank-dragoon.env is the authority for every number there.
|
||||
# contextWindow sits deliberately BELOW the engine ceiling
|
||||
# (HALOGEN_CTX): the clamp below leaves the gap that absorbs the
|
||||
# harness's chars/4 under-count, so a window equal to the ceiling
|
||||
# would make every long request a guaranteed 400.
|
||||
contextWindow: 155648
|
||||
# The clamp is max_tokens = min(maxTokens, contextWindow − est −
|
||||
# 4096); the chars/4 est UNDER-counts the rendered prompt by ~3 %
|
||||
# on code/agent traffic (measured 2026-10-07: est 152,938 ->
|
||||
# server 157,388). Worst-case request = window − 4096 + 3 % est
|
||||
# error ≈ 156.2k, inside the 200,704 engine ceiling with ~44k
|
||||
# margin (robust to a ~25 % under-count). Compaction fires at 85 %
|
||||
# of the window = 132,300 est ≈ 136k measured — the 180k target of
|
||||
# the 262,144 era is unreachable now (the window itself is 155,648)
|
||||
# — and rides on frank-zealot: input ≈ threshold − keepRecent(20k)
|
||||
# ≈ 112k, inside its 200,704 slot.
|
||||
# error ≈ 156.2k, inside the engine ceiling with ~44k of margin
|
||||
# (robust to a ~25 % under-count). Compaction fires at 85 % of the
|
||||
# window and rides on frank-zealot: input ≈ threshold −
|
||||
# keepRecent(20k), comfortably inside its per-slot window.
|
||||
# HALOGEN_MAX_TOKENS_CAP in frank-dragoon.env must stay >= this.
|
||||
maxTokens: 45056
|
||||
reasoning: true
|
||||
@@ -183,39 +151,20 @@ providers:
|
||||
# config.yml (192.168.0.143:7771 — this endpoint); the
|
||||
# compactionModel candidate keeps the wire model id valid there.
|
||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||
# Dankclaw's RTX 3090: llama.cpp PrismML fork build (vendor 10777,
|
||||
# commit e3de71522 — the ONLY loader for this payload's custom tensor
|
||||
# type 142 + prism.hadamard.* metadata, and the minimum build that
|
||||
# pairs MTP with a quantized V cache) serving
|
||||
# prism-ml/Ternary-Bonsai-2-9B-gguf at PQ2_0 (2,675,410,720 B) WITH the
|
||||
# MTP head in the file — qwen35 hybrid, 32 layers (8 full-attention +
|
||||
# 24 Gated DeltaNet), 4 KV heads x 256 dim, native 262,144 — in THREE
|
||||
# conversations (--ctx-size 786432 / --parallel 3), each slot at the
|
||||
# model's TRAINED window.
|
||||
# KV is q8_0 K + q8_0 V (17 KiB/token over the 8 full layers =
|
||||
# 13,056 MiB pool, ledger-exact); f16 KV would bill 26,112 MiB and
|
||||
# cannot boot this card. Boot ledger 17,876 used / 6,249 free. Vision
|
||||
# through the publisher's Q8_0 mmproj (624,229,664 B — Q8_0 is the
|
||||
# publisher's requirement for concurrent image requests), whose
|
||||
# worst-case lands outside the engine's fit projection. **MTP OFF —
|
||||
# measured, not assumed**: the draft context bills its own f16 KV over
|
||||
# the WHOLE pool (3,072 MiB) and at ubatch 2048 that squeeze halves
|
||||
# decode (167.2 → 109.2 t/s); SPEC_TYPE=draft-mtp + UBATCH_SIZE=1024 is
|
||||
# the 213.5 t/s arm if the card ever has room. 2026-10-18 swap from
|
||||
# Ternary-Bonsai-2-27B PQ2_0 at 2 × 200,704. Measured: 167.2 t/s
|
||||
# single-stream greedy decode, 280.2 t/s across three concurrent
|
||||
# streams, 2,877 t/s prefill at 106,418. Base-alignment: censored base
|
||||
# lineage; unlike the 27B there is NO CRACK re-export matching this
|
||||
# file. Runbook:
|
||||
# serverAppConfigs-docs/LLM/frank-zealot-llama-prism-bonsai9-mtp.md.
|
||||
# contextWindow is the PER-SLOT window (= the native 262,144): a longer
|
||||
# prompt needs compaction — llama.cpp answers a 400, never truncates.
|
||||
contextWindow: 262144
|
||||
# Dankclaw's RTX 3090 llama.cpp endpoint (PrismML fork build — the only
|
||||
# loader for this payload's custom tensor type). Shape, payload, eras,
|
||||
# laws, rollback: serverAppConfigs-docs/LLM/frank-zealot.md, and
|
||||
# scripts/frank-zealot.env is the authority for every number there.
|
||||
# contextWindow is the PER-SLOT window: llama.cpp divides --ctx-size
|
||||
# across slots and answers a longer prompt with a 400, never a
|
||||
# truncation, so this value must follow every reslice (live now:
|
||||
# 3 x 200,704 over f16 K + q8_0 V with the MTP drafter armed, ba69ea5).
|
||||
contextWindow: 200704
|
||||
# Prompt + budget must fit inside the slot window; thinking counts
|
||||
# against the budget. Compaction fires at 85 % of the window =
|
||||
# 222,822 est; worst-case prompt + budget ≈ 222,822 × 1.03 +
|
||||
# 16,384 ≈ 245,891 ≤ 262,144. This endpoint is the fleet's
|
||||
# compaction hub: the widest window in the fleet.
|
||||
# 170,598 est; worst-case prompt + budget ≈ 170,598 × 1.03 +
|
||||
# 16,384 ≈ 192,100 ≤ 200,704. This endpoint stays the fleet's
|
||||
# compaction hub: still the widest window in this file.
|
||||
maxTokens: 16384
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
@@ -269,14 +218,19 @@ providers:
|
||||
# when explicitly set, and sampleRate/bitRate are REJECTED outright (the
|
||||
# transport throws "openai-speech does not support sampleRate"), so the
|
||||
# codec comes from the output_path suffix and the only tunable that matters
|
||||
# is the voice name — which must be a real pack name (narrator1/2/3); an
|
||||
# unknown name is an engine error, not a fallback.
|
||||
# DELIBERATELY NOT WIRED YET: config.yml `modelRoles.speech` still points at
|
||||
# local/kokoro. Flip it to "frank-tts (dankclaw)/frank-tts" only after the
|
||||
# service is deployed and has answered a real synthesis, because `auth: none`
|
||||
# makes the provider read as AVAILABLE even while the container is down — the
|
||||
# role would then fail instead of falling back to Kokoro. The same flip must
|
||||
# re-key `speech.voice` (now am_fenrir, a Kokoro voice) to a pack name.
|
||||
# is the voice name — and it comes from ONE place: the tool's `voice_id`
|
||||
# argument. No setting feeds it (`speech.voice` is the speak-aloud Kokoro enum,
|
||||
# `tts.localVoice` is the local backend's), so a call that omits voice_id arrives
|
||||
# at 7772 with no `voice` key and the endpoint answers 400 "unknown voice None;
|
||||
# available voices: narrator1, narrator2, narrator3". Say the voice in the call.
|
||||
# WIRED 2026-10-18: `config.yml` `modelRoles.speech` is this row, flipped only
|
||||
# after the service was deployed and answered real synthesis (two packs,
|
||||
# different bytes, 24 kHz PCM16, RTF 0.57). Why the gate existed: `auth: none`
|
||||
# makes this provider read as AVAILABLE even while the container is down, and an
|
||||
# explicit speech role has NO fallback candidate, so a dead 7772 fails the device
|
||||
# loudly instead of quietly narrating in Kokoro. Reverting is ONE key:
|
||||
# `modelRoles.speech: local/kokoro`. `speech.voice` stays a Kokoro name either
|
||||
# way — it was never this endpoint's voice.
|
||||
# SCHEMA LAW, learned the hard way (2026-10-18): provider-level `api` must be
|
||||
# a CHAT transport — the models.yml schema accepts only the chat/judge APIs
|
||||
# there, and naming a runner API (`openai-speech`) at provider level fails the
|
||||
|
||||
@@ -3,30 +3,44 @@ name: grilling
|
||||
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
|
||||
---
|
||||
|
||||
> **Authored override.** Extracted 2026-10-18 from `mattpocock/skills` (identical to upstream at extraction) into the command-center repo so Frank can customize it. Removed from the `skills` CLI global store (`~/.agents/.skill-lock.json` pruned) so `skills update -g` never resurrects a duplicate. Edits to this file are repo prose: branch → PR → Frank OK.
|
||||
# Grilling
|
||||
|
||||
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
|
||||
Interview the user relentlessly until nothing is silently assumed and you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
|
||||
|
||||
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
|
||||
A decision is **frontier** when it can be asked now, without an answer you haven't heard yet. The interview is done when the frontier is empty: every branch of the tree visited.
|
||||
|
||||
Format a round like so:
|
||||
## Rounds
|
||||
|
||||
```
|
||||
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
||||
Work the tree in **rounds**: ask the whole frontier at once, then wait. A decision whose answer hangs on an open question or an unfetched fact joins a later round.
|
||||
|
||||
➡️ <your recommended answer>
|
||||
Ask each closed decision - anything expressible as 2–5 options - through the `ask` tool:
|
||||
|
||||
---
|
||||
- **One artefact per question**, counted before sending. Name the artefact each question decides; two names means two questions. Shared plumbing (an endpoint, a service, a build step) still leaves two artefacts.
|
||||
- Recommended answer first, marked `recommended: 0`, which labels it "(Recommended)" for one-glance acceptance. Every reply names what was picked, so the record is unambiguous.
|
||||
- Options quote the user's goal. Their words, their stated goal, ground the wording; your todo text and table rows stay out of it.
|
||||
- Your own idea is welcome: it gets its own recommended question. Attach it to its own question, so it rides there on its merits.
|
||||
- `multi: true` for pick-many, `header` for the branch chip, `preview` when the decision needs code or config in front of the user.
|
||||
|
||||
❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
||||
Open narrative - "walk me through the deploy flow" - has no options to offer; ask it as prose in chat. Without an `ask` tool the whole round degrades to text: `❓ **Q1 — title**: body`, then `➡️ recommendation`, phrased so one bare "yes" accepts every recommendation.
|
||||
|
||||
➡️ <your recommended answer>
|
||||
```
|
||||
The dialog is exclusive: dispatch scouts before the `ask` call in the same message, or they wait behind it; their results land between rounds. Esc ends the run outright, with no turn to wrap up, so the ledger is what survives it.
|
||||
|
||||
Word each question so "yes" accepts your recommended answer.
|
||||
## The ledger
|
||||
|
||||
Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
|
||||
The tree lives in the `todo` list while the interview runs, so the user watches it fall:
|
||||
|
||||
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
|
||||
- One task per decision, filed under the phase for its branch; `via: <parent decision>` in the task text carries the edge.
|
||||
- `pending` is the frontier; `block` with a one-line reason waits on a fact or an open decision; `done` is settled. Unblock the moment the dependency settles.
|
||||
|
||||
The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
|
||||
Write `local://grilling-state.md` at the end of every round: the tree, each decision's verdict and one-line rationale. It is the durable copy of the interview; keep it current so an Esc costs one round.
|
||||
|
||||
## Facts are yours, decisions are theirs
|
||||
|
||||
- Environment facts (filesystem, repo, live config): dispatch a **scout** per fact-cluster, batched into one `task` call, before the round's `ask`. Cluster tightly; doing so keeps each scout more aware of possible adjacencies.
|
||||
- Outside-world facts: call `web_search` directly, do not delegate to a subagent.
|
||||
- Cheap lookups: `read`/`grep` inline.
|
||||
- The main session is where questions happen: every spawn carries the exact facts to return, and comes back into the next round.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
The frontier is empty and nothing sits blocked. Distil the ledger into a **brief**: one line per decision, verdict plus rationale, grouped by branch. Put it in chat, then one closing `ask`: "Proceed on this brief?" Nothing executes before that yes. After it, fold the brief into the project's knowledge base, or a log markdown file, wherever the project keeps its decisions.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
tt.*
|
||||
.tests
|
||||
doc/tags
|
||||
debug
|
||||
.repro
|
||||
foo.*
|
||||
*.log
|
||||
data
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"neodev": {
|
||||
"library": {
|
||||
"enabled": true,
|
||||
"plugins": true
|
||||
}
|
||||
},
|
||||
"neoconf": {
|
||||
"plugins": {
|
||||
"lua_ls": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,4 @@
|
||||
# 💤 LazyVim
|
||||
|
||||
A starter template for [LazyVim](https://github.com/LazyVim/LazyVim).
|
||||
Refer to the [documentation](https://lazyvim.github.io/installation) to get started.
|
||||
@@ -0,0 +1,2 @@
|
||||
-- bootstrap lazy.nvim, LazyVim and your plugins
|
||||
require("config.lazy")
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"LazyVim": { "branch": "main", "commit": "999700997f72227187d49d8b92667183dc7fc809" },
|
||||
"base16-nvim": { "branch": "master", "commit": "7183774e90841f4bd27a47cb6e75cf532fd1192d" },
|
||||
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
|
||||
"bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" },
|
||||
"catppuccin": { "branch": "main", "commit": "edefef779ab08ce1a4a404713e3012b0d202bd35" },
|
||||
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" },
|
||||
"flash.nvim": { "branch": "main", "commit": "5f0f270fdc7c5b0c21d903ee85b9cb06f2ac636a" },
|
||||
"friendly-snippets": { "branch": "main", "commit": "b4d01b0fdf3c9a549961c2f9ffe8dc09be166219" },
|
||||
"gitsigns.nvim": { "branch": "main", "commit": "fd780920613e541cbdfac41bf711583ed49f9249" },
|
||||
"grug-far.nvim": { "branch": "main", "commit": "bbde8ef5ae46ceee2c3bd8b21fc048c5f8ffbfc0" },
|
||||
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
|
||||
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
|
||||
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
|
||||
"mason-lspconfig.nvim": { "branch": "main", "commit": "00b5473cca4db5b410ff4bae724e3b6e21f2687f" },
|
||||
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
|
||||
"mini.ai": { "branch": "main", "commit": "d0c4987a1ead2bcf4eb22cc9815c662d76677c08" },
|
||||
"mini.icons": { "branch": "main", "commit": "f916635dc5ae2f479fbb9ab8184bb9444b62e86c" },
|
||||
"mini.pairs": { "branch": "main", "commit": "03eb1f0062a2cee5314d1ac3939246e4bb21352e" },
|
||||
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
|
||||
"nui.nvim": { "branch": "main", "commit": "10fc361835c856ba4233ef5ea135b919bf3dce97" },
|
||||
"nvim-lint": { "branch": "master", "commit": "f6230a5c06ba59e6059c14637f1dbe29a7978b9b" },
|
||||
"nvim-lspconfig": { "branch": "master", "commit": "45a3a444b710511c39abca3b60bb6e23a21e3de5" },
|
||||
"nvim-treesitter": { "branch": "main", "commit": "29212d826054bb2948fa9d3a99eca9bf6a050eb2" },
|
||||
"nvim-treesitter-textobjects": { "branch": "main", "commit": "5c7b0263797dfd1bd6202f2b219f3b53a80b2187" },
|
||||
"nvim-ts-autotag": { "branch": "main", "commit": "d7220b55e16ed3c04e88086256def5d282429493" },
|
||||
"persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" },
|
||||
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
|
||||
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
|
||||
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },
|
||||
"tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" },
|
||||
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
|
||||
"ts-comments.nvim": { "branch": "main", "commit": "a59d6092213447450191122c9346f309161504cb" },
|
||||
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extras": [
|
||||
|
||||
],
|
||||
"install_version": 8,
|
||||
"news": {
|
||||
"NEWS.md": "11866"
|
||||
},
|
||||
"version": 8
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Autocmds are automatically loaded on the VeryLazy event
|
||||
-- Default autocmds that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/autocmds.lua
|
||||
--
|
||||
-- Add any additional autocmds here
|
||||
-- with `vim.api.nvim_create_autocmd`
|
||||
--
|
||||
-- Or remove existing autocmds by their group name (which is prefixed with `lazyvim_` for the defaults)
|
||||
-- e.g. vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell")
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Keymaps are automatically loaded on the VeryLazy event
|
||||
-- Default keymaps that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua
|
||||
-- Add any additional keymaps here
|
||||
@@ -0,0 +1,53 @@
|
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||
if not (vim.uv or vim.loop).fs_stat(lazypath) then
|
||||
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
|
||||
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
vim.api.nvim_echo({
|
||||
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
|
||||
{ out, "WarningMsg" },
|
||||
{ "\nPress any key to exit..." },
|
||||
}, true, {})
|
||||
vim.fn.getchar()
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
vim.opt.rtp:prepend(lazypath)
|
||||
|
||||
require("lazy").setup({
|
||||
spec = {
|
||||
-- add LazyVim and import its plugins
|
||||
{ "LazyVim/LazyVim", import = "lazyvim.plugins" },
|
||||
-- import/override with your plugins
|
||||
{ import = "plugins" },
|
||||
},
|
||||
defaults = {
|
||||
-- By default, only LazyVim plugins will be lazy-loaded. Your custom plugins will load during startup.
|
||||
-- If you know what you're doing, you can set this to `true` to have all your custom plugins lazy-loaded by default.
|
||||
lazy = false,
|
||||
-- It's recommended to leave version=false for now, since a lot the plugin that support versioning,
|
||||
-- have outdated releases, which may break your Neovim install.
|
||||
version = false, -- always use the latest git commit
|
||||
-- version = "*", -- try installing the latest stable version for plugins that support semver
|
||||
},
|
||||
install = { colorscheme = { "tokyonight", "habamax" } },
|
||||
checker = {
|
||||
enabled = true, -- check for plugin updates periodically
|
||||
notify = false, -- notify on update
|
||||
}, -- automatically check for plugin updates
|
||||
performance = {
|
||||
rtp = {
|
||||
-- disable some rtp plugins
|
||||
disabled_plugins = {
|
||||
"gzip",
|
||||
-- "matchit",
|
||||
-- "matchparen",
|
||||
-- "netrwPlugin",
|
||||
"tarPlugin",
|
||||
"tohtml",
|
||||
"tutor",
|
||||
"zipPlugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Options are automatically loaded before lazy.nvim startup
|
||||
-- Default options that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua
|
||||
-- Add any additional options here
|
||||
@@ -0,0 +1,70 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
require('base16-colorscheme').setup({
|
||||
base00 = '#181818',
|
||||
base01 = '#282828',
|
||||
base02 = '#323232',
|
||||
base03 = '#646464',
|
||||
base04 = '#95a99f',
|
||||
base05 = '#e4e4ef',
|
||||
base06 = '#e4e4ef',
|
||||
base07 = '#e4e4ef',
|
||||
base08 = '#f43841',
|
||||
base09 = '#9e95c7',
|
||||
base0A = '#96a6c8',
|
||||
base0B = '#f43841',
|
||||
base0C = '#a596e9',
|
||||
base0D = '#f8868c',
|
||||
base0E = '#96b0e9',
|
||||
base0F = '#becff4',
|
||||
})
|
||||
|
||||
local hi = function(group, opts)
|
||||
vim.api.nvim_set_hl(0, group, opts)
|
||||
end
|
||||
|
||||
-- telescope.nvim
|
||||
hi('TelescopeNormal', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('TelescopeBorder', { fg = '#646464', bg = '#181818' })
|
||||
hi('TelescopePromptNormal', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('TelescopePromptBorder', { fg = '#646464', bg = '#181818' })
|
||||
hi('TelescopePromptPrefix', { fg = '#f43841', bg = '#181818' })
|
||||
hi('TelescopePromptCounter', { fg = '#95a99f', bg = '#181818' })
|
||||
hi('TelescopePromptTitle', { fg = '#181818', bg = '#f43841' })
|
||||
hi('TelescopePreviewTitle', { fg = '#181818', bg = '#96a6c8' })
|
||||
hi('TelescopeResultsTitle', { fg = '#181818', bg = '#9e95c7' })
|
||||
hi('TelescopeSelection', { fg = '#e4e4ef', bg = '#323232' })
|
||||
hi('TelescopeSelectionCaret', { fg = '#f43841', bg = '#323232' })
|
||||
hi('TelescopeMatching', { fg = '#f43841', bold = true })
|
||||
|
||||
-- mini.pick
|
||||
hi('MiniPickNormal', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('MiniPickBorder', { fg = '#646464', bg = '#181818' })
|
||||
hi('MiniPickPrompt', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('MiniPickPromptPrefix', { fg = '#f43841', bg = '#181818' })
|
||||
hi('MiniPickBorderText', { fg = '#181818', bg = '#f43841' })
|
||||
hi('MiniPickMatchCurrent', { fg = '#e4e4ef', bg = '#323232' })
|
||||
hi('MiniPickPromptCaret', { fg = '#f43841', bg = '#323232' })
|
||||
hi('MiniPickMatchRanges', { fg = '#f43841', bold = true })
|
||||
end
|
||||
|
||||
-- Register a signal handler for SIGUSR1 (matugen updates).
|
||||
-- The handler re-requires this module, which re-runs the code below, so the
|
||||
-- previous handle is stopped first; otherwise handlers double on every signal.
|
||||
if _G.__matugen_signal then
|
||||
_G.__matugen_signal:stop()
|
||||
_G.__matugen_signal:close()
|
||||
end
|
||||
|
||||
local signal = vim.uv.new_signal()
|
||||
_G.__matugen_signal = signal
|
||||
signal:start(
|
||||
'sigusr1',
|
||||
vim.schedule_wrap(function()
|
||||
package.loaded['matugen'] = nil
|
||||
require('matugen').setup()
|
||||
end)
|
||||
)
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,197 @@
|
||||
-- since this is just an example spec, don't actually load anything here and return an empty spec
|
||||
-- stylua: ignore
|
||||
if true then return {} end
|
||||
|
||||
-- every spec file under the "plugins" directory will be loaded automatically by lazy.nvim
|
||||
--
|
||||
-- In your plugin files, you can:
|
||||
-- * add extra plugins
|
||||
-- * disable/enabled LazyVim plugins
|
||||
-- * override the configuration of LazyVim plugins
|
||||
return {
|
||||
-- add gruvbox
|
||||
{ "ellisonleao/gruvbox.nvim" },
|
||||
|
||||
-- Configure LazyVim to load gruvbox
|
||||
{
|
||||
"LazyVim/LazyVim",
|
||||
opts = {
|
||||
colorscheme = "gruvbox",
|
||||
},
|
||||
},
|
||||
|
||||
-- change trouble config
|
||||
{
|
||||
"folke/trouble.nvim",
|
||||
-- opts will be merged with the parent spec
|
||||
opts = { use_diagnostic_signs = true },
|
||||
},
|
||||
|
||||
-- disable trouble
|
||||
{ "folke/trouble.nvim", enabled = false },
|
||||
|
||||
-- override nvim-cmp and add cmp-emoji
|
||||
{
|
||||
"hrsh7th/nvim-cmp",
|
||||
dependencies = { "hrsh7th/cmp-emoji" },
|
||||
---@param opts cmp.ConfigSchema
|
||||
opts = function(_, opts)
|
||||
table.insert(opts.sources, { name = "emoji" })
|
||||
end,
|
||||
},
|
||||
|
||||
-- change some telescope options and a keymap to browse plugin files
|
||||
{
|
||||
"nvim-telescope/telescope.nvim",
|
||||
keys = {
|
||||
-- add a keymap to browse plugin files
|
||||
-- stylua: ignore
|
||||
{
|
||||
"<leader>fp",
|
||||
function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end,
|
||||
desc = "Find Plugin File",
|
||||
},
|
||||
},
|
||||
-- change some options
|
||||
opts = {
|
||||
defaults = {
|
||||
layout_strategy = "horizontal",
|
||||
layout_config = { prompt_position = "top" },
|
||||
sorting_strategy = "ascending",
|
||||
winblend = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- add pyright to lspconfig
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
---@class PluginLspOpts
|
||||
opts = {
|
||||
---@type lspconfig.options
|
||||
servers = {
|
||||
-- pyright will be automatically installed with mason and loaded with lspconfig
|
||||
pyright = {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- add tsserver and setup with typescript.nvim instead of lspconfig
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
dependencies = {
|
||||
"jose-elias-alvarez/typescript.nvim",
|
||||
init = function()
|
||||
require("lazyvim.util").lsp.on_attach(function(_, buffer)
|
||||
-- stylua: ignore
|
||||
vim.keymap.set( "n", "<leader>co", "TypescriptOrganizeImports", { buffer = buffer, desc = "Organize Imports" })
|
||||
vim.keymap.set("n", "<leader>cR", "TypescriptRenameFile", { desc = "Rename File", buffer = buffer })
|
||||
end)
|
||||
end,
|
||||
},
|
||||
---@class PluginLspOpts
|
||||
opts = {
|
||||
---@type lspconfig.options
|
||||
servers = {
|
||||
-- tsserver will be automatically installed with mason and loaded with lspconfig
|
||||
tsserver = {},
|
||||
},
|
||||
-- you can do any additional lsp server setup here
|
||||
-- return true if you don't want this server to be setup with lspconfig
|
||||
---@type table<string, fun(server:string, opts:_.lspconfig.options):boolean?>
|
||||
setup = {
|
||||
-- example to setup with typescript.nvim
|
||||
tsserver = function(_, opts)
|
||||
require("typescript").setup({ server = opts })
|
||||
return true
|
||||
end,
|
||||
-- Specify * to use this function as a fallback for any server
|
||||
-- ["*"] = function(server, opts) end,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- for typescript, LazyVim also includes extra specs to properly setup lspconfig,
|
||||
-- treesitter, mason and typescript.nvim. So instead of the above, you can use:
|
||||
{ import = "lazyvim.plugins.extras.lang.typescript" },
|
||||
|
||||
-- add more treesitter parsers
|
||||
{
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
opts = {
|
||||
ensure_installed = {
|
||||
"bash",
|
||||
"html",
|
||||
"javascript",
|
||||
"json",
|
||||
"lua",
|
||||
"markdown",
|
||||
"markdown_inline",
|
||||
"python",
|
||||
"query",
|
||||
"regex",
|
||||
"tsx",
|
||||
"typescript",
|
||||
"vim",
|
||||
"yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- since `vim.tbl_deep_extend`, can only merge tables and not lists, the code above
|
||||
-- would overwrite `ensure_installed` with the new value.
|
||||
-- If you'd rather extend the default config, use the code below instead:
|
||||
{
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
opts = function(_, opts)
|
||||
-- add tsx and treesitter
|
||||
vim.list_extend(opts.ensure_installed, {
|
||||
"tsx",
|
||||
"typescript",
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- the opts function can also be used to change the default opts:
|
||||
{
|
||||
"nvim-lualine/lualine.nvim",
|
||||
event = "VeryLazy",
|
||||
opts = function(_, opts)
|
||||
table.insert(opts.sections.lualine_x, {
|
||||
function()
|
||||
return "😄"
|
||||
end,
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- or you can return new options to override all the defaults
|
||||
{
|
||||
"nvim-lualine/lualine.nvim",
|
||||
event = "VeryLazy",
|
||||
opts = function()
|
||||
return {
|
||||
--[[add your custom lualine config here]]
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
-- use mini.starter instead of alpha
|
||||
{ import = "lazyvim.plugins.extras.ui.mini-starter" },
|
||||
|
||||
-- add jsonls and schemastore packages, and setup treesitter for json, json5 and jsonc
|
||||
{ import = "lazyvim.plugins.extras.lang.json" },
|
||||
|
||||
-- add any tools you want to have installed below
|
||||
{
|
||||
"williamboman/mason.nvim",
|
||||
opts = {
|
||||
ensure_installed = {
|
||||
"stylua",
|
||||
"shellcheck",
|
||||
"shfmt",
|
||||
"flake8",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Applies the hand-kept wallpaper palette (lua/matugen.lua) on top of LazyVim's
|
||||
-- colorscheme instead of living with tokyonight defaults.
|
||||
--
|
||||
-- base16-colorscheme writes highlights directly and never touches v:colors_name,
|
||||
-- so LazyVim keeps thinking tokyonight is loaded and its plugin-specific tweaks
|
||||
-- still run; ours is the last word on the UI groups.
|
||||
return {
|
||||
"RRethy/base16-nvim",
|
||||
lazy = false,
|
||||
priority = 900,
|
||||
config = function()
|
||||
-- LazyVim loads its colorscheme after plugin configs, so re-apply on every
|
||||
-- ColorScheme event: that also makes `:colorscheme foo` land on our palette.
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
desc = "Re-apply the matugen base16 palette after any colorscheme",
|
||||
callback = function()
|
||||
require("matugen").setup()
|
||||
end,
|
||||
})
|
||||
require("matugen").setup()
|
||||
end,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
column_width = 120
|
||||
@@ -275,13 +275,13 @@ transition = [ "fade" ]
|
||||
interval_seconds = 1200
|
||||
|
||||
[wallpaper.default]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9w8r5.jpg"
|
||||
|
||||
[wallpaper.last]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9w8r5.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9w8r5.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-2]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
@@ -290,7 +290,7 @@ transition = [ "fade" ]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
|
||||
|
||||
[wallpaper.monitors.eDP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9w8r5.jpg"
|
||||
|
||||
[weather]
|
||||
unit = "imperial"
|
||||
|
||||
@@ -4,3 +4,8 @@ source /usr/share/cachyos-zsh-config/cachyos-config.zsh
|
||||
[ -f ~/.config/secrets/hf_token ] && export HF_TOKEN="$(cat ~/.config/secrets/hf_token)"
|
||||
|
||||
[ -d "$HOME/.bun/bin" ] && export PATH="$HOME/.bun/bin:$PATH"
|
||||
|
||||
# LazyVim lives in its own NVIM_APPNAME profile (~/.config/lazyvim), so plain
|
||||
# `nvim` stays a vanilla quick-edit editor. NVIM_APPNAME swaps config, data,
|
||||
# state and cache dirs wholesale — no shared plugin tree between the two.
|
||||
lazyvim() { NVIM_APPNAME=lazyvim nvim "$@"; }
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
Agentic web research means that an AI drives the search loop. The agent formulates queries, reads results, decides what to read next, and writes a synthesis. It works best when search, extraction, and reasoning each run in the right place.
|
||||
|
||||
> **Your setup:** Pi Web Access calls SearXNG at `192.168.0.143:8081` (dankclaw) and serves models through llama.cpp at `192.168.0.235:7770`, `192.168.0.143:7770`, and `localhost:8080`. The pages below assume that topology.
|
||||
> **Fleet topology (2026-10):** SearXNG lives at `192.168.0.143:8081` (UI) /
|
||||
> `:8082` (JSON) on dankclaw; model endpoints are `frank-archon`
|
||||
> (dankserver :7770, radiance), `frank-dragoon` (dankclaw :7770,
|
||||
> halogen-flash-server), `frank-zealot` (dankclaw :7771, llama.cpp PrismML).
|
||||
> The pi-era `localhost:8080` llama.cpp endpoint is retired. See
|
||||
> [System hosts](/system/index.md).
|
||||
|
||||
This section connects four decisions:
|
||||
|
||||
|
||||
@@ -32,9 +32,15 @@ Qwen3 is the current state of the art for open function calling and tool use. It
|
||||
|
||||
The 8B and 14B models run on one consumer GPU. The 30B-A3B MoE runs on a single A100-class card or equivalent.
|
||||
|
||||
## Best Vision Model: Qwen2.5-VL-7B
|
||||
## Best Vision Model: Qwen3-VL (Apache 2.0)
|
||||
|
||||
Many web tasks need a model that can read a page image, not just text. Qwen2.5-VL-7B is the best open vision model for this. It reads text, charts, icons, and layout in screenshots. It supports computer-use and phone-use agents. Apache 2.0 license. The 7B model fits on a single GPU with 8 GB or more.
|
||||
Many web tasks need a model that can read a page image, not just text.
|
||||
**Qwen3-VL** supersedes Qwen2.5-VL as the strongest open-weights vision family
|
||||
(4B/8B dense through 30B-A3B/235B-A22B MoE): text, chart, icon, and layout
|
||||
reading in screenshots, plus computer-use agent support. Small dense sizes
|
||||
ship Apache 2.0; a quantized 4B/8B runs on a single consumer GPU. On this
|
||||
fleet, vision duty is carried by the mmproj vision towers on the endpoints
|
||||
(dragoon, zealot) — see [System hosts](/system/index.md).
|
||||
|
||||
Llama 4 Scout is the larger multimodal option. Verify the claims before you trust them:
|
||||
|
||||
@@ -47,7 +53,7 @@ Llama 4 Scout is the larger multimodal option. Verify the claims before you trus
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Consumer GPU 16-24GB] --> B[Qwen3-8B / 14B]
|
||||
A --> C[Qwen2.5-VL-7B]
|
||||
A --> C[Qwen3-VL-4B / 8B]
|
||||
D[Single server GPU 80GB] --> E[Qwen3-30B-A3B]
|
||||
D --> F[Llama 4 Scout INT4]
|
||||
G[Multi-GPU cluster] --> H[Qwen3-235B-A22B]
|
||||
@@ -71,7 +77,7 @@ llama-server -m qwen3-8b-q4_k_m.gguf -c 131072 --port 7770
|
||||
| Need | Pick |
|
||||
|------|------|
|
||||
| Best tool calling, one consumer GPU | Qwen3-8B or 14B |
|
||||
| Visually read a web page | Qwen2.5-VL-7B |
|
||||
| Visually read a web page | Qwen3-VL-4B / 8B |
|
||||
| Server-scale reliable agent | Qwen3-235B-A22B |
|
||||
| Cheapest acceptable agent | Qwen3-8B on a mid GPU |
|
||||
|
||||
@@ -79,7 +85,8 @@ llama-server -m qwen3-8b-q4_k_m.gguf -c 131072 --port 7770
|
||||
|
||||
- Qwen3 technical report: <https://qwenlm.github.io/blog/qwen3/>
|
||||
- Qwen3 repository: <https://github.com/QwenLM/Qwen3>
|
||||
- Qwen2.5-VL-7B model card: <https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct>
|
||||
- Qwen3-VL repository: <https://github.com/QwenLM/Qwen3-VL> (tech report: <https://arxiv.org/abs/2511.21631>)
|
||||
- Qwen2.5-VL-7B model card (predecessor): <https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct>
|
||||
- Meta AI Llama 4 announcement: <https://ai.meta.com/blog/llama-4-multimodal-intelligence/>
|
||||
- Ollama library: <https://ollama.com/library>
|
||||
- Browser Use supported models: <https://docs.browser-use.com/open-source/supported-models>
|
||||
|
||||
@@ -8,18 +8,41 @@ tags:
|
||||
- privacy
|
||||
- configuration
|
||||
- unverified
|
||||
verified: 2026-08-28
|
||||
verified: 2026-10-19
|
||||
timestamp: 2026-08-11
|
||||
resource: https://docs.searxng.org
|
||||
---
|
||||
|
||||
# SearXNG Tuning
|
||||
|
||||
SearXNG is a metasearch engine. It aggregates 274 or more upstream search services into one result set. Configuration lives in `settings.yml`. The recommended deployment is Docker Compose with a Valkey container.
|
||||
SearXNG is a metasearch engine. It aggregates hundreds of upstream search
|
||||
services into one result set (upstream `settings.yml` defines 388 engine
|
||||
entries at master, probed 2026-10-19; the live fleet instance loads 262).
|
||||
Configuration lives in `settings.yml`. The recommended deployment is Docker
|
||||
Compose (or Podman Quadlet) with a Valkey container.
|
||||
|
||||
This page lists the settings that matter for speed, privacy, and useful results.
|
||||
|
||||
> **Your setup:** Pi Web Access calls a remote SearXNG at [`192.168.0.143:8081`](../pi/packages/pi-web-access.md) (dankclaw). Apply these settings to that host, not to this machine.
|
||||
> **Fleet instance:** the harness `web/searxng` provider calls the instance at
|
||||
> `192.168.0.143:8081` (UI) / `:8082` (JSON API) on dankclaw. The pi-era
|
||||
> [Pi Web Access](../pi/packages/pi-web-access.md) called the same host.
|
||||
|
||||
## Live instance (probed 2026-10-19)
|
||||
|
||||
User Quadlet stack on dankclaw: `searxng.container`
|
||||
(`ghcr.io/searxng/searxng:latest`) + `searxng-valkey.container`
|
||||
(`docker.io/valkey/valkey:9-alpine`) + `gluetun.container` (VPN egress; the
|
||||
containers share its network namespace, which is why `podman ps` shows :8081/
|
||||
:8082 on several names) + `marreta`/`marreta_browser`. `limiter.toml`: enabled,
|
||||
`pass_searxng_org: true`.
|
||||
|
||||
**Policy drift flag:** this page's tuned engine policy (google and bing
|
||||
disabled; DuckDuckGo/Wikipedia/Google News enabled — verified via `/config` on
|
||||
2026-08-16) is NOT live: `/config` on 2026-10-19 reports all 262 engines
|
||||
enabled, google and bing included. Either the settings override was lost in
|
||||
the 2026-10 stack reinstatement or the policy was deliberately dropped.
|
||||
Confirm with Frank before re-applying.
|
||||
|
||||
|
||||
## Plan the Deployment
|
||||
|
||||
|
||||
@@ -29,10 +29,17 @@ Key ideas:
|
||||
- Ten watchdogs handle CAPTCHAs, crashes, popups, downloads, and security.
|
||||
- Real browser profiles carry saved logins.
|
||||
|
||||
**On the "87.4 percent" number:** that result refers to Browser Use's managed Cloud agent (the Hybrid type that blends structured browser tools with screenshots). Peer-reviewed results for vision-only (CUA) models are much lower, about 44.5 percent on the same task type. Treat any single accuracy number as one benchmark, not a universal score.
|
||||
**On the "87.4 percent" number:** that result refers to Browser Use's managed
|
||||
Cloud agent (the Hybrid type that blends structured browser tools with
|
||||
screenshots), not the open-source library you self-host. Academic
|
||||
computer-use results on comparable web tasks run far lower, and an earlier
|
||||
"44.5 percent peer-reviewed CUA" figure carried in this page could not be
|
||||
re-attributed to a specific published benchmark (checked 2026-10-19) and was
|
||||
dropped. Treat any single accuracy number as one benchmark, not a universal
|
||||
score.
|
||||
|
||||
```python
|
||||
# Local model via llama.cpp (OpenAI-compatible endpoint)
|
||||
# Local model via any OpenAI-compatible endpoint
|
||||
from browser_use import Agent
|
||||
from browser_use.llm.openai import ChatOpenAI
|
||||
|
||||
@@ -40,7 +47,7 @@ agent = Agent(
|
||||
task="Search for the latest AI news",
|
||||
llm=ChatOpenAI(
|
||||
base_url="http://192.168.0.235:7770/v1",
|
||||
model="frank-default-small",
|
||||
model="frank-archon",
|
||||
api_key="EMPTY",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -171,8 +171,14 @@ This pattern places the whole SearXNG process inside the tunnel namespace. The h
|
||||
4. Confirm that unrelated host traffic keeps the host IP.
|
||||
|
||||
```bash
|
||||
# From inside the tunnel with Gluetun
|
||||
docker exec gluetun curl -s ifconfig.me
|
||||
# Gluetun ships no curl; use its own endpoints or a throwaway container.
|
||||
podman exec gluetun sh -c 'test -e /sys/class/net/tun0 && echo tunnel-up'
|
||||
podman exec gluetun sh -c 'wget -qO- 127.0.0.1:9999/health' # "OK" when up
|
||||
# NOTE: :9999 is Gluetun's HTTP health server (200 "OK" / 500); :8000 is the
|
||||
# CONTROL server (/v1/* API), which 404s on everything else. Do not health-
|
||||
# probe :8000. Egress-IP check needs a real binary — run a companion
|
||||
# container on gluetun's network: docker run --rm --net container:gluetun
|
||||
# curlimages/curl -s ifconfig.me
|
||||
```
|
||||
|
||||
## Citations
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
Caddy is the reverse proxy and TLS terminator for the homelab. A single caddy
|
||||
container on dankclaw (192.168.0.143) fronts every service on both hosts. It
|
||||
runs Caddy v2.11.4 from the custom `franksdank/caddy-porkbun` image
|
||||
(xcaddy-built with the Porkbun DNS-01 module, built locally on dankclaw).
|
||||
runs Caddy v2.11.7 (verified in-container 2026-10-19) from the custom
|
||||
`localhost/franksdank/caddy-porkbun:latest` image (xcaddy-built with the
|
||||
Porkbun DNS-01 module, built locally on dankclaw).
|
||||
Since 2026-08-26 it is managed as a Podman Quadlet unit (`caddy.container`)
|
||||
installed to `~/.config/containers/systemd/`, providing boot-persistent,
|
||||
auto-restart service management.
|
||||
|
||||
* [[caddyfile|caddyfile]] — directive and matcher syntax, named matchers,
|
||||
directive sorting, and the `handle`/`route` order rule.
|
||||
|
||||
The Caddyfile lives in the serverAppConfigs repo at
|
||||
`dankclaw/Caddyfile`. `deploy.sh` renders basicauth hashes from the host `.env`
|
||||
@@ -27,4 +26,4 @@ resolves `*.franksdank.cloud` names to dankclaw.
|
||||
|
||||
The porkbun DNS module source: [caddy-dns/porkbun](https://github.com/caddy-dns/porkbun).
|
||||
|
||||
* [Caddyfile Syntax](caddyfile.md) — Caddy Caddyfile reference — directive and matcher syntax, named matchers, directive sorting, and the handle/route order rule. Grounded in caddyserver.com (fetched 2026-08-28).
|
||||
* [Caddyfile Syntax](caddyfile.md) — Caddy Caddyfile reference — directive and matcher syntax, named matchers, directive sorting, and the `handle`/`route` order rule. Grounded in caddyserver.com (fetched 2026-08-28).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user