Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6664f9090a | ||
|
|
7de07ee35c | ||
|
|
fcc7226c3b | ||
|
|
a8011c3d4d |
@@ -100,15 +100,18 @@ secret in every agent's context window on every machine that pulls this repo.
|
|||||||
|
|
||||||
| box | role | GPU | notes |
|
| 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` |
|
| 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` |
|
| 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
|
## Environment gotchas
|
||||||
|
|
||||||
- Gitea API needs a token: `GITEA_TOKEN` env (gitignored `.env`). No `tea` CLI
|
- 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
|
installed anywhere. Remote URLs carry NO embedded token (2026-10-09
|
||||||
file is never pushed; it is not a substitute for the secret gate.
|
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
|
- Passwordless sudo (`sudo -n`) works on Dankclaw and Dankserver for systemctl
|
||||||
verbs only; user units need no sudo at all.
|
verbs only; user units need no sudo at all.
|
||||||
- SSH into both servers is BatchMode-capable from here:
|
- SSH into both servers is BatchMode-capable from here:
|
||||||
|
|||||||
@@ -96,6 +96,17 @@ Never assume the user reports correct information. He is human, and humans make
|
|||||||
- Always practice Test Driven Development practices; do UNIT TESTS exist for the functionality about to be edited? Is there a LINTER? Is there a FORMATTER? Verify that they exist, and that they are configured as a pre-commit hook. If they do not exist, they MUST be written PRIOR to the code being written. Invoke `/skill:tdd` for preferred testing style. If tests do exist, ensure that they still match the planned changes. If you believe a PRE-EXISTING test must be changed prior to commencing work, ASK THE USER ABOUT IT.
|
- Always practice Test Driven Development practices; do UNIT TESTS exist for the functionality about to be edited? Is there a LINTER? Is there a FORMATTER? Verify that they exist, and that they are configured as a pre-commit hook. If they do not exist, they MUST be written PRIOR to the code being written. Invoke `/skill:tdd` for preferred testing style. If tests do exist, ensure that they still match the planned changes. If you believe a PRE-EXISTING test must be changed prior to commencing work, ASK THE USER ABOUT IT.
|
||||||
- Always ensure tests run on code changes after every commit, and all project tests run before every push using git hooks, EXCEPT in the event that the individual test in question takes an excessive amount of time. Always ask the user before disabling an individual due to running time or performance concerns.
|
- Always ensure tests run on code changes after every commit, and all project tests run before every push using git hooks, EXCEPT in the event that the individual test in question takes an excessive amount of time. Always ask the user before disabling an individual due to running time or performance concerns.
|
||||||
|
|
||||||
|
|
||||||
|
### 6. Keep the Todo List LIVE, Not Retrospective
|
||||||
|
|
||||||
|
The todo list is the user's cockpit view of your work. A list that only flips at the end is a history dump, not tracking - the user sees nothing while you work.
|
||||||
|
|
||||||
|
- Multi-step work: `todo init` BEFORE the first substantive step - the list must exist while the work is unseen, not after.
|
||||||
|
- Batch the todo transition with the step it describes: `done` + `start` ride along in the same message as the first tool calls of the next step. NEVER defer flips until the whole bill of tasks is finished.
|
||||||
|
- A task is `done` the moment its work is verified - not when the surrounding phase ends.
|
||||||
|
- Blocked on external input (user answer, subagent, service)? `block` it with a reason so the stall is visible; `unblock` the instant it is actionable.
|
||||||
|
- NEVER make a todo call the only call in a message - that is ceremony, not progress; pair every flip with real work.
|
||||||
|
|
||||||
## FORBIDDEN ACTS AND MISTAKES TO NOT REPEAT (DO NOT DO THESE):
|
## FORBIDDEN ACTS AND MISTAKES TO NOT REPEAT (DO NOT DO THESE):
|
||||||
- Editing node_modules or similar import module directories. This is third party code that will be overwritten next time `npm update` or a similar command is run.
|
- Editing node_modules or similar import module directories. This is third party code that will be overwritten next time `npm update` or a similar command is run.
|
||||||
- `git push` - unless directed, except if it's our local gitea instance on Dankclaw.
|
- `git push` - unless directed, except if it's our local gitea instance on Dankclaw.
|
||||||
|
|||||||
+18
-5
@@ -1,6 +1,10 @@
|
|||||||
providers:
|
providers:
|
||||||
openrouterVariant: online
|
openrouterVariant: online
|
||||||
fetch: auto
|
fetch: auto
|
||||||
|
maxInFlightRequests:
|
||||||
|
llamacpp (dankserver): 3
|
||||||
|
llamacpp (dankclaw): 2
|
||||||
|
llamacpp (dankclaw-3090): 3
|
||||||
modelRoles:
|
modelRoles:
|
||||||
slow: llamacpp (dankclaw)/frank-dragoon:medium
|
slow: llamacpp (dankclaw)/frank-dragoon:medium
|
||||||
vision: llamacpp (dankclaw-3090)/frank-zealot:xhigh
|
vision: llamacpp (dankclaw-3090)/frank-zealot:xhigh
|
||||||
@@ -13,7 +17,7 @@ modelRoles:
|
|||||||
default: llamacpp (dankserver)/frank-archon:medium
|
default: llamacpp (dankserver)/frank-archon:medium
|
||||||
smol: llamacpp (dankclaw-3090)/frank-zealot:medium
|
smol: llamacpp (dankclaw-3090)/frank-zealot:medium
|
||||||
web: web/searxng
|
web: web/searxng
|
||||||
speech: local/kokoro
|
speech: frank-tts (dankclaw)/frank-tts
|
||||||
judge: openrouter/~typesafe/jev-latest:auto
|
judge: openrouter/~typesafe/jev-latest:auto
|
||||||
dictation: local/parakeet-tdt-0.6b-v3
|
dictation: local/parakeet-tdt-0.6b-v3
|
||||||
memory: llamacpp (dankclaw-3090)/frank-zealot:medium
|
memory: llamacpp (dankclaw-3090)/frank-zealot:medium
|
||||||
@@ -145,17 +149,25 @@ display:
|
|||||||
subagentLivePreview: true
|
subagentLivePreview: true
|
||||||
task:
|
task:
|
||||||
showResolvedModelBadge: true
|
showResolvedModelBadge: true
|
||||||
maxConcurrency: 3
|
maxConcurrency: 2
|
||||||
agentModelOverrides:
|
agentModelOverrides:
|
||||||
designer: llamacpp (dankclaw-3090)/frank-zealot
|
designer: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
librarian: llamacpp (dankclaw-3090)/frank-zealot
|
librarian: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
reviewer: llamacpp (dankserver)/frank-archon
|
reviewer: llamacpp (dankserver)/frank-archon
|
||||||
scout: llamacpp (dankserver)/frank-archon
|
scout: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
security-reviewer: llamacpp (dankclaw)/frank-dragoon
|
security-reviewer: llamacpp (dankclaw)/frank-dragoon
|
||||||
sonic: llamacpp (dankclaw-3090)/frank-zealot
|
sonic: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
task: llamacpp (dankserver)/frank-archon
|
task: llamacpp (dankserver)/frank-archon
|
||||||
|
seance: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
eager: default
|
eager: default
|
||||||
enableLsp: true
|
enableLsp: true
|
||||||
|
maxRuntimeMs: 1800000
|
||||||
|
softRequestBudget: 120
|
||||||
|
maxRecursionDepth: 1
|
||||||
|
disabledAgents: []
|
||||||
|
isolation:
|
||||||
|
enabled: true
|
||||||
|
completionProbe: false
|
||||||
snapcompact:
|
snapcompact:
|
||||||
systemPrompt: none
|
systemPrompt: none
|
||||||
toolResults: false
|
toolResults: false
|
||||||
@@ -203,8 +215,8 @@ vault:
|
|||||||
tts:
|
tts:
|
||||||
localVoice: bm_george
|
localVoice: bm_george
|
||||||
speech:
|
speech:
|
||||||
enabled: true
|
enabled: false
|
||||||
mode: yield
|
mode: all
|
||||||
enhanced: true
|
enhanced: true
|
||||||
voice: am_fenrir
|
voice: am_fenrir
|
||||||
secrets:
|
secrets:
|
||||||
@@ -232,6 +244,7 @@ followUpMode: one-at-a-time
|
|||||||
tools:
|
tools:
|
||||||
approvalMode: yolo
|
approvalMode: yolo
|
||||||
format: auto
|
format: auto
|
||||||
|
maxTimeout: 600
|
||||||
marketplace:
|
marketplace:
|
||||||
autoUpdate: notify
|
autoUpdate: notify
|
||||||
bash:
|
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
|
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),
|
## Read first
|
||||||
when it **finishes its deliverable and still never exits**, when you want a hard cap
|
- 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.
|
||||||
before spawning, or when you must tell a busy agent to stop.
|
- `cfg://task` and `cfg://providers/maxInFlightRequests` — effective values, not the stock defaults.
|
||||||
|
|
||||||
## The knobs (`cfg://task/...`)
|
## The knobs (cfg:// writes; `/save` suffix persists)
|
||||||
|
| Knob | Effect | Gotcha |
|
||||||
| Key | Default | Meaning |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `maxRuntimeMs` | `0` (unlimited) | **Hard wall-clock limit per subagent (ms).** The only true time cap. |
|
| `task.maxConcurrency` | session semaphore; excess queues | stock default 32 — "2" is not a default |
|
||||||
| `softRequestBudget` | `200` | Soft cap on assistant **requests** per run; force-stop at **1.5×**. |
|
| `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 |
|
||||||
| `softRequestBudgetNotice` | `true` | Injects one "wrap up" steering message at the soft mark. |
|
| `task.maxRuntimeMs` | hard wall clock, overrun = `aborted` | default 0 = off |
|
||||||
| `agentIdleTtlMs` | `420000` | Parks an **idle** agent after 7 min. Useless against a busy-but-slow agent. |
|
| `task.softRequestBudget` | wrap-up notice at N, force-yield at 1.5N | scout/sonic may impose lower built-ins |
|
||||||
| `completionProbe` | `true` | Asks a working subagent to self-report completeness at 2/5/10 requests. |
|
| `task.agentIdleTtlMs` | park idle finished agents | `<= 0` disables parking |
|
||||||
| `maxConcurrency` | `32` (often lowered) | Concurrent subagents; sizes GPU/endpoint load. |
|
| `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 |
|
||||||
| `agentModelOverrides` | `{}` | Per-agent model routing, e.g. `{"scout":"provider/model-id"}`. |
|
| `tools.maxTimeout` | ceiling on any agent-requested tool timeout | kills `timeout: 0` runaways |
|
||||||
| `tools.maxTimeout` | `0` | Caps the timeout the agent may set on a **tool**, not on the agent. |
|
| `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
|
## Verify semantics from the bundle, not the label
|
||||||
alone unless the failure mode is a looping search agent (then lower it — request count,
|
Setting descriptions lie by omission. `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js`:
|
||||||
not the clock, is what bounds a 200-call research loop).
|
- 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
|
## Landing a policy change
|
||||||
any session that plans to rely on it. A session-only write is gone next session, and an
|
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.
|
||||||
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.
|
|
||||||
|
|||||||
@@ -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
|
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
|
# Adding (or repairing) a cloned voice on the dank fleet
|
||||||
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).
|
|
||||||
|
|
||||||
## 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
|
## Stack, fast
|
||||||
`POST /v1/audio/speech` (omp's TTS device may send `voice_id`, same value). Use:
|
|
||||||
|
|
||||||
- `^[a-z0-9][a-z0-9._-]*$`, **lowercase**, ≤ 64 chars. No slash, no `..`, no
|
- `frank-tts-engine` — arm A `qwentts.cpp tts-server`, container-internal `:8880`,
|
||||||
space, no uppercase; must not start with `.`/`_`/`-`.
|
publishes **no port**. Reach it with `podman exec frank-tts-engine curl …`.
|
||||||
- Why lowercase rather than just "alphanumeric": the shim accepts
|
- `frank-tts` — OpenAI façade on host `:7772`; resolves voice **by name**.
|
||||||
`[A-Za-z0-9][A-Za-z0-9._-]{0,63}` (`app.py`, `VOICE_NAME_RE`) but the **engine
|
- Packs: `/home/dank/voices/<name>/` with `reference.wav` (+ manifest, `.spk`,
|
||||||
lowercases names**, so `Narrator1` resolves on one side and not the other. The
|
`.rvq`). Builder: `scripts/build-voice-pack.sh` (crop 19.84–25 s, first 10 s of
|
||||||
boot warmer refuses a mixed-case pack loudly instead of half-working.
|
any source is forbidden).
|
||||||
- Names are user-visible: a 400 carries them in `available_voices`. Fleet
|
- Weights: `/home/dank/models/Qwen3-TTS-GGUF/` —
|
||||||
convention is `narrator1`, `narrator2`, … for the open-notebook podcast
|
`scripts/download-frank-tts-engine-qwentts.sh [--verify-only]` (1.4 GiB, ~18 s
|
||||||
speakers — open-notebook is configured with exactly `narrator1/2/3`, so renaming
|
on the LAN; verifies byte count + Hub LFS sha256).
|
||||||
those breaks podcast rendering, not just TTS — and a lowercase personal name for
|
|
||||||
a person's own voice.
|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
```
|
## Trap 1 — the kernel's argument cap (this broke the first deploy)
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## Trap 2 — two readers of the env file disagree about quotes
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
Knobs live in `scripts/voice-packs.env` (one `_SOURCE`/`_OFFSET`/`_DURATION`/
|
`bash set -a; source` strips `"…"`. podman's `--env-file` (how the unit really
|
||||||
`_SHA256` set per voice). Three things it enforces that an operator gets wrong:
|
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
|
## Trap 3 — locally built images skip rebuilds on a stale hash
|
||||||
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>/`.
|
|
||||||
|
|
||||||
Files must be world-readable (the container runs as uid 10000): `644` files,
|
`build_compose_image` compares a `cfhash` label; it must cover every file the
|
||||||
`755` dirs. `mkdir -p /home/dank/voices` as the deploy user if it is absent —
|
`Containerfile` `COPY`s, or an edited `app.py`/hook deploys "successfully" while
|
||||||
rootless podman auto-creates a missing bind source **root-owned**, and the
|
the container runs old code.
|
||||||
endpoint then sees no voices at all.
|
|
||||||
|
|
||||||
## 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
|
| thing | value |
|
||||||
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.
|
| first synthesis of a freshly registered voice | 1 883 ms |
|
||||||
Re-registering does **not** warm — only synthesizing does. So:
|
| 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
|
The trial's `6.2–8.0 s cold → 0.5 ms warm` claim does **not** reproduce; warming
|
||||||
# registers every pack that has a reference.wav, then speaks the warm sentence once
|
is for registration (registry is memory-only, re-register ≠ warm) plus ~0.6 s.
|
||||||
podman exec frank-tts-engine /app/warm-voices.sh
|
|
||||||
systemctl --user --no-pager --full status frank-tts-engine.service | tail -20
|
|
||||||
```
|
|
||||||
|
|
||||||
The unit's
|
## Ops notes
|
||||||
`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.
|
|
||||||
|
|
||||||
## Proof (do not report "added" without it)
|
- `systemctl --user stop` **removes** the quadlet container — `podman start` then
|
||||||
|
finds nothing. To run the engine without its `ExecStartPost` hook, use a
|
||||||
```bash
|
separate container, not stop+start.
|
||||||
curl -s localhost:7772/v1/audio/voices # name is listed
|
- `podman cp` of a fixed script into a running container is a **trial only**: the
|
||||||
time curl -s -o /tmp/a.wav localhost:7772/v1/audio/speech \
|
layer dies with the container. Merge + redeploy to make it durable.
|
||||||
-H 'content-type: application/json' \
|
- Nothing on the voice pair exposes `/metrics`; Prometheus can't scrape it (a
|
||||||
-d '{"model":"tts-1","voice":"NAME","input":"Checking the cloned voice.","response_format":"wav"}'
|
blackbox HTTP probe on 7772 is the only honest option).
|
||||||
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.
|
|
||||||
|
|||||||
@@ -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
|
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
|
## Where
|
||||||
**suggestion, not a PR**. Verify (`GET /pulls?state=open` → expect `[]`) before
|
- Token: `~/.config/secrets/gitea` (`http://user:TOKEN@192.168.0.143` single line).
|
||||||
claiming a PR "should exist"; create it via the API when asked.
|
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
|
## Recipes (all verified live 2026-10-18)
|
||||||
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>`
|
- **Open PR** (token only needed for private repos; this one reads publicly but
|
||||||
- Token lives in the git credential store configured by
|
POST needs auth): `POST /pulls` JSON `{title, head, base:"main", body}`.
|
||||||
`git config --get credential.helper` → `store --file ~/.config/secrets/gitea`,
|
- **PATCH PR title/body**: `PATCH /pulls/<n>` with JSON body `{title}` and/or
|
||||||
line format `http://user:TOKEN@192.168.0.143:3000`.
|
`{body}`. Returns 201 with the full PR. Use `jq -n --arg` to build JSON; for
|
||||||
- The repo-local `.env` `GITEA_TOKEN` (main checkout, gitignored) carries
|
multi-line bodies write the text to a temp file and `--arg b "$(cat b.txt)"`.
|
||||||
**`read:issue` too** (measured 2026-10-18): filing issues needs THAT token,
|
(jq CLI: a bare `---` argument after the program is parsed as a flag → error;
|
||||||
the credential-store one 403s on issues.
|
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
|
## Review comments: the API lies on 1.27.3 (learned 2026-10-18)
|
||||||
awk regexes silently grab the port instead; use this anchored sed):
|
- `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
|
## Body-footer law (AGENTS.md)
|
||||||
tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
|
Every issue/PR body ends with `---` line then `cwd: <wd> · session: <id>`. Keep
|
||||||
[ -n "$tok" ] && echo "token-extracted(len=${#tok})" # length check, never echo the value
|
it inside the --arg string (not a `---` CLI arg).
|
||||||
```
|
|
||||||
|
|
||||||
## Keep the token out of argv (repo secret law)
|
`cwd` is THIS session's own working directory (note a build worktree separately if they
|
||||||
`curl -H "Authorization: token $tok"` is world-readable in `/proc/<pid>/cmdline`.
|
differ). When `/status` is unreachable (headless/subagent context), recover the session id
|
||||||
Use a mode-600 config file:
|
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
|
## Gotchas
|
||||||
cfg=$(mktemp); chmod 600 "$cfg"
|
- Content-Type: POSTs with `-d @file.json` MUST carry
|
||||||
printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
|
`-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 —
|
||||||
- POST JSON needs a second header line: `header = "Content-Type: application/json"`.
|
law, not courtesy.
|
||||||
There IS NO `content-type` config option — curl rejects the file.
|
- `jq: cannot index "not found" with "user"` = endpoint 404'd into an error
|
||||||
- `rm -f "$cfg"` when done (a mode-600 token file left in /tmp is its own leak).
|
object; check `-w '%{http_code}'` before blaming jq.
|
||||||
|
- The token in `.git/config`'s remote URL is the same secret; the credential
|
||||||
## Operations
|
file (`~/.config/secrets/gitea`) is the one git actually consults (global
|
||||||
```bash
|
credential.helper) and the one to reuse for API calls.
|
||||||
# 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.
|
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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)`.
|
- 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
|
## Workflow
|
||||||
1. Edit the live file under `~/.config/niri/cfg/` (symlinked = repo file; except `noctalia.kdl`, edit live then `cp` to mirror).
|
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
|
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.
|
Bar widget layout on Frank's noctalia v5 workstations is GUI-override state, not config.toml.
|
||||||
|
|
||||||
## Where
|
## 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).
|
- 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).
|
- 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.
|
- 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"]`.
|
- Defaults when a key is absent: `start = ["launcher","wallpaper","workspaces"]`, `center = ["clock"]`, `end = ["notifications","clipboard","network","bluetooth","tray","battery","session","control-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.
|
|
||||||
|
|
||||||
## GUI→repo propagation (checkout-is-deployment)
|
## Clobber window and recovery (2026-10-18 incident law)
|
||||||
- `~/.config/noctalia/config.toml` is symlinked into the repo → GUI settings writes land in the repo working tree automatically.
|
- 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.
|
||||||
- `~/.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`.
|
- 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.
|
||||||
- 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.
|
- 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.
|
||||||
- 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.
|
- After recovery, commit the file immediately so the window closes.
|
||||||
|
|
||||||
## If the bar got wiped (recovery)
|
## Widgets (v5 census, verified 2026-10-17)
|
||||||
- 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`.
|
- 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.
|
||||||
- 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.
|
- `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.
|
||||||
- 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.
|
- `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)
|
## Bar styles (GUI-visible, settings.toml keys)
|
||||||
1. Edit the repo `settings.toml`.
|
- `[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`).
|
||||||
2. `noctalia config validate` (checks merged layers; also `noctalia config export [full] | yq -p toml` to inspect effective values).
|
- 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).
|
||||||
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.
|
- The GUI "Bar" page is the reference list; anything absent from settings.toml is a built-in default.
|
||||||
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`.
|
|
||||||
|
|
||||||
## Clock widgets
|
## Slowouts (window-height-aware bars/panels)
|
||||||
- `[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.
|
- `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`.
|
||||||
- **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.
|
- `position = "top"` + `exclusive = false` + slowouts on the monitor -> panel hides when a window touches its edge, re-reveals otherwise. `slowouts-visible` = toggle capsule.
|
||||||
- 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).
|
- **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.
|
||||||
- Local date + local 12h fit in ONE widget (same tz); the UTC 24h clock must be a second widget.
|
- 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.
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Taskbar recipes
|
## Layout modes and window title decorations
|
||||||
- Move window indicator beside workspaces: put `"taskbar"` in `start` after `"workspaces"`.
|
- `[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`.
|
||||||
- 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).
|
- `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.
|
- The `workspaces` widget itself has no window display; styles are regular/minimal/focus_hint only.
|
||||||
|
|
||||||
## Law
|
## 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).
|
- 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.)
|
- **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.
|
- 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
|
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
|
# Scope
|
||||||
|
|
||||||
`~/.omp/agent/models.yml` (or `models.yaml`) on a workstation: adding a **runner**
|
`~/.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
|
# The landmine
|
||||||
|
|
||||||
@@ -63,9 +65,52 @@ yielding.
|
|||||||
# Speech transport contract (`openai-speech`)
|
# Speech transport contract (`openai-speech`)
|
||||||
|
|
||||||
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
|
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
|
(`openai-speech does not support sampleRate`). Codec comes from the
|
||||||
`output_path` suffix; 15 000-character cap. An unknown `voice` name is a server
|
`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
|
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
|
*available*, so point `modelRoles.speech` at such a provider only after it has
|
||||||
answered a real synthesis.
|
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,
|
"v": 1,
|
||||||
"lastConsolidatedAt": 1792368375152,
|
"lastConsolidatedAt": 1792439707623,
|
||||||
"lastResult": {
|
"lastResult": {
|
||||||
"at": 1791599865775,
|
"at": 1791599865775,
|
||||||
"sessions": 1,
|
"sessions": 1,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"v": 1,
|
"v": 1,
|
||||||
"lastConsolidatedAt": 1792359704968
|
"lastConsolidatedAt": 1792401025778
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"v": 1,
|
"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
|
Swap the model, don't instrument the swap: edit the trio
|
||||||
- Format: PQ2_0 quantized GGUF
|
(`scripts/<svc>.env` + serve script + unit) → `./tests/run.sh` → PR →
|
||||||
- 27B parameter class
|
`deploy.sh <host> --service <svc>` → `/health` + one chat / tool-call / vision
|
||||||
- Vision: native BF16 mmproj projector for image input
|
probe → done. **No benchmarks, A/Bs, ladders, or acceptance tables unless
|
||||||
- MTP: OFF (no `nextn` tensors in checkpoint)
|
Frank explicitly asks for data.** If a payload misbehaves, revert the commit and
|
||||||
- Context: 256k per-slot window (`--ctx-size 401408 --parallel 2` = 200,704 tokens effective)
|
redeploy. When a bench IS ordered, the GPU-safety procedure (stop + mask, no
|
||||||
- Batch: 4096 / ubatch 2048 (CUDA optimum)
|
scratch ports, never `systemctl disable`) is `AGENTS.md` → *Model swaps*
|
||||||
- Log verbosity: 4
|
and *Deployment completeness*.
|
||||||
- Ports: 7771 on Dankclaw
|
|
||||||
|
|
||||||
### 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…`)
|
Models.yml resolution rules that are harness-local: a default model resolves as
|
||||||
- Format: v2 `.hgn` (flash-server exclusive)
|
a `defaultProvider` + `defaultModel` pair; a subagent override is a bare model
|
||||||
- MTP head: INSIDE checkpoint (`HALOGEN_MTP_HEAD` unset)
|
`id` and resolves only when unique across providers; two ports on one host need
|
||||||
- Vision: ON (v2 tower loads beside checkpoint)
|
two provider blocks — the file cannot express "same host, different port".
|
||||||
- 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
|
|
||||||
|
|
||||||
### 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…`)
|
- The old "Benchmarking Policy (MANDATORY)" wording demanded a bench harness per
|
||||||
- Format: fp8-KV `.rad` (121,969,901,568 B = 116,322 MiB)
|
payload; superseded 2026-10-18 by the swap law above.
|
||||||
- MTP head: MTP depth 2, MNBT 2048, Froggeric v22.5 template
|
- A "Subagent Lifecycle (MANDATORY)" rule (heartbeat ≤5 s, checkpoints, hard
|
||||||
- Vision: 27-layer Qwen-VL tower native
|
timeout) was recorded here 2026-09 and **never implemented anywhere**; it
|
||||||
- KV: 3 × 200,000 fp8-KV (`--max-num-seqs 3`)
|
described nothing that exists and is deleted 2026-10-18. Do not re-add it
|
||||||
- 30,720 MiB of 30,969 MiB claimable
|
without a real implementation.
|
||||||
- 44 GiB/rank pinned host pool, PLE pinned to RAM
|
- The fleet model inventory that used to live in this memory file drifted out of
|
||||||
- Base-alignment: censored (abliterated family lives on frank-dragoon)
|
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
|
||||||
## Resolution Rules
|
repo does. That is why it is pointers now.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
## Voice Generation
|
## 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.
|
- 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
|
||||||
|
|
||||||
- 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,
|
"v": 1,
|
||||||
"lastConsolidatedAt": 1792370846394,
|
"lastConsolidatedAt": 1792400853634,
|
||||||
"lastResult": {
|
"lastResult": {
|
||||||
"at": 1792370515419,
|
"at": 1792393886960,
|
||||||
"sessions": 1,
|
"sessions": 1,
|
||||||
"deltas": 1,
|
"deltas": 1,
|
||||||
"model": "frank-zealot"
|
"model": "frank-zealot"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
## top bar
|
## top bar
|
||||||
- Preserve top bar customizations; customizations lost or obliterated on subsequent runs
|
- 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,
|
"v": 1,
|
||||||
"lastConsolidatedAt": 1792370776395,
|
"lastConsolidatedAt": 1792381281460,
|
||||||
"lastResult": {
|
"lastResult": {
|
||||||
"at": 1792370420509,
|
"at": 1792381281460,
|
||||||
"sessions": 1,
|
"sessions": 1,
|
||||||
"deltas": 1,
|
"deltas": 1,
|
||||||
"model": "frank-zealot"
|
"model": "frank-zealot"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
## window outlines
|
## 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 candidate keeps the wire model id valid there.
|
||||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
name: "frank-archon"
|
name: "frank-archon"
|
||||||
# Dankserver: BOTH Radeon AI PRO R9700s (gfx1201, TP=2 over the
|
# Dankserver: BOTH Radeon AI PRO R9700s (gfx1201, TP=2, wht6 wire) on
|
||||||
# 2026-09-20 board swap's root ports, wht6 wire). Engine:
|
# the digest-pinned radiance image, plain `podman run` under a root
|
||||||
# radiance (StillDeadcode, Apache-2.0) from the digest-pinned
|
# unit. Payload, shape, eras, laws, rollback:
|
||||||
# stilldeadcode/radiance image (index digest sha256:4c3f4845..., 287
|
# serverAppConfigs-docs/LLM/frank-archon.md — and
|
||||||
# MiB, kernels prebuilt for gfx1201), plain `podman run` through
|
# scripts/frank-archon.env is the authority for every number there.
|
||||||
# scripts/serve-frank-archon.sh, root unit (User=dank, dank's
|
# contextWindow MUST quote the engine's --max-model-len: radiance
|
||||||
# rootless podman storage). Serving the OFFICIAL
|
# refuses a longer prompt and never truncates.
|
||||||
# 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).
|
|
||||||
contextWindow: 200000
|
contextWindow: 200000
|
||||||
# maxTokens must survive the harness compaction gap AND the chars/4
|
# maxTokens must survive the harness compaction gap AND the chars/4
|
||||||
# token-estimate under-run on code-heavy prompts. The clamp is
|
# 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
|
# compaction model candidate so the wire model id stays valid
|
||||||
# there even from a frank-dragoon session.
|
# there even from a frank-dragoon session.
|
||||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
# halogen-flash-server 0.16.2 (ghcr.io/peonist-ai/
|
# halogen flash-server on the Strix Halo iGPU. Payload, shape, eras,
|
||||||
# halogen-flash-server@sha256:0c61bf84, digest-pinned) serving the
|
# laws, rollback: serverAppConfigs-docs/LLM/frank-dragoon.md — and
|
||||||
# Quat3rnion NATIVE v2 ABLITERATED conversion of orcarouter's pinned
|
# scripts/frank-dragoon.env is the authority for every number there.
|
||||||
# BF16 Qwen3.8-Flash-Next (qwen38-flash-next-v2-abliterated.hgn,
|
# contextWindow sits deliberately BELOW the engine ceiling
|
||||||
# ~62 GiB resident, + REQUIRED n-gram table + vision tower, both
|
# (HALOGEN_CTX): the clamp below leaves the gap that absorbs the
|
||||||
# hardlinked from the swift15 dir) on the Strix Halo iGPU: TWO slots
|
# harness's chars/4 under-count, so a window equal to the ceiling
|
||||||
# over ONE 401,408-position KV pool (= 2 x ctx, raised 2026-10-18) at
|
# would make every long request a guaranteed 400.
|
||||||
# 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.
|
|
||||||
contextWindow: 155648
|
contextWindow: 155648
|
||||||
# The clamp is max_tokens = min(maxTokens, contextWindow − est −
|
# The clamp is max_tokens = min(maxTokens, contextWindow − est −
|
||||||
# 4096); the chars/4 est UNDER-counts the rendered prompt by ~3 %
|
# 4096); the chars/4 est UNDER-counts the rendered prompt by ~3 %
|
||||||
# on code/agent traffic (measured 2026-10-07: est 152,938 ->
|
# on code/agent traffic (measured 2026-10-07: est 152,938 ->
|
||||||
# server 157,388). Worst-case request = window − 4096 + 3 % est
|
# server 157,388). Worst-case request = window − 4096 + 3 % est
|
||||||
# error ≈ 156.2k, inside the 200,704 engine ceiling with ~44k
|
# error ≈ 156.2k, inside the engine ceiling with ~44k of margin
|
||||||
# margin (robust to a ~25 % under-count). Compaction fires at 85 %
|
# (robust to a ~25 % under-count). Compaction fires at 85 % of the
|
||||||
# of the window = 132,300 est ≈ 136k measured — the 180k target of
|
# window and rides on frank-zealot: input ≈ threshold −
|
||||||
# the 262,144 era is unreachable now (the window itself is 155,648)
|
# keepRecent(20k), comfortably inside its per-slot window.
|
||||||
# — and rides on frank-zealot: input ≈ threshold − keepRecent(20k)
|
|
||||||
# ≈ 112k, inside its 200,704 slot.
|
|
||||||
# HALOGEN_MAX_TOKENS_CAP in frank-dragoon.env must stay >= this.
|
# HALOGEN_MAX_TOKENS_CAP in frank-dragoon.env must stay >= this.
|
||||||
maxTokens: 45056
|
maxTokens: 45056
|
||||||
reasoning: true
|
reasoning: true
|
||||||
@@ -183,39 +151,20 @@ providers:
|
|||||||
# config.yml (192.168.0.143:7771 — this endpoint); the
|
# config.yml (192.168.0.143:7771 — this endpoint); the
|
||||||
# compactionModel candidate keeps the wire model id valid there.
|
# compactionModel candidate keeps the wire model id valid there.
|
||||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||||
# Dankclaw's RTX 3090: llama.cpp PrismML fork build (vendor 10777,
|
# Dankclaw's RTX 3090 llama.cpp endpoint (PrismML fork build — the only
|
||||||
# commit e3de71522 — the ONLY loader for this payload's custom tensor
|
# loader for this payload's custom tensor type). Shape, payload, eras,
|
||||||
# type 142 + prism.hadamard.* metadata, and the minimum build that
|
# laws, rollback: serverAppConfigs-docs/LLM/frank-zealot.md, and
|
||||||
# pairs MTP with a quantized V cache) serving
|
# scripts/frank-zealot.env is the authority for every number there.
|
||||||
# prism-ml/Ternary-Bonsai-2-9B-gguf at PQ2_0 (2,675,410,720 B) WITH the
|
# contextWindow is the PER-SLOT window: llama.cpp divides --ctx-size
|
||||||
# MTP head in the file — qwen35 hybrid, 32 layers (8 full-attention +
|
# across slots and answers a longer prompt with a 400, never a
|
||||||
# 24 Gated DeltaNet), 4 KV heads x 256 dim, native 262,144 — in THREE
|
# truncation, so this value must follow every reslice (live now:
|
||||||
# conversations (--ctx-size 786432 / --parallel 3), each slot at the
|
# 3 x 200,704 over f16 K + q8_0 V with the MTP drafter armed, ba69ea5).
|
||||||
# model's TRAINED window.
|
contextWindow: 200704
|
||||||
# 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
|
|
||||||
# Prompt + budget must fit inside the slot window; thinking counts
|
# Prompt + budget must fit inside the slot window; thinking counts
|
||||||
# against the budget. Compaction fires at 85 % of the window =
|
# against the budget. Compaction fires at 85 % of the window =
|
||||||
# 222,822 est; worst-case prompt + budget ≈ 222,822 × 1.03 +
|
# 170,598 est; worst-case prompt + budget ≈ 170,598 × 1.03 +
|
||||||
# 16,384 ≈ 245,891 ≤ 262,144. This endpoint is the fleet's
|
# 16,384 ≈ 192,100 ≤ 200,704. This endpoint stays the fleet's
|
||||||
# compaction hub: the widest window in the fleet.
|
# compaction hub: still the widest window in this file.
|
||||||
maxTokens: 16384
|
maxTokens: 16384
|
||||||
reasoning: true
|
reasoning: true
|
||||||
input: [text, image]
|
input: [text, image]
|
||||||
@@ -269,14 +218,19 @@ providers:
|
|||||||
# when explicitly set, and sampleRate/bitRate are REJECTED outright (the
|
# when explicitly set, and sampleRate/bitRate are REJECTED outright (the
|
||||||
# transport throws "openai-speech does not support sampleRate"), so the
|
# transport throws "openai-speech does not support sampleRate"), so the
|
||||||
# codec comes from the output_path suffix and the only tunable that matters
|
# 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
|
# is the voice name — and it comes from ONE place: the tool's `voice_id`
|
||||||
# unknown name is an engine error, not a fallback.
|
# argument. No setting feeds it (`speech.voice` is the speak-aloud Kokoro enum,
|
||||||
# DELIBERATELY NOT WIRED YET: config.yml `modelRoles.speech` still points at
|
# `tts.localVoice` is the local backend's), so a call that omits voice_id arrives
|
||||||
# local/kokoro. Flip it to "frank-tts (dankclaw)/frank-tts" only after the
|
# at 7772 with no `voice` key and the endpoint answers 400 "unknown voice None;
|
||||||
# service is deployed and has answered a real synthesis, because `auth: none`
|
# available voices: narrator1, narrator2, narrator3". Say the voice in the call.
|
||||||
# makes the provider read as AVAILABLE even while the container is down — the
|
# WIRED 2026-10-18: `config.yml` `modelRoles.speech` is this row, flipped only
|
||||||
# role would then fail instead of falling back to Kokoro. The same flip must
|
# after the service was deployed and answered real synthesis (two packs,
|
||||||
# re-key `speech.voice` (now am_fenrir, a Kokoro voice) to a pack name.
|
# 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
|
# 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
|
# 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
|
# 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.
|
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
|
||||||
|
|
||||||
```
|
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.
|
||||||
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
|
||||||
|
|
||||||
➡️ <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.
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Grilling"
|
|
||||||
short_description: "Stress-test thinking a round of questions at a time"
|
|
||||||
@@ -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
|
interval_seconds = 1200
|
||||||
|
|
||||||
[wallpaper.default]
|
[wallpaper.default]
|
||||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9w8r5.jpg"
|
||||||
|
|
||||||
[wallpaper.last]
|
[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]
|
[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]
|
[wallpaper.monitors.DP-2]
|
||||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
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"
|
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
|
||||||
|
|
||||||
[wallpaper.monitors.eDP-1]
|
[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]
|
[weather]
|
||||||
unit = "imperial"
|
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)"
|
[ -f ~/.config/secrets/hf_token ] && export HF_TOKEN="$(cat ~/.config/secrets/hf_token)"
|
||||||
|
|
||||||
[ -d "$HOME/.bun/bin" ] && export PATH="$HOME/.bun/bin:$PATH"
|
[ -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.
|
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:
|
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.
|
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:
|
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
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A[Consumer GPU 16-24GB] --> B[Qwen3-8B / 14B]
|
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[Single server GPU 80GB] --> E[Qwen3-30B-A3B]
|
||||||
D --> F[Llama 4 Scout INT4]
|
D --> F[Llama 4 Scout INT4]
|
||||||
G[Multi-GPU cluster] --> H[Qwen3-235B-A22B]
|
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 |
|
| Need | Pick |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Best tool calling, one consumer GPU | Qwen3-8B or 14B |
|
| 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 |
|
| Server-scale reliable agent | Qwen3-235B-A22B |
|
||||||
| Cheapest acceptable agent | Qwen3-8B on a mid GPU |
|
| 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 technical report: <https://qwenlm.github.io/blog/qwen3/>
|
||||||
- Qwen3 repository: <https://github.com/QwenLM/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/>
|
- Meta AI Llama 4 announcement: <https://ai.meta.com/blog/llama-4-multimodal-intelligence/>
|
||||||
- Ollama library: <https://ollama.com/library>
|
- Ollama library: <https://ollama.com/library>
|
||||||
- Browser Use supported models: <https://docs.browser-use.com/open-source/supported-models>
|
- Browser Use supported models: <https://docs.browser-use.com/open-source/supported-models>
|
||||||
|
|||||||
@@ -8,18 +8,41 @@ tags:
|
|||||||
- privacy
|
- privacy
|
||||||
- configuration
|
- configuration
|
||||||
- unverified
|
- unverified
|
||||||
verified: 2026-08-28
|
verified: 2026-10-19
|
||||||
timestamp: 2026-08-11
|
timestamp: 2026-08-11
|
||||||
resource: https://docs.searxng.org
|
resource: https://docs.searxng.org
|
||||||
---
|
---
|
||||||
|
|
||||||
# SearXNG Tuning
|
# 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.
|
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
|
## Plan the Deployment
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,17 @@ Key ideas:
|
|||||||
- Ten watchdogs handle CAPTCHAs, crashes, popups, downloads, and security.
|
- Ten watchdogs handle CAPTCHAs, crashes, popups, downloads, and security.
|
||||||
- Real browser profiles carry saved logins.
|
- 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
|
```python
|
||||||
# Local model via llama.cpp (OpenAI-compatible endpoint)
|
# Local model via any OpenAI-compatible endpoint
|
||||||
from browser_use import Agent
|
from browser_use import Agent
|
||||||
from browser_use.llm.openai import ChatOpenAI
|
from browser_use.llm.openai import ChatOpenAI
|
||||||
|
|
||||||
@@ -40,7 +47,7 @@ agent = Agent(
|
|||||||
task="Search for the latest AI news",
|
task="Search for the latest AI news",
|
||||||
llm=ChatOpenAI(
|
llm=ChatOpenAI(
|
||||||
base_url="http://192.168.0.235:7770/v1",
|
base_url="http://192.168.0.235:7770/v1",
|
||||||
model="frank-default-small",
|
model="frank-archon",
|
||||||
api_key="EMPTY",
|
api_key="EMPTY",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user