Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcdf84719c |
No files matched your search
@@ -1,39 +0,0 @@
|
||||
---
|
||||
name: bats-line-scoped-assert-traps
|
||||
description: "When writing or reviewing bats tests over multi-line command output, catch glued-glob adjacency that hops lines, env-exec of shell functions (127), and file-wise runs that hide full-suite reds."
|
||||
---
|
||||
|
||||
Three traps that produced false-green bats runs; check each when writing or reviewing tests that assert over multi-line `$output`.
|
||||
|
||||
## 1. `[[ "$output" == *"a b"* ]]` can hop lines
|
||||
`$output` is multiline; a glob `*url auth=Bearer key*` matches across newline boundaries, and inserting a new column into a per-line log format silently breaks *old* adjacency pins elsewhere in the file.
|
||||
|
||||
Pin per-line facts with a line-scoped grep instead:
|
||||
|
||||
```bash
|
||||
grep -Eq '/audio/speech .*auth=Bearer dk-speech' <<<"$output"
|
||||
```
|
||||
|
||||
`grep` is line-scoped by construction: the assertion can only match within one CALL/log line. When a fixture adds a column to its log lines, grep the test suite for glued globs over those lines first (`grep -n '\[\[ "\$output" == \*' tests/`).
|
||||
|
||||
## 2. `run env PATH=... my_shell_function args` is always 127
|
||||
`env` execs an external binary; bats test-file functions are invisible to it, and `BW01` (127) does not by itself fail the test — a following `[ "$status" -eq 0 ]` does, or worse, nothing checks it.
|
||||
|
||||
Inherit and extend PATH around the function call instead:
|
||||
|
||||
```bash
|
||||
export PATH="$WORK/bothbin:$PATH"
|
||||
DV_VAR=x my_function scenario
|
||||
```
|
||||
|
||||
If a restricted PATH is truly required, write an executable wrapper script into that bin (`exec bun "$DRIVER" "$@"` with an absolute interpreter path).
|
||||
|
||||
## 3. File-wise green ≠ suite green
|
||||
A single file (`bats tests/behavior.bats`) can pass while `bats tests/` is red (order-dependent state, shared fixtures, or simply a stale dist pin file in another file). Always run the whole gate — `bats tests/` plus unit runner (`bun test`) — on the exact final tree before opening a PR, and grep the log, never tail it:
|
||||
|
||||
```bash
|
||||
bats tests/ > /tmp/gate.log 2>&1; echo "EXIT $?"; grep "^not ok" /tmp/gate.log; grep -c "^ok" /tmp/gate.log
|
||||
```
|
||||
|
||||
## Verify a red is red for the stated reason
|
||||
When a freshly-added test fails immediately and inexplicably fast (seconds), suspect harness-level breakage (127, missing exec) rather than product behavior — inspect with a temporary `printf '%s\n' "$output" > /tmp/dbg.txt` dump inside the test, delete it after, and reproduce the actual line format before fixing assertions.
|
||||
@@ -1,48 +1,47 @@
|
||||
---
|
||||
name: browser-screenshot-disk-and-cookies
|
||||
description: "Save omp managed-browser screenshots to real files (tab.screenshot path is silently ignored), prove/clear cookie state (clearCookies is domain-scoped), and get embedded-image commits past the secret gate."
|
||||
description: Save omp managed-browser screenshots to real files (tab.screenshot path is silently ignored) and reset the shared Chromium cookie jar between persona captures.
|
||||
---
|
||||
|
||||
# omp managed browser from Eval: screenshots, cookies, page reads
|
||||
# Screenshots to disk with the omp `browser` eval helper
|
||||
|
||||
Friction-earned 2026-10-21 (cookie-state proof on a local stack), same day (static HTML guide verification — four wasted API calls from signature guesses), re-earned same day on the franksign screenshot-walkthrough campaign (19 embedded PNGs, two cookie traps, one secret-gate false positive).
|
||||
## Traps (both cost a full re-capture to discover)
|
||||
1. `tab.screenshot({ path })` **ignores `path` silently** — the option is not in
|
||||
its schema (`{selector, fullPage, silent, annotate, format, quality, ifChanged, threshold}`).
|
||||
The image attaches inline to the tool result and **nothing is written to disk**;
|
||||
`ls` shows an empty dir. Inline images may also be dropped from transcript
|
||||
archives later, so "attached to chat" is not "saved".
|
||||
2. The shared managed Chromium keeps **cookies across tabs and turns** (profile
|
||||
persists after `tab.close()`). A second capture script that expects the
|
||||
anonymous view lands signed-in and `waitForSelector("testid/…")` fails fast.
|
||||
|
||||
## Screenshots to real files
|
||||
- `tab.screenshot({ path })` returns the image to the model but the `path` option is **silently ignored** on the managed-browser path — no file appears on disk.
|
||||
- Working recipe: re-capture raw and decode base64 to disk:
|
||||
```js
|
||||
await tab.run(async ({ page }, p) => {
|
||||
const r = await page.screenshot({ encoding: "base64", type: "png" });
|
||||
await Bun.write(p, Buffer.from(typeof r === "string" ? r : r.data, "base64"));
|
||||
}, { args: ["/tmp/x.png"] });
|
||||
```
|
||||
This puppeteer variant returns the base64 **string directly**, not `{data}` — handle both or you write `undefined`.
|
||||
- `computer.window("Firefox").screenshot()` fails (`captureWindow: unavailable` under niri) — don't go that way.
|
||||
- Downscale before embedding: `magick "$f" -resize 1000x "$f"` keeps dark-theme UI PNGs at 20–80 kB each.
|
||||
## Procedure
|
||||
Write files via `tab.run`, which exposes the raw Puppeteer `page` — its
|
||||
`screenshot({path})` does write:
|
||||
|
||||
## Cookie state (corrected 2026-10-21 — the old claim was wrong)
|
||||
- Cookies DO cross `browser.open` calls: the headless browser is one shared process with one jar (a stale persona session from an earlier campaign hijacked an "anonymous" walkthrough tab).
|
||||
- `tab.clearCookies()` is **domain-scoped to the current page origin** — running it on `about:blank` clears nothing and silently leaves the previous session live (a "signer" tab showed the admin's dashboard). Correct sequence: `goto(app-origin)` → `clearCookies()` → `reload()`, then verify with `page.cookies()`, not with page text.
|
||||
- Cookie proof (ground truth):
|
||||
```js
|
||||
const cs = await page.cookies()
|
||||
return cs.map(c => `${c.name}=${c.value.slice(0,12)}… dom=${c.domain} exp=${c.expires}`).join("\n")
|
||||
```
|
||||
```js
|
||||
const dir = "/tmp/<session-dir>";
|
||||
const tab = await browser.open({ name: "caps", url });
|
||||
const shot = (file) =>
|
||||
tab.run(async ({ page }, f) => { await page.screenshot({ path: f }); return f; },
|
||||
{ args: [`${dir}/${file}.png`] });
|
||||
|
||||
## Committing screenshots: the secret-gate false positive
|
||||
- Data-URI PNGs in a diff WILL trip shape-based secret gates (`ASIA[A-Z0-9]{16}` etc. appear as random substrings in base64 streams, worse with case-insensitive grep).
|
||||
- Prove noise, don't assume: run the gate's own pattern with `grep -oEi` over `git diff --cached` and eyeball the matches (image-byte substrings, not credentials; confirm screenshots show passwords masked).
|
||||
- Then use the gate's sanctioned override with the evidence in the commit body: `ALLOW_SECRETS=1 git commit -m "... ALLOW_SECRETS: the N hits are ASIA-shape coincidences inside PNG base64 ..."`. Never `--no-verify`.
|
||||
await tab.clearCookies(); // ALWAYS before an anonymous capture
|
||||
await tab.goto(url);
|
||||
await tab.waitForSelector("testid/first-state");
|
||||
await shot("1-anonymous");
|
||||
// ...drive states with direct helpers: click/fill("testid/<id>", ...) ...
|
||||
await shot("2-next-state");
|
||||
await tab.close();
|
||||
```
|
||||
|
||||
## API signatures (guesses cost calls)
|
||||
- `browser.open({ url, name })` — options object; `browser.open(url)` → TypeError.
|
||||
- `tab.wait(ms)` does not exist → `await Bun.sleep(ms)`.
|
||||
- `tab.run(fnOrCode)` executes in an isolated **Node/Puppeteer realm** (`{ tab, page, browser }`, plus `args` as trailing params), NOT the page — `document is not defined` means you wanted `tab.evaluate(exprString)` (page-global expression; no top-level `return`; wrap an IIFE when you need one).
|
||||
- `tab.text("svg")` etc. are direct page helpers — prefer them over evaluate for plain reads.
|
||||
- For app UIs with labelled forms, `role/textbox[name="Exact label *"]` beats `label=...`: visible labels often include a required-marker the source selector lacks.
|
||||
Verify with `ls -la <dir> && file <dir>/*.png` — do not trust silent success.
|
||||
|
||||
## Eval cell output
|
||||
- `log(...)` inside a JS eval cell can be swallowed (printed nothing across several runs); **`return` from the cell always shows**. Put every value you need to see into the return string.
|
||||
|
||||
## Static page verification loop
|
||||
Serve with `python3 -m http.server <port> -d <dir>` spawned via `Bun.spawn`, open, `tab.evaluate` anchor cross-check (`hrefs` vs `ids`), `img` census (`complete && naturalWidth > 0`), screenshots, `tab.close()`, `proc.kill(9)`. If a rendered label looks wrong in a low-res screenshot, read the real DOM text before "fixing" markup — `<id>` renders as `<id>` and misreads as `</id>` at thumbnail size.
|
||||
## Notes
|
||||
- Selectors accept `testid/<id>` handler; prefer it over CSS on `[data-testid]`.
|
||||
- `tab.run(fn, { args })`: functions receive `{tab, page, browser, wait, assert}`
|
||||
first, then each arg; a bare code string runs in an **isolated runtime, not the
|
||||
page** — `document` is undefined there. Use direct helpers or `tab.evaluate`
|
||||
(page-global expression; top-level `return` invalid) for page access.
|
||||
- Byte-identical PNG sizes between two frames = same view; fine as proof of a
|
||||
round-trip (state A → B → A).
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: gitea-pr-api-cycle
|
||||
description: "\"Open/update/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000) with the token-safe sequence (POST /pulls rejects Basic with 'token is required'), numeric label ids, the form-encoded merge field, the async 405 'try again later' merge trap, the empty-token-returns-404 trap, merged-branch deletion via the branches endpoint, and post-merge live-checkout fast-forward plus the plugin-package-name upgrade gotcha.\""
|
||||
description: "\"Open/update/comment/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000) with the token-safe sequence (POST /pulls rejects Basic with 'token is required'), numeric label ids, the form-encoded merge field, the async 405 'try again later' merge trap, the empty-token-returns-404 trap, and post-merge live-checkout fast-forward plus the plugin-package-name upgrade gotcha.\""
|
||||
---
|
||||
|
||||
# Gitea PR/issue cycle — Dankclaw API
|
||||
|
||||
Friction-earned 2026-10-18 (PRs #15–#20, issue #18), re-earned 2026-10-19 on `dank/dankvoice` PRs #4–#6 (the merge payload claim was wrong; an empty token masqueraded as a missing repo), and re-earned 2026-10-20 on `dank/command-center` PRs #55…#57 (the 405 recalculation trap). Re-earned again 2026-10-20 on `dank/dankvoice` #25 (merge gate loop worked; suite verdict delivered as an issue comment). Re-earned 2026-10-21 on `dank/command-center` issue #58 / PR #59 (issue labels are numeric ids; the `<type>/<issue#>-<slug>` branch name has to be right *before* the first push). Same day on `dank/serverAppConfigs` #64: the Basic-vs-token split below cost two wasted POSTs. Re-earned 2026-10-21 on `dank/franksign` #30 (branch-delete endpoint guess cost one 404; docs bullet went stale under the merge).
|
||||
Friction-earned 2026-10-18 (PRs #15–#20, issue #18), re-earned 2026-10-19 on `dank/dankvoice` PRs #4–#6 (the merge payload claim was wrong; an empty token masqueraded as a missing repo), and re-earned 2026-10-20 on `dank/command-center` PRs #55…#57 (the 405 recalculation trap). Re-earned again 2026-10-20 on `dank/dankvoice` #25 (merge gate loop worked; suite verdict delivered as an issue comment). Re-earned 2026-10-21 on `dank/command-center` issue #58 / PR #59 (issue labels are numeric ids; the `<type>/<issue#>-<slug>` branch name has to be right *before* the first push). Same day on `dank/serverAppConfigs` #64: the Basic-vs-token split below cost two wasted POSTs.
|
||||
|
||||
## Where
|
||||
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>/{pulls,issues}` — repo varies (`command-center`, `dankvoice`, `serverAppConfigs`); numbering is per-repo, so a footer citing `#36` means nothing without the repo.
|
||||
@@ -31,7 +31,6 @@ Friction-earned 2026-10-18 (PRs #15–#20, issue #18), re-earned 2026-10-19 on `
|
||||
- **Issue labels are numeric ids, not names.** `POST /issues` with `"labels":["ready-for-human"]` answers `422 {"message":"[]: json: cannot unmarshal JSON string into Go int64 within \"/labels/0\""}`. Resolve first: `GET /labels` → pick `id` by `name` → post `"labels":[20]`. (PRs take no `labels` field on create; label afterwards via `/issues/N/labels` with `{"labels":[id]}`.)
|
||||
- Comments (the channel for late-arriving evidence, e.g. a test-suite verdict posted after PR creation): `POST /issues/N/comments` with JSON `{body}` — works for PRs too, returns **201**.
|
||||
- **Open the issue before the first commit**, so `Refs #N` fits the message, and get the branch name right before the first push. `git branch -m <new>` + `git push -u origin <new>` + `git push origin --delete <old>` is a clean rename with no PR yet; do it rather than ship a branch that violates `<type>/<issue#>-<slug>` (a `docs/…` branch carrying a `fix:` commit is the common slip, and `git grep` on the old name still shows it).
|
||||
- A PR whose branch carries already-merged commits plus one new one is fine: open against `main`, the diff is just the new commit — the cheap way to land a follow-up fix from a worktree whose branch history is the merged stack (no checkout/branch gymnastics needed).
|
||||
|
||||
## Merge (the 422 trap) — corrected
|
||||
`POST /pulls/N/merge` needs **form-encoded** fields; the merge form field name is lowercase **`do`**:
|
||||
@@ -70,15 +69,13 @@ All three landed within ~5 s of retrying. Treat `mergeable` as a readiness gate,
|
||||
When two open branches edit the same section of the same file, merge the sibling branch into the later one (`git merge docs/other-branch`) instead of rebasing — Frank rejects the history rewrite. Consequences to state in the PR body: merging the lower PR first reduces the upper diff to just its own work; merging the upper first makes the lower an already-contained no-op. Either order is clean; `git branch -d` on the lower branch refuses until its commits reach `main`, because `-d` checks against the current HEAD.
|
||||
|
||||
## Post-merge landing
|
||||
- Delete the merged branch via the **branches endpoint**: `DELETE /repos/dank/<repo>/branches/<urlencoded-branch>` (`chore%2Fname`) → **204**. The `refs/heads/…` path answers 404 — that is the wrong endpoint, not missing auth (auth failures also 404 here, so check which you changed last). If you then run `git push origin --delete <branch>` it errors `failed to push some refs` — harmless: the API already deleted it; don't "fix" the error.
|
||||
- Command-center: the checkout IS the deployment — `git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main`. This refuses if **any** incoming path is dirty in the worktree — see `land-merged-pr-with-blocked-tree-ops` for the shared-workspace case where `checkout` and `stash` are both policy-blocked.
|
||||
- serverAppConfigs: `deploy.sh` pushes the dev checkout's branch first — if the checkout's `main` is behind a merge you just made (via the API), the deploy dies with `error: failed to push some refs`. `git merge --ff-only origin/main` in the deploy checkout BEFORE running `deploy.sh` after any externally-merged PR.
|
||||
- **A PR that changes a path you edited live adds that path to the discard list.** Tonight: the live dotfile fix was dirty *and* incoming, so landing needed `git -C ~/.omp checkout -- <that file>` as well as the usual file. Discard is content-free only when you proved byte identity first (`cmp` the branch file against the live one before merging) — say so in the PR body, and note that if `live-sync.sh` commits the dirty file first the merge stays clean because both sides hold identical bytes.
|
||||
- Package repos (`~/Projects/<pkg>`): same `--ff-only`, then reinstall (`omp plugin upgrade <name>`) and `cmp` the installed `dist/index.js` against the repo `dist/index.js` to prove the live artifact is the merged one. Check `ls -ld node_modules` after landing anything that touched it.
|
||||
- **`omp plugin upgrade` takes the npm package name, not the short name.** `omp plugin upgrade dankvoice` fails with `Failed to upgrade dankvoice: dankvoice is not installed` while the plugin is plainly installed; the package is `omp-dankvoice`. Find the truth with `ls ~/.omp/plugins/node_modules` or `find ~/.omp -path '*<name>/dist/index.js' -not -path '*/Projects/*'` — the installed tree is unscoped (`~/.omp/plugins/node_modules/omp-dankvoice/dist/index.js`), not `@dank/dankvoice`.
|
||||
- **`cmp` exiting 1 BEFORE the upgrade is the expected state** (installed bundle is the pre-merge build, diff reported at some char/line). Do not treat it as a failed deploy and do not "fix" the build; run the upgrade, then `cmp` again for byte identity, and grep the new bundle for a string only the merged code contains (e.g. `grep -c "Heckle instead of summarizing"`) as a second, semantic confirmation.
|
||||
- Prune: `git worktree remove --force <path>` (force: untracked deps link), `git branch -D`, branch delete on the remote (branches endpoint above). Before removing a worktree, check no live process has a cwd inside it (`ls -l /proc/*/cwd | grep -c "wt/<name>"`) — same guard `live-sync.sh` uses — and copy any file you moved into a gitignored `.AGENTTRASH/` there to its permanent home first, or the removal destroys the only copy.
|
||||
- **Status bullets written pre-merge go stale at merge time.** A HANDOFF/readme line saying "unmerged (open PR)" is wrong the moment you merge — audit those lines as part of the merge turn and land a one-bullet docs PR (see the Create section's merged-stack branch trick for doing that without a fresh worktree).
|
||||
- Prune: `git worktree remove --force <path>` (force: untracked deps link), `git branch -D`, `git push origin --delete <branch>` (allowed: local Gitea). Before removing a worktree, check no live process has a cwd inside it (`ls -l /proc/*/cwd | grep -c "wt/<name>"`) — same guard `live-sync.sh` uses — and copy any file you moved into a gitignored `.AGENTTRASH/` there to its permanent home first, or the removal destroys the only copy.
|
||||
|
||||
## Attribution footer (AGENTS.md)
|
||||
Every issue/PR body ends with `---`, then `cwd: <session cwd> · session: <session id>`. Read the real values, never invent an id.
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
---
|
||||
name: gitea-token-from-http-credential-store
|
||||
description: Extract the Dankclaw Gitea token from ~/.config/secrets/gitea (a URL-line credential store using http:// and %3a-encoded port) without the empty-token 401; covers the https-only extraction trap and the $$..$$ shell-expansion trap.
|
||||
description: "Extract the Dankclaw Gitea token from ~/.config/secrets/gitea (it is the URL password, not key=value lines) for API calls, and diagnose the silent empty-token trap that makes public-repo reads pass while writes fail."
|
||||
---
|
||||
|
||||
# Gitea token out of the HTTP credential store
|
||||
|
||||
`~/.config/secrets/gitea` is a git **credential-store file**, not a bare token file. On the Dank
|
||||
boxes it reads:
|
||||
The Gitea credential store line on Frank's boxes is **http** (not https) with a percent-encoded port, and the **token is the URL password** — there are NO `username=`/`password=` lines:
|
||||
|
||||
```
|
||||
http://dank:b7caa2f689cdc00841854b85cd0173386f707d86@192.168.0.143%3a3000
|
||||
http://dank:<40-char-token>@192.168.0.143%3a3000
|
||||
```
|
||||
|
||||
Three traps, all of which produce a silent **401 Unauthorized** rather than a parse error:
|
||||
## Extraction that actually works
|
||||
|
||||
1. **Scheme is `http://`, not `https://`.** An extraction pattern written `s#^https://…#` matches
|
||||
nothing and yields an empty token. GNU BRE has no `?` quantifier — use the
|
||||
`http[s]*://` form or Python `https?://`:
|
||||
```bash
|
||||
tok=$(sed -nE 's#^[a-z]+://[^:]+:([^@]+)@.*#\1#p' ~/.config/secrets/gitea | head -1)
|
||||
echo "tok len ${#tok}" # must be 40 — assert before any call
|
||||
cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
|
||||
curl -s -K "$cfg" http://192.168.0.143:3000/api/v1/<path>
|
||||
rm -f "$cfg" # in the SAME session: the file holds a live token
|
||||
```
|
||||
|
||||
```bash
|
||||
TOK=$(sed -n 's#^http[s]*://[^:]*:\([^@]*\)@.*#\1#p' ~/.config/secrets/gitea)
|
||||
test -n "$TOK" || echo "extraction failed" # never ship an unasserted capture
|
||||
```
|
||||
## `git credential-store --file … fill` answers EMPTY — do not debug it
|
||||
|
||||
```py
|
||||
tok = re.search(r"https?://[^:]*:([^@]+)@", pathlib.Path.home().joinpath(".config/secrets/gitea").read_text()).group(1)
|
||||
```
|
||||
Tried 2026-10-21 while opening `dank/command-center` PR #61: the obvious extraction
|
||||
|
||||
Always assert non-empty (`test -n`) or print `${#TOK}` once: a correct token here is 40 chars,
|
||||
and the length check catches a wrong pattern without echoing the secret.
|
||||
```bash
|
||||
printf 'protocol=http\nhost=192.168.0.143\nport=3000\n\n' | git credential-store --file ~/.config/secrets/gitea fill
|
||||
```
|
||||
|
||||
2. **The token body contains `$$…$$`.** An omp output token inside a *file* is inert, but pasted
|
||||
literally into a shell script line it expands (`$$` = shell PID). Keep it in a variable or read
|
||||
it from the file — never inline it in command text.
|
||||
prints **nothing**, with no error. Not permissions, not a wrong path: the stored line encodes the port inside the host field (`@192.168.0.143%3a3000`), so a `host=`+`port=` query can never match it. Add `path=` or drop `port=` and it still answers empty. Read the file with `sed` (recipe above) and move on — the chain `TOKEN=$(…) && [ -n "$TOKEN" ] && …` fails silently at the test, which reads like a curl failure because the `&&` hides which link broke.
|
||||
|
||||
3. **Port is percent-encoded (`%3a3000`).** Don't rebuild the API URL from that line; the working
|
||||
endpoints are `http://192.168.0.143:3000/api/v1/...` and `https://gitea.franksdank.cloud/api/v1/...`
|
||||
— both serve the same API, so a host/port mismatch looks like an auth failure.
|
||||
## The silent-empty-token trap (bit 2026-10-21, cost a debug detour)
|
||||
|
||||
Auth header for every call: `Authorization: token $TOK` (the `token` keyword is required; a bare
|
||||
value 401s too).
|
||||
An `awk -F= '/^username=/{...}'` parse of this file yields `tok=""` **silently** — and reads still appear to work when the repo is **public**: `dank/serverAppConfigs` and `dank/command-center` return real issue/label data unauthenticated, so every census "passes" until the first **write** (issue comment, label, PR create/merge) fails with `{"message":"token is always required"}`-style errors or 404s (private repos 404 on an empty token — that is an auth problem, NOT a missing repo).
|
||||
|
||||
**Never print the token.** Extract into a variable, then use `$TOK`; read the file with `read
|
||||
path=...:raw` only when you must inspect the format.
|
||||
- `echo "tok len ${#tok}"` (must be 40) before believing ANY API result — reads included.
|
||||
- Prove auth once per session: `GET /api/v1/user` → `"login":"dank"`.
|
||||
- `curl -s -K <600-perm file>` carrying `header = "Authorization: token $tok"` — never `-H` on argv (`/proc/<pid>/cmdline` is world-readable; this leaked an HF token once).
|
||||
- `jq -n --arg t … --arg h … --arg b "$(/bin/cat /tmp/pr.md)" '{…}'` — `$(cat file)` gets optimised away in this harness, so use `--rawfile b /tmp/pr.md` instead.
|
||||
|
||||
Related: `gitea-pr-api-cycle` (POST/PATCH body shapes), `gitea-merge-form-encoded` (merge needs
|
||||
`do=merge&merge_style=no-ff`, answers 200 with an empty body — verify by fetching the PR, not by
|
||||
reading the response), `gitea-credential-store-url-format`.
|
||||
## Other notes
|
||||
|
||||
- `git credential fill` fails headless here (`could not read Username … terminal prompts disabled`); read the store file directly instead.
|
||||
- Never print the token: mask when inspecting the file —
|
||||
`sed -E 's#(://[^:]+:)[^@]+@#\1REDACTED@#' ~/.config/secrets/gitea`
|
||||
- Merge writes need form-encoded fields, not JSON — see `gitea-pr-api-cycle`.
|
||||
- Issue/PR numbers are **per repo**: `dank/dankvoice` #8–#11 (refactor, hardening, AbortError fix, menu defects) are unrelated to `dank/command-center` #8–#11 (SDLC, trunk-law). Read the body from the repo the work targets before referencing an issue in a commit message.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: handdrawn-svg-diagram-verification
|
||||
description: "Author legible hand-drawn SVG diagrams for zero-JS static pages (theme vars, shared markers, text-width budgets) and verify each by managed-browser screenshot loop that catches label collisions; includes the browser/eval API traps hit doing it."
|
||||
---
|
||||
|
||||
Use when adding diagrams to a static, zero-JS HTML artifact (guide/report) where mermaid/JS is not allowed, or when verifying hand-drawn SVGs render legibly.
|
||||
|
||||
## Authoring rules (what makes SVGs legible)
|
||||
- Theme colors ONLY via the page's CSS vars: define classes once — `svg rect.box {fill:var(--surface);stroke:var(--border)}`, `svg .mut {fill:var(--muted)}`, arrow paths stroke var(--muted)/var(--accent); status text via inline `style="fill: var(--ok|var(--err)|var(--warn))"` equivalents. Never hardcoded hex.
|
||||
- Markers once, shared: hidden `<svg width="0" height="0" style="position:absolute">` right after `<body>` holding `<marker id="arrM">` (fill var(--muted)) + one per stroke color used; reference via CSS `marker-end: url(#arrM)`. `orient="auto-start-reverse"`.
|
||||
- Text budget: chars × 0.535 ≈ px width at font-size 10.5 (0.56 at 11, ~0.72 bold 13-14). Keep every label ≤ box width − 16px; split long labels into 2–3 lines rather than shrink font.
|
||||
- Structure > decoration: dashed `rect.frame` lanes with a 11px bold lane label ("REQUEST TIME / AUTHORING TIME"); one crossing arrow between lanes max, labeled; lifelines = `class="dash arrow"` + `style="marker-end:none"`.
|
||||
- Collision suspects, pre-check: labels near box corners/edges, crossing curves over lane labels (flatten curve control points to arc OVER label text, or move label beside the curve's x-range, not under its start), two labels in the same y-band.
|
||||
|
||||
## Verification loop (catches what eyes miss in source)
|
||||
1. Serve: `Bun.spawn(["python3","-m","http.server","8899","-d",dir])`, `await Bun.sleep(500)`.
|
||||
2. `const tab = await browser.open({ url: "http://127.0.0.1:8899/#anchor" })` — options object, NOT url string.
|
||||
3. Position per diagram: `goto(url#anchor)` then `tab.evaluate("window.scrollBy(0, N)")`; shot each with `tab.screenshot({path})`. Attached images are the ground truth — read them for label overlaps, border grazes, clipped text.
|
||||
4. Fix coordinates with `edit`, re-shoot ONLY the touched regions. Expect 2 rounds: first shots expose collisions you designed in.
|
||||
|
||||
## Eval/browser traps (all personally hit)
|
||||
- `tab.wait` does not exist → `await Bun.sleep(ms)`.
|
||||
- `log()` output from JS eval cells is SWALLOWED — return the value from the cell instead (`return JSON.stringify(x)`).
|
||||
- `tab.evaluate(...)` can return undefined/nothing on managed hidden tabs without erroring — for content assertions prefer `tab.text("svg")` or parse the file with node; screenshots remain trustworthy.
|
||||
- `browser.open` errors list the correct signature — read it and fix in one retry.
|
||||
- Screenshot misreads at small font: a label that *looks* like mangled markup (`</id>`) in a downscaled shot is usually fine in source — settle with `tab.text(selector)` returning `[...]`, don't "fix" correct source.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
name: iptables-insert-order-drop-shadows-accepts
|
||||
description: "Diagnose or prevent a service port that times out beside a LISTENing socket because a deployer's iptables -I sequence landed a blanket DROP above its ACCEPTs; includes the sandboxed fake-iptables red/green bats pattern."
|
||||
---
|
||||
|
||||
# iptables `-I` order trap: blanket DROP shadowing ACCEPTs
|
||||
|
||||
## Failure signature
|
||||
- Client to a port gets `curl: (28) Connection timed out` (SYN swallowed), never refused.
|
||||
- `ss -ltn` shows the socket LISTENing; the process is fresh and healthy.
|
||||
- `sudo nft list ruleset | grep <port>` (or `iptables -L INPUT --line-numbers -v`) shows the catch-all `drop` rule ABOVE the specific ACCEPTs, with all counters on the drop and 0 on the accepts.
|
||||
|
||||
## Root cause pattern
|
||||
`iptables -I CHAIN ...` inserts at position 1. A deployer block like:
|
||||
```
|
||||
-C/-I ACCEPT -s A ; -C/-I ACCEPT -s B ; -C/-I ACCEPT -s C ; -C/-I DROP
|
||||
```
|
||||
ends with DROP at position 1 — above every ACCEPT. Worse: `-C` checks existence, NEVER order, so re-running the block "verifies clean" while the chain stays poisoned.
|
||||
|
||||
## Repair (converges on poisoned hosts)
|
||||
1. Delete the blanket DROP wherever it sits: `iptables -D INPUT ... -j DROP 2>/dev/null || true`
|
||||
2. Keep/insert the ACCEPTs at the head (`-C` before `-I`).
|
||||
3. APPEND the DROP (`-A`), so it is last.
|
||||
Delete-before-append keeps exactly one DROP per run (idempotent).
|
||||
|
||||
## Behavioral test pattern (bats)
|
||||
Text greps cannot see order. Fake the kernel: write a small `iptables` shim to `$BATS_TEST_TMPDIR/bin` backed by an ordered file `$FW_STATE` (`-C`=grep -Fxq, `-I`=prepend, `-A`=append, `-D`=delete or exit 1), prepend it to `PATH`, `eval "$(sed -n '/^<install_fn>()/,/^}/p' script.sh)"`, stub `run_privileged(){ "$@"; }`, `systemctl(){ :; }`, `curl(){ return 0; }`, and **seed the state file with the poisoned order** before running. Assert: exactly one DROP, its line number equals `wc -l`, and every ACCEPT line number is smaller. Prove red by running the same scenario against `git show origin/main:<script>`.
|
||||
|
||||
## Misc traps hit while doing this
|
||||
- zsh: `UID=...` is read-only/arith (renames blow up); a glob that matches nothing aborts the ENTIRE `&&`-less command line, silently skipping earlier `rm`s on that line.
|
||||
- `echo ===` in a remote zsh command parses as `=cmd` expansion — quote sentinel strings.
|
||||
- OpenWebUI `create_token` needs cwd `/app/backend` AND `WEBUI_SECRET_KEY` from PID 1's `/proc/1/environ` (`podman exec` sessions don't inherit the app process env).
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: new-service-deploy-gate-census
|
||||
description: "When a newly added serverAppConfigs fleet service fails to deploy (\"is not deployed on target\" or \"Unit svc.service not found\"), run the full gate census; use pre-flight when adding any new service."
|
||||
---
|
||||
|
||||
Adding a deployable service to serverAppConfigs touches SIX independent gates. PR #69 (blackbox-exporter, 2026-10-21) shipped with three of them missing and failed three separate deploys before all were found. Run this census pre-flight (new service) or as a debug ladder (deploy failure).
|
||||
|
||||
## The six gates, in failure order
|
||||
|
||||
| # | Gate | File | Failure signature |
|
||||
|---|------|------|-------------------|
|
||||
| 1 | client allow-list `valid_service()` | `scripts/deploy.sh` | `Service 'X' is not deployed on <target>.` (client-side, before anything runs) |
|
||||
| 2 | remote allow-list `service_is_allowed()` | `scripts/deploy-remote.sh` | same message from the remote runner |
|
||||
| 3 | dispatch case in `deploy_service()` | `scripts/deploy-remote.sh` | falls to catch-all `deploy_compose_service` which HARD-FAILS on dankclaw (no compose since 2026-09-16): `no quadlet handler` |
|
||||
| 4 | planner entry + `SERVICE_ORDER` | `scripts/deploy-plan.py` | service silently absent from auto-plans (explicit `--service` still reaches gates 1-3) |
|
||||
| 5 | quadlet unit validity | `<host>/.../quadlet/X.container` | `Failed to restart X.service: Unit X.service not found` — quadlet refuses to GENERATE a unit without `Image=`; also check every key the unit's comments claim (e.g. `Network=host` declared, not just argued) |
|
||||
| 6 | health gate in the dispatch tail | `deploy-remote.sh` `wait_for_health` | deploy runs but hangs/fails at health |
|
||||
|
||||
## Pre-flight commands
|
||||
|
||||
```bash
|
||||
# gates 1-3 in one shot (replace TARGET/SVC):
|
||||
grep -c "$TARGET:$SVC" scripts/deploy.sh scripts/deploy-remote.sh # both allow-lists > 0
|
||||
sed -n '/^deploy_service()/,/^}/p' scripts/deploy-remote.sh | grep "$SVC)" # dispatch case
|
||||
python3 scripts/deploy-plan.py --target "$TARGET" --paths <the-service-paths> # gate 4
|
||||
grep '^Image=' <host>/<svc>/quadlet/$SVC.container # gate 5 minimum
|
||||
```
|
||||
|
||||
## Test seams that pin the class (exist post-2026-10-21)
|
||||
|
||||
- `tests/deployment_flow.bats` "the client and remote allowlists agree per target" — drift only; blind to a service missing from BOTH.
|
||||
- `tests/deployment_flow.bats` "every gated <target> service is dispatched" — gate 3 (red-proof: plant a gated, undispatched name in a sandbox copy).
|
||||
- `tests/observability_quadlet.bats` "every observability unit declares an Image" — gate 5.
|
||||
|
||||
## Deploy-execution traps (same tranche)
|
||||
|
||||
- `deploy.sh` MUST run from a checkout literally on `main` (branch-name gate) with a clean tree. A detached worktree pointed at the `main` ref fails (branch gate / dirty index). The primary checkout `~/Projects/serverAppConfig` is the deploy host; switching it to `main` needs explicit user OK when workspace policy blocks checkout.
|
||||
- After merging, delete nothing by `git add -A` blind: a stray nested dir (e.g. a failed `git worktree add` stub) lands as an embedded-repo gitlink in the commit; `git rm --cached <dir> && git commit --amend` to excise, move debris to `~/.AGENTTRASH/`.
|
||||
|
||||
## Gitea merge invocation (consumed probe turns twice)
|
||||
|
||||
`POST /api/v1/repos/<o>/<r>/pulls/<n>/merge` with **JSON** body `{"Do":"merge"}` (capital D) + `Content-Type: application/json`. Form-encoded `Do=merge` 422s `[Do]: Required` on this Gitea despite older lore. Credential: `~/.config/secrets/gitea` holds `http://user:PW@host%3aport` (percent-encoded colon) — use **Basic** auth with `user:PW`; `Authorization: token` 401s. Never echo the password: build the header inside python from the file.
|
||||
@@ -1,21 +1,21 @@
|
||||
---
|
||||
name: niri-monitor-flap-refresh-triage
|
||||
description: "Diagnose niri/amdgpu monitor failures on blankpad — panel dropping, wrong refresh rate, black/flickering output, letterboxed 1080p on an ultrawide — and fix the refresh without reloading the compositor or logging Frank out."
|
||||
description: "Diagnose niri/amdgpu monitor failures on blankpad — panel dropping, wrong refresh rate, black/flickering output — and fix the refresh without reloading the compositor or logging Frank out."
|
||||
---
|
||||
|
||||
# niri monitor triage: flaps, phantom modes, 60 Hz fallback, stale kernel mode list
|
||||
# niri monitor triage: flaps, phantom modes, 60 Hz fallback
|
||||
|
||||
Frank's reaction to a broken display is immediate and loud; the fix must never log him out.
|
||||
Box: `blankpad` (Cezanne APU, `card1`, s2idle only), compositor `niri --session` as a user unit.
|
||||
|
||||
## Step 1 — separate the four diseases (do this before touching anything)
|
||||
## Step 1 — separate three different diseases (do this before touching anything)
|
||||
|
||||
```bash
|
||||
niri msg outputs # name, mode, position, full mode list per output
|
||||
for c in /sys/class/drm/card*-*/status; do echo "$(basename $(dirname $c)) $(cat $c)"; done
|
||||
systemctl --user show niri.service -p ExecMainStartTimestamp -p NRestarts # did niri restart?
|
||||
journalctl --user -b --no-pager | grep -iE 'disconnecting connector|new connector|Page flip commit failed|DMCUB|falling back to preferred|could not be found'
|
||||
journalctl -k -b --no-pager | grep -iE 'amdgpu|PM: suspend exit|\*ERROR\*|enabling link|DP_LANE_COUNT_SET|DP_LINK_BW_SET'
|
||||
journalctl --user -b --no-pager | grep -iE 'disconnecting connector|new connector|Page flip commit failed|DMCUB|falling back to preferred'
|
||||
journalctl -k -b --no-pager | grep -iE 'amdgpu|PM: suspend exit|\*ERROR\*'
|
||||
```
|
||||
|
||||
Reading it:
|
||||
@@ -25,25 +25,16 @@ Reading it:
|
||||
- `configured mode M could not be found, falling back to preferred` → **config asks for a mode the panel does not offer**; the panel silently runs at EDID-preferred (usually 60 Hz). This is the one you can fix instantly.
|
||||
- Config mtimes older than the symptom + no `niri` restart ⇒ the config did not cause it. Say so explicitly; agents get blamed for display problems they didn't cause.
|
||||
|
||||
## Step 2 — phantom-mode trap AND stale-mode-list trap (niri fails soft, so both look like hardware)
|
||||
## Step 2 — the phantom-mode trap (niri fails soft, so it looks like hardware)
|
||||
|
||||
Two different lies, told by two different layers. `output { mode "…" }` with an unavailable refresh does **not** error: niri logs a WARN and drops to `preferred` on every connect → janky 60 Hz ultrawide, blamed on the monitor. Check the requested mode against the panel's real list:
|
||||
`output { mode "…" }` with a refresh the EDID never advertised does **not** error. niri logs a WARN and drops to `preferred` on every connect → janky 60 Hz ultrawide, blamed on the monitor.
|
||||
|
||||
Compare the requested mode against the panel's real list:
|
||||
|
||||
```bash
|
||||
niri msg outputs | sed -n '/<monitor name>/,/^$/p' | grep -E '^\s+[0-9]+'
|
||||
```
|
||||
|
||||
If the kernel list lacks modes the EDID advertises (e.g. 3440x1440 absent, largest mode 1080p), discriminate parser gap vs stale cache — **compare the kernel's EDID blob with sysfs and decode it**:
|
||||
|
||||
```bash
|
||||
command -v edid-decode # parse the live EDID: edid-decode /sys/class/drm/card1-DP-1/edid
|
||||
modetest -M amdgpu -c 2>/dev/null | awk '/connected[[:space:]]+DP-1/,0' \
|
||||
| sed -n '/value:/,/DPMS/p' # the 384-byte blob DRM actually holds
|
||||
```
|
||||
|
||||
- Blob good (all extension blocks present, native DTD visible via `edid-decode`) **and** mode list bad → **stale mode list cached from a corrupted AUX/DDC read during a flapped hotplug**. DRM rebuilds modes only on a clean re-connect, and every subsequent flap re-caches garbage. Software cannot force the rebuild — the fix is a monitor **power cycle** (unplug power ~30 s; needs Frank's hands), then re-apply the mode. Do NOT conclude "kernel can't parse this EDID" and do not edit config for it.
|
||||
- Blob itself degraded → AUX hardware read failure, same physical remedy, worse prognosis (cable/port).
|
||||
|
||||
## Step 3 — apply the mode WITHOUT a config reload (the no-logout path)
|
||||
|
||||
```bash
|
||||
@@ -52,11 +43,7 @@ niri msg outputs | grep -A1 '<monitor>' # confirm the new mode
|
||||
pgrep -c niri # compositor still alive, same PID
|
||||
```
|
||||
|
||||
`niri msg output …` is documented as temporary ("not saved into the config file") — it changes one output and cannot log the session out. **`rc=0` proves nothing**: for a mode outside the kernel's published list niri exits 0 and silently no-ops — always re-read `niri msg outputs` to confirm. Avoid `niri action reload-config` when Frank is working: it reloads his whole config.
|
||||
|
||||
## Step 3.5 — flap census beyond the display: who else dies at the same second
|
||||
|
||||
Correlate DP flap timestamps against `journalctl -k | grep -E 'usb .*: (new |disconnect)|Synchronize Cache'`. A USB hub chain re-enumerating in lockstep with a DP flap means shared power/ground on the monitor or dock — and it identifies what else brown-outs (keyboard, webcam, USB SSD). `KT USB Audio` (idVendor 12d1 KTMicro) in the tree = the monitor's built-in DAC, i.e. the monitor's USB upstream port is in that hub chain. Kernel `enabling link 2 failed: 15` across *multiple boots* + `core_link_write_dpcd (DP_LANE_COUNT_SET/DP_LINK_BW_SET) failed` = physical layer (cable/dock/monitor PSU), not compositor, not config: prescribe cable swap / other input after a clean-boot power cycle, and say plainly which of the four diseases you did NOT fix.
|
||||
`niri msg output …` is documented as temporary ("not saved into the config file") — it changes one output and cannot log the session out. Avoid `niri action reload-config` when Frank is working: it reloads his whole config.
|
||||
|
||||
## Step 4 — persist, and check you edited the file that is actually live
|
||||
|
||||
@@ -74,4 +61,4 @@ Desk config law (its own header): panels keyed by **EDID identity**, not connect
|
||||
|
||||
## Escalate honestly
|
||||
|
||||
Reboot clears DMCUB/EINVAL state → schedule it, don't take it. Persistent drops after a reboot → cable / dock / monitor DP-version setting, i.e. hardware, not config. Report which of the four diseases you found and which you did **not** fix.
|
||||
Reboot clears DMCUB/EINVAL state → schedule it, don't take it. Persistent drops after a reboot → cable / dock / monitor DP-version setting, i.e. hardware, not config. Report which of the three diseases you found and which you did **not** fix.
|
||||
@@ -1,15 +1,14 @@
|
||||
---
|
||||
name: omp-models-yml-runner-api-placement
|
||||
description: "Add or edit a row in ~/.omp/agent/models.yml — non-chat (tts/image/embedding) runners and authenticated/remote providers with command-backed apiKey values — wire the speech role, verify with a poisoned scratch HOME, and diagnose a failing speech device without blaming a healthy endpoint"
|
||||
description: "Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml, wire the speech role, and diagnose a failing speech device without blaming a healthy endpoint"
|
||||
---
|
||||
|
||||
# Scope
|
||||
|
||||
`~/.omp/agent/models.yml` (or `models.yaml`) on a workstation: adding a **runner**
|
||||
model row (TTS / image / embedding / rerank / video) alongside chat providers,
|
||||
adding an **authenticated or off-network** chat provider without committing its
|
||||
secret, flipping `config.yml` `modelRoles` at it, and diagnosing a speech device
|
||||
that refuses to speak.
|
||||
flipping `config.yml` `modelRoles` at it, and diagnosing a speech device that
|
||||
refuses to speak.
|
||||
|
||||
# The landmine
|
||||
|
||||
@@ -52,46 +51,6 @@ which the runner transport never reads).
|
||||
Roles live in `config.yml` (`modelRoles`), not in `models.yml`. Kind roles
|
||||
(`image`, `web`, `speech`, `dictation`, `judge`) take no `:thinking` suffix.
|
||||
|
||||
# Authenticated / remote providers
|
||||
|
||||
Provider schema (`src/config/models-config-schema-bundle.ts`): `baseUrl`,
|
||||
`apiKey`, `api`, `headers`, `compat`, `authHeader`, `auth`
|
||||
(`"apiKey" | "none" | "oauth"`), `discovery`, `models`, `modelOverrides`,
|
||||
`transport`. Defining `models` without `apiKey` and without `auth: none|oauth`
|
||||
is a validation error, so an authenticated endpoint needs a key *shape* even if
|
||||
the value comes from outside the file.
|
||||
|
||||
**Never put the literal secret in `models.yml`.** `apiKey` (and any `headers`
|
||||
value) is resolved by `src/config/resolve-config-value.ts`:
|
||||
|
||||
- leading `!` → command-backed: `/bin/sh -c "<rest>"`, cwd = project dir,
|
||||
10 s timeout, stdout **trimmed**, result cached for the process lifetime
|
||||
(failure backs off 30 s);
|
||||
- otherwise the string is looked up as an **exact env-var name**; the literal is
|
||||
the fallback only when no such env key exists.
|
||||
|
||||
So the secret lives in a 600 file, and the tracked file stays clean:
|
||||
|
||||
```yaml
|
||||
apiKey: "!cat ~/.config/secrets/openwebui_api_key" # QUOTE it: an unquoted
|
||||
# leading ! is a YAML tag
|
||||
```
|
||||
|
||||
Laws: a literal `sk-…` in this repo trips the pre-commit shape gate *and* lands
|
||||
the key in every agent context on every box that pulls; rotation means rewrite
|
||||
the secret file **then restart the harness** (the `!cmd` value is process-cached,
|
||||
so a live session keeps serving the old key and a stale session reads exactly
|
||||
like a revoked one); every workstation needs its own copy of the secret file.
|
||||
|
||||
**A row behind a proxy copies its direct-connected twin's `compat` block**, it
|
||||
does not simplify it. Measured through an OpenAI-compatible gateway on
|
||||
2026-10-21: SSE forwards `reasoning_content` and `tool_calls` deltas plus final
|
||||
`usage` under `stream_options.include_usage`; `image_url` content parts reach
|
||||
the vision tower; `chat_template_kwargs` is forwarded (same prompt:
|
||||
`enable_thinking` false → 0 reasoning tokens, true → a real block). Dropping
|
||||
kwargs/effort maps because "it is only a proxy" silently changes thinking
|
||||
behaviour on that path. Check per gateway, never assume either direction.
|
||||
|
||||
# Verify after every edit (never assume the file loaded)
|
||||
|
||||
```bash
|
||||
@@ -103,25 +62,6 @@ omp models | grep -c 'validation failed' # must be 0
|
||||
A run that lists only built-ins means your row broke the file — revert before
|
||||
yielding.
|
||||
|
||||
**Poisoned-scratch-HOME proof** for a remote/credential row (the only test that
|
||||
shows a success came through the new path and not from a LAN endpoint):
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/ompx/.omp/agent /tmp/ompx/.config
|
||||
cp agent/models.yml agent/config.yml /tmp/ompx/.omp/agent/
|
||||
ln -s ~/.config/secrets /tmp/ompx/.config/secrets # REQUIRED: ~ in a !cmd value
|
||||
# expands to the scratch $HOME
|
||||
sed -i 's#http://192\.168\.0\.[0-9]*:[0-9]*/v1#http://127.0.0.1:9/v1#g' /tmp/ompx/.omp/agent/models.yml
|
||||
HOME=/tmp/ompx omp --model '<provider>/<id>:off' --tools bash --no-session --no-skills \
|
||||
--no-rules --no-extensions --no-pty --no-lsp --no-title --cwd /tmp -p '<ask for a real tool call>'
|
||||
```
|
||||
|
||||
Then `rm -rf /tmp/ompx` — that tree holds endpoint URLs. Cross-check the server
|
||||
side before believing the client's echo (gateway access log / container log line
|
||||
for the request you just made), and confirm the change never hit the live
|
||||
checkout until you mean it to: `git -C <live checkout> status --porcelain agent/models.yml`
|
||||
plus a grep for the new provider name.
|
||||
|
||||
# Speech transport contract (`openai-speech`)
|
||||
|
||||
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
|
||||
@@ -132,17 +72,13 @@ error, not a fallback — and `auth: none` makes a dead endpoint read as
|
||||
*available*, so point `modelRoles.speech` at such a provider only after it has
|
||||
answered a real synthesis.
|
||||
|
||||
A gateway's own `/api/v1/audio/speech` route is NOT an equivalent: OpenWebUI
|
||||
substitutes its own voice enum before forwarding, so a cloned pack name comes
|
||||
back 400 from the upstream. Keep `modelRoles.speech` on the direct endpoint.
|
||||
|
||||
# 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
|
||||
`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:
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: omp-plugin-modal-bats-harness
|
||||
description: "Drive a real omp-extension settings modal headlessly from bats (ui.custom stub, DV_KEYS key-replay) and ship the bundle with host pi packages external."
|
||||
---
|
||||
|
||||
# Headless modal testing + host-external bundling for omp plugins
|
||||
|
||||
Friction-earned 2026-10-21 on `dank/dankvoice` PR #31 (select menu → vendored pi-common settings modal). Three failed driver iterations and one 43 MB bundle before green.
|
||||
|
||||
## The build contract (do this first)
|
||||
|
||||
A modal imports runtime values (`SelectList`, `matchesKey`, `getSelectListTheme`) from `@oh-my-pi/pi-tui` / `@oh-my-pi/pi-coding-agent`. Bundling the root barrels = 43 MB and trips `--target node` on bun builtins. The plugin host IS bun and hoists every pi package into `~/.omp/plugins/node_modules/@oh-my-pi/` — the same ground pi-common stands on. So:
|
||||
|
||||
```
|
||||
bun build src/index.ts --outdir dist --target bun --format esm \
|
||||
--external @oh-my-pi/pi-coding-agent --external @oh-my-pi/pi-tui \
|
||||
--external omp-legacy-pi-modules
|
||||
```
|
||||
|
||||
- `omp-legacy-pi-modules` is a harness-internal virtual module behind a lazy dynamic import in the pi-coding-agent barrel; externalizing is safe, omitting it fails the build.
|
||||
- Declare the externals in `peerDependencies`.
|
||||
- The packed-artifact load test must reproduce the hoisted host layout: extract the tarball, then `ln -s <repo>/node_modules/@oh-my-pi <work>/node_modules/@oh-my-pi` before importing the entry — a bare temp dir has nothing to resolve against and the test fails for the wrong reason.
|
||||
|
||||
## The driver stub for `ctx.ui.custom`
|
||||
|
||||
The modal mounts through `ctx.ui.custom(factory, opts)`; factory signature is `(tui, theme, keybindings, done)`. The bats driver stub:
|
||||
|
||||
```ts
|
||||
custom: (factory: any) => {
|
||||
customCall += 1;
|
||||
let settleDone: () => void = () => {};
|
||||
const closed = new Promise<void>((r) => { settleDone = r; });
|
||||
// theme: Proxy answering EVERY style method; pass through the LAST arg —
|
||||
// style calls are (colorName, text); returning args[0] prints color names,
|
||||
// and a fixed {bg,fg,inverse} trio crashes the frame's theme.bold.
|
||||
const style = (...a: unknown[]) => String(a[a.length - 1] ?? "");
|
||||
const theme = new Proxy({} as Record<string, typeof style>, { get: () => style });
|
||||
const component = factory(
|
||||
{ requestRender: () => {}, terminal: { rows: 60, cols: 100 } }, // rows feeds list pagination; omit → NaN, nothing scrolls
|
||||
theme, {}, () => settleDone(),
|
||||
);
|
||||
const census = () => { for (const l of component.render?.(100) ?? []) console.log(`ROW ${l}`); };
|
||||
census();
|
||||
// keys from DV_KEYS / DV_KEYS<n> per call: up|down|enter|tab|esc|ctrl+u named,
|
||||
// name*N repeat, anything else typed.
|
||||
(async () => { /* for each token: beats below; await ~140ms after enter (async
|
||||
roster fetch), ~20ms otherwise; final ~150ms; settleDone(); */ })();
|
||||
return closed;
|
||||
},
|
||||
```
|
||||
|
||||
## Key-replay discipline (each cost a red iteration)
|
||||
|
||||
- **Never zero-interval.** A burst of `\x1b[B` bytes in one tick coalesces in SelectList — `down*3` lands one step. Beat ~25 ms between repeats.
|
||||
- **Type one character at a time.** A whole sentence written to the inline editor in one `handleInput` is swallowed (paste-shaped); iterate `[...text]` with ~10 ms beats.
|
||||
- **Sleep after every `enter`**, not just fixed small delays: Enter opens submenus whose list contents arrive from an async fetch.
|
||||
- Row math must be pinned: row order in `settingsFields` is the UI contract; tests reach row N with `down*N`.
|
||||
|
||||
## Vendored-modal semantics that bite
|
||||
|
||||
- Enum fields with `options.length <= cycleThreshold` (default) CYCLE inline on Enter instead of opening the picker — a curated list must set `cycleThreshold: 0` or an "open picker, down, enter" choreography silently cycles and the stray Enter activates the next row.
|
||||
- Submenus render inside the body component's `render()` output — one census sees both.
|
||||
- SelectList initial highlight = current value (`items.findIndex(value)`); choreography counts from there.
|
||||
|
||||
## Test layering that held
|
||||
|
||||
Component-level pins (field census, commits, audition) against fake deps with bun test; behavior bats drives the REAL bundle through the stub for wiring laws (persistence, byDir, notify strings). Don't duplicate choreographies across layers.
|
||||
|
||||
Related managed skills: `gitea-pr-api-cycle` (landing), `plugin-git-install-committed-artifact` (install validation).
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: screenshot-walkthrough-in-static-docs
|
||||
description: "Embed a real-browser user-flow walkthrough (screenshots as data URIs) into a single-file zero-JS static docs page; includes the selector laws, cookie isolation, and secret-gate proof earned driving franksign's full demo."
|
||||
---
|
||||
|
||||
name: screenshot-walkthrough-in-static-docs
|
||||
description: "Embed a real-browser user-flow walkthrough (screenshots as data URIs) into a single-file zero-JS static docs page."
|
||||
|
||||
# Screenshot walkthroughs in a single-file static guide
|
||||
|
||||
Friction-earned 2026-10-21 on `dank/franksign` `guide/index.html` §13: 19 un-retouched captures of every user flow (signup → studio → template → send → sign → freeze → signed PDF → claim shelf → void → 410 door), embedded as data URIs, guide still single file, zero JS.
|
||||
|
||||
## Recipe (end to end)
|
||||
|
||||
1. **Boot the prod-shaped stack** on a non-collision port (`PORT=3210`; e2e seams squat :3100, demos :3000). Kill it before running any test gate.
|
||||
2. **Walk flows as distinct identities sequentially** — the managed browser shares ONE cookie jar across tabs. Between roles: `tab.clearCookies()` then `tab.reload()` **on the app's origin, never on `about:blank`** (domain-scoped clear on about:blank clears nothing and the previous identity's session silently survives — you'll screenshot the admin where you meant the signer).
|
||||
3. Fresh per-checkout demo state is fine; re-running flows with new identities leaves harmless history that actually *helps* later shots (roster with completed + pending + voided rows).
|
||||
4. **Selectors**: prefer `role/textbox[name="Label *"]` / `role/button[name="Sign"]` / `testid/…`. Forms render required markers INTO the label text (`Email *`) — `label="Email"` misses; `text/…` clicks may silently no-op where `role/button[...]` works. Snapshot once per screen (`tab.ariaSnapshot(undefined,{compact:true,interactive:true})`) instead of guessing.
|
||||
5. **Screenshot to disk** (path option silently ignored — see `browser-screenshot-disk-and-cookies`): `tab.run(async ({page}, p) => { const r = await page.screenshot({encoding:"base64", type:"png"}); await Bun.write(p, Buffer.from(typeof r === "string" ? r : r.data, "base64")); }, { args: [path] })` — this variant returns a bare string, not `{data}`; handle both.
|
||||
6. **Shrink before embedding**: `for f in *.png; do magick "$f" -resize 1000x "$f"; done` → 17–83 kB each; 19 shots ≈ 800 kB raw, ~1.1 MB final guide.
|
||||
7. **Non-browser payoff shots**: rasterize artifacts the flow produces (e.g. signed PDF) with `pdftoppm -png -r 90` + resize — the deterministic-output image is the best single screenshot of a PDF pipeline.
|
||||
8. **Assembly**: write the section HTML with `__IMG:name__` placeholders via the write tool; a one-shot node script replaces tokens with `data:image/png;base64,…` and splices before `<footer>`. Never paste base64 through the edit tool.
|
||||
9. Update nav + add a pointer paragraph from the prose-journeys section to the walkthrough section.
|
||||
|
||||
## Verify
|
||||
|
||||
Serve the dir (`python3 -m http.server`), open, assert via one evaluate: `img` count == captured, `failed == 0` (`complete && naturalWidth>0`), and every `href^="#"` resolves to an id. Screenshot the section top + a mid scroll. Then kill the stack BEFORE the gate.
|
||||
|
||||
## Secret gate vs data URIs
|
||||
|
||||
A base64 wall trips shape patterns (`AKIA/ASIA[A-Z0-9]{16}` etc.) via case-insensitive grep at law-of-large-numbers rates. Prove noise, don't just override: `git diff --cached | grep -oEi '<patterns>' | sort -u` — hits that are substrings of PNG streams (mixed-case `AkIa…`, random tails) are image bytes. Then use the hook's documented deliberate override (`ALLOW_SECRETS=1 git commit`) and record the proof in the commit body. Confirm no real credential is visible in any shot first (password fields render masked; note demo tokens in captions are dev-DB throwaways).
|
||||
|
||||
## Voice/law notes
|
||||
|
||||
- Captions must state what the demo fakes (no SMTP → "link travels by copy-paste") and that captures are un-retouched with real demo values.
|
||||
- The walkthrough routinely surfaces UI laws the prose hid (e.g. Void `disabled={r.state !== "pending"}` while the API accepts completed voids) — write those discoveries into a closing law box, and check the prose section against them.
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: static-page-headless-verify-traps
|
||||
description: "Verify a static HTML artifact headlessly after writing it: serve on a scratch port, assert structure, screenshot — with the omp JS-eval/browser API traps (browser.open options object, no tab.wait, tab.run is Node-isolated, log() swallowed in JS cells)."
|
||||
---
|
||||
|
||||
# Headless verify of a static HTML file — traps that cost cells
|
||||
|
||||
Use after writing/rewriting a self-contained HTML artifact (guide, report, landing page) that must be proven to render before commit.
|
||||
|
||||
## Loop
|
||||
|
||||
1. Serve: `Bun.spawn(["python3","-m","http.server","<port>","-d","<dir>"])` inside the eval cell; kill at cell end (`proc.kill(9)`), or `pkill -f "http.server <port>"` first if a prior cell died mid-run.
|
||||
2. Open + assert + screenshot in ONE cell; also cross-check anchors statically with node (regex `href="#x"` vs `id="x"`) — no browser needed for that half.
|
||||
3. Entity/markup contamination sweep with grep on the FILE (`"`, `&#`, raw `<` inside text): what renders small can hide real damage; conversely a suspicious screenshot is often just low-res misread — settle label text via `tab.text("svg")`/`tab.text(selector)` before "fixing" markup.
|
||||
|
||||
## Eval/browser API traps (all hit live 2026-10-21)
|
||||
|
||||
- `browser.open({ url })` — options OBJECT; `browser.open(url)` is a TypeError.
|
||||
- `tab.wait(...)` does NOT exist — use `await Bun.sleep(ms)` or better `tab.waitForSelector/waitForText`.
|
||||
- `tab.run(fnOrString)` executes in the isolated Node-side runtime: `document` is undefined there. For page-world expressions use `tab.evaluate("...string...")` (expression, no top-level `return`; wrap a function IIFE when you need one) or the direct helpers (`tab.text(sel)`, `tab.count`, `tab.attr`).
|
||||
- **`log()` output from JS eval cells is silently swallowed** — even after successful browser calls. Extract data by `return`-ing the value from the cell (Python cells log fine; JS cells must return). A cell that "ran but printed nothing" usually means it printed via `log()` — re-run returning the payload.
|
||||
- Screenshots come back as webp images in the tool result AND save via `screenshot({path})`; image text under ~13px is unreliable to read — verify exact strings via `tab.text()` instead of squinting.
|
||||
|
||||
## Static anchor/claim audit before rendering
|
||||
|
||||
- Counts, schema/table names, endpoint bodies: re-grep the SOURCE per claim; never trust numbers carried in from conversation memory (this caught "public vs franksign schema", "OR signer_user_id join that doesn't exist", wrong test-counts pre-commit).
|
||||
- `git diff --stat` regenerated binary fixtures (PDF CreationDate churn) before committing docs — don't commit fixture noise the setup script wrote.
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
name: tts-synth-timeout-cap-run-triage
|
||||
description: "Triage a dankvoice \"The operation timed out.\" toast: prove/disprove a qwentts engine cap-run (2048-frame, no EOS) via Dankclaw journals before suspecting the client."
|
||||
---
|
||||
|
||||
# TTS synth-timeout triage
|
||||
|
||||
Symptom: omp shows `Error: dankvoice: The operation timed out.` — a real 60 s stall, not a client bug. `SYNTH_TIMEOUT_MS = 60_000` (`src/audio-io.ts`) + the notify at `utterance.ts:181` is the ONLY emitter of that text.
|
||||
|
||||
## Known root cause (chronic as of 2026-10)
|
||||
|
||||
The pinned `qwentts.cpp tts-server` (frank-tts-engine, Dankclaw, container port 8880) can miss EOS and run to its hard-coded 2048-frame cap: ~164 s of audio from a short sentence, ~90–100 s wall. Measured ~5/day, 68 in 14 days. The engine is a serial FIFO (`QWENTTS_MAX_BATCH=1`), so every narration queued behind a cap-run also times out — the toast comes in bursts. Shim logs `BrokenPipeError` at `wfile.write` (client gone when buffered audio finally ships). No frame-cap/deadline flag exists (`/app/tts-server --help`); the shim can't cancel (engine buffers before writing); `SHIM_TIMEOUT=600` deliberately outwaits it. **Fix belongs in `serverAppConfig` (watchdog restart or upstream feature request), never in dankvoice.**
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Endpoint is alive?** `curl -fsS -o /dev/null -w "%{http_code} %{time_total}s\n" -m 10 http://192.168.0.143:7772/v1/audio/voices` and time one synthesis with the session's actual voice (voice may be set per-directory in `~/.omp/cache/dankvoice.json` `byDir` — read that first).
|
||||
2. **Read the journals — from bash ssh, NOT the `ssh://dankclaw/` device.** The ssh:// device lands as a different user (blank) than the ssh config (dank@192.168.0.143) and sees no units, no sockets — false negatives everywhere:
|
||||
```bash
|
||||
ssh -o BatchMode=yes dankclaw 'journalctl --user -u frank-tts --since "-6h" --no-pager | grep -B1 -A20 BrokenPipeError | tail'
|
||||
ssh -o BatchMode=yes dankclaw 'journalctl --user -u frank-tts-engine --since "-6h" --no-pager | grep -E "Generation done : (2048|[89][0-9]{2}) frames"'
|
||||
```
|
||||
Signature = a facade `BrokenPipeError` whose timestamp is ~60 s after an engine `Admit slot=0`, and/or `Generation done : 2048 frames`. Window-correlate with the engine: `--since "HH:MM" --until "HH:MM"` and skip `[Sample]`/`Generated` noise.
|
||||
3. **Chronicity count:** `journalctl --user -u frank-tts-engine --since "-14 days" --no-pager | grep -c "2048 frames"`.
|
||||
4. **Verdict:** cap-run confirmed → the client behaved correctly (toast + next utterance speaks). Route the fix to `dank/serverAppConfigs` (`dankclaw/voiceservice/`): host-side watchdog on the journal signature restarting `frank-tts-engine.service`, and/or upstream per-request frame budget.
|
||||
|
||||
## Traps
|
||||
|
||||
- `systemctl --user` / `journalctl --user` on Dankclaw require the `dank` user — rootless quadlet units live there.
|
||||
- zsh on Dankclaw eats `===` separators and unquoted URLs as globs: quote args or avoid `echo ===`.
|
||||
- `tts-server` is not on the exec PATH: use `/app/tts-server`.
|
||||
- Roster may contain only cloned voices (narrator1-4 retired) — a 400 on a stale voice name is a separate bug; timeouts are the cap-run.
|
||||
- Elevated RTF right after idle (~2 min gap) is iGPU clock ramp, not contention.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
name: worktree-identical-file-tag-collision
|
||||
description: "Diagnose and repair edit-tool tag collisions when two git worktrees hold byte-identical copies of the same-named file, so edits land in the wrong checkout."
|
||||
---
|
||||
|
||||
## Symptom
|
||||
|
||||
You edit `path/to/file.ext` (bare or `~`-relative name) while another worktree under `~/.omp/wt/` holds a byte-identical copy of that file:
|
||||
|
||||
- the edit lands in the OTHER worktree (the most-recently-read copy), or
|
||||
- the tool rejects with "header needs this file's current tag #XXXX" even though you just read the target's #XXXX, or
|
||||
- after the edit, `grep -c "your marker"` in the intended worktree returns 0.
|
||||
|
||||
Root cause: the edit tool keys `[path#tag]` headers partly by hash; identical content ⇒ identical tag ⇒ ambiguous match, resolved to the freshest-read copy.
|
||||
|
||||
## Detection (before editing anything)
|
||||
|
||||
```bash
|
||||
grep -c "<distinctive marker you are about to add or just added>" <target-worktree>/<file> # 0 after a "successful" edit = collision
|
||||
```
|
||||
|
||||
Never trust the edit tool's success echo alone when sibling worktrees exist — verify the marker in the TARGET file.
|
||||
|
||||
## Repair (stray edit landed in the other worktree)
|
||||
|
||||
Restore that copy from its own HEAD — never hand-reverse:
|
||||
|
||||
```bash
|
||||
cd <other-worktree> && git show HEAD:<file> > <file>
|
||||
```
|
||||
|
||||
## Fix the collision, then re-edit
|
||||
|
||||
Give the two copies different hashes so tags stop colliding — cheapest is a scratch marker on the NON-target copy:
|
||||
|
||||
```bash
|
||||
cd <other-worktree> && echo "# <id> scratch copy" >> <file>
|
||||
```
|
||||
|
||||
Then re-issue the edit against the target with its original tag, and verify with `grep -c` again. (Editing the stray copy to a different content also works; anything that diverges the hashes.)
|
||||
|
||||
## Prevention
|
||||
|
||||
- When sibling worktrees exist, `read` the TARGET file by its worktree-scoped path immediately before the edit, and use that exact path in the header.
|
||||
- Prefer including a distinguishing context line in the edit anchor so a mis-landed edit is rejected instead of applied.
|
||||
- Same trap bites tests that copy fixtures (`$REPO_ROOT` resolves via the helpers file's location, not `$PWD`): red-proof a seam by copying the whole tests+fixture tree to a temp dir and running bats from there, or by testing the embedded command directly against a mutated fixture copy.
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792630061849
|
||||
"lastConsolidatedAt": 1792532762570
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792646195391,
|
||||
"lastConsolidatedAt": 1792439707623,
|
||||
"lastResult": {
|
||||
"at": 1791599865775,
|
||||
"sessions": 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792646220804
|
||||
"lastConsolidatedAt": 1792574106585
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792627210418
|
||||
"lastConsolidatedAt": 1792469747088
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
## Probe Network
|
||||
- DO NOT ALLOW PROBES TO LEAVE HOME NETWORK
|
||||
|
||||
## Device Access
|
||||
- AUTHORIZED TO ANY HOME NETWORK DEVICES BUT NO OTHERS
|
||||
- PRESERVE LOCAL API SETTINGS
|
||||
|
||||
## Change Policy
|
||||
- DO NOT ALTER OR MAKE CHANGES TO HOME NETWORK OR DEVICES
|
||||
|
||||
## Safety
|
||||
- DO NOT BREAK ANYTHING
|
||||
|
||||
## Recon Method
|
||||
- PURE DEEP RECON
|
||||
@@ -1,14 +1,14 @@
|
||||
## Agent Output Patterns
|
||||
|
||||
- Typed intro: "Update, [randomly generated epithet]!"
|
||||
- Heckle mode option
|
||||
- Typed outro: "More later, over and out!"
|
||||
- Generation frequency control
|
||||
|
||||
## Model Connectivity
|
||||
- CONNECT TO MODELS FROM OUTSIDE NETWORK
|
||||
|
||||
## Documentation Flow
|
||||
- Put any created docs in the existing knowledgebase
|
||||
|
||||
- Put any created docs in the existing knowledgebase instead of creating a new one
|
||||
|
||||
## Log Organization
|
||||
|
||||
- Logs kept explicitly to one file inside the knowledge base
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792627495365,
|
||||
"lastConsolidatedAt": 1792608451914,
|
||||
"lastResult": {
|
||||
"at": 1792626257549,
|
||||
"at": 1792573818108,
|
||||
"sessions": 1,
|
||||
"deltas": 2,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
## Okf Format
|
||||
- "final HTML report"
|
||||
|
||||
- "okf format"
|
||||
|
||||
## Human Readability
|
||||
|
||||
- "human readability"
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792632011734,
|
||||
"lastConsolidatedAt": 1792608445881,
|
||||
"lastResult": {
|
||||
"at": 1792625888033,
|
||||
"at": 1792574016725,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"deltas": 2,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792627453281,
|
||||
"lastConsolidatedAt": 1792608476331,
|
||||
"lastResult": {
|
||||
"at": 1792609389951,
|
||||
"at": 1792607267681,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"deltas": 7,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
## Documentation Guides
|
||||
- Step-by-step guide with platform screenshots for all user flows
|
||||
@@ -3,9 +3,5 @@
|
||||
- Compaction applied
|
||||
- The suite shouldn't loop through every epithet
|
||||
|
||||
## Operation flow
|
||||
- The operation timed out
|
||||
|
||||
## Testing behavior
|
||||
- Tests play ONE random Epithet
|
||||
- Tests must not depend on the external API
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792627572197,
|
||||
"lastConsolidatedAt": 1792608310239,
|
||||
"lastResult": {
|
||||
"at": 1792622122061,
|
||||
"at": 1792607323355,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792627428845
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792631623051
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792646196892
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792673523923
|
||||
}
|
||||
@@ -306,135 +306,3 @@ providers:
|
||||
name: "frank-tts (dankclaw) — cloned voices"
|
||||
api: openai-speech
|
||||
input: [text]
|
||||
|
||||
# ─── WAN door ───────────────────────────────────────────────────────────
|
||||
# The only block in this file that answers OFF the home network. Every
|
||||
# provider above is a LAN IP, and `modelRoles` (config.yml) still points at
|
||||
# those, so away from home you must NAME one of the rows below:
|
||||
# omp --model 'openwebui (franksdank.cloud)/frank-dragoon:medium'
|
||||
# The door is the OpenWebUI vhost `ai.franksdank.cloud` — Caddy → OWU on
|
||||
# Dankclaw:3005, Anubis-exempt because OWU enforces its own auth, public A
|
||||
# record present (LAN clients resolve 192.168.0.143 via Pi-hole, measured
|
||||
# 2026-10-21). It exposes an OpenAI-compatible surface at /api/v1 and
|
||||
# proxies to the SAME engines as the LAN rows — no second inference stack,
|
||||
# so every engine-side law below still holds through it.
|
||||
# CREDENTIAL LAW: the key is not in this file. `apiKey` accepts a
|
||||
# command-backed value (`!cmd`, resolved by src/config/resolve-config-value.ts:
|
||||
# stdout trimmed, cached for the process lifetime; anything else is tried as
|
||||
# an env-var name, then taken literally), so the secret lives in
|
||||
# ~/.config/secrets/openwebui_api_key, mode 600, like every other credential
|
||||
# on these boxes. A literal `sk-…` here would trip the pre-commit shape gate
|
||||
# and then live in every agent context on every box that pulls this repo.
|
||||
# Rotate: rewrite the secret file, then RESTART omp — the `!cmd` result is
|
||||
# cached per process, so a live session keeps serving the old key.
|
||||
# WHAT THE DOOR SHOWS (probed 2026-10-21 with this key): /api/v1/models
|
||||
# answers frank-dragoon and frank-zealot only. frank-archon is NOT published
|
||||
# in OpenWebUI's picker and the API answers 400 "Model not found" for it, so
|
||||
# the fleet's biggest endpoint has no WAN path until it is published
|
||||
# server-side on Dankclaw — a fleet change, not a harness change.
|
||||
# CONTRACT PROBED THROUGH THE DOOR (2026-10-21): streaming SSE carries
|
||||
# `reasoning_content` deltas and `tool_calls` deltas, finish_reason
|
||||
# `tool_calls`, and a final `usage` under stream_options.include_usage;
|
||||
# `image_url` content parts reach both vision towers (a 64 px red PNG comes
|
||||
# back "Red" from each); and `chat_template_kwargs` IS forwarded — proven on
|
||||
# frank-zealot, where enable_thinking false gives reasoning 0 chars and true
|
||||
# gives a real thinking block on the same prompt. Every compat block below is
|
||||
# therefore a copy of its LAN twin, never a simplification: dropping the
|
||||
# kwargs here would quietly change thinking behaviour on the WAN path.
|
||||
# Chat completions made with an API key create NO OpenWebUI chat row (the
|
||||
# chat list is unchanged after probing), so an off-network session leaves no
|
||||
# trail in the family's chat history.
|
||||
# NOT BEHIND THIS DOOR: speech. OWU does route /api/v1/audio/speech and it
|
||||
# does reach frank-tts:7772, but OWU substitutes its own voice enum first —
|
||||
# a cloned pack name comes back 400 from the upstream. `modelRoles.speech`
|
||||
# stays on `frank-tts (dankclaw)`; do not repoint it here.
|
||||
"openwebui (franksdank.cloud)":
|
||||
baseUrl: https://ai.franksdank.cloud/api/v1
|
||||
api: openai-completions
|
||||
# Quoted because an unquoted leading `!` is a YAML tag, not a command.
|
||||
apiKey: "!cat ~/.config/secrets/openwebui_api_key"
|
||||
models:
|
||||
- id: frank-dragoon
|
||||
# Same halogen flash-server as `llamacpp (dankclaw)` above, one hop
|
||||
# further out; the picker row and the engine share this id, so there
|
||||
# is no alias to keep in sync.
|
||||
name: "frank-dragoon (WAN)"
|
||||
# Off-network the compaction hub must be reachable too: the LAN
|
||||
# frank-zealot row is a dead address from anywhere but the house.
|
||||
# Same engine, same slot arithmetic, so the keepRecent/threshold
|
||||
# math in the LAN rows transfers untouched.
|
||||
compactionModel: openwebui (franksdank.cloud)/frank-zealot
|
||||
# Inherited from the engine, not invented: 200,000 under the native
|
||||
# 262,144 ceiling (the 400-zone law — never sit at the ceiling). The
|
||||
# proxy neither widens the window nor truncates: an over-ceiling
|
||||
# request is still a 400.
|
||||
contextWindow: 200000
|
||||
# Same clamp arithmetic as the LAN row (see frank_dragoon_unit.bats);
|
||||
# HALOGEN_MAX_TOKENS_CAP on the engine is unchanged.
|
||||
maxTokens: 45056
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
compat:
|
||||
# Verified through the door: reasoning arrives as
|
||||
# `reasoning_content` in the deltas, not folded into content.
|
||||
reasoningContentField: "reasoning_content"
|
||||
# `:off` must be a real switch, and it is: this engine honours
|
||||
# reasoning_effort "none"; the harness default would only send the
|
||||
# lowest effort, which still thinks.
|
||||
reasoningDisableMode: none-effort
|
||||
# Identity map, same reason as the LAN row — flash-server lists
|
||||
# minimal|low|medium|high|xhigh|max|none and 400s anything else.
|
||||
reasoningEffortMap:
|
||||
minimal: minimal
|
||||
low: low
|
||||
medium: medium
|
||||
high: high
|
||||
xhigh: xhigh
|
||||
max: max
|
||||
# Pinned so a server-side HALOGEN_TOP_K edit cannot change this
|
||||
# session's recipe; the engine accepts all three keys.
|
||||
extraBody:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 20
|
||||
- id: frank-zealot
|
||||
# The 3090 endpoint, same llama.cpp engine behind the proxy. Stays
|
||||
# the widest slot in the file, so it is the WAN compaction target.
|
||||
name: "frank-zealot (WAN)"
|
||||
compactionModel: openwebui (franksdank.cloud)/frank-zealot
|
||||
# PER-SLOT window: llama.cpp divides --ctx-size across slots and
|
||||
# answers a longer prompt with a 400, never a truncation. Follows
|
||||
# every reslice on the engine (3 × 200,704 today).
|
||||
contextWindow: 200704
|
||||
# Prompt + thinking budget inside the slot; compaction fires at 85 %
|
||||
# of the window on this row set (see the LAN twin for the full
|
||||
# worst-case arithmetic).
|
||||
maxTokens: 16384
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
compat:
|
||||
reasoningContentField: "reasoning_content"
|
||||
# LOAD-BEARING, exactly as on the LAN row: the Bonsai-2 template
|
||||
# accepts ONLY xhigh|medium|low and raises on anything else, so an
|
||||
# unmapped harness level is a hard failure on the role's first
|
||||
# request. Probed 2026-10-21 that the mapped value, not the
|
||||
# harness-level, is what reaches the template through this proxy.
|
||||
reasoningEffortMap:
|
||||
minimal: low
|
||||
low: low
|
||||
medium: medium
|
||||
high: xhigh
|
||||
max: xhigh
|
||||
# The payload's own sampling recipe plus the thinking switches.
|
||||
# chat_template_kwargs survives the proxy (verified), so
|
||||
# preserve_thinking keeps prior assistants' thinking blocks in
|
||||
# history for multi-turn tool traffic — same contract as at home.
|
||||
extraBody:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
presence_penalty: 0.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
preserve_thinking: true
|
||||
@@ -1,4 +1,4 @@
|
||||
config_version = 15
|
||||
config_version = 14
|
||||
|
||||
[audio]
|
||||
enable_overdrive = true
|
||||
@@ -275,13 +275,13 @@ transition = [ "fade" ]
|
||||
interval_seconds = 1200
|
||||
|
||||
[wallpaper.default]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
|
||||
[wallpaper.last]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-2]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
@@ -290,7 +290,7 @@ transition = [ "fade" ]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
|
||||
|
||||
[wallpaper.monitors.eDP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
|
||||
[weather]
|
||||
unit = "imperial"
|
||||
|
||||
@@ -15,6 +15,5 @@ These files define the structural conventions that govern the agent system itsel
|
||||
* [dankvoice](dankvoice.md) — Voice output is the `dank/dankvoice` package (`pi install git:`); this page keeps fleet wiring and the cutover procedure.
|
||||
* [OMP Status Line Customization](statusline.md) — How presets, composer shapes, and segment arrays actually interact; the fleet's tuned bar. (moved from /pi/, 2026-10-19)
|
||||
* [OMP Subagent Limits & Fan-out Policy](subagent-limits.md) — Live caps: concurrency 2, no nested spawns (the `maxRecursionDepth: 0` trap), per-endpoint in-flight ceilings, isolation policy. (moved from /pi/, 2026-10-19)
|
||||
* [WAN Model Access](wan-model-access.md) — Driving the fleet's models from outside the home network through the OpenWebUI door: the command, the key file and its rotation law, what the door publishes, and the off-network failure modes.
|
||||
|
||||
**Change log:** the bundle keeps exactly one, one level up — [Bundle Change Log](../log.md). A directory here never has its own.
|
||||
@@ -1,139 +0,0 @@
|
||||
---
|
||||
type: reference
|
||||
title: WAN Model Access (ai.franksdank.cloud)
|
||||
description: How to drive the fleet's models from outside the home network — the OpenWebUI door in agent/models.yml, the key file and its rotation, what the door does and does not publish, and the off-network failure modes.
|
||||
tags:
|
||||
- omp
|
||||
- models-yml
|
||||
- openwebui
|
||||
- remote-access
|
||||
- fleet
|
||||
verified: 2026-10-21
|
||||
timestamp: 2026-10-21
|
||||
---
|
||||
|
||||
# WAN Model Access (ai.franksdank.cloud)
|
||||
|
||||
Every provider in `agent/models.yml` except one is a LAN address, so a laptop
|
||||
that leaves the house loses the fleet. The exception is the
|
||||
`openwebui (franksdank.cloud)` block: the fleet's already-public OpenWebUI vhost
|
||||
speaks OpenAI-compatible HTTP at `/api/v1` and proxies to the same engines the
|
||||
LAN rows point at. One hop further out, no second inference stack.
|
||||
|
||||
Read this page when you are off-network and a model call fails, when the key
|
||||
needs rotating, or before adding another row behind the door.
|
||||
|
||||
## Run a session away from home
|
||||
|
||||
The door is added as a provider, never as a role default. `modelRoles` in
|
||||
`agent/config.yml` still names LAN providers, so away from home you must name
|
||||
the model explicitly:
|
||||
|
||||
```bash
|
||||
omp --model 'openwebui (franksdank.cloud)/frank-dragoon:medium'
|
||||
omp --model 'openwebui (franksdank.cloud)/frank-zealot:off'
|
||||
```
|
||||
|
||||
The same two ids work in the `/model` picker inside a running session — the
|
||||
provider label is what disambiguates them from the LAN rows, since the wire ids
|
||||
are identical by design (OpenWebUI's picker row and the engine share the id, so
|
||||
there is no alias to keep in sync).
|
||||
|
||||
## The key
|
||||
|
||||
`apiKey: "!cat ~/.config/secrets/openwebui_api_key"` — a **command-backed**
|
||||
value, resolved by `src/config/resolve-config-value.ts` in the harness: stdout
|
||||
is trimmed, an env-var name is tried next, and only then is the string taken
|
||||
literally. The secret itself is a 600-mode file in `~/.config/secrets/`, the
|
||||
same convention as `gitea` and `kagi_api_key`.
|
||||
|
||||
Laws:
|
||||
|
||||
- **Never inline the `sk-…`.** It is a tracked file; the pre-commit shape gate
|
||||
blocks `sk-…` anywhere in the staged diff, and a key that slipped through
|
||||
would live in every agent's context on every box that pulls this repo.
|
||||
- **Rotation is: rewrite the secret file, then restart omp.** The `!cmd` result
|
||||
is cached for the process lifetime, so a long-lived session keeps serving the
|
||||
old key — a stale-session failure reads exactly like a revoked key.
|
||||
- The key is an **OpenWebUI** key (User menu → Settings → Account → API keys),
|
||||
not a fleet endpoint key. It carries that user's model ACL, so what the door
|
||||
shows is whatever the picker shows that user.
|
||||
- Every other workstation needs its own copy of the file; nothing in the repo
|
||||
carries the value.
|
||||
|
||||
## What the door publishes
|
||||
|
||||
Probed 2026-10-21 with the fleet key.
|
||||
|
||||
|Behind the door|Notes|
|
||||
|---|---|
|
||||
|`frank-dragoon`|halogen flash-server, Dankclaw iGPU — same row as the LAN `llamacpp (dankclaw)`|
|
||||
|`frank-zealot`|llama.cpp on Dankclaw's 3090 — same row as `llamacpp (dankclaw-3090)`; also the WAN compaction target|
|
||||
|`frank-archon`|**not published.** `/api/v1/models` omits it and the API answers `400 "Model not found"`. Dankserver's endpoint has no public face at all. Giving it a WAN path is a fleet change on Dankclaw (publish it in the OpenWebUI picker), not a harness change|
|
||||
|`frank-tts`|route exists, contract does not. OpenWebUI forwards `/api/v1/audio/speech` to `frank-tts:7772` but substitutes its own voice enum first, and a cloned-pack name comes back `400` from the upstream. `modelRoles.speech` stays on the LAN `frank-tts (dankclaw)` row|
|
||||
|
||||
## Contract verified through the proxy
|
||||
|
||||
Each of these was measured end-to-end on 2026-10-21 — they are why the compat
|
||||
blocks in the WAN rows are copies of their LAN twins rather than simplifications:
|
||||
|
||||
- Streaming SSE carries `reasoning_content` **and** `tool_calls` deltas,
|
||||
`finish_reason: tool_calls`, and a final `usage` under
|
||||
`stream_options.include_usage`.
|
||||
- `image_url` content parts reach both vision towers.
|
||||
- `chat_template_kwargs` is forwarded — proven on `frank-zealot`, where
|
||||
`enable_thinking: false` yields zero reasoning and `true` yields a real
|
||||
thinking block on the same prompt. Drop the kwargs from a WAN row and you
|
||||
silently change thinking behaviour on the WAN path.
|
||||
- API-key completions create **no** OpenWebUI chat row: the chat list is
|
||||
unchanged after a probe, so an off-network session leaves nothing in the
|
||||
family's chat history.
|
||||
|
||||
Context windows, `maxTokens`, sampling and effort maps are inherited from the
|
||||
engines and documented on the LAN rows and in the fleet vault
|
||||
(`serverAppConfigs-docs/LLM/`). The proxy neither widens a window nor truncates:
|
||||
an over-ceiling request is still a `400`.
|
||||
|
||||
## Off-network failure modes
|
||||
|
||||
- **`Connection refused` / long timeout on a LAN id.** Not a service outage —
|
||||
you are not home. Switch to the WAN id.
|
||||
- **`Model not found` for `frank-archon`.** Expected; see the table above.
|
||||
- **Compaction wants a LAN address.** Each WAN row sets `compactionModel` to
|
||||
the WAN `frank-zealot`, but `compaction.remoteEndpoint` in `agent/config.yml`
|
||||
is a LAN URL. It is last in `methodOrder`, so the other methods run first and
|
||||
the remote attempt just fails — leave it unless off-network compaction turns
|
||||
out to need it.
|
||||
- **Roles still resolve to LAN.** A full off-network session that must not
|
||||
depend on hand-picked models needs `retry.fallbackChains` entries pointing
|
||||
the LAN defaults at the WAN rows; that is a deliberate `config.yml` change,
|
||||
not part of adding rows here.
|
||||
|
||||
## Verify the door
|
||||
|
||||
```bash
|
||||
# Key + model list (LAN clients resolve 192.168.0.143 via Pi-hole; the WAN
|
||||
# A record is what makes this reachable from elsewhere).
|
||||
curl -s https://ai.franksdank.cloud/api/v1/models \
|
||||
-H "Authorization: Bearer $(cat ~/.config/secrets/openwebui_api_key)" | jq -r '.data[].id'
|
||||
|
||||
# Registry loaded, WAN rows present, no validation failure (a failed load
|
||||
# silently disables EVERY custom provider — see
|
||||
# agent/models.yml's schema-law comment):
|
||||
omp models --no-extensions | grep -E 'franksdank|validation failed'
|
||||
```
|
||||
|
||||
A scratch-HOME run is the only real proof that a harness change reaches the
|
||||
door: copy `models.yml` + `config.yml` to `$TMP/.omp/agent/`, symlink
|
||||
`$TMP/.config/secrets` at the real one (a `~` in a `!cmd` value expands to
|
||||
`$HOME`), rewrite the LAN baseUrls to a dead port so a success cannot be
|
||||
faked, then `HOME=$TMP omp --model 'openwebui (franksdank.cloud)/…' -p …`.
|
||||
Delete the scratch tree afterwards — it holds endpoint URLs.
|
||||
|
||||
## Pointers
|
||||
|
||||
- Door topology: [Caddy](/caddy/index.md), [System hosts](/system/index.md).
|
||||
- Model registry law and per-provider limits: [Core config](index.md),
|
||||
[OMP Subagent Limits](subagent-limits.md).
|
||||
- Server-side record: `serverAppConfigs-docs/Dankclaw/OpenWebUI.md`
|
||||
(that repo's vault, not this bundle).
|
||||
+5
-19
@@ -24,24 +24,6 @@ long form.
|
||||
|
||||
## 2026-10-21
|
||||
|
||||
### The WAN door into the model registry
|
||||
|
||||
`openwebui (franksdank.cloud)` joins `agent/models.yml` — the already-public
|
||||
OpenWebUI vhost as an OpenAI-compatible provider, so a workstation outside the
|
||||
house still reaches `frank-dragoon` and `frank-zealot` while every LAN row stays
|
||||
untouched. The credential is command-backed
|
||||
(`apiKey: "!cat ~/.config/secrets/openwebui_api_key"`, 600-mode file): a literal
|
||||
`sk-…` in a tracked file is a key in every agent context on every box that pulls.
|
||||
|
||||
Laws: **OpenWebUI forwards `chat_template_kwargs`**, so a proxied row copies its
|
||||
LAN twin's compat block instead of simplifying it — proved on `frank-zealot`,
|
||||
`enable_thinking` false giving zero reasoning and true a real block on one
|
||||
prompt; **the picker IS the API's model list**, `frank-archon` answers `400
|
||||
"Model not found"` because it was never published, so a WAN path for it is a
|
||||
fleet change on Dankclaw and not a harness one; and **a `!cmd` value is cached
|
||||
for the process lifetime**, so rotating a key without restarting omp leaves the
|
||||
old key working. Page: [core/wan-model-access.md](core/wan-model-access.md).
|
||||
|
||||
### the audit grows an advisory table, and corrects three of its own numbers
|
||||
|
||||
`omp-docs/reports/fleet-security-audit-2026-10-21.html` folds in the two per-host vulnerability
|
||||
@@ -57,7 +39,11 @@ the OpenSSH advisory count (0 → **5** unpatched CVEs; the first query omitted
|
||||
|
||||
Laws: **an empty OSV response is not a clean bill** — verify the query shape against the distro
|
||||
tracker before writing "0 advisories"; and **a version quoted from a subagent gets re-probed
|
||||
before it enters a report**, this being the second mis-attribution the audit caught.
|
||||
before it enters a report**, this being the second mis-attribution the audit caught. Merged as PR
|
||||
[dank/command-center#66](https://gitea.franksdank.cloud/dank/command-center/pulls/66); the remediation
|
||||
clusters are filed as issues
|
||||
[dank/command-center#67](https://gitea.franksdank.cloud/dank/command-center/issues/67)–#77, listed in
|
||||
[reports/index.md](reports/index.md).
|
||||
|
||||
### reviewing a day of work, and finding checks that lie
|
||||
|
||||
|
||||
@@ -10,3 +10,16 @@ pages are the working data they were built from. The bundle-wide work log stays
|
||||
| [vuln-dankclaw.md](vuln-dankclaw.md) | reference | CVE/GHSA pass for the 33 Dankclaw quadlet images and the Ubuntu 26.04 host — the evidence base the audit's Dankclaw rows rest on. |
|
||||
| [vuln-dankserver.md](vuln-dankserver.md) | reference | CVE/GHSA pass for Dankserver (containers + host), blankpad's Arch package set, and danklivingroom's OpenSSH. |
|
||||
| [sleep-wake-investigation-report.html](sleep-wake-investigation-report.html) | report | blankpad suspend/resume investigation, 2026-09-28. |
|
||||
|
||||
Open remediation work is tracked as Gitea issues, one per host-and-package cluster:
|
||||
[#67](https://gitea.franksdank.cloud/dank/command-center/issues/67) Ubuntu OpenSSH on both servers ·
|
||||
[#68](https://gitea.franksdank.cloud/dank/command-center/issues/68) gnome-remote-desktop + rpcbind ·
|
||||
[#69](https://gitea.franksdank.cloud/dank/command-center/issues/69) SurrealDB ·
|
||||
[#70](https://gitea.franksdank.cloud/dank/command-center/issues/70) Alpine image bases ·
|
||||
[#71](https://gitea.franksdank.cloud/dank/command-center/issues/71) Pi-hole ·
|
||||
[#72](https://gitea.franksdank.cloud/dank/command-center/issues/72) Valkey ·
|
||||
[#73](https://gitea.franksdank.cloud/dank/command-center/issues/73) copyparty ·
|
||||
[#74](https://gitea.franksdank.cloud/dank/command-center/issues/74) redis ·
|
||||
[#75](https://gitea.franksdank.cloud/dank/command-center/issues/75) radiance-vllm ·
|
||||
[#76](https://gitea.franksdank.cloud/dank/command-center/issues/76) danklivingroom SSH ·
|
||||
[#77](https://gitea.franksdank.cloud/dank/command-center/issues/77) blankpad unfixed AVGs.
|
||||
Reference in new issue
Block a user