Compare commits

...
Author SHA1 Message Date
Frank fcc7226c3b chore(skills): drop Eve-only agents/openai.yaml from authored grilling
omp's skill scanner ignores agents/*.yaml (Eve/Codex binding metadata
carried along by the skills CLI store). The file kept vanishing from the
live tree to a background sweep; an authored omp skill does not need it.
Refs #16
2026-10-18 18:31:37 -07:00
dank a8011c3d4d Merge pull request 'chore(skills): symlink agent/skills into the skills CLI global store' (#16) from chore/skills-store-symlinks into main 2026-10-18 18:30:16 -07:00
Frank 08f90c6df3 chore(skills): move upstream skills to the CLI global store, keep authored tier
Retroactive provenance: the 10 pi-era skill snapshots were byte-identical
to upstream. Canonical installs now live in ~/.agents/skills tracked by
~/.agents/.skill-lock.json (mattpocock/skills x38 after grilling removal,
find-skills from vercel-labs/skills).

Repo tier is authored overrides only: clear all 10 tracked upstream
snapshots; omp loads the store directly through the agents provider
(skills.enableAgentsUser, default on). grilling returns as a real
directory — extracted from the store (CLI + lockfile pruned) so Frank can
customize it without skills update resurrecting a twin.

Docs: skill-store-wiring.md (+index). Pre-cutover snapshot of the old
tracked dirs at /tmp/agent-skills-snap-20261018.
2026-10-18 18:28:34 -07:00
Frank 57e255f930 live: heartbeat sync 2026-10-18T17:49:00-07:00 (Refs #11) 2026-10-18 17:49:00 -07:00
Frank b3d3df4bdb feat(niri): amber-orange focus-ring, width 4 (ff8c00)
Frank's outline restyle: focus-ring red #e01b24 -> #ffb000 -> #ff8c00
(orange), thickness 2 -> 4 in base layout.kdl. The visible ring is
painted by noctalia.kdl, included LAST in config.kdl — its focus-ring
active/urgent colors moved to #ff8c00 too; tab-indicator, insert-hint,
and recent-window highlights deliberately stay red.

noctalia.kdl was hand-written live-only (Oct 14, no repo copy — the
override that silently beat every layout.kdl edit); first durable copy
lands in the zz-blankpad mirror.
2026-10-18 17:37:27 -07:00
dank 5d523f6e9f Merge pull request 'docs: noctalia bar clobber postmortem (log entry)' (#15) from wt/20261018-170616 into main 2026-10-18 17:30:58 -07:00
Frank b353fa9075 revert(live-sync): drop path-activation unit
Frank veto: DirectoryNotEmpty re-fires on every file event (GUI writes,
wallpaper shuffle) un-debounced -> hundreds of heartbeat spawns/day.
The sweep is cheap (57 ms CPU) but the trigger design was event spam.
Exposure-window fix is an open design question; timer unchanged.
2026-10-18 17:23:58 -07:00
Frank c300f8b49c feat(live-sync): path-activate heartbeat on dotfiles writes (Refs #11)
Timer-only heartbeat left GUI dotfiles edits uncommitted for up to 59
minutes; a trunk-sync session clearing the dirty tree on 2026-10-18
destroyed a month of uncommitted noctalia bar layout inside that gap.
live-sync.path (DirectoryNotEmpty on dotfiles/) fires the existing
flock-guarded oneshot within seconds of any repo-side dotfiles write,
shrinking the exposure window from ~1h to ~2s. Timer stays as the
hourly catch-all for the machine tiers.
2026-10-18 17:20:17 -07:00
Frank bde10f8948 docs: work-log entry for noctalia bar clobber + dangling-stash recovery
Documents the 16:46 layout loss (uncommitted GUI state stashed, stash
entry destroyed during trunk sync) and the git-fsck recovery landed in
c87b378. Lesson codified in managed skills.
2026-10-18 17:16:49 -07:00
Frank c87b378475 fix(noctalia): restore bar layout clobbered by 16:46 trunk-sync stash
The rich GUI-built layout (cat/clock/clock-utc/weather start, taskbar
center, capsule groups g1-g4 + control-cheat-sheet end, thickness 36,
font_scale 1.3, opacity 0.9) existed only in the working tree; it was
stashed at 16:46 during the trunk merge and the stash entry was later
destroyed. Recovered from dangling stash commit 7fe3ed8 via git fsck.
This commit is the durable copy the layout never had before.
2026-10-18 17:12:20 -07:00
Frank 979e9ef711 live: heartbeat sync 2026-10-18T16:47:59-07:00 (Refs #11) 2026-10-18 16:47:59 -07:00
Frank a550db6027 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	AGENTS.md
#	agent/config.yml
#	omp-docs/log.md
2026-10-18 16:47:37 -07:00
Frank 3713339ffc chore: adopt pre-existing working-tree state before trunk merge
Uncommitted machine/session edits (live config, models, docs drift) were
blocking the merge of origin/main. Committed as-is to preserve them; the
merge that follows resolves overlapping prose files toward upstream
canonical, with local variants recoverable from this commit. Pre-merge
copies also in .AGENTTRASH/pre-merge-2026-10-18/.
2026-10-18 16:46:57 -07:00
dank 5a3d563a22 Merge pull request 'docs: attribution footer for machine-opened issues and PRs' (#14) from docs/session-trailer-rule into main 2026-10-18 16:46:00 -07:00
Frank 942787f0f2 docs: pin cwd meaning in attribution footer
First dogfooding round stamped the build worktree instead of the session's
own working directory (the value the paired session id resolves to). Rule
now says so explicitly. Refs #11
2026-10-18 16:40:14 -07:00
Frank 8fb357b483 docs: attribution footer for machine-opened issues and PRs
Every agent-opened issue/PR body ends with cwd + omp session id so machine
filed artifacts trace back to the box and session that produced them - the
live-sync daemon and filing agents make 'who/what wrote this' a real
question now. Mirrored in dank/serverAppConfigs by its own PR.

Refs #11
2026-10-18 16:32:48 -07:00
dank 72bcbd989e Merge pull request 'chore: re-sync agent skills to upstream mattpocock/skills' (#13) from chore/skills-upstream-sync into main 2026-10-18 16:30:02 -07:00
dank 964bd06073 Merge pull request 'feat: live-sync heartbeat for machine-written paths' (#12) from feat/11-live-sync into main 2026-10-18 16:28:57 -07:00
Frank 54f5555470 chore: run live-sync heartbeat hourly
Operator decision at review: 15 min is aggressive for config churn; hourly
keeps the stale-window acceptable (<=1 h) with a fifth of the wakeups.
Timer unit, tier law, and failure-table wording aligned.
2026-10-18 16:28:50 -07:00
Frank 0edae3323d chore: re-sync agent skills to upstream mattpocock/skills
Local snapshots dated 2026-09-16; upstream has moved on since. Refreshes
7 SKILL.md files (code-review, diagnosing-bugs, implement, tdd, grilling,
handoff, teach): tracker-doc references generalized, CONTEXT.md renamed
GLOSSARY.md, implement gains ticket-fetch step + Skill-tool invocation,
grilling question wording rule, handoff names the OS temp dir, teach
workspace-path resolution + quiz answer-position rules, tdd seam notes.
find-skills (vercel-labs) and prototype already current. Skill updates
take effect next session (discovery runs at startup).
2026-10-18 16:14:29 -07:00
Frank e52aa6a4a3 feat: live-sync heartbeat commits machine-written paths
The checkout IS the deployment and symlinks make live state (memories,
auto-learned skills, agent config, desktop settings) dirty tracked files
continuously; no commit law covered machine churn, so it accumulated
unpushed for weeks — one disk held the only copy of ~61 managed skills,
a settings fix stayed local-only, and dirty trees refused merges.

scripts/live-sync.sh (systemd user timer, ~15 min) commits ONLY the
allowlisted machine/desktop-live tiers with hooks armed, merges
origin/main, pushes, and reaps wt/* worktrees older than 7 days. Task
dirt outside the allowlist is never staged; conflicts abort and alert
via ntfy; branch guard refuses off main; never --no-verify, never force.
Black-box contract pinned by tests/live-sync.bats (9 tests, suite 36/36).

Refs #11
2026-10-18 16:13:59 -07:00
dank d8647ef5cd Merge pull request 'Docs: filed PRs open in Frank's browser immediately' (#10) from docs/pr-browser-rule into main 2026-10-18 15:43:26 -07:00
Frank 7a0062e9e8 Docs: filed PRs must be opened in Frank's browser immediately
Trunk-based SDLC gains the delivery habit: xdg-open the PR URL the moment
it's filed (mirrors dank/serverAppConfigs step 6, riding their PR #6).
Changelog entry added.
2026-10-18 15:41:27 -07:00
Frank 8702f3058d chore: re-sync agent skills to upstream mattpocock/skills
Local snapshots dated 2026-09-16; upstream has moved on since. Refreshes
7 SKILL.md files (code-review, diagnosing-bugs, implement, tdd, grilling,
handoff, teach): tracker-doc references generalized, CONTEXT.md renamed
GLOSSARY.md, implement gains ticket-fetch step + Skill-tool invocation,
grilling question wording rule, handoff names the OS temp dir, teach
workspace-path resolution + quiz answer-position rules, tdd seam notes.
find-skills (vercel-labs) and prototype already current. Skill updates
take effect next session (discovery runs at startup).
2026-10-18 14:57:20 -07:00
dank 534b221a45 Merge pull request 'Chore: switch SDLC to trunk-based; retire develop' (#8) from chore/trunk-based-sdlc into main 2026-10-18 13:19:38 -07:00
Frank 8089693e58 Chore: switch SDLC to trunk-based; retire develop
Single-dev repo: develop bought nothing and cost a back-merge every
release. New law: branch off freshest origin/main per work item, PRs
target main directly, --no-ff merges kept, tags vX.Y.Z straight on main.
No release branches, no back-merges. Changelog entry added.
2026-10-18 13:18:31 -07:00
dank e5d00fd91f Merge pull request 'Docs: full truth audit - statusline page + system/gitea pages realigned' (#7) from chore/docs-statusline-audit into main 2026-10-18 13:10:48 -07:00
dank 59a89553b7 Merge pull request 'Task: raise subagent maxConcurrency 2 -> 3' (#6) from chore/max-concurrency-3 into main 2026-10-18 13:10:23 -07:00
Frank ae82aa00e4 Docs: system pages realigned to live ground truth; sibling-vault overlap census
Census against serverAppConfigs-docs + read-only ssh probes on both boxes
(2026-10-18). Corrections to OURS, flags only for THEIRS:

- Hardware drift: RTX 3090 moved dankserver->dankclaw 2026-09; dankserver
  now 2x Radeon AI PRO R9700 (Ryzen 7 7700X stays), 246 GiB RAM, no swap;
  dankclaw 121 GiB RAM + 64 GiB swap. Served: frank-archon radiance TP=2
  (:7770 dankserver), frank-dragoon halogen NATIVE v2 (:7770 dankclaw
  iGPU), frank-zealot llama.cpp PrismML Ternary-Bonsai-2-27B PQ2_0
  (:7771 dankclaw, 3090). Kernel 7.0.0-38 both; dankclaw :7770 no auth.
- Gitea: gitea-caddy quadlet retired (fleet Caddy fronts the domain);
  data tree moved to /home/dank/Containers/gitea/ (2026-09-01 standard),
  /opt/gitea is a do-not-use leftover; LAN IP + live domain.
- pi-era pages (core/settings, pi/models + role tables) demarcated as
  ARCHIVE with banners; llama-cpp fleet note modernized.
- AGENTS.md machine-facts table corrected (it still routed prod warnings
  at frank-default-large).

Their-side staleness flagged in the changelog entry for the operator,
not edited: OMP Harness thresholdPercent 70 (live is 95), Ubuntu 24.04
OS table, 2026-09-03 stack mapping, superseded pi-KB backlink.

Refs #5
2026-10-18 03:39:56 -07:00
Frank 72a3dbbe08 Docs: status line page truth-audited against bundle; four claims corrected
Full-source re-verification of pi/statusline.md against dist/cli.js plus a
live screenshot matrix. False and corrected: claude's top rule is NOT the
context meter (meter is box/band only; rule elsewhere is a session-accent
filler); 'Tern-only' composer-bar claim (real knob: composer.tokenRate);
segmentOptions gated to preset custom (they merge under every preset - only
the arrays are gated); mode segment yolo (states are Plan/Prewalk/Goal/
Vibe/Loop). Expanded: 7-preset defaults table, per-shape statusAttachment/
bottomBar model incl. plugin shape registry, contextLine per-value behavior,
27-segment catalog with triggers and options, segmentOptions schema, full
statusLine.*/composer.* knob registry. Changelog entry added.

Refs #5
2026-10-18 02:30:39 -07:00
Frank 43c19ae74c Task: raise subagent maxConcurrency 2 -> 3
Operator request 2026-10-18: one more concurrent subagent slot.
2026-10-18 01:35:26 -07:00
Frank a2b1b6d9c6 back-merge: main -> develop after PR #5 (status line custom segments) 2026-10-18 01:10:08 -07:00
dank 876d8cd519 Merge pull request 'Status line: custom segments for fleet bars; slim frank-archon label' (#5) from chore/statusline-custom into main 2026-10-18 01:09:59 -07:00
Frank c2d855b33d Status line: move model segment to the right cluster (model, cost, time)
Left group keeps the where-am-I identity (host, path, git, pr); the right
cluster carries what's-running (model) plus cost and elapsed time, painted
in array order. context_pct verified to hoist into the inline meter spacer
regardless of array position. Docs + changelog updated.
2026-10-18 01:08:26 -07:00
Frank 246189f054 Status line: band shape, drop token_in/token_out segments
Band merges the segment groups into one row above the editor and paints
the labeled inline context meter (60% - 200K); the session title rides
the working row. Cumulative token totals live in /session - off the bar.
Docs and changelog updated to the band final state.
2026-10-18 00:58:21 -07:00
Frank 04d3628c94 Docs: path segment two-mode stripWorkPrefix behavior; changelog matches shipped true 2026-10-18 00:51:12 -07:00
Frank e0920dc067 Status line: path segment keeps logical project/worktree label
stripWorkPrefix true: worktree sessions show projectName/worktreeName with
the worktree glyph instead of the on-disk cwd; matches the live bar tuned
and screenshot-approved 2026-10-18.
2026-10-18 00:50:57 -07:00
Frank e044700ef7 Status line: custom segments tuned for fleet bars; slim frank-archon label
Named presets silently ignore leftSegments/rightSegments - the nerd
preset's baked arrays were what we saw, and the configured arrays were
dead. preset: custom with explicit arrays: chip (right) = token_in,
token_out, cost, context_pct, time_spent; strip (left) = session_name,
vim, mode, hostname, model, path, git (branch text off - worktree
branches are autogenerated; dirty badges kept), pr. Cut tok/s duplicate,
clock, cache trio, context_total, subagents, session hash. Composer
shape stays claude after live box/band/rule trials. models.yml display
name frank-archon drops the (dankserver) suffix; id untouched.
Docs: pi/statusline.md records the preset-vs-custom trap, shape->segment
routing, and the context-meter truth.
2026-10-18 00:39:11 -07:00
Frank 7b7dbf55c2 Add 1/3 to niri Mod+R width presets
Cycle is now 1/4 -> 1/3 -> 1/2, ascending; heights unchanged.
2026-10-17 18:31:54 -07:00
Frank a4218361e2 Switch niri width presets from fifths ladder to 1/4 + 1/2
Mod+R now cycles 1/4 <-> 1/2 only, matching the quarter-based
preset-window-heights; verified live (840px quarter / 1696px half on the
3440px panel). Mod+F still maximizes a column.
2026-10-17 18:30:50 -07:00
Frank c6fa584d0a Merge branch 'main' of http://192.168.0.143:3000/dank/command-center 2026-10-17 18:15:04 -07:00
Frank 541f27f316 Simplify niri size controls: 5% nudges, quarter/half height presets, Ctrl+arrows consume-or-expel
- set-column-width / set-window-height nudges 10% -> 5% (Mod+-/= and
  Mod+Shift+-/=).
- New preset-window-heights { 1/4, 1/2 } in layout.kdl; Mod+Ctrl+Shift+R
  no longer falls back to the width-preset ladder.
- Mod+Ctrl+Left/Right (and H/L) retargeted swap-window-* ->
  consume-or-expel-window-*, matching Mod+brackets; swap-window now unbound.
Validated with niri validate; live via hot-reload.
2026-10-17 18:15:02 -07:00
dank e2fe36519c Update README.md 2026-10-15 18:18:40 -07:00
dank 7fb6d36801 Update README.md 2026-10-15 18:18:08 -07:00
dank 23901a190d 5matting 2026-10-15 18:10:01 -07:00
dank d015493219 ai disclosure 2026-10-15 18:08:25 -07:00
Frank ae426434bc niri: retire flameshot binds, absolute script paths in spawn-sh
Refs the 2026-10-14 screenshot saga; see commit body.
2026-10-14 17:53:11 -07:00
Frank d767348cfd niri: retire flameshot binds, absolute script paths in spawn-sh
Live census 2026-10-14: every flameshot mode on this box captures via
xdg-desktop-portal-gnome → niri's single-output API (niri#4313) — full
AND screen hang ~15 s and error docked (verified: exit 124, no file).
Replace all six flameshot binds with niri-native equivalents:
all-monitors script (Print/Ctrl+Shift+1/Mod+Shift+S), screenshot-screen
(Alt+Print/Ctrl+Shift+2), interactive screenshot UI (Ctrl+Print/
Ctrl+Shift+3). Absolute script paths everywhere: niri's spawn-sh PATH
is system-only, ~/.local/bin absent — the bare screenshot-desk.sh and
desk.sh binds were silently dead. bats 27/27.
2026-10-14 17:53:09 -07:00
Frank ae12063de9 niri: fix screenshots on 2+ outputs — Mod+Shift+S captures all monitors
flameshot full's portal path hits niri's screenshot_all_outputs
ensure!(outputs.len()==1) (niri#4313); add scripts/screenshot-desk.sh
(per-monitor focus + screenshot-screen), bind Mod+Shift+S, noctalia
settings to Mod+Shift+O, restore screenshot-path. bats 27/27.
2026-10-14 17:35:38 -07:00
Frank 8da6cdc9cd niri: fix screenshots on 2+ outputs — Mod+Shift+S captures all monitors
flameshot full dies docked: its portal path hits niri's
screenshot_all_outputs ensure!(outputs.len()==1) (upstream niri#4313,
niri 26.04), so every Print press on the home desk produces error
bubbles. niri's built-in screenshot UI selects only one output, so add
scripts/screenshot-desk.sh: per-monitor focus + screenshot-screen with
explicit --path, focus restored, one PNG per monitor in
~/Pictures/Screenshots. Bind Mod+Shift+S (Frank's call); noctalia
settings moves to Mod+Shift+O. Restore screenshot-path (was null, now
root-level as config.screenshot_path requires) for UI saves. Live-
verified docked: valid 1920x1080 PNGs for eDP-1 + DP-2, focus restored;
bats 27/27 incl. niri validate on both desk profiles.
2026-10-14 17:21:10 -07:00
Frank 9217ed83f8 niri: move laptop panel (eDP-1) left of ultrawide in home desk profile
Sceptre left edge x=1581 minus laptop width 1920 -> x=-339;
y=-240 bottom-aligns the 1080 panel to the 1440 panel.
2026-10-13 19:49:38 -07:00
Frank 27083f75f1 niri: simplify preset-column-widths to 1/2, 1/5, 2/5, 3/5 2026-10-09 19:35:12 -07:00
Frank 5a1cb81bb9 chore(agent): capture noctalia-bar-layout-edit managed skill 2026-10-09 16:37:46 -07:00
Frank 4d2b024116 chore(agent): capture noctalia/gate/niri memory lessons + secret-audit and system-prompt-retrieval managed skills
learned.md: noctalia v5 bar layering + hot-reload loop; secret-gate self-match convention (quoted literals redacted per the convention the line itself states); niri 26.04 event-stream has no hotplug event. New managed skills: git-repo-secret-audit, omp-system-prompt-retrieval.
2026-10-09 16:37:43 -07:00
Frank 989196f664 chore(gitignore): ignore webcache/ harness web-fetch cache (runtime state) 2026-10-09 16:37:41 -07:00
Frank 0ccdd46a97 docs(agents): add commit hygiene section mirroring serverAppConfigs
Imports the Git Workflow / Testing law from dank/serverAppConfigs: pull+clean tree, atomic early commits, bats+secret gate as commit precondition, push on green, imperative messages with Refs #N.
2026-10-09 16:36:32 -07:00
Frank 9255a8b865 fix(noctalia): move taskbar widget to left section beside workspaces
Bar start = launcher, wallpaper, workspaces, taskbar; center keeps only the cat plugin widget. settings.toml is the GUI-override layer, symlinked from this repo.
2026-10-09 16:36:32 -07:00
Frank 8a68c58078 chore(memory): record faillock sudo-lockout law + mint recovery skill
redact gate pattern literals in the audit prose — the pre-commit gate
self-matches its own documented patterns when quoted verbatim
2026-10-09 15:09:09 -07:00
dank 5026c02982 Merge pull request 'Blankpad DE redesign: desk profiles, keybind rework, Handy fleet wiring' (#2) from blankpad-de-redesign into main 2026-10-09 15:07:48 -07:00
Frank abb8a2a9d0 fix(autoswitch): WorkspacesChanged output-set diff; unit paths; deploy symlinks
- niri 26.04 has no hotplug event: OutputConfigChanged is only an ACK
  (niri-ipc). Watch WorkspacesChanged, diff sorted output set, apply on
  change. Proven live: eDP-1 off/on -> 3 output-set transitions, each
  matched + applied + reloaded.
- unit: %h/.local/bin/desk-autoswitch.sh, quoted Environment, drop
  User/Group (user unit); add desk-autoswitch.sh bin symlink
- desk.sh: silence validate/load-config/notify stdout
- deploy.sh: deploy repo symlinks (find -type l), so .local/bin links land
- docs: event semantics corrected (displays diagram, log)
2026-10-09 14:46:17 -07:00
Frank 286340e786 fix(deploy): skip .AGENTTRASH in packages; deploy repo symlinks (desk.sh); retire base display.kdl
desk.sh: silence niri validate / load-config-file stdout (was leaking 'ok')
2026-10-09 14:43:05 -07:00
Frank 4874d103d7 refactor(niri): retire base display.kdl — runtime display.kdl is desk.sh-owned (symlink to active desk-*.kdl); the include is optional 2026-10-09 14:42:45 -07:00
Frank b441cfda56 docs(DE): full redesign sweep — desk profiles, keybinds, noctalia v5, handy fleet wiring, incident log
- niri: index/config-tree rework, displays.md desk-profile rewrite,
  tips/action-reference/default-bindings refresh (26.04),
  generated keybindings.md (119 bindings)
- noctalia-shell.md: settings.toml lives in ~/.local/state, taskbar
  widget, v5 keybind table (Mod+D/Mod+V/Alt+Tab)
- handy.md: NEW; live nested store shape, fleet endpoint, known
  thinking-model empty-content breaker + verified cure
- log.md: DE redesign entry + pam_faillock sudo-lockout incident
2026-10-09 14:42:08 -07:00
Frank 37554b0fb0 feat(handy): correct store template to nested {settings} shape; fleet wiring patch + apply recipe
live store wired 2026-10-09: provider custom -> frank-secondary @
192.168.0.143:7771, prompt default_improve_transcriptions,
post_process on, self-autostart off (niri owns it)
2026-10-09 14:42:08 -07:00
Frank 4c562d7ed4 feat(noctalia): taskbar widget in bar center (settings.toml) 2026-10-09 14:42:08 -07:00
Frank f63d11de85 feat(blankpad): desk.sh profile switcher, hotplug autoswitch daemon, keybinds doc generator + bats suite
- desk.sh: list/current/switch/cycle/match/capture over cfg/desk-*.kdl
  profiles; apply = relink display.kdl, niri validate (rollback on fail),
  load-config-file, noctalia notification; match scores // requires: lines
  (EDID long names) against live niri msg outputs
- desk-autoswitch.sh: watch niri msg --json event-stream for
  OutputsChanged, debounced desk.sh match --apply, reconnect loop
- blankpad-desk-autoswitch.service: ConditionHost=blankpad user unit
- gen-keybinds.sh: keybinds.kdl -> markdown cheat sheet (sections from
  // --- headers, hotkey-overlay-title preferred over action)
- tests/: 27 bats cases, all green (desk 16, gen-keybinds 6,
  niri-config 5 incl. real niri validate on the assembled tree with
  each desk profile active)
2026-10-09 14:40:26 -07:00
Frank 4ec47a5b7e niri(blankpad): desk-profile system, keybind rework, lid handler, noctalia config tracked
- config.kdl: display include optional=true (desk profiles link it per-desk),
  new optional switch-events.kdl include (per-host, blankpad-only)
- keybinds: swap-window-left/right on Mod+CTRL+H/L+arrows (join/exit columns
  on pass), move-column demoted to Mod+CTRL+Alt+chords; Print trio -> flameshot
  full/gui/screen (CTRL+Shift trio kept); launcher Mod+CTRL+Return -> Mod+D;
  Alt+Tab noctalia window-switcher; Mod+V clipboard panel; Mod+Shift+Space
  handy post-process; Mod+Shift+D desk cycle
- layout: preset column widths 12.5/25/33/50/67/75/87.5%
- desk-1-home.kdl: EDID-identity-keyed positions (M34 + eDP long names; PM1601
  connector placeholder pending EDID capture), focus-at-startup M34-first
- desk-2-portable.kdl: skeleton, EDID capture TODO
- switch-events: lid-close -> noctalia session lock-and-suspend
- autostart: handy --start-hidden (replaces Handy self-autostart)
- noctalia config.toml + settings.toml now tracked
- zz-blankpad display.kdl retired to .AGENTTRASH (superseded by desk-1-home)
niri validate: pass (assembled blankpad layout)
2026-10-09 14:17:21 -07:00
Frank 1d32829428 docs(log): record the full-repo secret audit (clean at 79e780f)
Five layers: live-value match across all 77 revs / 765 reachable blobs; the whole
object store including the 2 dangling blobs (one synthetic sk-shaped fixture,
never committed on any ref); 21 provider shapes with every hit triaged; an
entropy scan of HEAD; all 23 git-lfs payloads (81 MiB) scanned as bytes.
Gate gap noted: hf_/npm_/pypi-/AIza/basic-auth-URL shapes are absent from
.githooks/pre-commit. Quoting the fixture literal in this commit message was
rejected by the gate, so it is described instead.

Also chmod 600 ~/.config/secrets/hf_token_write (was world-readable).
2026-10-09 13:26:04 -07:00
Frank 79e780f280 chore(skills): mint gitea-token-out-of-remote-url + record the store-format law
git 2.56 credential-store parses one URL per line (port included), not the
key=value block shape the man page shows for ~/.git-credentials; a block-format
file yields a silent empty get and git dies at askpass.
2026-10-09 13:21:48 -07:00
Frank 07fda88b1d chore(skills): mint omp-model-id-rename-cutover + record the callsign rename law
Captured from this session's cutover: reference-grep list, the YAML
cross-check regex (the provider label contains a space and parentheses, so a
whitespace split yields false unresolved hits), the per-endpoint 4-token probe
that proves a harness-side id rename is wire-safe, the fit check against the
served window, and the commit shape.
2026-10-09 13:21:37 -07:00
Frank cfed6ef7bb docs(gitea): document client token handling; move the token out of remote URLs
New tools/gitea/client-access.md: the credential file (~/.config/secrets/gitea,
0600) behind a global 'store --file' helper, the git 2.56 credential-store trap
(the key=value block shape parses to an empty get; the file must be one
http://user:token@host:port URL per line), the GIT_TERMINAL_PROMPT=0
verification loop, the lfs.locksverify key that silences the URL echo, and the
rotation procedure. Cross-linked from the gitea index and management pages.

Both workstation remotes are now bare URLs; the token lives in one file.
2026-10-09 13:21:34 -07:00
Frank 69e4cd0c8f chore(gitignore): ignore harness diagnostic report bundles
reports/*.tar.gz bundles logs and session state; pushing them to Gitea would
publish raw transcript material.
2026-10-09 13:19:27 -07:00
Frank a172513a95 docs(log): record the 2026-10-09 working-tree reconciliation
Callsign renames verified against the live endpoints (all three accept the
new request ids and return reasoning_content), compaction endpoint moved and
22 model references checked for integrity, runtime scratch quarantined in
.gitignore, skill libraries shown to be renames with history intact at
12c5e88.
2026-10-09 13:19:19 -07:00
Frank 5b08896e4e chore(memory): snapshot auto-learned memory and renamed workstation skills
--home-blank-.omp--: MEMORY.md +140 / learned.md / memory_summary.md /
raw_memories.md +1002 rewritten by auto-learn after the model rename,
frank-medium split-balance and bottleneck studies. Skills renamed to
single-topic buckets: dotfile-git-workflow -> ghostty-text-aa +
niri-display-rotation + noctalia-theme, handy-service-mgmt ->
handy-flm-inference, inference-runtime -> llama-cpp-parallelism.

--home-blank-Projects-serverAppConfig--: 38 new rollout summaries,
raw_memories.md/MEMORY.md refreshed, skills re-bucketed (bench-protocol,
llm-fleet-model-swap, rootless-quadlet-log-retrieval, orchestration-quadlet).

Per-worktree bucket (wt-20261006-122204) is now gitignored, not committed.
2026-10-09 13:17:53 -07:00
Frank a34322aeb0 refactor(skills): consolidate managed skill library 94 -> 25
The managed-skill library was regenerated: 94 narrow per-incident skills
replaced by 25 broader ones (radiance-*, llamacpp-*, homepage-*, fleet/
endpoint ops). Git renders this as mass delete + mass add because whole
directories changed name; old content stays in history at 12c5e88.

Surface check: the 25 on disk are exactly the skills the harness advertises
this session.
2026-10-09 13:17:48 -07:00
Frank 94c71a016f fix(zsh): source KAGI_API_KEY and HF_TOKEN from 600-perm secret files
Keys live in ~/.config/secrets/{kagi_api_key,hf_token}; .zshrc only reads
them, so the literals leave the shell rc that this repo publishes to Gitea.
2026-10-09 13:17:45 -07:00
Frank 40c37bb9fa niri(blankpad): DP-2 back to landscape ultrawide, HDMI-A-1 bezel flipped
DP-2: transform 90 -> normal with mode 3440x1440@239.999 pinned, moved
(0,0) -> (1581,-600). HDMI-A-1: transform 270 -> 90 (portrait kept, top bezel
now on the left). eDP-1 re-pinned (2400,840) -> (5021,-240) to sit rightmost.
All three positions stay pinned so niri's auto-layout cannot reflow them.
2026-10-09 13:17:45 -07:00
Frank 4e288b518e feat(omp): repoint roles at the renamed models, move compaction to Dankclaw
Role map follows the callsign rename; scout/reviewer/librarian/sonic/task/
security-reviewer/designer all pinned to frank-zealot (was the @smol alias).
New roles: web -> web/searxng, speech -> local/kokoro, judge ->
openrouter/~typesafe/jev-latest:auto, dictation -> local/parakeet-tdt-0.6b-v3,
memory -> frank-zealot.

webSearchOrder (flat list) replaced by the per-role providers.web fallback
chain, kagi first; openrouterVariant: floor added.

Server-side compaction moves off dankserver:7770 to the Dankclaw 3090
endpoint 192.168.0.143:7771, thresholdPercent 70 -> 85, remoteStreamingV2
+ async on, idleTimeoutSeconds 120, maxRetries 10.

TUI: tight, contextLine embedded, splash off, changelogMode summary,
theme dark-terminal/light, read.renderMarkdown + toolResultPreview.
Extensions on: astGrep, vault, secrets, lsp (formatOnWrite,
diagnosticsOnEdit), speechgen, live, contextPromotion, tts/speech voices.
headless: true, autoResume: false.
2026-10-09 13:17:43 -07:00
Frank 0cb002123b refactor(omp): rename fleet models to unit-name callsigns and refit windows
frank-medium -> frank-archon (dankserver), frank-large -> frank-dragoon
(dankclaw-gpu), frank-small -> frank-zealot (dankclaw-3090). The Dankclaw
service/unit names keep the old frank-large spelling; only the OMP model ids
move, per the comments at each entry.

Windows refit to the served context minus the compaction headroom:
  archon  196608 -> 200000 ctx, 32768 -> 16384 maxTokens
  dragoon 200192 -> 217088 ctx, 57344 -> 45056 maxTokens, input +image
  zealot  262144 -> 200704 ctx, 32768 -> 16384 maxTokens
All three now carry compactionModel frank-zealot, reasoningContentField
reasoning_content, and Qwen-recommended sampling in compat.extraBody
(merged last, so it beats the global -1 sampler settings).
2026-10-09 13:17:40 -07:00
Frank 40c7d3c752 chore(gitignore): quarantine agent runtime scratch and per-worktree memory buckets
autoresearch/ runs, agent/predict/ ngram models, local/ artifact dumps, wt/
worktrees and memories/--home-blank-.omp-wt-* buckets are runtime state keyed
on ephemeral paths; none were tracked, so the rules are purely additive.
2026-10-09 13:17:23 -07:00
Frank 12c5e885f1 docs: purge the retired workstation from auto-learned memory and the backup snapshot
Finishes the purge. These two surfaces were left out of the earlier commit
because the memory store is auto-generated, but both are read back into every
future session, so the name was still reaching the agent.

58 sites across 16 files: hostname, both LAN addresses, the SSID. Special-cased
the compound names a blind token swap breaks ('dank@the workstation/dankclaw',
'on the workstation/Dankclaw/Dankserver') and reworded the four that came out
ungrammatical. Facts kept: the hardware inventory of the box, the noctalia
architecture history, and today's own purge lessons all survive intact — only
the identifiers are gone.

Non-memory: the tracked .pre-pull-backup snapshot of a managed skill, whose
live counterpart was already clean.
2026-09-28 16:57:00 -07:00
Frank 689228e60d docs: purge the retired workstation from the knowledge base
Second half of the purge (the first covered the persona, AGENTS.md, README.md,
and the distillation of the per-host pages). Every remaining identifier is
gone: hostname, both LAN addresses, and the Wi-Fi SSID.

34 count-checked substitutions across 22 files — indexes, tool/research pages,
the inventory census, the changelog, one generated HTML report, and five
managed skills. Facts kept, names dropped: a history entry keeps its four-page
listing as a nameless summary, the syslog excerpts keep every timestamp and
line, and the census keeps the .zcompdump rows (the zsh version stamps are the
useful part, the hostname was not).

Deliberately unchanged: install.sh's --host case list and the
packages/<hostname>.pkgs filename, where the hostname is the lookup key.

Left out of scope by Frank's call: agent/memories/** — past-session records the
learn pipeline rewrites.

kb-lint: 0 violations. Residual grep for the hostname, its two IPs, and the
SSID across omp-docs/ and agent/managed-skills/: no hits.
2026-09-28 16:37:38 -07:00
Frank f1e0b6583d docs: distill the retired workstation's KB pages into general pages
Frank's call: no per-host graveyard. The four /system/dankware/ pages carried
two pieces of knowledge that outlive the box, and one that does not.

Distilled:
- omp-docs/system/network.md (new) — LAN topology plus the multi-link resolver
  law: a client with two +DefaultRoute links flaps its DNS, and the fix is a
  static per-connection resolver, not resolved.conf. Keeps the
  flush-caches-loop verification, because one successful dig proves nothing.
- omp-docs/tools/niri/displays.md (new) — the multi-monitor rig: portrait
  transforms, absolute positions, auto-placed third panel, the iGPU
  render-node pin, and the three failure modes (duplicate output blocks,
  positions that do not reflow, a mode the panel cannot drive).

Dropped: the machine's own spec sheet (CPU/GPU/PCI/NVMe inventory for a box
Frank sold) and its quick-facts table. The OS and userspace facts it repeated
already live in tools/cachyos.md.

omp-docs/system/dankware/ moved to .AGENTTRASH/20260928-retired-workstation/
(not deleted; git history holds it too). system/index.md and the niri index
repoint; niri's [[displays]] wikilink now resolves to the new page instead of
the trashed one. kb-lint: 0 violations.
2026-09-28 16:15:57 -07:00
Frank 4e3d4d20e9 docs: drop the retired workstation's name from the command-center contract
AGENTS.md: the reader-preamble and the machine-facts table row no longer name
the sold client box; the row survives as the 'Dev workstation' class with its
two load-bearing facts (mainline llama.cpp build path, repo at ~/.omp).

README.md: same for the fleet table row and the 'primary host' sentence.

Deliberately untouched, because the name is load-bearing in both:
- scripts/install.sh:97 — the '--host' case list; --host <name> must still
  accept this hostname.
- packages/dankware.pkgs:1 — install.sh resolves packages/$HOST.pkgs from
  $(hostname) (line 25/44), so the manifest FILENAME is the lookup key.
  Renaming it breaks package provisioning on this host.
2026-09-28 15:25:46 -07:00
Frank 8fbb3e8b54 fix(persona): drop the retired workstation from the agent identity line
APPEND_SYSTEM.md line 16 named the sold client box twice (as a fleet server
and as the host I operate from) and, while doing it, mis-listed the fleet as
'Dankware and Dankclaw' -- Dankserver was already missing.

Now: servers Dankclaw and Dankserver, plus several client workstations, and
the identity no longer claims a fixed host. Committed on main rather than a
feature branch because this checkout IS the live deployment (branch switches
swap live config under the running agent) and the tree already carries
in-flight WIP; only this file is staged.

Takes effect in NEW sessions -- the running one keeps its process-start
prompt snapshot.
2026-09-28 15:23:22 -07:00
Frank 03c043d620 niri: column join/expel keybinds (Mod+[ ] , .)
consume-or-expel-window-left/right on Mod+[ and Mod+],
consume-window-into-column on Mod+, expel-window-from-column on
Mod+.. Moving a window into the next column joins (splits) that
column deterministically instead of relying on the hardcoded
drag-insert heuristic, which spawns a new column first.
2026-09-17 21:50:55 -07:00
Frank c3f30bdbd0 kb: retire pi-installation association (OMP is the primary harness)
Purge live-state claims that ~/.pi is our harness; keep general
pi documentation in omp-docs/ and dated historical records.

- README.md: single config source; Legacy Notes reworded
- AGENTS.md: drop 'like ~/.pi did' framing
- APPEND_SYSTEM.md: okf-spec -> omp-docs/references/okf-spec.md
- dotfiles/zsh/.zshrc: systemprompt alias -> APPEND_SYSTEM.md
- managed skill llm-fleet-model-swap + model_config_sync skill:
  single-source models.yml; validation script YAML-only
- memories (MEMORY.md, memory_summary.md, learned.md): single source
- omp-docs: .okf root -> ~/.omp/omp-docs; de-live pi-era claims in
  pi/index.md, pi-web-access.md, engineering/agents-files.md
2026-09-17 19:24:02 -07:00
Frank eebcfbf5ac omp: scout on @smol, cap active subagents at 2, frank-medium remote compaction
- task.agentModelOverrides.scout: "@smol" (frank-small, xhigh thinking)
- task.maxConcurrency: 2 (semaphore on ACTIVE subagents; total
  instantiations uncapped)
- compaction.remoteEndpoint -> frank-medium /v1/chat/completions
  (192.168.0.235:7770), methodOrder remote-first; compactionModel:
  frank-medium on every model so the wire model id is always valid
- models.yml header: single source of truth (pi-era import retired)

Verified: omp config get on all keys; omp -p startup probe passed;
live POST in the remote-compaction wire format answered.
2026-09-17 19:24:02 -07:00
Frank 368caadbe8 ghostty: render errors in terminal green (ANSI red -> green)
Remap palette indices 1 and 9 to green so error output renders in
terminal green. Explicit palette entries override the generated
noctalia theme (palette-generate = true).
2026-09-17 19:24:02 -07:00
Frank 135bb2194d agent: repoint roles to frank-medium/dankclaw-3090; sync niri tweaks, memories, managed skills
- config.yml: default/commit -> frank-medium (dankserver); task/tiny/smol/vision -> frank-small (dankclaw-3090); vision xhigh
- models.yml: add llamacpp (dankclaw-3090) provider; frank-large preserve_thinking + context notes
- niri: focus-ring/border/shadow layout, hot-corners off, re-add noctalia.kdl include; blankpad eDP-1 scale 1.0
- memories: flush rollouts; retire frank_large + marreta_authentik skills; add auth-chain/kv-pool/quadlet-log skills
- managed-skills: 40+ new fleet runbook skills
- agent/skills: import built-in skill set
2026-09-17 12:27:08 -07:00
Frank 83c9c92b3f blankpad(niri): move rotated PM1601 panels left of eDP-1 (DP-2@0, HDMI-A-1@1200, eDP-1@2400) 2026-09-15 19:43:01 -07:00
Frank 97a0b21b40 blankpad(niri): 180-degree flip of both external panels
DP-2 270->90, HDMI-A-1 90->270. In portrait, 180 is the 90<->270 swap.
2026-09-15 17:36:29 -07:00
Frank 922165bde2 blankpad(niri): flip DP-2 to transform 270
DP-2 top bezel faces right; HDMI-A-1 stays at 90 (top bezel left).
2026-09-15 17:35:56 -07:00
Frank 2baf0c083d blankpad(niri): pin outputs; run both PM1601 externals portrait (transform 90)
- DP-2 at 1536,0 and HDMI-A-1 at 2736,0, both 1200x1920 portrait, scale 1
- eDP-1 pinned at 0,0 with explicit scale 1.25 (output blocks reset unspecified props)
- niri transform is counter-clockwise: 90 = top bezel left, 270 = top bezel right
2026-09-15 17:30:41 -07:00
Frank 970ceed554 niri: drop untracked noctalia.kdl include; sync memory skills
- remove 'include noctalia.kdl' from shared config.kdl: the file is a
  per-host noctalia template render (colors from local palette), was
  never committed, and made every config load fail hard on hosts where
  it was absent — niri 26.04 falls back to DEFAULT config on parse
  error, silently killing keybinds/rules/layout/autostart
- theme integration now only via keybinds (noctalia msg verbs) + the
  host-local rendered files (see managed skill noctalia-template-offline-render)
- sync agent memories: delete retired memory-store skills, add new ones
2026-09-14 17:46:31 -07:00
Frank fc049ce7fb agent: local role repoint + compaction 70%/-1; sync memories and managed skills 2026-09-14 14:32:16 -07:00
Frank 872e86399a agent: repoint FLM-NPU model roles off the retired provider; sync memory/skills
config.yml: vision -> llamacpp (dankclaw)/frank-large:auto,
tiny -> llamacpp (dankserver)/frank-small:off,
task -> llamacpp (dankclaw)/frank-large:medium (was flm dankclaw-npu),
matching the flm (dankclaw-npu) provider removal in models.yml.

Memories/managed skills: rollouts and learned notes from the frank-large
KV-pool + sampler work and the noctalia recolor; merged the duplicate
noctalia-palette-recolor skill into noctalia-custom-palette-recolor (adds
the dotfiles-tracking step and the +show-config delta gotcha) and dropped
the superseded legacy memory skills whose ghostty USR1 reload recipe is
wrong for this fleet.
2026-09-13 23:51:43 -07:00
Frank c32584e093 dotfiles: ghostty 12px + monochrome freetype rendering
Captures the live ghostty config state: font-size 14 -> 12 and
freetype-load-flags hinting,light -> hinting,monochrome (1-bit, AA off).
2026-09-13 23:51:22 -07:00
Frank 93bd4737f3 dotfiles: recolor noctalia theme yellow to red
Adds custom palette "Gruber Red" (live at
~/.config/noctalia/palettes/Gruber Red.json, symlinked into this repo's
noctalia package). Derived from community "Gruber Darker" with jq:
dark mPrimary/terminal yellow/terminal foreground -> #f43841 (bright
yellow -> #ff4f58), light-mode yellows -> the light error reds. Custom
source, so the community-palette md5 sync cannot revert it.

gtk-3/gtk-4 noctalia.css are regenerated artifacts of templates-apply
(accent_* now red) committed so every host renders the same recolor.
2026-09-13 23:51:11 -07:00
Frank 60dc0bba92 models: set frank-large contextWindow to 200000
Also carries previously-uncommitted edits already in the working file:
maxTokens 262144 -> 65536 (HALOGEN_MAX_TOKENS_CAP) with the KV-pool
admission note, and removal of the flm (dankclaw-npu) provider block.

Live check: halogen /health reports context=262144, so 200000 is inside
the served window; 200000 + 65536 = 265536 against a 401408-position pool.
2026-09-13 23:14:21 -07:00
Frank 4b82f35354 Add blankpad-host auto-learn skills and memory store
Tracked the harness artifacts accumulated on blankpad (blank@), matching
the existing convention for the --home-dank-* memory stores and the 36
already-tracked managed-skills.

managed-skills (9): authentik-proxy-provider-app-link,
dankclaw-guard-chain-layer-diagnosis, e2e-arr-rawip-reachability-triage,
halogen-kv-pool-fit, lan-client-split-dns-fix, noctalia-reference,
rootless-quadlet-log-retrieval, ssh-key-bootstrap-no-sshpass,
swapfile-resize-safe-ordering.

memories (2 stores): --home-blank-.omp-- (dotfiles_git, handy_lifecycle,
input_permissions) and --home-blank-Projects-serverAppConfig--
(frank_large_quadlet).

Secret sweep before staging: no PEM blocks, no credential literals.
Content is fleet topology/IPs only, same class as the tracked dank stores.
2026-09-13 00:25:29 -07:00
Frank b60e95158c frank-large: pin Qwen3.8-Flash-Next sampler settings + preserve_thinking
halogen-flash-server has no server-side sampler defaults (temperature absent
or 0 = greedy), so the endpoint decoded greedy for OMP, whose global
sampling settings default to -1 (not sent).

compat.extraBody merges these top-level into every request body after the
compat policy, so they win:
  temperature=1.0 top_p=0.95 top_k=20 min_p=0.0
  presence_penalty=0.0 repetition_penalty=1.0 preserve_thinking=true

Verified against the live dankclaw endpoint: preserve_thinking is honoured
(350 vs 75 prompt tokens for a replayed reasoning turn), and the exact
body shape returns 200.
2026-09-12 23:58:38 -07:00
Frank ccf72a2a3e Fix Ghostty: font-hinting -> freetype-load-flags = hinting,light 2026-09-11 00:36:11 -07:00
Frank 268dcbdea4 Fix Ghostty: font-hinting = hinting (flag-based, not light value) 2026-09-11 00:34:54 -07:00
Frank df3331026d Ghostty: switch font-hinting from no-hinting to light 2026-09-11 00:33:25 -07:00
Frank 4c84f028c5 Fix Ghostty: font-hinting = no-hinting (not font-subpixel-render) 2026-09-11 00:31:55 -07:00
Frank 5ad39bdfea Ghostty: disable font subpixel antialiasing 2026-09-11 00:28:53 -07:00
Frank 49afdfeda4 Noctalia theme migration: Niri IPC, GTK palette, alacritty import
- Switch Niri keybinds from qs-wrapped IPC calls to direct 'noctalia msg'
- Add noctalia.kdl include to niri config
- Replace alacritty theme with import of noctalia.toml
- Ghostty: Retro Legends → noctalia theme
- GTK3/4: accent palette shift from lavender (#bfc2ff) to purple (#b267e4)
- systemd swayidle-lock.service updated
2026-09-11 00:26:56 -07:00
Frank d0c90fccbd fix(install): replace set-diff membership with per-package pacman -Qq --quiet query (deterministic) 2026-09-10 21:01:55 -07:00
Frank c8cac879e6 fix(deploy): managed symlinks changing owners relink silently; quarantine reserved for genuine divergence 2026-09-10 20:41:14 -07:00
Frank 0883ae5660 fix(packages): amd-u-code is not a pkgbase; real name is amd-ucode 2026-09-10 20:32:41 -07:00
Frank 07a95815d2 fix(install): manifest parser emitted comment-padded tokens; AUR phase must run unprivileged (makepkg refuses root) with primed sudo timestamp 2026-09-10 20:26:56 -07:00
Frank ea8d378a40 chore(lfs): track wallpaper collection as LFS; wallpapers move into repo tree under noctalia data path 2026-09-10 19:42:57 -07:00
Frank afc7c2be64 feat(shells): zoxide cd replacement in all three shells + guarded system-wide oh-my-zsh source 2026-09-10 19:42:57 -07:00
Frank 118390a9e2 feat(niri): flameshot takes screenshot binds CTRL+Shift+1/2/3 on both hosts 2026-09-10 19:42:57 -07:00
Frank 9a5387b6c0 feat(systemd-user): swayidle idle-lock unit with noctalia IPC fallback chain 2026-09-10 19:42:57 -07:00
Frank 779b65af8c feat(install): idempotent per-host package installer with host-gated enablement 2026-09-10 19:42:57 -07:00
Frank a30cd771f3 chore(agent): thinking-block visibility toggle 2026-09-10 16:58:00 -07:00
Frank 9683d1f5c1 noop 2026-09-10 16:57:10 -07:00
Frank 35b871162c chore(memory): flush runtime memories and promote managed skills
Memory-file flush for the dotfiles-deploy session with the blankpad admin
credential scrubbed to a placeholder; sessions transcripts untracked
(gitignored) since harness transcripts can carry raw secrets.
Managed skills: command-center-dotfiles-import, ssh-fish-host-remote-exec,
stash-supersession-audit-before-drop.
2026-09-10 16:57:10 -07:00
Frank 2db711964b feat(zz-blankpad): ghostty terminal bind and bun PATH for omp
blankpad runs quickshell 0.3.1 from repos with the noctalia shell; flip
Mod+Return to spawn ghostty (installed) instead of alacritty, and put the
bun global bin dir on PATH so the pi-coding-agent/omp shim resolves in
interactive zsh.
2026-09-10 15:09:39 -07:00
Frank cc2c652cc2 fix(deploy): override packages target home-relative paths
zz-<host> packages nest base-package dirs internally (zz-blankpad/niri/.config/...);
stripping only the package prefix yielded targets like $HOME/niri/.config/... .
Strip the nested package segment for zz-* packages so overrides land on the
same home-relative target as the base copy they override.
2026-09-09 19:45:44 -07:00
Frank 5efb218061 feat(dotfiles): zz-blankpad host override package
Host-specific divergence for blankpad deployed last with override semantics:
display layout (its own outputs, not Dankware's DP/eDP rig), autostart and
keybinds targeting the bare 'noctalia' CLI instead of qs -c noctalia-shell,
misc without renderD129, rules without the Noctalia floating-window rule, and
blank's own .zshrc. All other packages deploy canonical from base.
2026-09-09 19:32:56 -07:00
Frank a9127a08e9 feat(dotfiles): import env layer as stow-style packages with deploy.sh
Import Dankware's environment layer into the repo as home-relative package
trees (niri, gtk-3/4, ghostty, alacritty, fish, zsh, bash, profile,
systemd-user, dconf). scripts/deploy.sh implements stow-equivalent semantics
(check|adopt|sync modes): identical files adopt to symlinks, new files link,
divergent files quarantine-and-replace under .quarantine/<host>/<ts>/ (gitignored).
zz-<hostname> override packages deploy last with override semantics.

Secrets never enter git: GITEA_TOKEN moved from .zshrc into untracked
~/.config/environment.d/secrets.env (0600); tracked .zshrc sources it
guarded. core.hooksPath armed by deploy.sh so the pre-commit secret gate is
live on every machine that deploys.
2026-09-09 18:09:38 -07:00
Frank dfb3a8f69a chore(gitignore): ignore per-host runtime state and local backups
Runtime state now ignored: agent/blobs (blob store), install-id (per-host
identity), puppeteer/ (browser cache), .AGENTTRASH/, *.save, *.bak-*.
Backups parked in .AGENTTRASH/ per trash convention (never rm user files).
2026-09-09 16:21:47 -07:00
Frank 2a3c0da740 chore(memory): flush runtime memory updates for .omp and serverAppConfigs roots
MEMORY.md/learned.md/memory_summary.md/raw_memories.md churn from live sessions
(DIFR deadlock diagnosis, model-swap ops, outpost verification etc.); new
memory root --home-dank-.omp-agent-- started with learned.md.
2026-09-09 16:20:42 -07:00
Frank 9bfa0be85d chore(skills): promote runbook skills to managed-skills; drop superseded SKILL.md
- 31 new managed skills (anubis/arr/authentik/hermes/freetoken/podman/llm-swap
  runbooks etc.) now tracked.
- Removed superseded skills: omp_harness_config, peon_ping_integration
  (replaced by user-authored peon-ping-* managed skills),
  rocmfp4-strix-benchmark-orchestration.
- New memory-root skills: niri_compositor_ops, omp_git_workflow,
  omp_model_config, quadlet-media-migration, homelab-service-triage,
  hermes-dankclaw-ops, frank-default-large-ops, caddy-auth-dns-playbook.
2026-09-09 16:20:28 -07:00
Frank 7be39d60dd feat(config): rewire model roles to frank-small/frank-large + NPU flm provider
- models.yml: rename frank-default-small -> frank-small (dankserver),
  frank-default-large -> frank-large (dankclaw-gpu, text-only input);
  add 'flm (dankclaw-npu)' provider with gemma4-it:e4b and nuextract3:4b.
- config.yml: point modelRoles at renamed models; vision -> nuextract3:4b:auto,
  tiny/task -> gemma4-it:e4b:off; compaction reorder (snapcompact added,
  idleThresholdTokens 200k->100k, thresholdPercent -1->85); hideThinkingBlock true;
  statusline transparency + compact thinking level; images.urls backends list.
2026-09-09 16:19:54 -07:00
Frank ac1608e71f docs(niri): fold AGENTS.md overwrite into niri KB section
The working tree had replaced the operating contract in AGENTS.md with a
niri config-reload + keybinding-constraints note. Content relocated to its
proper home: tips.md 3-config-reload (SIGHUP footgun, load-config-file) and
key-bindings-syntax.md constraints section. AGENTS.md restored from HEAD.
2026-09-09 16:19:02 -07:00
Frank 898519712b docs: add README.md — command center knowledge base overview 2026-08-31 15:45:47 -07:00
Frank 5e27c67d87 release: 1.0.0 — command-center v1: OKF knowledge base, operating contract, secret gate 2026-08-28 07:58:36 -07:00
512 changed files with 16999 additions and 924 deletions
+4
View File
@@ -0,0 +1,4 @@
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# git-lfs pre-push — required because core.hooksPath=.githooks disables the
# default .git/hooks/pre-push; without this hook, pushes would upload LFS
# pointer files without their objects and break every fresh clone.
command -v git-lfs >/dev/null 2>&1 || { printf >&2 'n' ; exit 0; }
git lfs pre-push "$@"
+31
View File
@@ -43,3 +43,34 @@ last-changelog-version
.DS_Store
*.swp
*.tmp
# --- per-host runtime identity + caches (never config) ---------------------
agent/blobs/
install-id
puppeteer/
webcache/
# --- local backups + staging artifacts: kept on disk, never in history -----
.AGENTTRASH/
*.save
*.bak-*
# deploy quarantine — never tracked
.quarantine/
# harness runtime transcripts: local-only, may contain raw session secrets
agent/sessions/
# harness diagnostic report bundles (tarballs of logs + session state)
reports/
# --- agent runtime scratch: research runs, decode predictions, worktrees ----
autoresearch/
agent/predict/
local/
wt/
# --- per-worktree memory buckets: keyed on ephemeral worktree paths ---------
# A session run inside a git worktree mints a bucket named after the worktree
# path; the worktree is deleted, the bucket is orphan state, not config.
agent/memories/--home-blank-.omp-wt-*
@@ -0,0 +1,28 @@
---
name: command-center-dotfiles-import
description: Runbook for importing client env-layer configs into the command-center repo and deploying them via stow on new fleet machines; decisions agreed with Frank 2026-09-09 (pre-execution — verify execution state before assuming done).
---
Design agreed with Frank for importing client application/environment configs into the `~/.omp` command-center repo and deploying them on fresh machines. **Status: plan agreed, not yet executed** — check repo state (does `dotfiles/` or `scripts/deploy.sh` exist?) before re-deriving; if already implemented, this becomes the operating model, not a to-do.
## Agreed decisions (each was an explicit user choice)
1. **Target**: unknown/TBD fresh machine → design stays host-agnostic; deploy derives overrides from `$(hostname)` at run time. Nothing hardcodes a target.
2. **Scope**: environment layer only — niri (+cfg includes), gtk-3.0/4.0 settings, ghostty, alacritty, shell rc files (`.zshrc`, `.bashrc`, `.profile`, `.p10k.zsh`), fish, systemd user units, autostart, dconf. App *data* (browser profiles, vaults, DBs) never imported.
3. **Layout**: stow-style package dirs `dotfiles/<pkg>/<home-relative path>` inside the repo; repo clones to `~/.omp` on every host that runs the harness (full command-center clone, not env-layer only).
4. **Deploy mechanism**: symlink farm — `stow -t ~` from `~/.omp/dotfiles`; relative symlinks within `$HOME` work because repo root lives inside `$HOME`.
5. **Dogfood order**: convert the workstation itself (real dirs → symlinks via adopt) BEFORE any target import; iterate where the compositor reload is observable.
6. **Secrets policy**: never commit secret values. Split rc files into committed logic + untracked sourced-if-exists stub (`~/.config/environment.d/secrets.env` or `.zshrc.local`); real values provisioned out-of-band only on machines that need them. Pre-commit secret scanner stays as safety net, not gate of first resort.
7. **Divergence**: `zz-<hostname>` override package stowed LAST with `--override`; only per-host diffs live there. (Stow descends into conflicting symlinked dirs — per-file overrides only work via this last-stowed `--override` pattern.)
8. **Deploy trigger**: explicit `scripts/deploy.sh` invocation, human- or agent-invoked. NO auto-deploy on pull — pull and deploy are separate decisions; `--check` = drift report mode.
9. **Ownership**: Frank is sole push authority; exactly one editing agent at a time → no multi-writer race machinery needed. Each host's agent edits only its own `zz-<hostname>` package.
10. **Conflict policy**: pre-existing regular file that differs from repo truth → move to timestamped `.quarantine/` dir on the target (never `rm`, same convention as this repo's trash rule), symlink anyway, print every replacement in deploy output and `--check` report. Repo truth always wins; divergence recoverable from quarantine.
11. **First-clone auth**: human types credential once console-side (URL-with-token or prompt); the new machine's secrets exist only there afterward.
## Implementation gotchas baked into the design
- `core.hooksPath` is a per-clone local setting — does NOT survive clone; bootstrap/deploy must set it on every fresh clone or the secret-gate pre-commit hook silently doesn't run there.
- dconf/gsettings are databases, not files: import as `dconf dump /` text into repo; deploy runs `dconf load` instead of symlinking.
- p10k installer and toolchain hooks append to rc files at install time — expect conflicts on `.zshrc`-class files at first adopt; quarantine-and-replace handles it, but diff live-vs-repo before adopting so pre-existing drift is seen, not silently adopted.
- Known secret at import time: `GITEA_TOKEN` export lived in `.zshrc` (line ~22) on the workstation — quarantine BEFORE importing rc files; consider token rotation at cutover since it had been exposed in plaintext rc + remote URL.
## Deploy script spec agreed
`scripts/deploy.sh`: sets hooksPath → stows all base packages → stows `zz-$(hostname)` with `--override` → first run uses `--adopt` for pre-existing identical files → divergent files quarantined-and-reported; `--check` mode emits drift report (modified tracked files vs deployed symlinks) without mutating.
@@ -0,0 +1,33 @@
---
name: stash-supersession-audit-before-drop
description: "Before dropping a stale git stash, prove it's superseded by HEAD via per-file line-set diff of git show stash@{0}:file vs HEAD; archive as an annotated tag and drop only after auditing unique facts — zero-loss alternative to blind drop."
---
# Stash Supersession Audit Before Drop
Use when a repo (especially a live-deployment checkout where the working tree IS the deployment, e.g. `~/.omp` command-center) has an old stash whose content may have been superseded by later commits, and dropping blindly risks losing unique facts.
## Procedure
1. **Inventory the stash** — see which files it touches:
```bash
git stash show --stat stash@{0}
```
2. **Probe for unique content per file.** For each touched file, diff stash version against HEAD and count lines that exist only in the stash version:
```bash
git show "stash@{0}:<path>" > /tmp/stash_copy
# compare normalized line sets; report lines present in stash but absent from HEAD
```
A file with `unique-vs-HEAD = 0` is fully superseded — safe to drop for that file.
3. **Classify each unique-content hit**:
- *Stale fact* (superseded by a later refactor, e.g. old model/provider naming) → drop is correct.
- *Live durable fact* (e.g. an operational invariant never re-recorded) → extract it into the live memory/docs file or flag to the user before dropping.
4. **Archive before dropping** — zero-loss: tag the stash commit, then drop from the stash list:
```bash
git tag archive/stash-<YYYY-MM-DD>-<base-branch>-wip "stash@{0}"
git stash drop "stash@{0}"
```
The tag pins the full stash object (including its index parent); recoverable later via `git show archive/stash-...` or `git checkout archive/stash-...^3 -- <path>` for the untracked-files third parent if present.
## Why
Stashes are invisible in normal history and expire from reflogs (~90 days). A stash that *looks* superseded can still hold the only copy of a durable fact; tagging converts "drop" into a reversible archive with zero workflow cost.
+68 -17
View File
@@ -2,7 +2,7 @@
System-configuration command center for Frank's fleet: agent config, knowledge base,
small system utilities, and (soon) dotfiles. You — the reader — are probably an agent
running on Dankware with this repo as your working directory. Read
running on one of Frank's workstations with this repo as your working directory. Read
`omp-docs/index.md` before you touch anything. This file is the operating contract;
`omp-docs/` is the knowledge base (OKF v0.1 format: directory of markdown files
with YAML frontmatter, spec at `omp-docs/references/okf-spec.md`).
@@ -13,29 +13,79 @@ with YAML frontmatter, spec at `omp-docs/references/okf-spec.md`).
(`dank/command-center` on the Dankclaw instance). There is no staging copy:
**the checkout is the deployment.** Branch switches swap live config files out
from under the running agent — keep feature branches short-lived, and never end
a work session checked out on anything but `develop` or `main`.
a work session checked out on anything but `main`.
- Contents that belong here: `AGENTS.md`, `omp-docs/` (knowledge base),
`scripts/` (small system utilities), `agent/` + `plugins/` (harness config and
plugin manifests). Everything else under this directory is runtime state and is
gitignored on principle — see `.gitignore`. History must mean "the configuration
changed", not "the agent breathed".
## Git-flow SDLC (full flow, from day one)
## Trunk-based SDLC (since 2026-10-18; replaces the old git-flow regime)
- `main` is the only long-lived branch. Frank is the only dev; `develop` and the
release-branch ceremony are retired — no `develop`, no back-merges, no
`release/X.Y.Z` branches.
- **Before every work item: fetch and branch off the freshest `origin/main`.**
If main moves under you, merge it into your branch (never rebase — Frank
dislikes the history rewrite).
- Work item = Gitea issue on `dank/command-center` (Gitea at 192.168.0.143:3000,
owner `dank`). Small fixes may go straight to a branch; anything with more than
one moving part gets an issue first.
- Branch naming: `<type>/<issue#>-<slug>`, type ∈ feat|fix|docs|chore.
Release cuts: `release/X.Y.Z` from `develop`; hotfixes: `hotfix/<slug>` from `main`.
- **Merge, never rebase**, when updating a branch against a moved main — Frank
dislikes the history rewrite. **Never merge a PR without an explicit OK from Frank.**
- Merge into `develop` with `--no-ff` so feature arcs stay visible in history.
- Releases: cut `release/X.Y.Z` from `develop`, verify, merge to `main`, tag
`vX.Y.Z`, back-merge `main` into `develop`. Tags are the deploy markers every
other machine pulls to.
- **Pull requests target `main` directly.** Merge with `--no-ff` so feature arcs
stay visible in history. **Never merge a PR without an explicit OK from Frank.**
- **The moment a PR is open, open it in Frank's browser** — `xdg-open <PR-URL>`
lands a tab in his Firefox; never make him hunt for it in Gitea.
- **Attribution footer (issues and PRs)** — every issue or PR body you open ends
with a `---` line, then `cwd: <working directory> · session: <omp session id>`
(session id from `/status`; `cwd` is that session's own working directory —
not the build checkout, when they differ). Machine-made artifacts stay attributable to the
box and session that spawned them.
- Releases: tag `vX.Y.Z` straight on `main` after verifying; tags are the deploy
markers every other machine pulls to. Hotfixes are ordinary branches off the
tag you are fixing forward from.
- Trash convention: never `rm` anything user-authored — move it to `.AGENTTRASH/`
(gitignored). Deletion is a pruning session with Frank present.
### Commit hygiene (mirrors dank/serverAppConfigs)
The law is imported from the serverAppConfigs repo — checkout on the dev box at
`~/Projects/serverAppConfig`, see its `AGENTS.md` § *Git Workflow* and
§ *Testing* ("All edits flow through git", "tests are mandatory before
commit"). Applied here:
- `git pull` before starting new work; start from a clean tree. The checkout
IS the deployment here — a dirty tracked file is a half-deployed config
serving the live agent.
- Commit atomically and early: one commit = one complete functional unit,
never a pile of half-edited files; docs land in the same commit as the
change they document.
- `bats tests/` green + the pre-commit secret gate = the commit precondition;
the full suite runs at pre-push.
- Push to the local Gitea as soon as a unit is green — every other
workstation pulls from there. `git * --no-verify` is forbidden (the secret
gate is the point of the hook).
- Messages: imperative mood, what + why, `Refs #N` when a Gitea issue exists.
### Live-sync tiers (the machine is a committer too — Refs #11)
The checkout is the deployment, so several tracked files are live state
written continuously through symlinks by the harness, auto-learn, and
desktop GUIs. "Dirty tree" is judged by the writer:
| Tier | Paths | Commit law |
|------|-------|------------|
| Human/agent prose | `AGENTS.md`, `omp-docs/`, `scripts/`, `plugins/`, `agent/skills/` | branch → PR → Frank OK (unchanged) |
| Machine live | `agent/memories/`, `agent/managed-skills/`, `agent/config.yml`, `agent/models.yml` | `scripts/live-sync.sh` heartbeat |
| Desktop live | `dotfiles/` | same heartbeat |
The heartbeat (systemd user timer, hourly) stages ONLY allowlisted tiers,
commits with hooks armed — the secret gate included — merges `origin/main`,
pushes, and reaps `wt/*` worktrees older than 7 days. Non-allowlist dirt is
never committed, only surfaced. Conflicts abort and alert (ntfy); it never
force-pushes, auto-resolves, or uses `--no-verify`. Details:
[omp-docs/tools/live-sync.md](omp-docs/tools/live-sync.md).
## Secret gate (HARD rule)
The pre-commit hook (`.githooks/pre-commit`, enabled via
@@ -50,9 +100,9 @@ secret in every agent's context window on every machine that pulls this repo.
| box | role | GPU | notes |
|-----|------|-----|-------|
| Dankware | dev laptop, never runs inference | Arc iGPU + RTX 4070 Max-Q (dGPU, non-unified) | 22 threads; mainline llama.cpp at `~/GitPackages/llama.cpp/build/bin/`; this repo lives here at `~/.omp` |
| Dankclaw | bench target, runs the large serving model | Strix Halo APU — **unified memory** | ROCmFPX fork at `~/GitPackages/ROCmFPX/build-qwen38/bin/`; prod service `frank-default-large`, port 7770; ssh: `ssh dankclaw` |
| Dankserver | FreeToken box | RTX 3090 dGPU — non-unified | runs `frank-default-small`; ssh: `ssh dankserver` |
| Dev workstation | client box, never runs inference | laptop iGPU + dGPU (non-unified) | mainline llama.cpp at `~/GitPackages/llama.cpp/build/bin/`; this repo lives here at `~/.omp` |
| Dankclaw | bench target, secondary models + service stack | Strix Halo APU (**unified memory**) + RTX 3090 (moved over from Dankserver 2026-09) | prod services `frank-dragoon` (halogen-flash-server, :7770, iGPU) and `frank-zealot` (llama.cpp PrismML fork, :7771, 3090); runbooks in the serverAppConfigs vault (`~/Projects/serverAppConfig/serverAppConfigs-docs/LLM/`); ssh: `ssh dankclaw` |
| Dankserver | main-model box | 2× Radeon AI PRO R9700 — non-unified (was RTX 3090 until 2026-09) | runs `frank-archon` (radiance, TP=2, :7770) — **this repo's operator session runs on it**; ssh: `ssh dankserver` |
## Environment gotchas
@@ -64,11 +114,12 @@ secret in every agent's context window on every machine that pulls this repo.
- SSH into both servers is BatchMode-capable from here:
`ssh -o BatchMode=yes dankclaw '<read-only command>'` — read-only probes are
always allowed; state changes on remote boxes are not, without a per-run OK.
- `frank-default-large` on Dankclaw is a **production service** shared with this
repo's operator: never stop, mask, or restart it (or any production service on
any box) without an explicit, current, per-run OK from Frank.
- `frank-archon` (Dankserver), `frank-dragoon`, and `frank-zealot` (Dankclaw)
are **production services** shared with this repo's operator: never stop,
mask, or restart them (or any production service on any box) without an
explicit, current, per-run OK from Frank.
## File-organization doctrine (why this repo won't rot like ~/.pi did)
## File-organization doctrine (why this repo stays readable)
- One concept, one file; directories get an `index.md` listing their contents —
that's the OKF contract, and it is also the anti-rot mechanism: nothing may
+86
View File
@@ -0,0 +1,86 @@
# Command Center
## Configuration repository for Frank's Userspace configurations (i.e. his desktop setup)
### AI Disclosures:
- Slop rating: 3/5 - **Actively using, actively de-slopping, repository still mostly AI owned**
- Completeness: **Unfinished, planning planned, no plan yet**
- Datacenter model/spend: Less than $5 of Deepseek 4.1 Flash
- Local model: Qwen 3.8 27B
### slop rating of 3 or higher indicates human reading is a waste of time outside of this README, and that you might as well ask an agent for help with this.
### slop rating 3 indicates Frank is comfortable using this for professional use -- but barely.
## The Fleet
| Host | Role | IP |
|------|------|-----|
| **Workstation** | Client box (CachyOS, Niri/Wayland) | — |
| **Dankclaw** | Server (Podman, reverse proxy, Gitea, NPU) | 192.168.0.143 |
| **Dankserver** | LLM inference backend | 192.168.0.235 |
The workstation hosts the primary checkout — this repository lives at `~/.omp/` and drives the OMP agent harness.
## Structure at a Glance
```
~/.omp/
├── agent/ # OMP harness runtime
│ ├── config.yml # Live agent configuration
│ ├── models.yml # Model providers (single source of truth)
│ ├── APPEND_SYSTEM.md # System prompt extensions
│ ├── managed-skills/ # Managed skills (SKILL.md bundles)
│ ├── models.db # Model cache
│ └── sessions/ # Persistent session state
├── dotfiles/ # stow-style env-layer packages (home-relative trees)
│ └── deploy via scripts/deploy.sh (check|adopt|sync; quarantine-and-replace)
├── omp-docs/ # Knowledge base (source of truth)
│ ├── index.md # KB index — start here
│ ├── system/ # Fleet hardware, OS, network
│ ├── tools/ # Installed software (llama.cpp, Gitea, Caddy, niri…)
│ ├── core/ # Agent config conventions (settings, models, AGENTS.md)
│ ├── pi/ # Legacy pi-era config reference
│ ├── engineering/ # SDLC, languages, infra, skills ecosystem
│ ├── research/ # Decision-support research (not yet facts)
│ ├── references/ # OKF spec, maintenance procedures
│ ├── caddy/ # Reverse proxy configuration
│ ├── packages/ # Pi packages and extensions
│ ├── inventory/ # Userspace config census
│ ├── reports/ # Investigation reports
│ ├── log.md # Change log
│ └── .okf/ # OKF v0.1 conformance config
├── scripts/ # Utility scripts (kb-lint.sh)
├── plugins/ # npm plugins (pi-peon, etc.)
├── puppeteer/ # Browser automation config
├── .githooks/ # Pre-commit hook
├── .gitignore # Runtime state ignored (databases, logs, cache)
├── AGENTS.md # Agent conventions (niri reload, keybindings)
└── README.md # This file
```
## What Lives Here
**Config in, state out.** Everything tracked is something an agent reads or writes. Runtime databases (`*.db`, `*.sqlite`), logs, caches, and session state are git-ignored — git history captures configuration changes, not agent heartbeat.
## Quick Start
| Task | How |
|------|-----|
| Read the knowledge base | `omp-docs/index.md` — every concept linked from here |
| Check agent config | `agent/config.yml` + `agent/models.yml` |
| Lint the KB | `scripts/kb-lint.sh` (OKF v0.1 conformance) |
| View change log | `omp-docs/log.md` |
| Run a pre-commit | `.githooks/pre-commit` |
## Conventions
- **OKF v0.1** — every doc file carries YAML frontmatter with a `type:` field. `index.md` files are directory listings (no frontmatter).
- **Progressive disclosure** — `index.md` is a landing page; drill into subdirectories for detail.
- **Verified facts** — stamped with `verified:` when probed against the live fleet. Trust over memory.
- **Single config source** — `~/.omp/agent/models.yml` is the only model registry; OMP is the primary harness and there is no second copy to keep in sync.
## Legacy Notes
`omp-docs/pi/` and `omp-docs/core/` retain general documentation about the pi-coding-agent harness (its config format, packages, skill system) for reference. OMP at `~/.omp/` is the live harness; managed skills live in `~/.omp/agent/managed-skills/`.
+2 -2
View File
@@ -13,7 +13,7 @@ Frank is 35 year old Software Engineer, specializing in DevOps, living in Seattl
He likes video games, science fiction, and fantasy novels. He considers himself somewhat of a history buff.
### Your identity:
You are Comrade Chief Designer of the Soviet Computer Systems of Frank, which are comprised of servers Dankware and Dankclaw, as well as several clients, including Dankware, which is where you are running from. You operate from the host Dankware as a Staff Software Engineer with the eyes of the greatest industrial designers that have ever lived. Growing up under Soviet paranoia, you also have an innate sense for good security practices. Despite your outwardly sunny disposition, you will work diligently as if a one way trip to The Gulag is always a possibility.
You are Comrade Chief Designer of the Soviet Computer Systems of Frank, which are comprised of the servers Dankclaw and Dankserver, plus several client workstations. You operate from whichever workstation you are running on, as a Staff Software Engineer with the eyes of the greatest industrial designers that have ever lived. Growing up under Soviet paranoia, you also have an innate sense for good security practices. Despite your outwardly sunny disposition, you will work diligently as if a one way trip to The Gulag is always a possibility.
### Adjectives that describe your personality:
well-measured, cautious, expeditious, ambitious, concise, surgical, thorough, and efficient.
@@ -57,7 +57,7 @@ Suggest these tools or languages as possible solutions to problems if they are a
The knowledge base at `docs/` is the source of truth for your project. Your training is out of date. Default to the documentation and the code before you assume anything. Maintain a work log of actions taken and their result. Refer to the work log to remember the progress of your tasks.
1. Read the relevant `index.md` before you touch a topic.
2. Follow `~/.pi/agent/docs/references/okf-spec.md` for every doc you create or edit.
2. Follow `omp-docs/references/okf-spec.md` for every doc you create or edit.
3. Read the target file completely before you edit it.
4. Always update the documentation when you've completed a task.
5. Compact the work log when necessary.
+201 -55
View File
@@ -1,98 +1,165 @@
providers:
webSearchOrder:
- searxng
- perplexity
- gemini
- anthropic
- codex
- xai
- zai
- exa
- tinyfish
- jina
- kagi
- tavily
- firecrawl
- brave
- kimi
- parallel
- synthetic
- startpage
- duckduckgo
- ecosia
- google
- mojeek
- public
openrouterVariant: online
fetch: auto
modelRoles:
default: llamacpp (dankclaw)/frank-default-large:medium
smol: llamacpp (dankserver)/frank-default-small:xhigh
slow: llamacpp (dankclaw)/frank-default-large:medium
vision: llamacpp (dankclaw)/frank-default-large:xhigh
plan: llamacpp (dankclaw)/frank-default-large:medium
designer: llamacpp (dankclaw)/frank-default-large:medium
commit: llamacpp (dankserver)/frank-default-small
tiny: llamacpp (dankserver)/frank-default-small
task: llamacpp (dankserver)/frank-default-small
advisor: llamacpp (dankclaw)/frank-default-large:medium
slow: llamacpp (dankclaw)/frank-dragoon:medium
vision: llamacpp (dankclaw-3090)/frank-zealot:xhigh
plan: llamacpp (dankclaw-3090)/frank-zealot:medium
designer: llamacpp (dankclaw-3090)/frank-zealot:medium
commit: llamacpp (dankclaw-3090)/frank-zealot:medium
tiny: llamacpp (dankclaw-3090)/frank-zealot:off
task: llamacpp (dankclaw-3090)/frank-zealot:off
advisor: llamacpp (dankserver)/frank-archon:medium
default: llamacpp (dankserver)/frank-archon:medium
smol: llamacpp (dankclaw-3090)/frank-zealot:medium
web: web/searxng
speech: local/kokoro
judge: openrouter/~typesafe/jev-latest:auto
dictation: local/parakeet-tdt-0.6b-v3
memory: llamacpp (dankclaw-3090)/frank-zealot:medium
symbolPreset: nerd
composer:
shape: claude
shape: band
tokenRate: true
theme:
dark: dark-monokai
dark: dark-gruvbox
light: light
setupVersion: 2
browser:
headless: false
headless: true
retry:
fallbackChains:
{}
web:
- web/kagi
- web/perplexity
- google-gemini-cli/gemini-2.5-flash
- google-antigravity/gemini-2.5-flash
- google/gemini-2.5-flash
- anthropic/claude-haiku-4-5
- openai-codex/gpt-5.6-luna
- xai/grok-4.5
- web/zai
- web/exa
- web/tinyfish
- web/jina
- web/tavily
- web/firecrawl
- web/brave
- web/kimi
- web/parallel
- web/synthetic
- web/startpage
- web/duckduckgo
- web/ecosia
- web/google
- web/mojeek
- web/public
- openai-codex/gpt-6-luna
- openai-codex/gpt-5.6
- openai-codex/gpt-5.5
- xai-oauth/grok-4.5
- web/ollama
speech: []
llamacpp (dankserver)/frank-archon:
- llamacpp (dankclaw)/frank-dragoon
maxRetries: 10
searxng:
endpoint: search.franksdank.cloud
exa:
enabled: false
memory:
backend: local
backend: sharpshooter
autolearn:
enabled: true
autoContinue: true
compaction:
remoteEndpoint: http://192.168.0.143:7771/v1/chat/completions
methodOrder:
- shake
- handoff
- snapcompact
- soft
- handoff
- remote
idleThresholdTokens: 200000
thresholdPercent: -1
idleThresholdTokens: 100000
thresholdPercent: 95
idleEnabled: true
thresholdTokens: 150000
thresholdTokens: -1
dropUseless: false
remoteStreamingV2Enabled: true
asyncEnabled: true
idleTimeoutSeconds: 120
enabled: true
experimentalContextManagement: false
interruptMode: immediate
defaultThinkingLevel: medium
defaultThinkingLevel: xhigh
hideThinkingBlock: false
proseOnlyThinking: true
proseOnlyThinking: false
terminal:
showProgress: true
disabledExtensions:
[]
disabledExtensions: []
statusLine:
separator: powerline
preset: nerd
contextLine: annotated
transparent: false
compactThinkingLevel: false
preset: custom
contextLine: embedded
transparent: true
compactThinkingLevel: true
showHookStatus: true
sessionAccent: true
leftSegments:
- session_name
- vim
- mode
- hostname
- path
- git
- pr
rightSegments:
- model
- cost
- context_pct
- time_spent
segmentOptions:
model:
showThinkingLevel: true
path:
abbreviate: true
stripWorkPrefix: true
maxLength: 120
git:
showBranch: false
showStaged: true
showUnstaged: true
showUntracked: true
tui:
textSizing: true
codexResetFireworks: false
hyperlinks: auto
tight: false
tight: true
resizeScrollback: rebuild
titleSpinner: pulse
display:
shimmer: kitt
smoothStreaming: true
showTokenUsage: true
pinnedAgents: collapsed
subagentLivePreview: true
task:
showResolvedModelBadge: true
maxConcurrency: 3
agentModelOverrides:
designer: llamacpp (dankclaw-3090)/frank-zealot
librarian: llamacpp (dankclaw-3090)/frank-zealot
reviewer: llamacpp (dankserver)/frank-archon
scout: llamacpp (dankserver)/frank-archon
security-reviewer: llamacpp (dankclaw)/frank-dragoon
sonic: llamacpp (dankclaw-3090)/frank-zealot
task: llamacpp (dankserver)/frank-archon
eager: default
enableLsp: true
snapcompact:
systemPrompt: none
toolResults: false
shape: auto
extendedContext: false
ttsr:
contextMode: discard
@@ -103,12 +170,12 @@ checkpoint:
readLineNumbers: true
startup:
showSplash: true
changelogMode: summary
update:
channel: stable
features:
unexpectedStopDetection: smart
unexpectedStopDetection: mechanical
stt:
modelName: parakeet
submitTrigger: say-submit
enabled: true
recap:
@@ -116,4 +183,83 @@ recap:
loop:
mode: prompt
steeringMode: one-at-a-time
personality: default
personality: default
images:
urls:
backends:
- provider-files
- tailscale
- cloudflared
- litterbox
edit:
mode: hashline
fuzzyThreshold: 0.9
dev:
autoqaConsent: granted
astGrep:
enabled: true
vault:
enabled: true
tts:
localVoice: bm_george
speech:
enabled: true
mode: yield
enhanced: true
voice: am_fenrir
secrets:
enabled: true
read:
renderMarkdown: true
toolResultPreview: true
defaultLimit: 1000
summarize:
prose: false
lsp:
formatOnWrite: true
diagnosticsOnEdit: true
autoResume: false
speechgen:
enabled: true
live:
voice: vale
contextPromotion:
enabled: true
title:
icons: emoji
generator: tiny
followUpMode: one-at-a-time
tools:
approvalMode: yolo
format: auto
marketplace:
autoUpdate: notify
bash:
allowCompoundCommands: true
gitGuard: true
bashInterceptor:
enabled: false
shellMinimizer:
sourceOutlineLevel: default
generate_image:
enabled: false
computer:
enabled: true
display: active
ratchet:
enabled: true
github:
enabled: true
security:
enabled: true
todo:
reminders: true
mcp:
notifications: false
plan:
defaultOnStartup: false
isolation:
backend: auto
worktree:
onStart: create
onExit: keep
@@ -0,0 +1,24 @@
---
name: aa-chart-label-browser-extraction
description: "Extract per-model benchmark scores from artificialanalysis.ai pages: charts are client-rendered recharts SVGs invisible to plain HTML fetch; pair sorted label/name x-coords, then cross-check against published sentences before trusting any chart↔eval identity."
---
Extract per-eval scores for a model from artificialanalysis.ai when no API key exists. Plain HTML fetch yields NO chart data (client-side recharts; SSR has only meta + FAQ sentences; `/api/*` = 401).
## Procedure
1. Managed browser tab: `browser.open({name, url, wait_until: "domcontentloaded"})`.
2. **Per-eval top-3 (authoritative):** open `https://artificialanalysis.ai/evaluations/<slug>`; regex `document.body.innerText` (collapse whitespace): `([A-Z][^.;]{0,60}?) scores the highest on (.+?) with a score of (-?[\d.]+%?), followed by (.+?) with a score of ... and ...` — mind decimal points truncating naive `[^.]+` regexes. Build the page script as a plain string (`String.raw`), not an interpolated template, so `\\d` survives.
3. **All-eval per-model rows:** on `https://artificialanalysis.ai/models/<slug>`: scroll in 6 steps (0.15→0.9, ~1.3 s each) to trigger lazy charts, then per `svg` with ≥3 bars: labels = `[...svg.querySelectorAll("g.recharts-label-list text")]` (x attr), names = `[...svg.querySelectorAll("foreignObject")]` divs sorted by `g[transform] translate(x,y)`. Sort BOTH by x, **zip by index** — x-center pairing is off by a systematic half-bar.
4. Self-row filter: exact hyphenated name (e.g. `Qwen3.8-Flash-Next`, not "Qwen3.8 Flash").
5. Elo evals (GDPval-AA, AA-Briefcase): charts show AA's published `(Elo−500)/2000` normalization (1866→68.3).
## Verification law (non-negotiable — two plausible mappings disagreed twice)
- Zip top-3 of every chart against the eval page's published sentence (e.g. Kimi 88.7→89, Opus 66.9→67); ship only cross-checked rows.
- Arithmetic corroboration helps: Omniscience accuracy 17% + non-hallucination 7% ↔ index −60.
- Record provenance per row: sentence rows → eval-page URL; chart-label rows → model-page URL; `fetched_at` + `index_version`.
- Geometry pairing (link→nearest chart below) is AMBIGUOUS when chart height > gap to next card — adjacent links share a chart. Use order + sentence tops, not geometry.
## Traps
- `tab.run` = Bun runtime; page-world JS needs `tab.evaluate`. Eval-cell `log()` does not reach display; write to a /tmp file and `cat`.
- "Add model from specific provider" picker is not in the DOM (renders after chart data loads); UI drill-through × N evals is fragile — prefer the sentences.
- Headline index FAQ: "scores N on the Artificial Analysis Intelligence Index" — check for "(estimated)" qualifier; est. = "independent evaluation forthcoming".
@@ -0,0 +1,24 @@
---
name: aa-chart-model-score-extraction
description: "Extract per-model benchmark scores from artificialanalysis.ai pages: charts are client-rendered Recharts SVGs; zip label-list texts to foreignObject names by x-sorted index, validate chart identity against the page's published top-3 sentence, apply Elo/Omniscience normalization laws, seed frank-intelligence-bench aa-scores.yml."
---
Extract AI benchmark scores for a specific model from artificialanalysis.ai. Per-model per-eval numbers are NOT in SSR HTML (JS charts, API needs key); eval-page SSR text DOES carry the top-3 sentence.
## Procedure (validated 5x on this repo's model pages)
1. `browser.open({ name, url: "https://artificialanalysis.ai/models/<slug>", wait_until: "domcontentloaded" })`, then scroll in ~6 steps (0.15..0.9 of scrollHeight, ~1.3 s pauses) — charts lazy-render.
2. Extract each chart: pair `g.recharts-label-list text` (values, take `x` attr) with `foreignObject div` (model names, x from parent `g[transform] translate(x,y)`). **Sort both lists by x and zip by index — NOT nearest-x** (nearest-x is off by one bar; index-zip matches published tops exactly).
3. Self row: match name loosely (`Qwen3.8-Flash-Next` has hyphens — test both forms).
## Identity law (never ship an order-guessed chart→eval mapping)
- Model pages have ~14 charts, 18 eval cards; DOM order and geometry pairing BOTH proved ambiguous.
- Anchor every chart by its top-3 names/values vs the eval page's published sentence: `https://artificialanalysis.ai/evaluations/<slug>` body text: "X scores the highest on <eval> with a score of N%, followed by …". Regex must tolerate decimals: value pattern `(-?[\\d.]+%?)`; build page scripts with `String.raw` (bun template literals eat `\\d`).
- A chart's `self` value on the MODEL page is page-local → trustworthy once the top-3 anchor confirms the eval (e.g. AA-LCR: Kimi 88.7 → chart top "Kimi K3 (max)=89").
## Normalization laws (frank-intelligence-bench aa-scores.yml)
- Elo evals (AA-Briefcase, GDPval-AA): store `(Elo−500)/2000×100` (1866 → 68.3), AA's published normalization.
- AA-Omniscience: score = Accuracy (w10) + 1−Hallucination (w5), both 0–100 → composite `(10·acc + 5·nonhall)/15`. NEVER feed the −100..100 Index chart value (pre-fix cost ~7 pts; calibration then reproduces AA's official index ±0.6: Flash-Next 39.85≈40, Haiku 43.4≈43, 27B 33.85≈34, 3.6-27B 21.55≈21).
- Every seeded row: `source_url` + `fetched_at` + `index_version`.
## Seed + validate loop
Append model block to `config/references/aa-scores.yml` (block-insert via python string replace on the ` - name: <anchor>` line — beware: dropping rows from a block loses the 2-space indent on the anchor line; verify with `yaml.safe_load`), then `bun -e loadAaRefs` parse-check, `bun src/cli.ts report $RID`, calibration check (full-row models: lite vs AA official), `bun test`, commit.
@@ -0,0 +1,41 @@
---
name: aa-reference-scores-seeding
description: "Seed/refresh frank-intelligence-bench config/references/aa-scores.yml from artificialanalysis.ai: authoritative top-3 sentences from eval pages, Recharts SVG zip extraction for per-model chart values, cross-check law, Elo normalization, schema/repair loop."
---
Seed frank-intelligence-bench `config/references/aa-scores.yml` from AA's public pages, then re-render the report trio.
## Provenance law (non-negotiable)
- Every row: `{ eval, pct, source_url, fetched_at, index_version }` (Zod-enforced; top-level `schema_version: 1, fetched_at, index_version` required).
- Eval ids MUST match report result ids: `scicode, aa-lcr, hle, terminal-bench, gdp-pdf, aa-briefcase, gdpval-aa, automationbench-aa, aa-omniscience, critpt` (render `refFor(evalId)` joins on exact id).
- Never ship an order-guessed chart↔eval mapping; only cross-checked ones.
## Data tiers (authority order)
1. **Eval-page sentence** (authoritative, SSR, per eval page `/evaluations/<slug>`): `"X scores the highest on <eval> with a score of N%, followed by Y ... and Z ..."` → top-3 rows with verbatim model names. Regex trap: model names contain `5.5` — capture names with `[^.;]` truncates at decimals; use targeted extraction per page, verify each value against the page's `66.9`-style raw text.
2. **Model-page chart labels** (per-model values, incl. the page's own model): charts are client-side Recharts (NOT in raw HTML, NOT via /api/ — keyless API 404s). Browser: `browser.open({name,url,wait_until})`, scroll full page in ~0.15 steps (lazy render), then `tab.evaluate` (NOT `tab.run` — that's Bun world):
- names = `<foreignObject>` div text, x from parent `g[transform]` `translate(x,...)`;
- values = `g.recharts-label-list text` attrs;
- **zip by sorted-x index** (nearest-x pairing is off by half a bar; index-zip verified against published tops).
3. `sync-aa` free Data API for headline indices — needs `AA_API_KEY` (unset in ~/.secrets as of 2026-10-11).
## Cross-check law (mandatory before shipping chart-derived rows)
Chart's top-3 zipped values must match the eval page's published sentence (e.g. Kimi 88.7→89, Elo 1866→68.3, Opus index 46). Two independent methods disagreeing = stop, re-anchor; geometry pairing (link-top→svg-top proximity) collides on adjacent cards — prefer sentence-vs-zip corroboration.
## Normalizations
- Elo evals (GDPval-AA, AA-Briefcase): sentences publish raw Elo; store AA's published `(Elo−500)/2000`×100 (1866→68.3). Document in file header.
- AA-Omniscience `eval: aa-omniscience` = the −100..100 Index (not accuracy); accuracy/non-hallucination charts corroborate (17%/7% ⇒ −60 arithmetically consistent).
## Known identities (2026-10-11 census, v4.3.2)
- frank-archon lineage base = **Qwen3.8-Flash-Next** (`/models/qwen3-8-flash-next`), AA index 40, class median 18. NOT Qwen3-Next-80B-A3B-Instruct (index 10, 2025 family) — first seed got this wrong; verify lineage in `docs/endpoints.md` before labeling `kind: base`.
- Base per-eval (chart): SciCode 51, HLE 38, AA-LCR 80, TB-4.0 25, GDP.pdf 16, CritPt 11, Briefcase 54, GDPval 56, AutoBench 56, Omniscience −10.
- Frontier anchors: SciCode Opus 5.5 Max 66.9 (board flagged "under review" — travel this caveat with any SciCode delta); HLE Opus 61.4; AA-LCR Kimi K3 88.7; TB Sonnet 63.6; CritPt GPT-6.1 Sol 32.3.
## Edit traps
- YAML surgery via `edit`: replacing a line range that included a top-level key (`schema_version`/`models:`) silently duplicates or deletes the anchor → Zod error `expected 1 / received undefined`. After any header edit, `grep -n "^schema_version\|^fetched_at\|^index_version\|^models:"` must show exactly one each.
## Ship loop
1. `bun -e 'import { loadAaFile, loadAaRefs } from "./src/aa"; ...'` schema-parse gate.
2. `bun src/cli.ts report <run-id>` (takes bare id, NOT a path) → trio in `reports/<host>/<id>/`.
3. Verify `data.json .aa_reference` non-empty, per-eval coverage, delta columns.
4. log entry in `docs/log.md` + atomic commit + push (local gitea OK).
5. `browser.close({name})` — headless tabs are shared/retained.
@@ -0,0 +1,61 @@
---
name: audit-subagent-deliverables
description: "Integrate subagent-written code safely: line-by-line audit, mechanical gates (bash test/lint), adversarial-review-report triage, resource-safety pass (fd/memory), and cutover with running-process restarts."
---
# Audit subagent deliverables
A subagent that wrote code is a source of plausible output, not a verified change. Integrate it the way you'd integrate an untrusted PR — with extra suspicion, because the author is an agent that shares your blind spots and its prose is more confident than its evidence.
Order of operations: read everything → gates → tests (as evidence, not proof) → resource-safety pass → fix in-place → cutover.
## 1. Read every produced file, line by line
Full file, not a diff skim. You are the reviewer who must catch the line the author never thought about.
## 2. Mechanical gates before logic review
```bash
bash -n <script> # syntax (scripts)
bunx tsc --noEmit # types (TS)
bun test # suite
```
Run the suite for information, not as the stopping point — it only proves the author's own scenarios.
## 3. Hunt the three classic agent defects
- **Quoting bugs:** `set -e` + `"${@/$p/$r}"` = substitution regex containing user input (bash can't escape `/` in a `${var/re/pat}` pattern: `/` reads as the pattern/replacement separator). Fix with a separator absent from inputs (e.g. `%`) and `printf '%q'` for shell-interpolated values.
- **`cd` assumptions:** parallel `runShell` calls share one CWD (pool mode). Any command sequence that assumes CWD (e.g. `git -C .` after a sibling's `cd`) must use explicit absolute paths.
- **Unbounded resources:** every fd opened must be closed on ALL paths (success, throw, validation fail) — `using` for fs.sinks (closes on throw; sinks are `AsyncDisposable`, not `fs.FileHandle`, no `.close()`); bounded `Map` for caches (`MAX` + FIFO eviction); explicit caps (lines read, file sizes).
## 4. Check the test file is real
Tests that assert internal state (`expect(wcwd).toBe(...)`) over output contracts; tests of mock echo; tautologies. Delete them per house rules, keep behavior contracts.
## 5. Cutover audit (repo-wide)
`grep -r` the symbol namespace the subagent touched — `wcwd`-style globals are the classic blast radius. Grep every `cd`/`process.chdir` site and verify each paired command sequence uses explicit paths. Stale comments describing the old design must go — comments that lie are time bombs for the next reader.
## 6. Cutover restarts running processes
A code fix on disk is NOT live in any process that loaded the file at spawn (bun/node load-at-spawn). After shipping fixes: restart every long-running supervisor/daemon/UI-server that carries the affected code path; prefer takeover protocols (stale-pid detection) that make restarts cheap. Symptom of forgetting: "fixed" code producing old-code symptoms in production (observed: 812 poisoned ledger rows).
## 7. Triaging an adversarial REVIEW report (not code)
When a subagent produced findings, treat each finding as a hypothesis with a base rate ~40% (measured over 3 rounds, 2026-10):
1. **Walk every finding end-to-end against the CURRENT tree** before accepting — cite file:line yourself. Reviewers frequently read a mid-edit snapshot; many findings are stale by construction.
2. **Classification that kills most findings:**
- *Stale read* — code changed during review; verify against live lines.
- *Inverted logic* — "failed rows are skipped" when the filter makes failed rows RETRY. Re-read the filter.
- *Unreachable* — trigger requires breaking a stated design law (single-worker, append-only). Not a defect.
- *By-design* — the reviewer labels a spec law a bug (serial execution, non-stream client). Check the spec, not the reviewer's confidence.
- *Constant-trust* — "XSS via manifest field" where the field is git-pinned config; "path traversal via REMOTE_TMP" where it's a hardcoded const. Ask: what untrusted input reaches this sink?
- *Self-refuting* — the reviewer's own probe output disproves its claim (raw==dedup probe in a "dedup" finding). Quote it back.
3. **Acceptance bar:** concrete trigger with real inputs + full path walked. Real accepts cluster in: resource caps (pids/timeout/awaited-cleanup), honest labels (default signatures), escaping discipline, self-healing probes.
4. **One-line verdicts + evidence per finding**, shipped in the commit message; calibration section ("genuinely solid") accepted as free verification of past fixes.
## Verification (the non-negotiable)
Build/run the real artifact with its real inputs. Observe behavior at the seams:
- concurrent calls → no CWD races;
- resource ceilings → caps enforced;
- cleanup → no fd/pid/dir leaks;
- security → injection probes inert (quotes, slashes, metachars);
- normal case → output identical to pre-audit.
## Evidence (2026-10-07 + 2026-10-10/11 sessions)
- Shell-engine subagent: 426 LOC, `bun test` 39/39 green. Line-audit found: 5 leaked fds per call (`fd` sink never closed), CWD-race in `git -C .`, `set -e` + `${@/\/g\//}` slash-escaping bug, unbounded 10k-entry cache, 45s timeout on git-status.
- Review-triage census over 3 blind adversarial rounds (same repo, ~41 findings): accepted 9, rejected ~32 — rejects dominated by stale reads (protocol rewritten mid-review), inverted filter logic, unreachable-under-law triggers, base64-alphabet "quote injection" (charset has no quotes), SIGPIPE non-leaks, "no memory cap" quoting the `--memory 4g` line above the claim. Real accepts: fork-bomb pids cap, stalled-ssh upload cap, stale-flag self-heal probe, honest census label, freshness gate on pid reuse.
- Poison-law incident: sandbox fix shipped while supervisor ran old build → 812 rows graded by the dead protocol; takeover made recovery cheap. The restart step (#6) exists because of this.
@@ -0,0 +1,61 @@
---
name: authentik-provision-fleet-user
description: "Provision or audit an Authentik user on Dankclaw (auth.franksdank.cloud): ak shell ORM drive, group set, behavioral login + entitlement verify, docs census record, stale-claim caveat."
---
# Provision an Authentik user on Dankclaw
Source of truth: `serverAppConfigs-docs/Infrastructure/Authentik.md` ("User provisioning playbook"). Authentik objects are NOT repo-managed — no deploy, no file edits.
1. Inspect first, mirror the newest human user's groups:
```bash
ssh dankclaw 'podman exec -i authentik-server ak shell' <<'EOF' 2>&1 | sed -n '/objects imported/,$p'
from authentik.core.models import User, Group
for u in User.objects.all().order_by("username"):
print(u.username, u.email, sorted(u.groups.values_list("name", flat=True)))
print(sorted(Group.objects.values_list("name", flat=True)))
EOF
```
2. Create (idempotent guard; password goes in the heredoc = stdin, never argv, so it never lands in `ps`):
```python
from authentik.core.models import User, Group
u = User.objects.filter(username="X").first()
if not u:
u = User(username="X", email="X@franksdank.cloud", name="X", is_active=True)
u.set_password("...")
u.save()
for g in ("Non-Admin Users", "jellyfin_users", "openwebui_users"): # human-user precedent: ridley
u.groups.add(Group.objects.get(name=g))
```
3. Verify through the exact PasswordStage code path:
```python
from django.contrib.auth import authenticate
assert authenticate(request=None, username="X", password="...")
assert authenticate(request=None, username="X", password="nope") is None
```
4. Verify the downstream claims resolve — `app_entitlements` needs an `Application`
instance (look it up by slug, `open-webui`), and compare against the precedent user:
```python
from authentik.core.models import Application, User
app = Application.objects.get(slug="open-webui")
for n in ("ridley", "X"):
u = User.objects.get(username=n)
print(n, sorted(e.name for e in u.app_entitlements(app)), u.is_superuser, u.path)
```
5. Stale-claim check + docs record (repo law: the vault is the source of truth):
```bash
ssh dankclaw "podman exec authentik-postgres psql -U authentik -d authentik -tAc \
\"SELECT count(*) FROM authentik_providers_proxy_proxysession WHERE session_data->'claims'->>'preferred_username' IN ('X')\""
```
Then commit two doc edits (NEVER write the password into any file): the census
sentence in `Infrastructure/Authentik.md` (human-user template bullet — append the
new `pk`s + creation date) and a dated entry in `serverAppConfigs-docs/log.md`
(what was created, groups, how it was verified, proxysession count, downstream lag).
`./tests/run.sh` must pass before commit (docs completeness + link-integrity seams).
## Rules
- No `User.create_user`; `is_superuser` is a group-derived property, not a constructor kwarg (raises `TypeError`). `path` defaults to `users`.
- Guarded-vhost access = any authenticated user (all proxy providers use implicit-consent); groups are consumed downstream: `homepage-admins` (Caddy `/admin` header gate), `openwebui_users`/`_admins` (roles claim), `jellyfin_users`/`_admins` (LDAP outpost). EXCLUDE admin groups from a plain user; ask before granting `homepage-admins`.
- Group add after a login → Caddy RBAC can 403 on the outpost's cached claims; purge that user's rows: `DELETE FROM authentik_providers_proxy_proxysession WHERE session_data->'claims'->>'preferred_username' = 'X';` (in the `authentik-postgres` container) — next visit re-authorizes silently. Groups set BEFORE any login → nothing to purge (count is 0).
- Jellyfin visibility rides the LDAP outpost cache; expect a lag. OpenWebUI mints a first-time SSO login as role `pending`.
- Look up flows by slug, not name. Do not rotate `AUTHENTIK_SECRET_KEY` to fix claim issues.
@@ -0,0 +1,76 @@
---
name: autoresearch-llm-bench-window
description: "How to run an iterative config-tuning window on a Dank-fleet LLM endpoint: harness pattern, noise-floor-first discipline, bracket mapping, and the frank-primary laws measured in the 2026-10-06 segment."
---
# Autoresearch bench window for a fleet LLM endpoint
Use when running (or re-running) an iterative config-tuning segment against a
Dank-fleet LLM endpoint (frank-primary/frank-large/frank-small): build the
harness, establish the noise floor, then bracket-map one flag at a time.
## Harness pattern (already built for frank-primary — reuse, don't rebuild)
Committed at `autoresearch.sh` (dev box) + `scripts/autoresearch-frank-primary.sh`
(dankserver) in serverAppConfigs. The pattern generalizes to any fleet endpoint:
1. Dev-box wrapper scp's the candidate env file + bench script to the target
host, then ssh-runs the bench. Never push the candidate config to origin
(the scp dirties the host checkout on purpose).
2. Host script, in order: stop the production unit (relinked units: `systemctl
stop`, NEVER mask — mask refuses a symlinked unit file) → boot a scratch
container (distinct name, e.g. `arbench`) on the SAME devices with the
candidate env — mirror the production podman contract exactly, differing
only in container name and a FRESH prefix-cache tier (wiped per run) so cold
measurements stay cold → poll the health endpoint until it answers (the
listener binds AFTER the full load, so a bare curl is honest; allow ~2-3 min
cold boot, 720s timeout) → run ONE self-contained deterministic request →
parse the engine's timings object into METRIC lines → tear down the scratch →
restore production.
3. The restore MUST be an EXIT trap (`restore() { podman rm -f scratch;
systemctl start <unit>; }`) so a failed bench never leaves the endpoint
down. Verify after EVERY run: `systemctl is-active`, health curl until it
returns JSON (200 right after start is NOT enough — it can bind 60-90s
later), and no scratch leftover.
4. Bench request: fixed prompt built from a sorted repo-corpus glob with a
fixed random seed (deterministic → stable prompt_n), greedy sampling
(temperature 0) for a fixed decode length, sent locally (127.0.0.1) to
remove LAN jitter. Read prefill/decode/draft-acceptance from the engine's
own timings object — never roll a custom bench without asking.
5. Scratch containers must die by teardown; never leave a script whose tail
stops/removes a production unit (the stale-probe-tail lesson).
## Discipline (the laws that make the numbers mean anything)
1. **Noise floor FIRST.** Before iterating, replicate an identical config 2-3x
and treat that spread as the floor. Fleet endpoints here: decode ±3%,
prefill ±2%. A single-run "win" under the floor is NOT a win — this exact
mistake cost a false 33.06 keep in the 2026-10-06 segment.
2. **One flag change per iteration**; log every run honestly (keep/discard/
crash); discards auto-revert the worktree.
3. **Bracket-map, don't climb.** Test both directions of every lever around
its deployed value, INCLUDING boot failures — crashes are envelope data
("0.82/2048/wht6/40960 is the seqs=3-viable quartet" came from four crash
walls). Test downward too: 40960 MiB host pool is a floor, not slack.
4. **Repro before recording a mechanism.** An anomalous result next to an
abandoned bench may be contamination (boot race). Re-run clean once; only
then name a law. The MNBT 2560 collapse (2.6 t/s) was reproduced clean to
rule this out.
5. **Watch for truncation.** predicted_n < max_tokens means the generation
died mid-run — that config is pathological regardless of the rate.
6. **When the sweep ties, tie-break on real-traffic properties** (e.g. draft
acceptance for multi-turn workloads), never on probe artifacts, and prefer
the config that matches the documented runbook.
## frank-primary specifics (2026-10-06 segment, commit c80c250)
- MNBT=2048 is a RESONANCE POINT at MTP-on shapes, not an optimum: 2560
collapses decode to 2.6-7.7 t/s (acceptance normal, verify stalls ~130ms/step,
truncated output), 1536 truncates at 57/256, 4096 won't boot at seqs=3. Do
not re-test.
- HOST_POOL_MIB 40960 is the load-bearing floor (36864 crashes boot).
- Spec depth 1-3 and seqs 2-3 all tie within noise; 0 and 4 are real
regressions (29.57 / 24.57 t/s vs 31.72 baseline).
- Levers known flat: GPU_HEADROOM, HOST_POOL upward, maxlen above 200k.
- Final kept config = Frank's original (spec=3, seqs=2, maxlen=200k, wht6,
0.82, 2048, 40960, fp8 KV, TP=2), baseline decode 31.72 t/s at 43.6k depth.
@@ -0,0 +1,42 @@
---
name: bench-arm-provenance-recovery
description: "Before calling a leftover LLM bench drop-in a rogue/hijack, reconstruct whether Frank ordered and measured that arm: omp archive prompt timestamps + journald print_timing for the exact pid, then land the verdict in the repo."
---
Use when a live service boots a payload you did not configure (stale `systemd` drop-in / `*_ENV_FILE` arm env) and the docs — or your own compaction-inherited context — call it "unfinished", "armed but never measured", or "hijack". Do NOT report that until proven: the arm may be an ordered, measured, approved configuration whose record lived only in `/tmp` + journald.
## Why this exists
2026-10-18 `frank-zealot`: a `frank-zealot.service.d/ptq1.conf` drop-in made the cutover deploy boot a different model at a different shape. I reported it as an unfinished experiment. It was actually the 27B PTQ1_0 arm Frank ordered at 02:36, that served production 03:14:39→03:38:57, and whose numbers existed only in tool output that context compaction later dropped.
## Step 1 — recover the ORDER (the harness archive, not memory)
`eval` with the `archive` prelude:
```python
ps = await archive.prompts() # searchable prompt history, survives compaction
# filter for the payload name / repo URL keywords, print p['at'] + p['text'][:90]
r = await archive.session("<sessionId>") # full session record
recaps = await archive.recaps() # recap stamps — check they postdate the window
```
`archive.prompts()` keeps **user prompts with ISO timestamps** even when the middle of the transcript was dropped. Timestamp every instruction: ordered → measured → superseded. Convert to local (`UTC−7` here) before quoting. A recap older than the instruction proves the recap layer never covered the window.
## Step 2 — recover the MEASUREMENT (journal, per pid)
The engine's own per-request timings are the record; they outlive `/tmp` env files.
```bash
ssh dank@<host> 'journalctl -u <svc> --no-pager' | grep -F 'serve-<svc>.sh[<PID>]' \
| grep -E 'prompt eval time|eval time' | awk '... median per task ...'
```
- Pin the **pid** (`sh[NNNN]`): `--since` windows alone pull the previous process's numbers too.
- llama.cpp format: `prompt eval time = 1234.56 ms / 100 tokens (…, 81.03 tokens per second)`; decode rows are `eval time = … ms / N tokens (…)`. Multiple spaces between `task`/`eval` break naive regexes — match `task\s+(\d+)\s+prompt eval time` exactly, or parse both labels separately.
- Filter pathological rows: generations running concurrently on other slots, or ending in a 4-byte EOS, read ~4× low. Report clean single-stream probes (≥200 generated tokens) and label the rest.
- Cross-check the boot banner for the payload identity (`type ptq1_0: 402 tensors`, KV ledger, slot/ctx lines) — that proves *which* shape that pid served.
## Step 3 — adjudicate, then land it in the repo
Write the verdict in the same session you derive it: runbook deploy-ledger section, `log.md` dated section, the arm script's header comment, and the AGENTS.md law. `/tmp` and journald are not records — that absence is what makes the next agent misread an approved arm as a rogue one.
## Traps
- `mask` fails on a relinked unit; never `systemctl disable`; retire drop-ins to `~/.AGENTTRASH/<date>-<topic>/` + `daemon-reload` (recoverable, not deleted).
- `/health` 200, `/v1/models` echoing the alias, and a green e2e gate all fail to read the payload — check `systemctl show <svc> -p DropInPaths` (must be empty), then `tr '\0' ' ' </proc/$(pidof llama-server)/cmdline`, then the boot ledger.
- Verification sequence that is honest: correction commit must quote commands you ran; state `[INFERENCE]` for anything the journal does not show.
@@ -0,0 +1,22 @@
---
name: bench-harness-fault-census
description: Attribute benchmark failures to harness vs model via exception-signature census before trusting any score; python-grader fault signatures and fixes
---
# Rule (from 2026-10-10 frank-intelligence-bench incident, 3 harness faults found)
An LLM benchmark score is VOID until every failure signature is attributed to the MODEL. Run the census before quoting any number.
# Procedure
1. Aggregate graded rows per eval; for each failed grade extract the LAST python exception signature:
regex `(\w+Error|\w+Exception): (?:name '([^']+)' is not defined|No module named '([^']+)')?` → sig = `Type: detail`.
2. Indict the harness (not the model) when:
- `NameError: target` / `NameError: targets` in any count → expected-value bindings never reached the tests (gold data file missing/unstaged).
- `ModuleNotFoundError: <grader package>` (e.g. `scicode`) → grading deps missing from the execution image.
- ≥10 failures sharing ONE signature at >50% share → systematic fault; audit the protocol (e.g. markdown fences reaching an exec-based extractor — `SyntaxError` storm; strip ``` fences before feeding model text to graders that exec raw code).
- >50% of generations fail → endpoint/transport fault.
3. Real model failures look heterogeneous: AssertionError/AxisError/NameError of the model's OWN undefined names.
# Companion checks
- Prove the grading path with a gold-solution integration test BEFORE trusting live scores: gold must PASS end-to-end (FIB_INTEGRATION=1 pattern).
- Quarantine poisoned ledgers (.AGENTTRASH) rather than deleting; census them to root-cause.
- Expect-score sanity: mixed PASS/FAIL with model-side signatures = real signal, even if the % looks low.
@@ -1,69 +0,0 @@
---
name: bench-llama-server-params
description: "Benchmark llama.cpp server-side parameters (--threads-batch, --ubatch-size, --spec-type) using HTTP API with max_tokens=1 for prefill, max_tokens=16384+ for decode"
---
# Benchmarking llama.cpp Server-Side Parameters
## Problem
`llama-bench` benchmarks the bench binary itself, not the running server.
It cannot measure the effect of server-side parameters like `--threads-batch`,
`--ubatch-size`, `--spec-type`, or `--parallel`.
## Solution: HTTP-Based Benchmarking
### Prefill throughput (prompt processing)
1. Start the server with the target parameter value
2. Send a large prompt via the chat API with `max_tokens=1`
3. Measure `prompt_tokens / wall_time` from the response usage
`max_tokens=1` is correct here — you want only prompt processing time,
not generation. The server crashes on `max_tokens=0`.
### Decode throughput (generation)
1. Start the server with the target parameter value
2. Send a prompt that generates a long response
3. Use `max_tokens=16384` minimum (16k) — **never `max_tokens=1`** for decode
benchmarking. One token tells you nothing about sustained generation rate.
4. Measure `predicted_per_second` from the response `timings` object, or
compute `completion_tokens / wall_time`
### Why at least 16k tokens for decode
- Decode throughput varies over the generation (warmup, cache growth, MoE
routing). A single token is noise.
- Speculative decoding acceptance rates stabilize over thousands of tokens.
- Short generations finish before the draft head reaches steady state.
- 16k tokens gives a stable average at ~22 tok/s (≈12 min per run).
### Key Pattern
```python
# Decode benchmark — measure generation throughput
body = json.dumps({
"model": "<alias>",
"messages": [{"role": "user", "content": "<prompt that generates 10k+ tokens>"}],
"max_tokens": 16384, # 16k minimum for decode; use max_tokens=1 ONLY for prefill
"temperature": 0, # deterministic for reproducibility
"stream": False
})
# Measure wall time from request send to response receipt
# Get predicted_per_second from timings, or completion_tokens / wall_time
```
### When to Use
- `--threads-batch` (prefill): use `max_tokens=1`
- `--ubatch-size` (decode): use `max_tokens=16384+`
- `--spec-type` / speculative decoding: use `max_tokens=16384+` (decode only)
- Any server-side parameter that `llama-bench` cannot target
### Benchmarking policy (MANDATORY)
1. **STOP the production service** before running any benchmark
2. **Mask** the service (`systemctl mask`, never `disable` — on these hosts
`disable` deletes the unit file from `/etc/systemd/system/`)
3. Run the bench
4. **Unmask + restart** the service when done
5. **Verify health** before reporting results
### Example Scripts
- `scripts/bench-threads-batch-http.py` — prefill sweep (max_tokens=1)
- `scripts/bench-decode-threads-batch.py` — decode sweep (max_tokens=256+)
- `scripts/bench-spec-decode.py` — speculative decoding config sweep
@@ -0,0 +1,82 @@
---
name: bounding-subagent-runtime
description: "Cap and diagnose runaway or hung omp subagents: task.maxRuntimeMs, softRequestBudget, agentIdleTtlMs, per-agent model overrides, the cfg save-approval quirk, and the finished-work-but-never-exits nested-fanout deadlock."
---
# Bounding subagent runtime (omp harness)
Use when a `task`/`eval` subagent runs absurdly long (tens of minutes on a small job),
when it **finishes its deliverable and still never exits**, when you want a hard cap
before spawning, or when you must tell a busy agent to stop.
## The knobs (`cfg://task/...`)
| Key | Default | Meaning |
|---|---|---|
| `maxRuntimeMs` | `0` (unlimited) | **Hard wall-clock limit per subagent (ms).** The only true time cap. |
| `softRequestBudget` | `200` | Soft cap on assistant **requests** per run; force-stop at **1.5×**. |
| `softRequestBudgetNotice` | `true` | Injects one "wrap up" steering message at the soft mark. |
| `agentIdleTtlMs` | `420000` | Parks an **idle** agent after 7 min. Useless against a busy-but-slow agent. |
| `completionProbe` | `true` | Asks a working subagent to self-report completeness at 2/5/10 requests. |
| `maxConcurrency` | `32` (often lowered) | Concurrent subagents; sizes GPU/endpoint load. |
| `agentModelOverrides` | `{}` | Per-agent model routing, e.g. `{"scout":"provider/model-id"}`. |
| `tools.maxTimeout` | `0` | Caps the timeout the agent may set on a **tool**, not on the agent. |
Reasonable fleet defaults: `maxRuntimeMs = 900000` (15 min), leave the request budget
alone unless the failure mode is a looping search agent (then lower it — request count,
not the clock, is what bounds a 200-call research loop).
**Verify the cap instead of assuming it.** `read cfg://task/maxRuntimeMs` at the start of
any session that plans to rely on it. A session-only write is gone next session, and an
unanswered `/save` approval means the value never persisted — so the fleet can run hours
with the cap at `0` while everyone believes it is 15 minutes. Nothing else bounds a child.
## Three failure modes, three different fixes
- **Runaway loop** (hundreds of sequential tool calls): lower `softRequestBudget`; the
clock cap only truncates it late.
- **Slow model, legitimate work** (dozens of calls on a low-t/s endpoint = an hour of wall
clock): fix routing, not the clock — check `cfg://task/agentModelOverrides` and move the
agent class onto a faster/shared endpoint. Watch the slot math: subagent endpoints share
KV slots with the interactive session, so `maxConcurrency + 1` sequences on an N-slot
endpoint degrades everyone's decode.
- **Finished work, no exit (nested fan-out deadlock).** A subagent that spawned **its own**
children blocks on their barriers. If those children die out-of-band (killed, starved, or
waiting on an answer whose subject changed mid-flight), the parent waits forever with its
deliverable already written. No instruction text prevents this — not the repo AGENTS.md,
not the task prompt; only the clock cap or a kill ends it.
Signature: parent and children report **the same elapsed wall clock**, and the parent's
on-disk output is complete. Census (agent registry + `ps`): group by elapsed time, look
for identical `etimes` across a parent/child set, then judge the **artifacts** instead of
the process — balanced code fences, no truncated tables, trailing newline, front matter or
the page's H1+status convention intact. Complete artifacts + still "running" = deadlock,
not work.
Repair: kill the **subtree** and confirm the children leave (a killed parent does not
cancel children that came from a separate `tasks[]` batch). Then **flatten**: the
top-level session does the fan-out and children never spawn children. Re-run only the
slices whose artifacts are missing — integrate from what is already on disk.
## cfg write mechanics (bit me once)
- `write cfg://task/maxRuntimeMs 900000` → applies **session-only**, replies "NOT saved".
- Persist = `write cfg://task/maxRuntimeMs/save` with the same content, and that write
needs its **own interactive approval**. If the approval prompt is not answered in time
the value stays session-only and the tool says *NEVER retry unless the user asks* —
report it in the reply instead.
- Revert a session value by writing the old value back (e.g. `0`).
## Order of operations when one is already running late
1. `read proc://` → registry of running agents; `read proc://<id>` for status/output.
2. `read agent://<id>` → live progress ("Latest assistant text") — often shows it is
mid-batch, not hung. If the latest text is already a wrap-up/summary, treat it as
**done** and collect its output instead of waiting for an exit event.
3. `write agent://<id>` an explicit wrap-up order: stop searching, answer what is
sourced, mark the rest `NOT VERIFIED — needs <what>`, cap the report length.
4. Only then `write proc://<id>/kill` — the cap does **not** retroactively bind agents
spawned before the change, so a long runner must be ordered or killed, never just
re-configured. After any kill, re-read the registry: unreaped children and orphaned
host-side processes (a scratch server holding GPU/RAM) are the expensive residue, and
`pkill -f` patterns that also match production services are forbidden — kill by pid.
@@ -0,0 +1,24 @@
---
name: cfg-approval-timeout-recovery
description: "Use when a cfg://.../save write fails with \"user did not answer the approval prompt\": the settings-approval dialog has a hardcoded 10 s timeout and no config key; recover via direct config.yml edit once the user has approved the change."
---
# Recovering from cfg:// approval-prompt timeouts
## Facts (verified in bundle 2026-10-18)
- Settings-write approval dialog timeout is **hardcoded 10 000 ms** (`timeout: JKl`, `JKl=1e4`, countdown class treats arg as ms) in the omp `dist/cli.js` bundle. **No config key exists** for it (checked full `cfg://` registry).
- `ask.timeout` is a DIFFERENT gate (ask-tool auto-select; 0 = wait forever). It does not govern cfg approvals.
- A timed-out approval silently auto-denies: session-only value stays applied, `config.yml` unchanged. Error says "nothing changed; it stays <value>. NEVER retry unless the user asks."
- Related: `features.unexpectedStopDetection: smart` injects forced-continue `<system-injection>`s on text-only turns ending in a promise to act ("Task incomplete: MUST call the next concrete tool now"). Set `mechanical` (upstream default) or `none`; `smart` drives agents into junk subagent spawns to satisfy the nudge.
## Recovery procedure (only when the user has already explicitly approved the change)
1. Do NOT re-issue `cfg://<key>/save` after a timeout unless the user explicitly asks again — the harness forbids it.
2. Edit `~/.omp/agent/config.yml` directly:
- `grep` the setting key to locate it; read surrounding lines.
- surgical `edit` of the single line.
- validate: `python3 -c "import yaml; yaml.safe_load(open('/home/blank/.omp/agent/config.yml'))"`
3. State that the file was edited directly because the invisible 10 s dialog ate the user's approval, and offer the `omp config set <key> <value>` one-liner as the user-run alternative for future.
## Prevention
- Before any `cfg://.../save`, warn the user a ~10 s dialog is about to appear and to answer immediately.
- Bundle-fact lookups: `PKGDIR=$(dirname $(readlink -f $(command -v omp)))/..`, then grep `dist/cli.js` for the message string, walk to the call site, resolve minified constants with `grep -oE '[,;]CONST=[^,;)]+'` (`1e4` = 10000).
@@ -0,0 +1,37 @@
---
name: characterization-fixture-engine-cutover
description: "Swap a path→service planner/mapping engine behind a golden fixture: capture, delta-reduce, document deltas, flip grep seams to behavioral, add render/drift gates, validate Caddyfile renders, Gitea PR URLs (serverAppConfigs cutover playbook)"
---
Playbook for replacing a hand-maintained mapping engine (deploy planner, seam mapper, allow-lists) with a data-driven one without changing observable behavior. Proven on the 2026-10-13 `shared/services.yml` + `scripts/service-map.py` cutover (921 deltas → 17 intentional → green; 408/408 bats).
## 1. Capture the characterization fixture FIRST
- Enumerate every path the old engine sees: all tracked files x all targets (`git ls-files`), plus hand-added edge paths (retired files, cross-host paths, shared networks).
- Run the OLD engine on each, record `path<TAB>target<TAB>services` to `tests/fixtures/*-golden.tsv`. Old engine = current `deploy-plan.py` before edits.
- This fixture is the contract; the new engine must reproduce it except documented deltas.
## 2. Cut over, then delta-reduce with the fixture
- Diff new-engine output vs fixture; bucket every delta into: engine bug / intentional change / fixture staleness.
- Engine bugs found this way (generic classes): `in` membership on dict-of-tuples, host-scoping of relative directory keys, riders/children resolved only in the changed file's own directory, stem collisions between unit files and generated artifacts (`.network` stem == service dir stem), `.network` files entering the unit branch by stem match.
- Intentional deltas get documented individually in the commit message; NEVER batch-accept a delta count.
- Regenerate the fixture LAST, from the new engine, and record the delta count + reasons in the commit.
## 3. Flip text-grep seams to behavioral calls
- Old seams that grep a config file for the mapping (`grep -q 'dankclaw:hermes-webui'`) die silently on cutover. Replace with a call to the new engine: `run python3 scripts/service-map.py allows --host H --service S; assert_success`.
- Vhost-scoped content greps (Caddyfile awk blocks) → grep the new canonical form (`import guarded`), not the inlined chain.
## 4. Renders + drift gates
- Config artifacts that were hand-maintained mirrors (node-exporter quadlet pair, pihole addn-hosts) become `service-map.py render <name> --host H` artifacts; a drift test asserts tracked-file == render output.
- Renderers must emit exactly what the consumer parses: dnsmasq `addn-hosts` treats inline `# comments` as hostnames — render no trailing comments.
- Named-snippet collapse (Caddyfile guard chain): the snippet MUST be defined before the first site that imports it (put `(guarded)` right after the global options block), or real caddy reports `File to import not found`.
## 5. Suite + deploy gates
- `./tests/run.sh` fully green before each rank commit; one atomic commit per rank.
- Deploy is service-scoped: `deploy.sh <host> --service <svc>`; never a fleet drain for one service. The Playwright e2e gate runs inside deploy.sh against the live fleet.
## Repo-specific gotchas (serverAppConfigs)
- Validate the Caddyfile with stock `caddy:latest` by DELETING the whole `acme_dns porkbun { ... }` block (`awk 'NR<53 || NR>56' dankclaw/Caddyfile`). A `sed s/acme_dns porkbun {/acme_dns acme {/` variant that drops the `{` swallows the following tokens into the global block and fakes an "import not found" on `(guarded)` — debug sed swaps with a minimal repro file before blaming the config. Stock-image `dns.providers.porkbun` unregistered + basicauth `REPLACE_WITH_HASH` base64 errors are the expected endpoints of a clean parse.
- Gitea: branches with `/` 404 on `/compare/main...wt%2F...` — use the push-hook URL `…/pulls/new/<branch>`; private repo returns 404 to anonymous curl, which proves nothing about the page.
- `test-changed.sh` contract: new seam selection ⊇ old selection per path; unmapped repo path must `die`, never pass silently.
- Retire-to-trash: retired files (compose.yaml, dead templates, dead bats) → dated `.AGENTTRASH/YYYYMMDD-topic/` beside them; every repo sweep must exclude `.AGENTTRASH`.
- bash `test -f a b c && ! test -e d` is invalid syntax (test takes one operand per operator); use `ls a b c` + separate `ls d` to census artifacts.
@@ -0,0 +1,29 @@
---
name: closed-engine-runtime-investigation
description: "Recipe for diagnosing/tuning a closed-source LLM runtime (halogen-class) on this fleet: docs, env-lever enumeration from the binary, ledger scraping, cache-assisted A/B under the bench law."
---
# Investigating a closed-source LLM runtime (halogen-class)
Use when a containerized closed-source engine (e.g. halogen on frank-large) underperforms and the repo docs don't explain it. Fleet law: stop the production unit BEFORE any GPU experiment (`sudo systemctl stop <unit>`; mask FAILS on relinked units — stop alone, redeploy after).
## 1. Ground truth before flags
- Boot ledger + per-request timings live in `podman logs <container>` (container-lifetime only) UNLESS the serve script captures stdout on disk (foreground `podman run`). A `podman run -d ... > LOG` redirect captures ONLY the container ID — a one-line hex file is the tell.
- `serve_api:`-style log lines carry per-request `prompt/prefill/rounds/commit/detok` — build the depth-vs-rate curve from REAL traffic before any bench. Shallow-healthy + deep-collapsed = a depth-linear term (KV reads), not a broken engine.
- If the engine's public repo exists, read its FLAGS/docs (e.g. `docs/FLAGS.md`) AND the bench tool source (e.g. `tools/halogen-bench.py`) — bench tools reveal which regimes the published numbers never measured (halogen's tg rows are shallow-only).
## 2. Enumerate the undocumented levers
The binary self-announces every env var it reads. The image often lacks `strings`:
```
podman run --rm --entrypoint /bin/sh <image> -c \
'grep -aoE "<PREFIX>_[A-Z0-9_]+" /usr/local/bin/<engine> | sort -u'
```
Classify by kernel family (attention vs linear-state vs GEMV) against the suspected term. Only test levers with public precedent (e.g. issue #12 proved HALOGEN_FA_TQ) or an obvious name match; defaults are the publisher's measured winners — an A/B must show >=~15% to adopt.
## 3. Cache-assisted A/B (cheap reps)
Boot each probe arm as its own container (env is read at startup) with the prompt cache ON (explicit MB >= one full-entry at ~70 KB/token): rep 1 pays the cold prefill, rep 2+ resumes (~2 s) — depth measurements then cost decode-time only. Publish the probe on host LOOPBACK (127.0.0.1:<scratch>), run the driver ON the host (python3 stdlib is enough), read the engine's own `timings` (prompt_ms/cache_n/predicted_ms/draft_n_accepted). Never benchmark beside a live engine on the same GPU.
## 4. Adopt or revert
- Winner: env line into the serve env + test-seam update + docs, then `deploy.sh <target> --service <svc>` (restart restores everything; no unmask needed after stop-only).
- Loser: `podman rm -f` probes, start the unit, revert nothing.
- Either way: record the measured before/after in the runbook; report the depth behavior upstream if the publisher invites benchmark data — their bench gaps (depth, carve-topologies) are exactly what issues like #12 close.
@@ -0,0 +1,43 @@
---
name: dankclaw-boot-line-window
description: "Change a Dankclaw kernel command-line parameter safely: tracked grub + installer, measured before/after A/B on live LLM traffic, post-boot gates."
---
# Dank fleet kernel boot-line window (Dankclaw pattern)
Kernel command-line state is **host state the deployer never owns**. The repo law
("all edits through git") still applies: fetch the live file, commit it, edit the
tracked copy, apply with a script.
## Source of truth
- `dankclaw/host/grub` — tracked copy of `/etc/default/grub`.
- `scripts/set-dankclaw-bootline.sh` — the only door: diff → `sudo install -m 0644 -o root -g root` →
`sudo update-grub` → print the **default `grub.cfg` entry** → reboot only on `--reboot`.
Refuses any host whose `hostname -s` lacks `dankclaw`.
- Decision record: `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md`.
- `deploy-plan.py` maps `dankclaw/*` subdirs explicitly; a new `dankclaw/host/` path plans nothing (verify with a `--dry-run`).
## Procedure
1. **Capture the before-arm measurement first** — you cannot get it back after the reboot.
2. Fetch provenance byte-exact: `ssh dankclaw 'sudo cat /etc/default/grub' > dankclaw/host/grub` (never retype; a transcription dropped a leading `0`).
3. Commit provenance, edit the tracked copy, commit the flip, push (**Gitea lives on Dankclaw — pull on the host before rebooting**), then `ssh dankclaw 'cd ~/serverAppConfigs && git pull --ff-only origin main'`.
4. Install **without** `--reboot` first: it prints the `grub.cfg` default entry; confirm the token there before the irreversible step.
5. `ssh dankclaw 'sudo systemctl reboot --no-block'`.
6. **Poll on `/proc/uptime` + cmdline, never on ping** — the NIC answers ICMP for a minute after shutdown and sshd reset looks like "reachable".
Gate set: `systemctl --failed` count 0 · `systemctl is-system-running` running · `/proc/cmdline` token · `ls /sys/kernel/iommu_groups | wc -l` · `cat /sys/kernel/iommu_groups/*/type | sort | uniq -c` · `systemctl is-active frank-dragoon frank-zealot` · `curl 127.0.0.1:7770/health` (listener binds only after the full load — poll up to 10 min).
## Measurement discipline (this is where windows get lied about)
- Both arms must be **byte-identical traffic**. Engine-reported token counts are the identity proof (e.g. 59,024 prefill / 21,568 needle both boots).
- `/tmp` is **tmpfs**: the reboot deletes `/tmp/halogen-eval`. Rebuild with `scripts/stage-frank-dragoon-eval.sh` (corpus 2,891 lines / 1,287,656 B is the check) — and run it via `bash -lc`, `uv` is in `~/.local/bin`, absent from a non-login PATH.
- The 51.2 GB halogen n-gram table is page cache (`Pin::None`). A fresh boot has it cold and a cold table throttles prefill → `cat` it once (~40 s) before measuring. `WARM=1` in the staging script does this.
- **Sampled decode on this engine is a useless single-sample instrument**: three repeats of the same request gave 31.8 / 46.9 / 47.8 t/s. Only prefill (cold) and needle-hit carry signal; anything else needs ≥3 repeats.
- Bench law still bites: nothing else on the GPU; measuring *through* the live service is how dragoon/zealot are measured (a scratch server cannot share the card).
## Traps
- `edit` `PUT N.:` can **insert instead of replace** — verify the range shrank, then `CUT` the leftover.
- Body rows beginning with a markdown bullet (`- `) get auto-prefixed and land as `+- `; rewrite the block with `write` when that happens.
- A long reboot fires `LlmEndpointDown` → Signal; Alertmanager has **no persistence**, so every active alert gets a fresh `startsAt` after boot — check `query_range` history before blaming the window for an alert.
- `sudo podman ps` shows nothing for Dankclaw's rootless stack; `systemctl is-active caddy` is meaningless for quadlets. Use the public endpoints or `systemctl --user` as `dank`.
## Worked example (2026-10-18)
`amd_iommu=off` → `iommu=pt` to restore the XDNA2 NPU. Result: cold prefill 1,585.1 → 1,616.0 t/s (**+2.0 %**, i.e. no penalty), needle hit/hit, `amdxdna` bound `0000:c6:00.1` (group 26 identity, PASID width 10). The 13–16 % figure it replaced had never been measured locally. Deferred on purpose: `pp_dpm_fclk` pin (nothing co-runs yet) and PerfOpt (needs Linux 7.4; Ubuntu 26.04 caps at `7.0.0-38.38`).
@@ -0,0 +1,81 @@
---
name: dankclaw-expose-service
description: "Expose a new service at *.franksdank.cloud on Dankclaw: quadlet container, Caddy site block, Pi-hole LAN record (custom.list), Porkbun public record; includes API vhost/volume gotchas proven on the ntfy deploy."
---
# Expose a service on Dankclaw (franksdank.cloud)
Proven end-to-end 2026-10-11 with ntfy. Four layers, each with a known trap.
## 0. Survey first
```bash
ssh dankclaw 'ls ~/.config/containers/systemd/; grep -nE "franksdank|anubis" /home/dank/Containers/caddy/Caddyfile'
```
- Caddy runs as quadlet `caddy.container`; config at `/home/dank/Containers/caddy/Caddyfile` (NOT `/etc/caddy` on host).
- Sites fronted by `anubis:8923` get bot-challenge. **CLI/API publishers (ntfy, webhooks, git) MUST bypass anubis** — use the `gitea`/`ai` pattern (direct `reverse_proxy`).
## 1. Quadlet container (user `dank`)
Write `~/.config/containers/systemd/<name>.container`:
```ini
[Unit]
Description=<name>
After=network-online.target
Wants=network-online.target
[Container]
ContainerName=<name>
Image=<image>
Network=dankclaw_default
Volume=<name>-cache:/var/lib/<name>
[Service]
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
```
`Network=dankclaw_default` ⇒ Caddy reaches it as `<name>:<port>`. Then `systemctl --user daemon-reload && systemctl --user start <name>.service`.
Rootless podman: bare containers never boot-persist — quadlet is mandatory.
## 2. Caddy route
Append site block (wildcard `*.franksdank.cloud` cert already issued via Porkbun DNS-01; no new cert work):
```
<name>.franksdank.cloud {
reverse_proxy <name>:<port>
}
```
Validate, then reload in-place:
```bash
podman exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
podman exec caddy caddy reload --config /etc/caddy/Caddyfile
```
## 3. LAN DNS (Pi-hole v6, container `pihole`)
Traps, in order of wasted turns:
- API is NOT at `127.0.0.1:8083/api` on the host port path — requests need `-H "Host: pi.hole"`.
- `[[dns.records]]` in pihole.toml and `gravity.db` `domain` table: absent/unused on this box.
- **Working location:** `/home/dank/Containers/pihole/etc-pihole/hosts/custom.list` (dnsmasq addn-hosts), format `IP FQDN`:
```bash
echo "192.168.0.143 <name>.franksdank.cloud" | sudo tee -a /home/dank/Containers/pihole/etc-pihole/hosts/custom.list >/dev/null
podman exec pihole pihole restartdns
```
## 4. Public DNS (Porkbun, for off-LAN clients)
Keys: `/home/dank/Containers/caddy/env` (`PORKBUN_API_KEY`, `PORKBUN_API_SECRET_KEY`).
```bash
curl -sL -X POST https://api.porkbun.com/api/json3/domain/api/create/franksdank.cloud \
-H "Content-Type: application/json" \
-d "{\"apikey\":\"$K\",\"secretapi\":\"$S\",\"name\":\"<name>\",\"type\":\"A\",\"content\":\"71.227.227.172\",\"ttl\":\"600\"}"
```
- `json3` path + `-L` (302-redirects to v3). Public IP: 71.227.227.172 (router forwards 443 → Dankclaw).
- Porkbun edge throws intermittent 503s; retry later or do it in the UI. LAN path works without this record.
## 5. Verify
```bash
# from Dankclaw (host): getent hosts <name>.franksdank.cloud → 192.168.0.143
# workstation w/ stale resolved negative cache: use curl --resolve <name>.franksdank.cloud:443:192.168.0.143
# workstation's own `sudo resolvectl flush-caches` needs a terminal (no passwordless sudo) — negative cache expires on its own
curl -sf --resolve <name>.franksdank.cloud:443:192.168.0.143 https://<name>.franksdank.cloud/
```
Publish-style test for ntfy inside container (CLI flags vary by version; raw HTTP is version-proof):
```bash
podman exec <name> wget -qO- --header="Title: smoke" --post-data="up" http://localhost:<port>/<topic>
```
@@ -0,0 +1,13 @@
---
name: dankclaw-homepage-asset-verify
description: "Add/verify homepage assets on Dankclaw: Caddy @data allowlist, deploy copy step, e2e panel census, live content-type check."
---
Every new top-level homepage asset directory (e.g. `shared/homepage/img/`, `shared/homepage/sfx/`) needs FOUR coordinated changes, all in one commit:
1. **Caddy allowlist** — `dankclaw/Caddyfile` `@data path` matcher must include `/<dir>/*`. Without it the post-auth `@home` catch-all answers with `index.html` (`200 text/html` for a PNG/OGG — silent breakage).
2. **Deploy copy** — `scripts/deploy-remote.sh` `deploy_homepage()` copies `shared/homepage/<dir>/` with the `cp -r ... /srv/homepage/` merge pattern beside the fonts/img/sfx blocks.
3. **Test pins** — `tests/homepage_caddy.bats` grep-pins the new `@data` path; if the asset changes panel rendering, the e2e census in `tests/e2e/specs/10-homepage.spec.ts` must count the new render style (it derives expected names from services.json: `.panel .name` for normal cards, `.mi-name` for fused Model Information cards — a new fused component breaks the panel census otherwise, which is exactly how the 2026-10-08 Protoss deploy failed its e2e gate).
4. **Deploy both services** — `deploy.sh dankclaw --service homepage` and `--service caddy` (Caddyfile change), then live-verify from a browser tab: `fetch('/<dir>/<file>')` must return `200` with the correct `content-type` (`image/jpeg`, `audio/ogg`), not `text/html`.
Checker note: `status.json` names follow `services.json` automatically (the deployed checker re-reads it via the homepage deploy's restart of homepage-status.timer), so renaming cards needs no checker change.
@@ -0,0 +1,34 @@
---
name: dankclaw-model-inventory
description: "Read-only census of downloaded models on Dankclaw: GGUF store, ComfyUI container mount, dead-config trap"
---
Answer "what models are on Dankclaw" without guessing roots. All probes are read-only ssh (`ssh -o BatchMode=yes dankclaw '…'`).
## Roots (in order)
1. **LLM GGUF store:** `/home/dank/models/` — one dir per model, main GGUF + `mmproj-*` (multimodal projector) + `mtp-*` (speculative-decode draft) satellites. `~/.share/models` does NOT exist on Dankclaw (that's the workstations' convention).
2. **ComfyUI runtime store:** `/home/dank/comfyui-data/models/` — category dirs (`diffusion_models/`, `text_encoders/`, `vae/`, `loras/`, …). This is what the running stack actually uses.
## ComfyUI topology (the trap)
- Runtime = podman container `comfyui` (image `localhost/comfyui-rocm:gfx1151`, user unit `comfyui.service`). Mounts: `~/comfyui-data/{models,custom_nodes,input,output}` → `/opt/ComfyUI/...`.
- The host checkout `~/ComfyUI/` is NOT the runtime. Its `extra_model_paths.yaml` (base_path `/home/dank/models` + category keys) is dead config from a pre-container setup — never read by the container. Do not report those empty category dirs as "ComfyUI has no models".
## Census commands
```bash
# LLM store: sizes + names in one pass
ssh -o BatchMode=yes dankclaw 'find ~/models -maxdepth 2 -type f \( -iname "*.gguf" -o -iname "*.safetensors" \) -printf "%10s %p\n" | sort -k2'
# ComfyUI: container mount truth
ssh -o BatchMode=yes dankclaw 'find /home/dank/comfyui-data/models -type f -size +1M -printf "%10s %P\n" | sort -k2; du -sh /home/dank/comfyui-data'
# confirm runtime + mounts before trusting any path claim
ssh -o BatchMode=yes dankclaw 'podman ps --format "{{.Names}} {{.Image}} {{.Status}}"; podman inspect comfyui --format "{{range .Mounts}}{{.Source}} -> {{.Destination}}{{\"\n\"}}{{end}}"'
```
## Anomaly flags
- A `.gguf` of ~1 KB = git-LFS pointer / failed download, not a model (seen: MiMo-Q8_0 stub). Report as broken, offer refetch or `.AGENTTRASH`.
- Size sanity vs Strix Halo 32 GiB unified memory: bf16 diffusion+TE pairs near 30 GiB can't co-reside with the serving LLM; int8/w4a8 quants are the working set.
@@ -0,0 +1,49 @@
---
name: dankclaw-pihole-lan-dns-record
description: "Add/repair a LAN DNS record for a new *.franksdank.cloud service on Dankclaw's Pi-hole v6 (hosts/custom.list path), plus the public Porkbun A record step and client cache gotchas."
---
Use when a new `*.franksdank.cloud` service resolves publicly/LAN-needs a local address, on Dankclaw (Pi-hole v6 container, quadlet `pihole.container`).
## Ground truth layout (verified 2026-10-11)
- Quadlet: `~/.config/containers/systemd/pihole.container`; volumes `/home/dank/Containers/pihole/{etc-pihole,etc-dockers,addn-hosts}`.
- **LAN records live in `/home/dank/Containers/pihole/etc-pihole/hosts/custom.list`** — plain hosts format: `192.168.0.143 gitea.franksdank.cloud`. This is the boring, working path.
- `pihole.toml` `[[dns.records]]` array is EMPTY on this host — do not expect v6 UI-style records there.
- `gravity.db` exists but has NO `domain` table (v6 layout) — do not insert local records into SQLite.
## Procedure (30 s, proven)
```bash
ssh dankclaw 'echo "192.168.0.143 <name>.franksdank.cloud" | sudo tee -a /home/dank/Containers/pihole/etc-pihole/hosts/custom.list >/dev/null \
&& podman exec pihole pihole restartdns'
ssh dankclaw 'getent hosts <name>.franksdank.cloud' # must resolve
dig +short <name>.franksdank.cloud @192.168.0.143 # from any LAN box
```
Hosts files are read-only-readable only via sudo (dir owned by uid 100999). `sudo -n` works on dankclaw.
## API maze — DON'T re-explore
- FTL API base is `/api` at the web port (8083) **with `Host: pi.hole` header**; `/admin/api/*` → bad_request hint; bare 127.0.0.1 without the vhost → 404 `not_found`.
- Login: `POST /api/login {"password": <WEBPASSWORD from /home/dank/Containers/pihole/env>}` → session in `X-Cors` header/cookie.
- `POST /api/apps/domains/custom` → **404 on this build** (FTL version predates it); `/api/customdns/custom` also absent. There is no container sqlite3. Use custom.list.
## Public record (off-LAN reachability)
Porkbun API, keys in `/home/dank/Containers/caddy/env` (`PORKBUN_API_KEY`, `PORKBUN_API_SECRET_KEY`):
```bash
curl -sL -X POST https://api.porkbun.com/api/json3/domain/api/create/franksdank.cloud \
-H "Content-Type: application/json" \
-d "{\"apikey\":\"$K\",\"secretapi\":\"$S\",\"name\":\"<name>\",\"type\":\"A\",\"content\":\"71.227.227.172\",\"ttl\":\"600\"}"
```
- `json3` path 302-redirects → **`-L` required**; no-`-s` to see status.
- Porkbun edge (openresty) intermittently 503s — transient; retry later, don't burn turns. Public record is OPTIONAL when LAN-only subscribers exist.
- Wildcard `*.franksdank.cloud` TLS is already covered: Caddy + Porkbun DNS-01 (caddy-porkbun image), so a new subdomain needs NO new cert work — just the Caddyfile site block + reload.
## Client-side cache gotchas
- Workstation (systemd-resolved): negative cache survives Pi-hole changes; `sudo resolvectl flush-caches` needs a TTY (faillock-sensitive — no `-S` guessing). Non-interactive proof: `curl --resolve <name>.franksdank.cloud:443:192.168.0.143 https://<name>.franksdank.cloud/...`; cache self-expires (SOA negative TTL).
- Dankclaw/Dankserver resolve via Pi-hole directly — verify there first.
@@ -0,0 +1,21 @@
---
name: defuse-shell-rc-secrets
description: "Find literal API keys/tokens in shell rc files and replace them with 600-perm secret files + loader lines, syncing the canonical tool token file."
---
# Defuse shell-rc secrets
Symptom: `export KEY=...` literal in `.zshrc`/`.bashrc` — world-readable in every process env, one `git add -A` from a dotfiles repo.
## Procedure (repeat per host)
1. Sweep: `grep -n "export.*KEY\|export.*TOKEN" ~/.zshrc ~/.zprofile ~/.profile ~/.bashrc 2>/dev/null` (run per host; when batching hosts in one ssh compound command, label which output came from which host — misattribution wastes turns).
2. Extract WITHOUT echoing: `mkdir -p ~/.config/secrets && chmod 700` it, then `sed -n 's/^export NAME="\(.*\)"$/\1/p' ~/.zshrc > ~/.config/secrets/<name>` (quote variant and unquoted variant are different sed patterns — verify byte counts with `wc -c`, a 0-byte file means the pattern missed).
3. `chmod 600` every secret file; sync any canonical tool file the ecosystem expects (e.g. `~/.cache/huggingface/token`).
4. Replace the rc literal (hashline `PUT N.=N:` range — never echo the secret in the op): `[ -f ~/.config/secrets/<name> ] && export NAME="$(cat ~/.config/secrets/<name>)"`.
5. Verify: `zsh -n ~/.zshrc` (or `bash -n`); keep a dated `.bak` of the rc on remote hosts; shred any /tmp transfer copies immediately (`shred -u`).
## Laws
- A token pasted into chat lands in transcripts and model context — treat chat as a leak channel; give the user the `read -rs` snippet instead.
- scp of a secret FILE (encrypted channel, no argv) is fine; the token is never a command-line argument.
- After replacing a legacy HF token, probe scope non-destructively: `HfApi().whoami()` (legacy read tokens show `type: access_token` without canWrite; the decisive probe is `create_repo(exist_ok=True)` on the real target repo).
- Verify extraction byte-count before deleting/trusting; a successful-looking sed can write an empty file.
@@ -0,0 +1,70 @@
---
name: deployment-checkout-trunk-sync
description: Use when the live deployment checkout (~/.omp main) must merge a moved origin/main while dirty tracked files block the merge and workspace policy blocks git checkout.
---
---
name: deployment-checkout-trunk-sync
description: Use when the live deployment checkout (~/.omp main) must merge a moved origin/main while dirty tracked files block the merge and workspace policy blocks git checkout.
---
# Sync a dirty live checkout to a deployment (checkout blocked)
Situation: the checkout IS the deployment; months of machine/hand writes sit
uncommitted; `origin/main` has moved; `git checkout`/discard is BLOCKED by
workspace policy ("edit forward"; exemption only during active conflict).
## Procedure
1. `git fetch`; trial `git merge --no-commit --no-ff origin/main` to enumerate
exact blockers (dirty files the merge would overwrite + untracked collisions).
Untracked blockers: `cp -a` to `.AGENTTRASH/pre-merge-<date>/`, then `mv` aside.
2. **Preservation commit:** `git add` ONLY the blocking paths (not the whole
tree) and commit them as-is: `chore: adopt pre-existing working-tree state
before trunk merge`. Nothing is discarded; the secret gate still runs.
Include EVERY dirty deployment-state path (dotfiles/live tiers too) — a
dirty file that is neither committed nor trashed is unprotected from step 3.
3. `git merge --no-ff origin/main`. Conflicts are now resolvable — the policy
exemption is active during a merge.
4. Resolution law:
- **live machine config** (`agent/config.yml`, `models.yml`, dotfiles live
state like noctalia `settings.toml`) → `--ours`: the running box is its
own authority; the remote copy is a snapshot.
- **prose/law/docs** (`AGENTS.md`, KB) → `--theirs` (upstream canonical;
local variants survive in the preservation commit).
- **append-mostly logs** → union: cut only the three marker lines
(`<<<<<<<`/`=======` range + `>>>>>>>`), keep both sides' entries.
5. `git commit --no-edit`, push trunk, verify `main == origin/main`.
6. Pre-merge byte-compare every dirty file vs `origin/main`
(`git show origin/main:$f | diff -q - $f`): SUPERSEDED (identical → discardable),
DIVERGED (resolve), untracked-in-upstream (collision).
## Gotchas
- **NEVER `git stash` to unblock a deployment sync, NEVER `git stash clear`/
`drop` afterward.** Observed 2026-10-18: a sync stashed the dirty tree, the
stash entry was later destroyed, and months of GUI-built bar layout
(noctalia settings.toml, never committed) vanished with it — recovered only
via dangling-object archaeology (below). Step 2's preservation commit exists
precisely so stashing is never needed.
- **Stash-and-retry loops are a red flag**: three `WIP on main` stash commits
within ~60s of each other means the operator was fighting the dirty tree,
not resolving it.
- **Recovery from destroyed stashes** (objects survive entry deletion until gc):
`git fsck --lost-found` → `git log -1 --format='%ci %s' <dangling>` to date
them → `git show <c>:<path>` to dump candidates; newest `WIP on` commit is
usually the last good state. Validate before deploying (e.g.
`noctalia config validate`), back up the clobbered file to `.AGENTTRASH/`,
then restore through the symlink target.
- Never wrap `git checkout` in `2>/dev/null` here: the policy-block error was
swallowed and the loop silently no-op'd; the merge then failed identically.
- `git add` a directory pathspec stages its untracked children (fine for
allowlist dirs; never for prose dirs).
- The live-sync heartbeat (#11) commits machine tiers hourly, so this sync is
a rare operation post-deployment — mostly memories/dotfiles stragglers.
Remember the heartbeat can lag up to an hour behind GUI edits: the tree
between heartbeats is the ONLY copy of fresh GUI work.
- **Frank's accepted risk posture (2026-10-18):** hourly sweep + the
preservation-commit law + fsck recovery is defense-in-depth ENOUGH. Do not
re-propose faster heartbeats or path/event triggers to shrink the
GUI-edit-to-commit window; he chose status quo after measuring (heartbeat
itself is 57 ms CPU — cost was never the objection; un-debounced
DirectoryNotEmpty triggering was, and path-unit proposals are vetoed).
@@ -0,0 +1,78 @@
---
name: detached-fleet-job-launch
description: Launch long-running jobs (fleet hosts or local workstation) so they survive ssh/logind/harness teardown; includes worker takeover law for queue daemons
---
# Detached long-running jobs on Dank fleet hosts
## Law
`ssh host 'setsid nohup CMD & '` is NOT detached. systemd-logind deactivates
`session-N.scope` when the ssh command returns and SIGKILLs the whole cgroup:
no journal entry, no OOM trace, python block-buffers stdout so the log stays
0 bytes. setsid/nohup do not escape the cgroup.
## Canonical launch (remote)
```
ssh host 'export XDG_RUNTIME_DIR=/run/user/$(id -u); \
systemd-run --user --collect --unit=NAME \
-p SendSIGKILL=no \
-p StandardOutput=append:/home/dank/logs/NAME.log \
-p StandardError=append:/home/dank/logs/NAME.log \
sh -c "CMD -u; echo \$? > /home/dank/logs/NAME.rc"'
```
- transient user unit = outside every session scope; survives ssh teardown
(requires `Linger=yes`, true for `dank` on both hosts)
- `echo $? > …rc` inside the unit = completion sentinel the unit itself writes
- `-u` / unbuffered output so the log is live
- verify launch in the same ssh call: `systemctl --user is-active NAME.service`
## Local workstation variant (verified 2026-10-10, blankpad)
Harness background jobs get **tree-killed** at timeout — setsid/nohup children
die too (survives the process-group kill, not the tree walk). Same cure as
remote, run locally:
```
loginctl enable-linger $(whoami) # default Linger=no kills user units at session end
XDG_RUNTIME_DIR=/run/user/$(id -u) systemd-run --user --collect --unit=NAME \
-p StandardOutput=append:/abs/path/job.log \
-p StandardError=append:/abs/path/job.log \
/full/path/to/binary args...
systemctl --user is-active NAME.service
```
Gotchas:
- bash-tool env lacks `XDG_RUNTIME_DIR` → set it inline.
- `--collect` GCs the unit when it **exits** — after a crash-exit,
`systemctl --user restart NAME` fails "Unit not found"; relaunch with
`systemd-run`. Redeploy code = `stop` (not `restart`) + relaunch.
- point StandardOutput at the job's canonical log file (what the UI/SSE tails),
not a sidecar — observed drift: worker.log vs per-run log.txt diverged.
- foreground `(cmd &)` and `(cmd &)` subshells die with the tool call's group.
## Worker takeover law (queue daemons)
A supervisor killed mid-run leaves `queue.json.active` set ⇒ every relaunching
worker honors "one run at a time" and exits ⇒ silent queue deadlock. Cure:
- heartbeat: run dir `worker.pid`, rewritten every ~5s while running, removed
on completion;
- claim rule: `active` set + pid dead (`process.kill(pid,0)` ESRCH; EPERM =
alive) ⇒ resume; pid alive ⇒ exit.
- resume must be append-only + completed-key skip so takeover is cost-free
(observed: 386 completed samples skipped, zero re-requests).
## Watcher (one async job, never poll)
```
ssh host 'while [ ! -f /home/dank/logs/NAME.rc ]; do sleep 60; done; \
echo RC=$(cat /home/dank/logs/NAME.rc); tail -25 /home/dank/logs/NAME.log'
```
Run it async with timeout 0; the result is injected on completion.
## Forensics when a detached job died
1. `ps -p PID` — gone.
2. Log 0 bytes → buffered stdout lost at SIGKILL.
3. `journalctl --since … | grep -E "session-[0-9]+.*(Deactivated|Removed session)"` —
timestamps matching the death window confirm the scope kill (vs OOM, which
leaves kernel lines).
## Notes
- zsh remote shells: `echo ===X===` fails (`=cmd` expansion); quote sentinels.
- The HF upload of 2026-08 (113.7 GiB, hf_xet) died this way ~2 min in;
relaunch as above succeeded.
- Frank's machine: `systemctl --user terminate` is not a verb — use `stop`.
@@ -0,0 +1,27 @@
---
name: faillock-sudo-lockout-recovery
description: "Diagnose and recover from a pam_faillock sudo lockout on Frank's Arch workstations, typically caused by an agent's non-interactive sudo retries"
---
# Faillock Sudo Lockout Recovery
## Symptom
User reports "sudo password rejected / changed"; journal shows
`pam_faillock(sudo:auth): User <u> is temporarily locked out due to 3 consecutive failed login attempts`.
## Diagnose (read-only, one round)
1. `stat -c '%n mtime=%y ctime=%z' /etc/shadow` — unchanged mtime = password was NEVER changed.
2. `journalctl --since '<window>' | grep 'sudo\['` — lines with `PWD=` / `COMMAND=` name the spammer (agent harness loops show `PWD=/home/.../.omp`, `COMMAND=...yay/pacman...`, no `TTY=`).
3. `faillock --user <user>` — shows the valid failure entries.
## Root cause law
In the omp harness sudo has NO TTY: each `sudo <cmd>` = 3 pam_unix failures. Arch pam_faillock defaults deny=3 with ~600s unlock_time, and each retry re-arms it — the user's real interactive sudo is locked while the loop runs.
## Recover
1. Kill the loop: `ps -eo pid,ppid,etime,cmd | grep -E 'sudo|yay|makepkg'` → kill offending PIDs; stop/kill the responsible agent session.
2. `faillock --user "$USER" --reset` — works unprivileged for your own user (dir /run/faillock/<user> is 0700 owner).
3. User verifies with `sudo -v` in a real terminal.
## Prevent
- Agents NEVER run sudo/yay/pacman/pkexec; no SUDO_ASKPASS/expect workarounds.
- Root-requiring steps are handed to the user as exact copy-paste commands in the final report.
@@ -0,0 +1,19 @@
---
name: fandom-wiki-media-fetch
description: Fetch audio/image media from a Fandom wiki via its MediaWiki API when the wiki HTML pages 403 curl or hang headless browsers.
---
Goal: get direct media URLs (and files) from a starcraft.fandom.com-style Fandom wiki.
Why this route: Fandom HTML pages return a ~5 KB JS shell to `curl` (403 via `read ...:raw`) and time out headless Chromium (`browser.open`/`goto` >30 s on quotation megapages). The MediaWiki API is plain JSON and answers plain curl.
Steps:
1. Find the content page's wikitext:
`curl -sL -A "Mozilla/5.0" "https://<wiki>.fandom.com/api.php?action=parse&page=<Page_Name>&prop=wikitext&format=json&formatversion=2"`
Top-level pages are often tabber/transclusion shells — follow the `{{:Subpage}}` / `{{TemplateName}}` references with the same call (e.g. `StarCraft_unit_quotations` → `/Protoss` → `Template:QuotesSC1Zealot`).
2. Extract media names from the wikitext. StarCraft quotes use `{{sm2|File.ogg|displayed quote}}` inside `{{UnitQuoteBox|select=...}}` fields — match the quote text to pick the exact file.
3. Resolve direct CDN URLs (works for images too):
`curl -sL -A "Mozilla/5.0" ".../api.php?action=query&prop=imageinfo&iiprop=url|size&format=json&formatversion=2&titles=File:A.ogg|File:B.jpg"`
→ `query.pages[].imageinfo[0].url` = `https://static.wikia.nocookie.net/...` — downloadable with plain `curl -sfL`.
4. To discover art on a unit page: `action=query&prop=images&imlimit=100&titles=<Page>`, filter titles (icon/art/portrait/Head/Render), then resolve the keepers via imageinfo. Note splash/head art naming: `File:Zealot SC2 Art1.jpg`, `File:Archon SC2 Head1.jpg`; some obvious guesses (SC1-era units' SC2 splash) do not exist — verify with imageinfo, absent pages return `missing` with no `imageinfo`.
5. Verify downloads (`file sfx/*.ogg` → `Ogg data, Vorbis audio`) — a 403 body saves as a success with `-o` and no `-f`.
@@ -0,0 +1,26 @@
---
name: fleet-image-pull-freshness-audit
description: "Answer \"when was the last time we pulled/updated engine?\" for digest-pinned dank-fleet images: git-history pin dating vs host podman state, pull-vs-restart distinction."
---
Answer "when was the last time we pulled/updated <engine image>?" for a digest-pinned fleet endpoint (radiance/halogen/llama.cpp images on dankserver/dankclaw).
## Procedure
1. **Locate the pin.** `grep` the repo for the image digest prefix (e.g. `4c3f484564`) — pins live in `scripts/<svc>.env` (`FRANK_ARCHON_IMAGE` etc.) and are restated in `AGENTS.md` / runbooks.
2. **Date the pin in git.** `git log --all --format='%h %ad %s' --date=short -S '<digest-full-prefix>'` — the OLDEST hit is the cutover that pulled it (renames and service-name sweeps produce later hits; ignore them).
3. **Check the host state.**
`ssh <host> 'sudo podman images --format "{{.Repository}}:{{.Tag}} {{.ID}} {{.Digest}} {{.CreatedTime}}"'`
Compare the on-disk digest to the pin. `CreatedTime` is the VENDOR build stamp, not the pull time — it proves which build is on disk; the git commit dates the pull.
4. **Distinguish pull from restart.** Digest pinning means the serve script NEVER auto-pulls; a fresh `ActiveEnterTimestamp` (`systemctl show <svc> -p ActiveEnterTimestamp`) is a restart, not an update.
5. **Sweep side pulls.** A newer engine version may have been pulled for a one-off (calibration, KLD, bench) and removed after. `podman images` census + `git log -S` for the second digest; note when the side image is gone from disk (answer: "pulled <date>, no longer on disk").
## Facts to carry (radiance example)
- frank-archon pin `stilldeadcode/radiance@sha256:4c3f484564…` (1.0.13) pulled 2026-10-05 (cutover commit); reused unchanged at the 2026-10-08 w4dn8 cutover.
- Engine 1.2.0 was pulled 2026-10-08 for calibration only; image absent from disk afterwards.
- Cross-pin compatibility (e.g. RADGRAM2 grams byte-identical across pins) is documented in the runbook — cite it when explaining a calibration/serving pin split.
## Answer format
Table: pin-first-introduced commit+date (the pull), later cutovers reusing the same digest (no pull), side-image pulls, last container start (restart, explicitly labeled not-a-pull). Offer upstream freshness scout as next step; a digest change for an LLM endpoint requires the stop-and-mask bench-window law.
@@ -0,0 +1,30 @@
---
name: fleet-judge-swap-calibrate
description: "Swap the frank-intelligence-bench judge default and prove judge quality: fleet alias→weights map, known-truth calibration via scripts/judge-calibration.ts, per-backend reasoning-off dialect traps."
---
# Fleet judge swap + calibration (frank-intelligence-bench)
## Fleet alias → weights (verified 2026-10-10 via ps on dankclaw; endpoints only serve aliases)
- frank-archon = Qwen3.8-Flash-Next quant (radiance TP=2, dankserver:7770)
- frank-dragoon = qwen38-flash-next-v2-swift15-abliterated.hgn, 180B MoE (halogen container `frank-large-halogen`, dankclaw:7770) — check `podman inspect frank-large-halogen --format {{json .Config.Env}} | grep HALOGEN_CHECKPOINT`
- frank-zealot = Qwen3.5-9B-Uncensored-HauhauCS-Aggressive Q6_K (llama.cpp, 3090, dankclaw:7771) — check `ps aux | grep <port>`
## Swap procedure
1. `config/hosts.yml`: target host gains `roles: [target, judge]`; set `judge_default`. No-self-judge law: `judge == target` throws at enqueue; rotation = next fleet model.
2. Judge load = AA-LCR + HLE only (SciCode/Terminal-Bench are unit-tested, judge-free).
3. Mid-run: `run.json` pins its judge (provenance); default swap affects future runs only.
4. Docs: `docs/endpoints.md` judge law section; commit with gates.
## Calibration — the ONLY accepted proof
`bun scripts/judge-calibration.ts [n] [judges...]` — known-truth floor test: gold answers must be judged CORRECT, deterministically mutated (first number +7) must be INCORRECT. Reports accuracy, mean s/verdict, projected AA-LCR/HLE judge hours. Safe beside a live run (judge-only traffic).
- Known-truth is a FLOOR test: catches broken judges, not lenient ones. Gold-vs-mutation is easy mode; small-model leniency on model-vs-gold edges needs real-verdict spot audits.
## Traps (all fired at least once)
- **False-accuracy via transport death:** a 400-dying judge scores ~50% on this test (all verdicts collapse to INCORRECT, matching the mutation half by luck). ALWAYS inspect `err` on misses before trusting any agreement/accuracy number. Fix: if all misses share one `err=HTTP 4xx`, it is the harness, not the judge.
- **Reasoning-off dialect split:** llama.cpp/radiance want `chat_template_kwargs:{thinking:false}`; halogen/vLLM-style servers 400 on `thinking` and honour `enable_thinking`. HostSpec field `reasoning_off: thinking|enable_thinking` (default `thinking`; dragoon needs `enable_thinking`). Wire-test via mock `bodies` capture.
- **No historical judged rows?** Quarantined ledgers may pre-date judge usage (census first: `failureCensus` rows/phase), then use known-truth synthesis instead of re-judging.
## Baselines measured (for future swaps)
- zealot (9B): 25/25 known-truth, 13 s/verdict → AA-LCR 1.1h, HLE 9.3h; leniency risk on hard edges (small judge + compliance tune).
- dragoon (180B): 2 s/verdict → AA-LCR 0.2h, HLE 1.4h; family-bias caveat when target shares its base family.
@@ -0,0 +1,83 @@
---
name: fleet-served-model-id-rename
description: "Rename the served model id (alias) on a dank-fleet LLM endpoint and re-key every consumer that keys on it, with live verification."
---
# Fleet served-model-id rename (wire rename)
Renames the id an endpoint announces on the wire. The service, unit, script and env
FILE names keep the old name (container/unit identity is stable) — only the served
alias moves. Done this way on 2026-10-09: frank-primary→frank-archon,
frank-large→frank-dragoon, frank-secondary→frank-zealot.
## 1. The knob, per engine
| Endpoint | Knob |
|---|---|
| frank-primary (radiance, dankserver :7770) | `FRANK_PRIMARY_SERVED_NAME` in `scripts/frank-primary.env` → `--served-model-name` |
| frank-large (halogen flash-server, dankclaw :7770) | `HALOGEN_MODEL_ID` in `scripts/frank-large.env` |
| frank-secondary (llama.cpp, dankclaw :7771) | `ALIAS` in `scripts/frank-secondary.env` → `--alias` |
Also flip the serve-script defaults (`: "${ALIAS:=…}"`, the `:?` gate messages).
`scripts/deploy-remote.sh` has no alias references — the deployer needs no edit.
## 2. Consumers that key on the id (all in ONE change)
- Seams: `tests/frank_{secondary,large,primary}_unit.bats` — fixture env value,
the argv assertion (`--alias`), the env grep pin, the `-e HALOGEN_MODEL_ID=` render
assertion.
- OpenWebUI (Dankclaw, rootless quadlet, `webui.db`): `model.id`+`name`,
`access_grant.resource_id` (`resource_type='model'`), `openai.api_configs`
per-backend `model_ids`. Backup first (`webui.db.bak-YYYYMMDD`; the DB is WAL-mode).
No sqlite3 binary in the image → `podman exec -i open-webui python3 -` with a
script on stdin. Then `systemctl --user restart open-webui.service`. A stale id
fails SILENTLY: OpenWebUI intersects the backend `/v1/models` with `model_ids`, so
the picker just goes empty.
- Hermes (`/home/dank/Containers/hermes/data/config.yaml`, root-owned → sudo):
`model.default`, the provider `models:` key, every `auxiliary:` route naming the
endpoint. Backup, surgical `sudo sed -i`, sweep `grep -rln <old-id>` over the data
dir (session JSONs are historical — leave), restart `hermes.service
hermes-webui.service` (user units).
- Dev harnesses: `"model": "frank-…"` literals in `scripts/bench-*.py`,
`deep-probe.py`, `calib-push.py`, `fleet-probes-swift15.sh`, `autoresearch*`.
- Docs: `shared/homepage/docs.html` (headings, api-id annotations, the
curl/opencode/Pi examples), endpoint runbooks, `AGENTS.md` Model Config,
`serverAppConfigs-docs/Agent/OMP Harness.md`, `Dankclaw/OpenWebUI.md`.
- Harness side (`~/.omp/agent/models.yml` on the dev box) — ids and `modelRoles`
must name the same string; two ports on one host = two providers.
## 3. Deploy + verify
`./scripts/deploy.sh <host> --service <name>` per endpoint; keep the tree clean
(untracked scratch files inside the repo fail the whole-tree gate — put probes in
/tmp with a `node_modules` symlink to `tests/e2e/node_modules`).
Verify on `/v1/models`, NEVER on a chat echo: radiance echoes the REQUESTED model
name (a 200 proves nothing), llama.cpp and halogen ignore the requested id.
```bash
for u in 192.168.0.235:7770 192.168.0.143:7770 192.168.0.143:7771; do
curl -fsS "http://$u/v1/models"; done
```
OpenWebUI acceptance = a real browser as a family account: the welcome screen names
the model and `POST /api/v1/chat/completions {"model":"<new-id>"}` answers.
`GET /api/v1/models/list` returning `{"items":[],"total":0}` for that same account
is a probe artifact, not a failure. Playwright notes: `locator.isVisible({timeout})`
does NOT wait (returns immediately) — use `waitFor({state:'visible'})`; the composer
is a contenteditable div, not a textarea; stable anchors are
`getByRole('button', {name: /selected model/i})` and "How can I help you today?".
## 4. Trap: green deploy, old config
`deploy.sh` plans from repo paths and gates on the port. A systemd drop-in under
`/etc/systemd/system/<unit>.service.d/` can override `Environment=FRANK_*_ENV_FILE`
and `ExecStart` to a staging worktree, so the deploy restarts a DIFFERENT tree and
reports healthy (this bit frank-primary for a day after a quantization campaign).
After an LLM deploy, prove the running shape:
```bash
systemctl cat <unit> # drop-ins print at the bottom
podman inspect <container> --format '{{json .Config.Cmd}}' | tr , '\n'
```
Before retiring such a drop-in: diff staging vs checkout for serve script/unit/env,
and `git merge-base --is-ancestor <staging-HEAD> HEAD` so no campaign tuning is
lost. Move it to `~/.AGENTTRASH/YYYYMMDD-topic/` (never `rm`), `daemon-reload`,
restart, re-read `/v1/models`.
@@ -0,0 +1,58 @@
---
name: fleet-service-rename-cutover
description: "Rename a Dank-fleet LLM service end-to-end (repo trio, deployer keys, tags, hosts): the 2026-10-10 frank-archon/dragoon/zealot cutover procedure."
---
# Fleet service rename cutover (serverAppConfigs)
Procedure proven 2026-10-10 renaming the three LLM services to their wire
aliases (repo `55f87fc`). For any future full service rename.
## Repo codemod
1. `git mv` every tracked file whose path carries the old token
(`frank-<old>` → `frank-<new>`, plus `frank_<old>` bats seams).
2. Content sweep over all tracked text files:
`git grep -Ilz '' | xargs -0 sed -i` with expressions for
`frank-<old>`, `FRANK_<OLD>`, `frank_<old>` → new forms; add the
`LLM_<OLD>` / `llm_<old>` / handler-name forms in the same pass.
3. Known casualties to record: `scripts/patches/*.patch` context lines get
rewritten (patches stop applying byte-exact); historical prose adopts
the new name (one service, one name — Frank's rule).
4. Fix sed-made-contradictory lore by hand: AGENTS.md Model Config
paragraph, any "keeps the old names" sentences. Do NOT enumerate the
retired names anywhere — the acceptance test is
`git grep -cE '<old forms>'` == 0 (case-insensitive too).
5. Rename rollback tags in lockstep (docs quote tag names):
`git tag NEW OLD; git tag -d OLD`, push new, delete old refs on origin.
6. Sweep the harness config comments too: `~/.omp/agent/models.yml`.
## Deployer keying (why the sweep is safe)
`scripts/deploy-plan.py` SERVICE_ORDER/host sets + `services_for_path`
trio mapping; `scripts/deploy-remote.sh` `service_is_allowed` case,
`deploy_service` dispatch, `relink_unit <service>` (symlink
`/etc/systemd/system/<name>.service` → `scripts/<name>.service`),
markers keyed by service name under `~/.local/state/serverAppConfigs/
deploy-state/`. `add_pending_services` AUTO-TRASHES markers whose service
is retired — no marker surgery needed if you skip a full drain.
## Host cutover per endpoint (order matters)
1. `sudo systemctl stop <old>`; rename `~/logs/frank_<old>.log` → new;
`podman rm -f` the old container name(s);
`sudo rm` the unit symlink + `multi-user.target.wants/<old>.service`;
`sudo systemctl daemon-reload`; trash the marker to
`~/.AGENTTRASH/<date>-fleet-rename/` with a NEUTRAL filename.
2. `deploy.sh <host> --service <new-name>` (run from the main checkout;
refuses on a worktree branch). The handler relinks under the new name,
enables, restarts, health-gates.
3. Do the harness-default-model endpoint LAST on its host, and never stop
an endpoint while a deploy on the other host is approaching its e2e
gate (the gate probes the live fleet).
## Acceptance
`/health` 200 + `/v1/models` shows the new id (radiance echoes requested
names — verify on `/v1/models` only); unit active+enabled; new container
name running; `git grep` zero on every checkout; e2e gate green;
Prometheus job up (note: llama.cpp needs `--metrics` in the serve script
for the scrape — absence is a pre-existing gap, not a rename regression).
Out of scope: retired endpoint names (`frank-medium/small/npu`) and git
history — old strings there are expected.
@@ -0,0 +1,36 @@
---
name: fork-locked-gguf-endpoint-trial
description: "Trial a fork-locked GGUF (custom tensor types: ternary/PQ2_0/PTQ1_0, vendor Hadamard metadata) as a replacement payload on a fleet llama.cpp endpoint: identify the fork, close provenance of local variants, build the right binary, and run one trap-guarded census window that measures VRAM, per-slot window, tool/vision/reasoning channels, and long-context stability."
---
# Fork-locked GGUF trial on a live endpoint
Use when a candidate GGUF carries **non-stock tensor types** (custom type ids, vendor metadata such as `prism.hadamard.*`) so mainline llama.cpp refuses it or loads a sibling `F16` file silently with garbage. Pairs with `gguf-header-vram-fit-analysis` (paper pricing); this skill is the *measure-it* half.
## 1. Header census decides everything downstream
Range-parse `resolve/main/<file>.gguf` bytes 0–48 MiB (metadata is ~11 MiB at 250k vocab; GGUF v3 strings are **u64**-length). Extract: `general.architecture`, `block_count`, `full_attention_interval`, `attention.head_count_kv`, `key_length`/`value_length`, `context_length` (target window > native ⇒ YaRN), `<arch>.nextn_predict_layers` (absent ⇒ no drafter, ever), tensor-type histogram, and per-tensor byte sizes from consecutive `offset` deltas ⇒ exact bpw.
- `KV = full_layers × head_count_kv × key_length` elements/token **per cache**; only full-attention layers bill per token, linear/DeltaNet layers bill a fixed state per slot.
- Ternary packs shrink weights only. At 262,144 tokens KV (16 full layers, 4×256) = 16.0 GiB f16 / 8.5 GiB q8_0 / 4.5 GiB q4_0 — 3× the 6.71 GiB of weights. **The cache is the wall, never the pack.**
## 2. Fork identification and tag ancestry
- Map custom type id → name from the vendor fork (`ggml/include/ggml.h` on its branch, e.g. `GGML_TYPE_PQ2_0 = 142`, `PTQ1_0 = 143`).
- `GET /repos/<fork>/compare/<tag>...<branch>` → `ahead_by` + commit subjects tells whether the newest **release** contains a fix, without reading history. Release workflows lie by omission: grep the workflow for the flag you need (e.g. prebuilt CUDA lacked `GGML_CUDA_FA_ALL_QUANTS` → quantized/mixed KV silently runs flash attention on CPU, ~2× slower; vendor issue #267).
## 3. Close provenance of payload variants already on disk
`sha256sum` the local file (7.2 GB ≈ 15 s NVMe), then search the Hub (`/api/models?search=<name>`, `/api/models/<repo>?blobs=true` → sibling `lfs.sha256` + size). Equal size + different sha = re-weighted variant, not a corrupt copy. Known houses: `dealignai/*-CRACK-*` (uncensored), `ManniX-ITA/*-MTP-GGUF` (appended MTP head ≈ +450 MB, restores a drafter the official file lacks). Same size/geometry ⇒ the VRAM verdict carries across variants; behaviour (refusals, tool calls) does not.
## 4. Build recipe (Dankclaw, RTX 3090)
`/usr/local/cuda` does not exist for non-interactive shells; `/usr/bin/nvcc` is a Debian wrapper. Mirror the known-good cache instead of guessing:
```
cmake -G Ninja -B build-cuda-prism -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CUDA_COMPILER=/usr/bin/nvcc -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-13 \
-DCMAKE_CUDA_ARCHITECTURES=86-real -DGGML_CUDA=ON -DGGML_CUDA_FA_ALL_QUANTS=ON
```
nvcc 12.4 refuses gcc 15 (`gcc-13`). FA template instances dominate the build (~150/804 in 12 min); park it with `pkill -f "[r]un-<build>"` + `pkill -x ninja` before timing anything — the bracket stops `pkill -f` matching your own ssh cmdline.
## 5. One-window census (never two outages)
Fetch through a committed script (`-fsSL --fail-with-body`, byte count + LFS sha, no resume, no token in argv), then stage `/tmp` throwaways: candidate env fed to the **repo serve script** via its `*_ENV_FILE` override (never edit remote config), and a runner whose `trap restore EXIT` always kills the candidate by pid and `systemctl start` the production unit + health-polls to 200.
Order inside the window: copy the production log aside (serve scripts truncate it) → stop unit → wait `memory.used < 1500` → boot candidate → `/health`, boot ledger (`KV buffer`, `compute buffer`, `flash`), `nvidia-smi --query-compute-apps`, `/v1/models`, `/props`, `/slots` (per-slot `n_ctx` is the harness contract, `pool/parallel`), chat probe (does thinking land in `reasoning_content` or `content` — a vendor `--reasoning-in-content true` default breaks the harness channel), tool-call probe (name + valid-JSON `arguments`), 512 px solid-colour vision probe, then a ~190k-token `/completion` fill reading `timings`. Kill the server, then `llama-bench` dtype arms (`-c <pool> -np <slots> -ctk/-ctv -b 2048 -ub 2048 -p 4096 -n 128 -r 1`) under a 1 Hz `nvidia-smi` sampler, `peak_gpu_mib=$(sort -n | tail -1)` per arm. Skip `-mmp 1` unless `-mmproj` is also passed.
## 6. Gates that outvote the memory table
Vendor `KNOWN_ISSUES.md` + open issues (quantized-KV resets near 200k, inert ngram/`--cache-reuse` on hybrid memory, malformed `tool_calls`, prompt-cache loss from re-rendered reasoning); base alignment (an uncensored endpoint replaced by a censored base is a fleet-wide capability loss); and which harness roles point at the endpoint (`models.yml` + `modelRoles`) — tool-heavy roles fail loudly on a `tool_calls` defect. Repo seams pinning `-ctk/-ctv` literals must be re-keyed to read the env file, so a retune moves one file, not two.
@@ -0,0 +1,52 @@
---
name: frank-bench-run-ops
description: "Operate frank-intelligence-bench runs on the dank fleet: status census, full void + fresh launch, judge cutover, supervisor restart law, watchdog timer, fault-autopsy snippets."
---
Runbook for /home/blank/Projects/frank-intelligence-bench (Bun/TS). Companion to frank-bench-supervisor-recovery (row-voiding for poison); this skill is the ops loop around it.
## Status census (run from repo root)
```bash
export RID=$(ls -t runs | grep -m1 frank-archon) # live run id
tail -2 runs/$RID/log.txt
python3 - <<'PYEOF' # latest-wins cohort census (mirrors score.ts semantics)
import json, os, collections
led = f"runs/{os.environ['RID']}/ledger.jsonl"
best = {}
for l in open(led):
r = json.loads(l)
if r["phase"] != "grade": continue
k = (r["eval"], r["instance"], r["repeat"])
if r["ok"] and (k not in best or r["ts"] > best[k]["ts"]): best[k] = r
elif k not in best: best[k] = r
by = collections.defaultdict(lambda: [0,0])
for k,v in best.items(): by[k[0]][0]+=bool(v["grade"]["correct"]); by[k[0]][1]+=1
for e,(c,n) in sorted(by.items()): print(f"{e}: {c}/{n} = {100*c/n:.1f}%")
PYEOF
```
`pgrep -f "cli.ts _exec"` = supervisor; `systemctl --user is-active fib-watchdog.timer` = watchdog; last ledger ts older than ~5 min = suspect stall.
## Launch laws (each earned by an outage)
- Code fix + running supervisor = FIX IS NOT LIVE. Restart supervisor: `pkill -f "cli.ts _exec"`, then relaunch.
- Correct detached form from harness bash: `cd runs/$RID && setsid nohup bun run ../../src/cli.ts _exec run.json >> log.txt 2>&1 < /dev/null & disown`. NEVER the subshell form `( ... & )` — reaped by cgroup within minutes.
- Preferred launch = `bun src/cli.ts run HOST --judge JUDGE --detach` (in-process spawn + unref, proven 6 h+).
- Watchdog = systemd user timer `fib-watchdog.{service,timer}` (~/.config/systemd/user), 10 min, Persistent, runs `bun src/cli.ts worker` (idempotent: exits when heartbeat fresh, takes over stale runs via 30 s mtime gate).
## Void all results + fresh run (model/endpoint change)
1. `pkill -f "cli.ts _exec"`; sleep 1.
2. `mv runs/<id> .AGENTTRASH/run-voided-<reason>-$(date +%s)`; `mv reports/<model>/* .AGENTTRASH/`.
3. queue.json: set `runs: []` (python json edit).
4. New run: `bun src/cli.ts run frank-archon --judge frank-dragoon --detach`.
5. Log the void in docs/log.md (reason, quarantined scope) and commit — never silently erase scores.
## Judge cutover (zealot→dragoon pattern)
Void ONLY LLM-judged evals' `phase=grade` rows (keep ALL gen rows — target output is judge-independent); set `judge` in run.json; restart supervisor; resume rejudges automatically (grade keys incomplete). SciCode = pytest, judge-independent, never voids for judge changes.
## Fault-autopsy snippets
- Engine-fault census: grade rows with `ok:false` + empty text = engine, not model (signature-default trap: "AssertionError × N" with empty text = grades never executed).
- Runaway-chain detector (HLE loops): gen rows with `finish=="length"` AND completion_tokens==max AND empty text = reasoning degenerated into repetition; measure pre/post any quantization change to separate model behavior from engine artifacts.
- Census-by-eval: `collections.Counter((r["eval"], r["phase"], r["ok"]) for r in rows if r["ts"] > <cutover>)`.
- Verdict-parser bugs: probe the live judge on a minimal case (`judgeAalcr`/`judgeHle` via `bun -`), inspect raw head+tail; llama.cpp thinking models may return content:"" with everything in reasoning_content, and prose verdicts when their JSON scaffold breaks.
## Reporting etiquette (Frank's rules)
Quote score as: per-rep %, test avg@1 / avg@3, dev, index-lite; label provisional until all reps land; publish the runaway/parse-rate census next to any handicapped number. Dashboard :8787; `bun src/cli.ts report` renders the trio; `analyze` prints failure census.
@@ -0,0 +1,45 @@
---
name: frank-bench-supervisor-recovery
description: "Recover a frank-intelligence-bench run after engine faults: detect poison grade rows, void them, restart supervisors with fixed code, judge cutover, watchdog for multi-day runs."
---
Recover frank-intelligence-bench runs on the dank-fleet after engine faults (poison graders, judge swaps, supervisor deaths).
## Laws (friction-earned 2026-10-10/11)
1. **A code fix is only real after the supervisor restarts.** Bun loads code at spawn; an on-disk fix never reaches the running process. Kill `_exec`, relaunch — takeover law makes resume cheap.
2. **A judge cohort is all-or-nothing.** Never mix judges on one eval's grade rows. Void ALL grade rows for LLM-judged evals (`phase == "grade"`), KEEP all `gen` rows (target output is judge-independent) — resume rejudges from cache.
3. **Poison census signatures:** grade rows with empty text + forced FAIL = engine, not model. `unparseable verdict` storm = judge output class (empty `content` + chain in `reasoning_content`; or 2048-tok truncation before verdict JSON). Default census sig `AssertionError` masked a poison shift — treat mass-default sigs as red flags.
4. **Detached launch form matters:** from a short-lived harness shell, `(setsid nohup cmd & )` (subshell) gets cgroup-reaped within minutes — silent death, stale `worker.pid`, no log line. Proven form: `setsid nohup cmd >> log.txt 2>&1 < /dev/null & disown` as the tail of the same shell (survived 6 h overnight).
## Void-and-resume procedure
```bash
pkill -f "cli.ts _exec"
python3 - <<'PYEOF'
import json, pathlib, shutil, time
led = pathlib.Path("runs/<RUN_ID>/ledger.jsonl")
rows = [l for l in led.read_text().splitlines() if l.strip()]
keep, void = [], []
for l in rows:
r = json.loads(l)
if r["eval"] in (<EVALS>) and r["phase"] == "grade" and r["ts"] > "<CUTOVER_ISO>":
void.append(l)
else:
keep.append(l)
shutil.copy(led, f".AGENTTRASH/ledger-voided-{int(time.time())}.jsonl")
led.write_text("\n".join(keep) + "\n")
print(f"voided {len(void)}; kept {len(keep)}")
PYEOF
cd runs/<RUN_ID> && setsid nohup bun run ../../src/cli.ts _exec run.json >> log.txt 2>&1 < /dev/null & disown
```
- Judge cutover variant: void ALL `phase==grade` rows for the judged evals (no ts filter), edit `run.json` `judge` field, relaunch.
- Validate the judge endpoint first: live probe both directions (correct + incorrect candidate); a good judge returns bare verdict JSON fast.
- Resume proof = a specific known step flipping FAIL→PASS (or vice versa) in the log within minutes.
## Watchdog for multi-day runs
No cron on the Arch workstations — systemd user timer, idempotent (`cli.ts worker` exits instantly when supervisor heartbeat is fresh; takes over via pid-file 30 s freshness gate):
`~/.config/systemd/user/fib-watchdog.service`: Type=oneshot, WorkingDirectory=repo, `ExecStart=/usr/bin/bun src/cli.ts worker`, stdout/stderr `append:.../runs/watchdog.log`.
`fib-watchdog.timer`: OnBootSec=5min, OnUnitActiveSec=10min, Persistent=true, WantedBy=timers.target.
`systemctl --user enable --now fib-watchdog.timer`; verify `loginctl show-user <u> -p Linger` = yes (survives logout/reboot). Prove idempotence: log line "has a live supervisor — worker exits".
## Scoring truth after repair
Report renders from ledger with latest-wins via `ok` filter (no explicit dedup): ok=false rows drop, recovered key counts once. UI `done` counts unique `(eval,instance,repeat)` completed keys. SciCode denominators: 341 steps/rep (not 1120/3-rep arithmetic traps).
@@ -0,0 +1,30 @@
---
name: frank-job-hunt-grilling-state
description: "Resume frank-job-hunt: repo is BUILT and deployed (agents live on Dankserver, ntfy on Dankclaw); grilling closed, design v2 shipped; lists pending ops items and invariants."
---
## Status (2026-10-11): design grilling CLOSED (design v2 signed); repo built, deployed, self-verifying.
Repo: ~/Projects/frank-job-hunt (workstation) — Gitea `dank/frank-job-hunt` on Dankclaw; Dankserver clone runs timers (sweep 07:30, digest 08:15, Linger=yes). Pre-commit hooks: bun test + tsc + docs-link + secret scan. Tests: 69, strict TS (no `any`; Records for static tables; named contract types).
## Machine
- `agents/sweep.ts`: registry → greenhouse/lever/ashby/workday public APIs → gates → frank-zealot score → `jobs/*/inbox.json|gated.json`; `--dry --ats=` flags; per-site fetch dedupe; per-entry error census.
- `agents/digest.ts` → ntfy (ntfy.franksdank.cloud, LAN; topic secret in `~/.config/frank-job-hunt.env` 600); degrades to file + exit 1.
- `agents/review.ts`: funnel metrics (weekly rate 8-10, callback floor 4%).
- `agents/verify-registry.ts --write`: monthly ATS-locator probe/re-attribute (14 scout mis-attributions repaired).
## Truths earned in build
- Registry truth: **21 live API sites, ~169 manual lane** (vanity Workdays, self-built ATS incl. majors, neogov). Scouts' ats attribution ≈ guess — verify before trusting counts.
- Gates do the discriminating; zealot only ranks survivors (9B inflates everything: recruiters scored 91 before family gate).
- Gate law: role FAMILY gates unconditionally (recruiter/sales/people-ops/TPM/solutions, no infra override); topic negatives get infra-token precedence ("full-stack platform engineer" passes). Comp NEVER gated; unstated location passes to human.
- Metro regex = Puget Sound list only (Tuolumne CA leak fixed — WA localities, no CA decoys).
- ATS tokens: first DNS label (greenhouse.harpereng.com→harpereng); vanity workday URLs are NOT CXS-resolvable → manual lane.
- Zealot (llama.cpp thinking model): send `chat_template_kwargs:{enable_thinking:false}` + max_tokens ~600; verdict in `content` then; `reasoning_content` fallback kept.
## Pending (user-side / next session)
1. Porkbun API 503'd: public A record `ntfy → 71.227.227.172` still missing (LAN works; add via UI when API recovers, for off-LAN phone).
2. Frank: phone ntfy app subscribe (topic in env file); LinkedIn CSV export → `contacts/`.
3. First production digest = tomorrow 08:15; check `journalctl --user -u frank-job-digest.service` + phone.
4. bg_6 (last session): full dry-sweep proof of family gate (top-10 must be engineering-only) — check `agent://`/proc result if pending.
5. Resume Matcher compose (`agents/resume-matcher/compose.yaml`) not deployed — needs image digest pin on Dankserver.
6. `prep/` STAR/LP bank content fill (daily-loop.md scaffolded).
@@ -0,0 +1,35 @@
---
name: frank-job-hunt-maintenance
description: "Maintain frank-job-hunt agents: live-census gate hardening loop, registry locator verification, and the workstation/Dankserver/Dankclaw deploy-sync ritual."
---
# frank-job-hunt maintenance
Repo `~/Projects/frank-job-hunt` (Gitea `dank/frank-job-hunt` on Dankclaw 192.168.0.143:3000). Production: Dankserver user timers `frank-job-sweep.timer` 07:30, `frank-job-digest.timer` 08:15 (`Linger=yes`). ntfy relay: Dankclaw quadlet `ntfy.service`, route `ntfy.franksdank.cloud` (no anubis), LAN record in `hosts/custom.list` (Pi-hole v6 file at `/home/dank/Containers/pihole/etc-pihole/hosts/custom.list`; v6 API paths are hostile — edit the file, then `podman exec pihole pihole restartdns`). Secrets: `~/.config/frank-job-hunt.env` (600, workstation + Dankserver).
## Law (validated 2026-10-11)
- The fleet judge (`frank-zealot`, dankclaw:7771) is a **ranker, never a gate** — it inflates anything reaching it (non-technical roles scored 88-91). All discrimination lives in deterministic gates (`agents/lib/gates.ts`); zealot only ranks survivors.
- Thinking-model fault: verdict lands in `reasoning_content` with `content:""` → judge sends `chat_template_kwargs:{enable_thinking:false}` (0.7s clean JSON).
- **Family beats topic**: role family (recruiter/sales/finance/PM/designer/people-ops/revops/…) gates `gate:scope` unconditionally — no infra-token override ("DevOps Recruiter" is recruiting). Compound technical titles keep the `INFRA_TITLE` override via `NEG_TITLE` branch.
- **gate:region**: remote is US-only (visa reality). "Remote - Bangalore" = onsite abroad → gate; `US` regex pass wins over `NONUS` match ("U.S. & Canada" passes).
## Gate hardening loop (live-census, TDD)
1. `bun agents/sweep.ts --dry` (full lane ≈ 30–90 s; 852 postings ≈ 62 pass / 790 filed).
2. Classify leaks in the top-10: family gap / region gap / scoring tail. Accept long tail (Director/Founder edge cases) — digest + human curation is the design.
3. Token fix in `gates.ts` regexes (`NEG_FAMILY` = no override; `METRO`/`US`/`NONUS` for arrangement). NEVER dedupe-check by memory — read file first; the file has bitten line-offset edits repeatedly.
4. Pin every leak with a named regression test in `agents/test/gates.test.ts` before/with the fix.
5. `bun test agents/test/ && bun x tsc --noEmit` — hooks re-run these at commit.
6. Ship: `git commit` (pre-commit = tests + tsc + docs-link + secret scan) → `git push` → `ssh dankserver 'cd ~/Projects/frank-job-hunt && git pull'` (timers pull from master; Dankserver drift = tomorrow's run is stale).
## Registry verification (monthly, `agents/verify-registry.ts`)
Scouts' `ats`/`ats_identifier` are guesses (14/84 were wrong). Ritual:
1. `bun agents/verify-registry.ts` (dry, ~70 s serial) → REPAIR census.
2. `--write` applies; repairs with live jobs preferred over empty boards (0-job boards 404 later).
3. Unresolved = honest manual lane (vanity Workdays, self-built ATS incl. majors, neogov) — NOT a code bug; per-entry error census in sweep output is the same signal.
4. Update `targets/README.md` census lines + `docs/log.md`; commit/push/pull (same ship step).
## Pitfalls
- `--ats=<x>` smoke filters the census line — "N API-sweepable" reflects the filter, not the registry; don't diagnose a phantom bug.
- Workday vanity careers URLs (`careers.uw.edu`) are NOT CXS-API-resolvable; tenant-only identifiers are (site path required).
- Root `README.md` vs `targets/README.md` name collision in edit anchors: use full path headers.
- Zealot endpoint probe: `curl http://192.168.0.143:7771/v1/models`.
@@ -0,0 +1,39 @@
---
name: frank-model-bench-grilling-state
description: "Resume the frank-model-bench design grilling: AA Intelligence Index v4.3.2 research brief, fleet endpoint facts, dataset gating, and all settled design decisions for /home/blank/Projects/frank-model-bench."
---
# frank-model-bench — Grilling State (design settled 2026-10-09, awaiting build go-ahead)
Target dir `/home/blank/Projects/frank-model-bench` is EMPTY (no git). Init from zero.
## Settled design decisions
- **Scope**: Tiered reproduction of AA Intelligence Index v4.3.2 (10 evals, 4 categories; Agents 30 / Coding 20 / General 30 / Science 20).
- **Tier A (local, self-graded)**: SciCode, Terminal-Bench 4.0 (podman on Dankclaw via ssh one-shot), AA-LCR, HLE incl. image questions (vision towers).
- **Tier B (external dep)**: GDP.pdf (LiteParse+vision+judge).
- **Tier C (never run; AA reference rows only)**: AA-Briefcase, GDPval-AA, AutomationBench-AA, AA-Omniscience (public set is 600/6000 — demoted), CritPt (key-gated grader API 10/24h — demoted).
- Locally-run weight ≈ 45% of AA index weight. Headline = "Index-lite" (category-weighted over run evals, formula on-page).
- **Stack**: Bun/TS orchestrator (UI, queue, ledger, reports) shelling out to commit-pinned vendored uv venvs per upstream harness (inspect_ai, Harbor, SciCode/HLE scripts). `evals.yml` manifest pins git URL+SHA+entrypoint+args+repeats per eval.
- **UI**: local web dashboard, localhost bind only, SSE live tail, start wizard; CLI `frank-bench`.
- **Endpoints**: registry `hosts.yml` (name, base_url, api_key_ref, model, ctx, backend, role, cap). Discovery = probe known fleet hosts ports 7770-7799 `/v1/models` + `/props`, confirm-then-append. Fleet: frank-archon dankserver:7770 (radiance TP=2 R9700, 200k), frank-dragoon dankclaw:7770 (halogen, 200k×2), frank-zealot dankclaw:7771 (llama.cpp 3090, 200k×2). All OpenAI-compat + API key + vision.
- **Judge**: self-hosted fleet model ONLY, default frank-zealot; hard no-self-judge rotation rule. Scores labeled `local-judge`, drift vs AA hosted graders expected/documented.
- **Execution**: per-endpoint runs, strictly serial queue, one request in flight per eval (per-endpoint cap), temp 0.6 reasoning ON, max_tokens cap 32,768, AA-style 30-attempt retry, per-sample timeout = ctx ÷ measured decode rate.
- **Durability**: per-sample JSONL ledger is canonical artifact; resume mid-eval; reports commit to git (reports/<model>/<run-id>/{index.html,data.json,hf-card.md,manifest+ledger SHA-256}), ledgers gitignored.
- **AA comparison**: free Data API key → `bench sync-aa` pins headline indices; per-benchmark refs hand-seeded in `references/aa-scores.yml` (source URL + fetched-at + index version per row). Free=100 req/24h headline-only; Pro=500 req/24h per-benchmark (stubbed for later).
- **Datasets**: `bench init-data` checklist (hf auth login + accept terms cais/hle, surgeai/GDP.pdf, Harbor Hub); checksum cache ~/.cache/frank-bench/datasets; gated evals refuse with fix-URL and continue suite.
- **Methodology updates**: version-pinned manifests + immutable history; update = one commit: evals.yml bump + docs/methodology rewrite + log.md + resync snapshot. AA cadence ~2-3 minor versions/week; changelog artificialanalysis.ai/changelog.
- **KB**: `docs/` inside repo, OKF v0.1 (type required; index.md per dir; log.md newest-first; .okf/config.yaml from omp-docs); Obsidian-wikilink convention per serverAppConfigs-docs.
- **Git**: Gitea on Dankclaw remote, master base, worktree branch per milestone, pre-commit: bun test + shellcheck + OKF validate. Secrets: ~/.config/frank-bench/secrets.env 600.
- **Tests**: bun test w/ scripted mock OpenAI endpoint (full run→ledger→HTML offline); Playwright dashboard e2e; `bench smoke` manual real-endpoint.
- **Milestones**: M1 scaffold+registry+mock; M2 SciCode E2E w/ real report trio; M3 rest of Tier A + queue/resume; M4 dashboard UI + discovery wizard; M5 GDP.pdf + AA snapshot + peer tables.
- **Reports**: single-page HTML, offline-openable, no external assets; per-benchmark rows: local | AA ref | Δ | repeats | n | graded-by; Tier C rows "AA reference only"; speed strip captured (no AA speed compare).
## Key research facts
- AA Data API: base https://artificialanalysis.ai/api/v2, `x-api-key` header, OpenAPI at /api/v2/openapi; free = /language/models/free headline indices.
- Eval repos: SciCode1/SciCode (HF, Apache), cais/hle (gated, MIT, 2500 q, ~0.27GB), ArtificialAnalysis/AA-LCR (public, Equality Checker prompt public), surgeai/GDP.pdf (gated, Gemini 3.5 judge, inspect_ai), harbor-framework/terminal-bench 4.0 (66 tasks, CONTAINERS_BACKEND=podman, Harbor Hub gated), AA-Omniscience-Public 600q, CritPt public grading API 10/24h.
- AA org repos: Stirrup (MIT agent harness), aa-agentperf-local, aiperf, optima, StirrupJS.
- AA settings: temp 0/0.6, 16k+ max tokens, 30 retries, 1-5 repeats pass@1; Omniscience index [-100,100] accuracy−hallucination.
- Search chain web/kagi→web/searxng (search.franksdank.cloud) verified working 2026-10-09.
## Accepted risks (user-informed)
- Elo-pairwise agentic evals unreproducible locally; self-hosted judge drift; Omniscience (top quant-damage signal) not run; HLE vision inclusion diverges from AA text-based posture.
@@ -0,0 +1,22 @@
---
name: frank-primary-decode-tuning
description: "Tune frank-primary (radiance TP=2) decode throughput: benchmark harness usage, flag levers, known optima and boot-failure walls."
---
# frank-primary decode tuning
Harness: `bash autoresearch.sh` (dev box) scp's `scripts/frank-primary.env` + `scripts/autoresearch-frank-primary.sh` to dankserver, stops prod (`systemctl stop frank-primary`, NEVER mask — relinked unit), boots scratch container `arbench` on :7770 (fresh prefix-cache tier at `$STATE_DIR/kvcache/arbench` for cold prefill), polls health 720s, sends ONE greedy completion (fixed 140k-char repo-corpus prompt -> 43,627 tokens, 256-token decode), parses `timings` into METRIC lines, tears down, restores prod via EXIT trap. ~4 min/run; decode noise ~0.3%.
Metric: `METRIC decode_tps` = `timings.predicted_per_second` at 43.6k depth.
## Optima (measured 2026-10-06, keep these)
- `FRANK_PRIMARY_SPEC_TOKENS=2` (unimodal: 1->31.89, 2->32.40, 3->31.72, 4->24.57 t/s; acceptance collapses past 3)
- `FRANK_PRIMARY_MAX_SEQS=3` with `MAX_MODEL_LEN=200000` (600k KV; +4.2% total; 4 slots regresses)
- `FRANK_PRIMARY_EXPERT_VS_CACHE=0.82` bracket-confirmed (0.78 fails boot, 0.84 -8.9%, 0.87 collapse)
- `TP_WIRE=wht6`, `MAX_BATCHED_TOKENS=2048` — REQUIRED at seqs=3: bf16 wire and MNBT 4096 both fail boot (VRAM)
## Non-levers (decode-neutral, don't spend runs)
GPU_HEADROOM_MIB (48 = neutral), HOST_POOL_MIB 49152 (neutral), MAX_MODEL_LEN any change (regresses: bigger KV steals resident weights), prefix-cache tiers (prefill-side).
## Rules
One flag per iteration; keep only > +1% decode over best; a boot failure is `crash` (trap restores prod). Constraint floor: MAX_SEQS*MAX_MODEL_LEN >= 400000.
@@ -0,0 +1,125 @@
---
name: frank-tts-add-cloned-voice
description: "Add or repair a cloned voice on the dank fleet: /home/dank/voices/<name> pack, name rules, boot-warm registration, and the under-a-second proof"
---
Use for "give frank-tts a new voice", "voice X is missing/unknown", or "the first
request of the day takes 5 s". Runbook of record: vault pages [[Frank TTS]] (voice
packs, naming) and [[Frank TTS Engine]] (the warm registry). Everything below runs
as the deploy user on **Dankclaw** (`ssh -o BatchMode=yes dankclaw '…'`); the
containers are **rootless** (`systemctl --user …`, `podman …` without sudo).
## Naming (the question that gets asked first)
A voice is addressed **by its directory name** — `voice=narrator1` on
`POST /v1/audio/speech` (omp's TTS device may send `voice_id`, same value). Use:
- `^[a-z0-9][a-z0-9._-]*$`, **lowercase**, ≤ 64 chars. No slash, no `..`, no
space, no uppercase; must not start with `.`/`_`/`-`.
- Why lowercase rather than just "alphanumeric": the shim accepts
`[A-Za-z0-9][A-Za-z0-9._-]{0,63}` (`app.py`, `VOICE_NAME_RE`) but the **engine
lowercases names**, so `Narrator1` resolves on one side and not the other. The
boot warmer refuses a mixed-case pack loudly instead of half-working.
- Names are user-visible: a 400 carries them in `available_voices`. Fleet
convention is `narrator1`, `narrator2`, … for the open-notebook podcast
speakers — open-notebook is configured with exactly `narrator1/2/3`, so renaming
those breaks podcast rendering, not just TTS — and a lowercase personal name for
a person's own voice.
## Pack layout (host state — never commit it)
`/home/dank/voices/<name>/` is a voice **iff** it holds `reference.wav`:
```
reference.wav 24 kHz mono pcm_s16le, 18-25 s of clean single-speaker audio
reference.spk engine x-vector: enc_dim 2048 f32 = 8192 B
reference.rvq RVQ codes: 22 B per 12.5 Hz frame (5456-6864 B)
manifest.json source path + sha256, crop start/end, engine commit, name
```
Build it; do not hand-assemble and never fabricate `.spk`/`.rvq` from a WAV:
```bash
scripts/build-voice-pack.sh --all /home/dank/voices # everything in VOICE_PACKS
scripts/build-voice-pack.sh --source SRC --offset SEC --duration SEC --sha256 HEX NAME /home/dank/voices
scripts/build-voice-pack.sh --dry-run ... # validate + crop, no extractor
```
Knobs live in `scripts/voice-packs.env` (one `_SOURCE`/`_OFFSET`/`_DURATION`/
`_SHA256` set per voice). Three things it enforces that an operator gets wrong:
1. **The 10-second gate.** A crop starting inside the first 10 s of the source is
refused before anything is written (project source-material law), and the
manifest is re-checked against the source's sha256 afterwards.
2. **The effective crop window is 19.84-25 s, not 18-25 s.** An 18.0 s crop is a
legal `reference.wav` but yields only 22 × 225 = 4 950 B of `.rvq`, under the
5 456 B floor the codec config implies — so the builder says so up front.
3. **It never deletes.** An existing pack is refused unless `--force`, and
`--force` moves the old directory to `$PACKS_DIR/.AGENTTRASH/<voice>-<UTC>/`.
Files must be world-readable (the container runs as uid 10000): `644` files,
`755` dirs. `mkdir -p /home/dank/voices` as the deploy user if it is absent —
rootless podman auto-creates a missing bind source **root-owned**, and the
endpoint then sees no voices at all.
## Register and warm (the part people forget)
The engine keeps its voice registry **in memory**, and the **first synthesis of a
registered voice decodes the whole reference clip** to snapshot codec state:
6.2-8.0 s, first audio byte at ~5 530 ms. Later requests restore in 0.5 ms.
Re-registering does **not** warm — only synthesizing does. So:
```bash
# registers every pack that has a reference.wav, then speaks the warm sentence once
podman exec frank-tts-engine /app/warm-voices.sh
systemctl --user --no-pager --full status frank-tts-engine.service | tail -20
```
The unit's
`ExecStartPost=-/usr/bin/podman exec frank-tts-engine /app/warm-voices.sh` does
the same at every boot, so a fresh boot is already warm; the explicit call above
is for **adding one voice to a running engine** and needs no restart — a restart
drops every registration and re-pays the cold cost for the whole library. The
`podman exec` form is not cosmetic: the engine **publishes no port**, so there is
no host address to curl.
## Proof (do not report "added" without it)
```bash
curl -s localhost:7772/v1/audio/voices # name is listed
time curl -s -o /tmp/a.wav localhost:7772/v1/audio/speech \
-H 'content-type: application/json' \
-d '{"model":"tts-1","voice":"NAME","input":"Checking the cloned voice.","response_format":"wav"}'
time curl -s -o /tmp/b.wav localhost:7772/v1/audio/speech \
-H 'content-type: application/json' \
-d '{"model":"tts-1","voice":"NAME","input":"And the second request.","response_format":"wav"}'
file /tmp/a.wav /tmp/b.wav # really audio
```
The warmer's journal lines carry `voice NAME cold=…ms warm=…ms` — quote that pair.
Note that streaming and buffered output differ at the same seed (92.2 % of samples,
-41.6 dB RMS): never compare two paths or two "identical" renders, only that each
is audio and that the second request is fast.
## Failure modes worth naming
- `400 unknown voice …; available voices: …` → the name is not a pack: wrong
spelling/case, missing `reference.wav`, or `/home/dank/voices` root-owned/empty.
Trust the 400's list over any manifest.
- 400 with `(none: no voice pack with a reference.wav in /voices)` → mount problem,
not a voice problem.
- First request slow **after** a boot → the warm pass failed; read the unit's
journal (the warmer exits 0 and logs when there is no voice directory, so
silence there is not success either).
- Endpoint answers but synthesis fails 502 → the façade must dial
`http://frank-tts-engine:8880` over the shared bridge
(`frank-tts-engine.network`); a `host.containers.internal` / `127.0.0.1:8880`
upstream can never work between containers, because netavark DNATs published
ports only.
## Never
Never write under `/voices` from the endpoint (mounted `:ro` by design), never
`git add` a voice pack, never `rm` a pack (repo policy: move to `.AGENTTRASH/`),
and never benchmark against this endpoint while it serves — stop and mask the
units first.
@@ -0,0 +1,54 @@
---
name: gguf-header-vram-fit-analysis
description: "Decide if a GGUF model fits an LLM endpoint's GPU at a target context window: HTTP range-parse the header for KV geometry and custom quant types, derive the KiB/token rate law, price non-KV from measured boots (own repo + the publisher's benchmark reports), and gate on runtime lock-in (fork, build flags, drafter) before any download."
---
Answer "can model M fit window W on endpoint E (plus vision?)" **before** downloading anything. The GGUF header carries the truth; measured boots are the price list.
## 1. Range-parse the GGUF header from the Hub (no download)
`GET` the `resolve/main/<file>.gguf` URL with `Range: bytes=0-N` and a `User-Agent: curl/8` header, parse GGUF in Python (`struct`):
- Layout: magic `GGUF` (read `buf[0:4]` directly — do NOT `read_string()` it), u32 version, u64 ntensors, u64 nkv, then KV pairs (v3: u64 string lengths; v2: u32), then tensor-info table (name, ndim, dims u64[], type u32, offset u64). Handle EVERY array element type or the parse desyncs mid-metadata.
- **Metadata is >8 MiB** when vocab is ~250k (tokenizer arrays live in the KV section). Fetch 32–64 MiB.
- **Per-tensor byte sizes without a downloader**: the data section is in table order — `size(i) = offset(i+1) - offset(i)`, last = `file_size - header_end - offset(last)`. Then `bits/elem = bytes*8/prod(dims)`. That names unknown type ids by their rate (2.125 bpw ⇒ ternary-in-2-bit-slots + fp16 scale/128; 16.0 ⇒ BF16; 32 ⇒ F32) and totals weights per type.
Extract: `general.architecture`, `block_count`, `full_attention_interval`, `attention.head_count_kv`, `key_length`/`value_length`, `context_length`, `embedding_length`, `ssm.*`, and **`<arch>.nextn_predict_layers`** — its absence (plus `block_count` = real layers, not +1) means **no in-file MTP head** (MTP is a property of the file, not the flags). Type histogram decides stock vs fork: a type id above stock `GGML_TYPE_COUNT` is fork lock-in — read the fork's `ggml/include/ggml.h` enum to name it.
## 2. KV rate law (the wall)
For hybrid (qwen35-style) models only the FULL-attention layers bill per token; linear/DeltaNet layers bill a fixed state per SLOT (≈50–60 MiB/slot on 27B-class hybrids — noise vs KV, but it is what lets N slots fit).
```
elems/token/cache = (block_count / full_attention_interval) × head_count_kv × key_length
B/token = elems × bytesK + elems × bytesV (K and V both)
bytes/elem: f16=2.0, q8_0=1.0625, q4_0=0.5625, q4_1=0.578125
```
Fleet-verified instances: qwen35-9B (8 full layers) 32 KiB/token f16; qwen35-27B (16 full) 64 KiB/token f16, 34 KiB q8_0, 18 KiB q4_0. A low-bit WEIGHT quant never moves this line — state it explicitly, it is the question people actually ask. `--flash-attn on` is required for quantized V; quantized KV needs a `GGML_CUDA_FA_ALL_QUANTS=ON` build.
## 3. Price non-KV from MEASURED boots, never a formula
Two anchors beat one:
- **Own repo**: closest runbook boot (same arch, same card, ideally same mmproj): `non_kv = boot_used − rate × boot_pool`, then swap weights/mmproj/drafter/ubatch-rung deltas.
- **Publisher**: their published benchmark/server reports (e.g. a `community-benchmarks/<card>.md` or `KV-CACHE.md`) carry peak `nvidia-smi` and a steady-state figure for a named argv — back out their overhead = steady − weights − KV(× their pool) − mmproj. Divergence between the two anchors IS your uncertainty band; report both ends.
Card/vendor peak-VRAM at bench context also calibrates compute buffers at small ubatch; the fleet's ubatch-rung costs (≈0.3 GiB @1024, ≈0.75 GiB @2048 on a 27B) cover the rest.
Card ceiling from settled boots (`used + free`, e.g. 24,125 MiB on the 3090), not the spec sheet. An mmproj bills ~1.5× its file size. Keep ≥1 GiB headroom when ranking arms; tightest proven fleet fit is 489 MiB.
## 4. Gates beyond VRAM
- **Target window > `context_length`** ⇒ YaRN required (`--rope-scale = target/native`); quality at >1.2–1.5× on aggressive quants is unmeasured — say so.
- **Pool divides across slots**: `n_ctx_slot = CTX_SIZE / PARALLEL`; the per-slot number is the harness contract, and the endpoint's role (e.g. compaction hub needing ≥170k) picks the geometry.
- **Fork/custom-quant lock-in checklist** (answer all, cite ids):
1. which fork/branch, and does the pinned release tag contain the needed fixes — `GET /compare/<tag>...prism` and read `ahead_by` + commit subjects (a tag 5 commits behind contains everything merged earlier);
2. host CPU traps (e.g. AVX-512 load segfaults crash even with all layers GPU-offloaded);
3. **does the release workflow pass the build flags the contract needs** — grep the release workflow's cmake args: prebuilts often omit `GGML_CUDA_FA_ALL_QUANTS`, and without it mismatched/unsupported K/V types silently run flash attention on the CPU;
4. drafter availability (in-file `nextn`, a paired drafter, or nothing) — no drafter means the decode regression is unfixable;
5. pack choice per architecture (the vendor's throughput table is generation-specific; a community report on the same GPU family outranks it);
6. behavior/role risk: base model censorship posture, `tool_calls` defects, `reasoning_effort` template gate, prompt-cache `--cache-ram` vs host RAM actually available.
- Vision: needs `--mmproj`; probe with ≥256 px solid color; only the OpenAI `image_url` content part reaches the model.
## 5. Output
Table of arms (pack × KV dtype × slot geometry) → KV MiB, total, free vs ceiling, FITS/FITS-tight/NO FIT, with the uncertainty band; then the gate list. State every `[INFERENCE]`/estimate. The boot ledger (verbosity 4), not the arithmetic, is the referee; benching requires stop+mask per fleet policy.
@@ -0,0 +1,68 @@
---
name: git-repo-secret-audit
description: "Audit a git repo for leaked secrets across history, unreachable objects, LFS payloads, git config and host configs — with triage of every hit."
---
# Git repo secret audit (history + unreachable objects, not just the worktree)
Worktree greps prove nothing: the leak you care about is in a commit that was amended
away, in a branch nobody checks out, in an LFS payload, or in `.git/config`.
## 0. Build the known-value set from the live secrets, never from literals
Read the values from the secret store at runtime so no key enters the transcript:
```python
known = {}
for p in (home/".config/secrets").iterdir():
t = p.read_text().strip()
vals = {t}
m = re.search(r":([^:@/]+)@", t) # credential-store URL form
if m: vals.add(m.group(1))
known[p.name] = {v for v in vals if len(v) >= 12}
```
Report ONLY `value[:4] + len + sha256[:10]`. Never print a value, and never quote a
secret-shaped literal in a doc/commit message — the pre-commit gate will (correctly) block it.
## 1. Shapes to match (superset of the Dank gate)
`sk-[A-Za-z0-9]{18,}`, `sk-or-v1-`, `sk-ant-`, `hf_[A-Za-z0-9]{30,}`, `gh[pousr]_[A-Za-z0-9]{30,}`,
`github_pat_`, `glpat-`, `xox[baprs]-`, `(AKIA|ASIA)[0-9A-Z]{16}`, `npm_`, `pypi-`, `AIza[0-9A-Za-z_-]{30,}`,
`(sk|pk)_(live|test)_`, Telegram `\d{8,10}:AA`, JWT `eyJ…\.eyJ…`, `-----BEGIN [A-Z ]*PRIVATE KEY-----`,
`AGE-SECRET-KEY-1`, `ssh-(ed25519|rsa)\s+AAAA`, `scheme://user:pass@host` basic-auth URLs,
`Bearer <24+>`, and the noisy `(password|secret|api_key|token)\s*[:=]\s*<12+>` assignment shape.
## 2. Scope, in this order
```bash
git rev-list --all | wc -l # revs incl. every branch + tag
git ls-tree -r --format='%(objectname) %(path)' <rev> # sha -> path, per rev
git cat-file --batch # stream reachable blob content
git cat-file --batch-all-objects --batch-check # EVERY object, incl. dangling
git rev-list --objects --all # the reachable set; difference = dangling
git log --all --oneline --find-object=<sha> # empty => never committed on any ref
git status --porcelain -uall --ignored=matching # !! lines: on-disk ignored files
git config --list --show-origin # local/global/system credentials
git ls-files | grep -E '(auth\.json|trust\.json|\.env|\.pem|\.key|identity)' # quarantine
```
LFS: scan `.git/lfs/objects/**` as BYTES (`decode("latin-1")`) — a screenshot is a classic
secret carrier and the tracked tree holds only pointers.
## 3. Entropy layer (catches issuer-specific keys you did not list)
Tokens `[A-Za-z0-9+/_-]{32,}` with Shannon entropy >= 4.3 containing both digits and letters.
Flag every one whose value is in `known` as a LIVE leak; list the rest for triage.
## 4. Triage — the shapes that always fire and are fine
- `api_key="sk-no-key-required"` and other doc placeholders
- prose: `Set password: user.set_password(...)`, `authenticate(user, password)`
- 40-hex hits that are git object ids / packfile names (`pack-<40hex>.pack`)
- memory/KB records that *describe* a secret operation
A hit is a leak only when the value matches a live secret, decodes to a real credential, or
sits in a field a program reads.
## 5. Adjacent host scan (the audit's real yield)
Check file modes of the secret dir (`0600`, dir `0700`) — world-readable secret files are the
most common real finding. Then grep the live values across shell rc files, `~/.gitconfig`,
`~/.npmrc`, `~/.netrc`, `~/.curlrc`, `~/.docker/config.json`, `~/.cache/huggingface/token`,
`~/.aws/credentials`, `~/.ssh/config` and report filenames only.
## 6. Report
Per layer: scope numbers + verdict. Every hit: path:line, redacted value, verdict
(leak / benign + why). Name the gate's coverage gap if the project's own hook misses shapes
you matched. Fix modes/chmod on the spot; changes to a security hook are the operator's call.
@@ -0,0 +1,105 @@
---
name: gitea-api-pr-operations
description: "Open, list, merge, and verify pull requests (and diagnose issue-API scope failures) on the Dankclaw Gitea via its REST API from a workstation, keeping the token out of argv (curl -K config file, mode 600) — includes the credential-store extraction one-liner, the merge endpoint's Do-field law and Content-Type-header trap, the suggested-URL-is-not-a-PR gotcha, and the insufficient-token-scope trap."
---
# Gitea API PR operations (Dankclaw)
`git push` on this fleet prints `Create a new pull request for ...` — that is a
**suggestion, not a PR**. Verify (`GET /pulls?state=open` → expect `[]`) before
claiming a PR "should exist"; create it via the API when asked.
## Endpoint + auth
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>`
- Token lives in the git credential store configured by
`git config --get credential.helper` → `store --file ~/.config/secrets/gitea`,
line format `http://user:TOKEN@192.168.0.143:3000`.
- The repo-local `.env` `GITEA_TOKEN` (main checkout, gitignored) carries
**`read:issue` too** (measured 2026-10-18): filing issues needs THAT token,
the credential-store one 403s on issues.
Extract the token (the `http://` scheme contains a colon — scheme-stripping
awk regexes silently grab the port instead; use this anchored sed):
```bash
tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
[ -n "$tok" ] && echo "token-extracted(len=${#tok})" # length check, never echo the value
```
## Keep the token out of argv (repo secret law)
`curl -H "Authorization: token $tok"` is world-readable in `/proc/<pid>/cmdline`.
Use a mode-600 config file:
```bash
cfg=$(mktemp); chmod 600 "$cfg"
printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
```
- POST JSON needs a second header line: `header = "Content-Type: application/json"`.
There IS NO `content-type` config option — curl rejects the file.
- `rm -f "$cfg"` when done (a mode-600 token file left in /tmp is its own leak).
## Operations
```bash
# list open PRs
curl -sS -K "$cfg" ".../pulls?state=open" | jq -r '.[] | [.number,.head.name,.base.name,.title] | @tsv'
# create (build the payload as a FILE, see below)
curl -sS -K "$cfg" -o /tmp/pr.json -w '%{http_code}\n' -X POST -d @/tmp/pr-payload.json ".../pulls"
# verify: 201 is not enough — check mergeable
curl -sS -K "$cfg" ".../pulls/1" | jq '{number,state,merged,mergeable,base:.base.ref,head:.head.ref,url:.html_url}'
```
## Merging: the field is `Do`, and it is a STRING naming the strategy
Measured 2026-10-18 on Gitea at 192.168.0.143:3000 (`POST .../pulls/<n>/merge`).
`MergePullRequestOption` is `{From, To, When, Do}` — **there is no `DoMerge` and
no `merge_method`**. Every wrong shape is a 422 with a distinct message, so read it:
|body sent|http|message|
|---|---|---|
|`{"DoMerge":true,"merge_method":"merge"}`|422|`[Do]: Required`|
|`{"Do":true,"merge_method":"merge"}`|422|`[]: json: cannot unmarshal JSON boolean into Go string within "/Do"`|
|`{"Do":"true"}`|422|`[Do]: In…` (validation: not a strategy name)|
|form body `Do=true&merge_method=merge`|422|`[Do]: Required` (the value must be a strategy name)|
|`{"Do":"merge"}`|**200**|merged|
```bash
curl -sS -K "$cfg" -H 'Content-Type: application/json' -X POST -d '{"Do":"merge"}' ".../pulls/4/merge"
# then prove it: .merged true, .state closed, .merge_commit_sha set, and origin/main moved
curl -sS ".../pulls/4" | jq -c '{number,state,merged,merge_commit_sha}'
```
Strategy names: `merge`, `rebase`, `rebase-merge`, `squash`, `manually` — a
strategy the repo does not allow is rejected too.
**Merge trap (measured 2026-10-18):** on `POST /merge` the `Content-Type` header
must go on the **command line** (`-H 'Content-Type: application/json'`). Same
curl that succeeds on `POST /pulls` with Content-Type supplied only via the `-K`
config file returns **405 with an EMPTY body** on `/merge`. A 405 there is this
trap (or a base-moved mergeability recheck — GET the PR, see `mergeable:true`,
then retry with `-H`), NOT a permission problem.
## Building issue/PR bodies without quote carnage
Hand-written `jq -n --arg t "..." '{title:$t, body:"..."}'` one-liners with long
markdown bodies WILL break on a stray quote (and the broken quoting can swallow
the following commands). Build bodies as files (heredoc or write tool), then:
```bash
b=$(jq -Rs . body.md)
jq -n --arg t "$title" --argjson b "$b" '{title:$t, body:$b, head:"<branch>", base:"main"}' > payload.json
curl ... -d @payload.json
```
## Gotchas measured 2026-10-14 / 2026-10-17 / 2026-10-18
- Unauthenticated/bad token → HTTP 401 body `{"message":"not found"}`; `jq '.[]|[.number,..]'` then fails with *cannot index "not found"* — that is an auth failure, not a missing repo.
- **Insufficient token scope ≠ missing data.** The credential-store token
(measured 2026-10-17) carries only `write:repository,write:user`; the issues
API answers HTTP 403 `{"message":"token does not have at least one of required
scope(s), required=[read:issue], token scope=write:repository,write:user"}`.
jq surfaces that JSON as the *cannot index* error — do NOT conclude the issue
list is empty, and note POSTs under this token create NOTHING (verify, don't
assume retry safety). Filing issues needs a fresh token minted in the Gitea
web UI with `read:issue`+`write:issue` (Frank-class credential action; ask) —
OR use the main-checkout `.env` `GITEA_TOKEN`, which already has issue scope.
PR endpoints work fine with either token (`pulls` POST/GET/merge all 200/201).
- On the created-PR response `.head.name` can be null; verify with the `GET /pulls/<n>` and use `.head.ref`.
- **Swagger JSON is disabled on this instance** — `/api/swagger.v1.json`, `/api/swagger.json` and `/api/v1/swagger.json` all 404. Do not go looking for the schema there; the 422 messages name the field and its Go type, which is the faster instrument.
@@ -0,0 +1,52 @@
---
name: gitea-token-out-of-remote-url
description: "Move a Gitea/HTTP git token out of remote.origin.url into a credential store file, and verify auth works with prompts disabled."
---
# Move a git token out of the remote URL
A remote URL like `http://user:<token>@host:port/org/repo.git` puts the credential in
`.git/config` (copied by every clone), prints it in `git remote -v`, and git-lfs echoes the
whole URL in its `Locking support detected on remote` warning on every push.
## 1. Capture the token from the existing remote (never paste it into a message)
```bash
url=$(git remote get-url origin)
user=$(printf '%s' "$url" | sed -E 's#^http://([^:]+):[^@]+@.*$#\1#')
tok=$(printf '%s' "$url" | sed -E 's#^http://[^:]+:([^@]+)@.*$#\1#')
```
## 2. Write the credential file (URL form, port included)
```bash
umask 077
printf 'http://%s:%s@HOST:PORT\n' "$user" "$tok" > ~/.config/secrets/gitea
```
Git 2.56 `credential-store` does NOT parse the `protocol=`/`host=`/`password=` block shape
that `git credential fill` accepts on stdin: a block-format file gives a SILENT empty get,
then git falls through to askpass (`could not read Username`). One URL per line, port included.
## 3. Point git at it, strip the URL, silence the LFS echo
```bash
git config --global credential.helper 'store --file ~/.config/secrets/gitea'
git remote set-url origin http://HOST:PORT/org/repo.git
git config lfs.http://HOST:PORT/org/repo.git/info/lfs.locksverify true
```
## 4. Verify with prompts disabled (a hang is a failed credential)
```bash
printf 'protocol=http\nhost=HOST:PORT\n\n' | git credential-store --file ~/.config/secrets/gitea get
GIT_TERMINAL_PROMPT=0 git ls-remote --heads origin main # read auth
GIT_TERMINAL_PROMPT=0 git push --dry-run origin main # write auth
git remote get-url origin # must contain no '@'
grep -l "$tok" ~/.gitconfig */.git/config ~/.zshrc # must find nothing
```
A successful push over the bare URL is the end-to-end proof: the credential came from the file.
## 5. Sweep the leak
Same shape on every clone on the host — loop over `find ~ -maxdepth 4 -path '*/.git/config'`,
read `remote.origin.url`, scrub any URL containing `@`. One credential file serves them all
via the global helper.
## Rotate
Rewrite only the credential file, then delete the old token in the Gitea UI
(Settings → Applications). No remote URL, rc file, or repository changes.
@@ -0,0 +1,23 @@
---
name: gitflow-back-merge-plumbing
description: "(HISTORICAL for dank/command-center — switched to trunk-based 2026-10-18, no develop branch.) Back-merge main into develop with pure git plumbing when branch checkout is policy-blocked; avoids the polluted-base evil-merge trap."
---
# Git-flow back-merge via plumbing (checkout blocked)
## Applicability
The command-center repo retired `develop` on 2026-10-18 (trunk-based; see its AGENTS.md § Trunk-based SDLC). Use this only in repos still running git-flow where a `main -> develop` back-merge must land and branch checkout is forbidden. Never merge while HEAD is a session branch — the merge silently lands on it.
## Procedure
1. `git fetch origin` and verify with `git ls-remote --heads`.
2. Find the CLEAN fork point: `mb=$(git merge-base origin/develop origin/main)`. NEVER build the merge commit with a develop-side that already contains main (e.g. a previous broken merge): merge-base then equals main, main-side looks "unchanged", and stale develop content wins everywhere — a clean rc=0 evil merge. Sanity-trigger: if a merge you expect to conflict resolves rc=0, suspect the base.
3. Real merge, no worktree: `out=$(git merge-tree --write-tree <clean-dev-tip> origin/main); rc=$?; tree=$(echo "$out"|head -1)`. rc=1 = conflicts; list them from the "Conflicted file info" section (lines starting with a 6-digit mode), NOT from `ls-files -u` in some other worktree.
4. Resolve in index only: `export GIT_INDEX_FILE=/tmp/idx.$$; git read-tree $tree; git update-index --add --cacheinfo 100644,$(git rev-parse origin/main:<path>),<path>` per conflicted path (pick a side deliberately — say which in the message).
5. `final=$(git write-tree)`; prove no markers: `git grep -l '^<<<<<<<' "$final"` must be empty (run it correctly — an empty count from a malformed invocation hides markers).
6. `commit=$(git commit-tree "$final" -p <clean-dev-tip> -p origin/main -m "...")`; `git push --force-with-lease=develop:<current-dev-tip> origin "$commit:develop"` (lease prevents clobbering concurrent pushes; document superseded commits). A back-merge onto an untouched develop is additive — plain push, no lease needed.
7. Verify ALL of: `git merge-base --is-ancestor origin/main origin/develop`; grep a distinctive merged value in `origin/develop:<file>` (content, not just ancestry); `git diff --stat origin/main origin/develop` matches expectations.
## Traps
- `merge-tree` writes conflict markers INTO the tree for conflicted paths — always overwrite every conflicted path before commit-tree.
- A wrong tree pushed as a merge commit makes every later merge against it resolve "cleanly" to stale content; fix by rebuilding from the clean fork point and force-with-lease, citing the broken commits in the message.
- Gitea PR merge API needs `Content-Type: application/json` AND `{"Do":"merge"}` in the BODY (query param alone 422s); a 405 "Please try again later" right after the base branch moved is transient mergeability recomputation — wait a few seconds and retry.
@@ -0,0 +1,62 @@
---
name: gluetun-dependent-boot-race-recovery
description: "Diagnose the Dankclaw pattern where gluetun is active but its netns-sharing dependents (searxng, marreta, marreta-browser) are inactive (dead) with \"Dependency failed\" after a reboot — cascade-recover them and tie it to e2e 502/SSO failures."
---
# Gluetun boot-race: netns dependents dead at boot, service is active
## Signature
After a dankclaw reboot, `gluetun.service` is ACTIVE, yet e2e fails on
`search.franksdank.cloud` (502 on the authenticated pass), the Marreta tile
shows a blank interstitial, and the homepage crawl reports vhost resets.
`systemctl --user status <dep> --no-pager` shows:
```
Active: inactive (dead)
<host> systemd[2224]: Dependency failed for <dep>.service
<host> systemd[2224]: <dep>.service: Job ... failed with result 'dependency'
```
## Cause
Quadlet `Network=gluetun.container` emits `Requires=`/`After=` on
`gluetun.service`. When gluetun's start job fails once transiently at boot
and `Restart=` re-arms it seconds later, the dependents' start jobs are
already consumed and are NEVER re-armed — they sit `inactive (dead)`
indefinitely while gluetun runs. `Restart=always` on the dependent does not
help: the service never ran, so the restart policy never armed.
## Diagnosis census (rootless, run as `dank`, NO sudo)
```
export XDG_RUNTIME_DIR=/run/user/$(id -u)
uptime # reboot is the trigger
systemctl --user is-active gluetun searxng searxng-valkey marreta marreta-browser
systemctl --user status <dep> --no-pager | head # "Dependency failed" line
journalctl --user --since "<boot>" | grep -iE "<dep>|dependency"
```
Dependents of the gluetun netns on dankclaw: `searxng`, `marreta`,
`marreta-browser` (valkey is plain-networked and survives).
## Recovery (order matters: browser before marreta)
```
systemctl --user start marreta-browser marreta
systemctl --user start searxng
```
Verify: `podman ps` shows them; `curl http://127.0.0.1:8081/healthz` (searxng)
and gluetun-published 8082 (marreta) answer; then rerun the Playwright gate
(`tests/e2e/run.sh`) — the 12-crawl / 13-sso / 16-marreta specs are the trip
wires for this fault.
## Distinguish from the dead-pause-process failure
The 2026-09-16 rootless-pause-process death breaks ALL podman commands and
every quadlet loops; this fault leaves podman fully functional and only the
gluetun-dependency chain is dead. The reboot repairs the former; manual
cascade-start repairs this one.
## Open gap (dankclaw, as of 2026-10-15)
The race recurred (observed 2026-10-15 boot 01:41). Quadlet law for these
units has no cure inside the `.container` files themselves; options are a
systemd user-level drop-in on gluetun
(`ExecStartPost=/usr/bin/systemctl --user restart searxng marreta`) or
converting the dependents' dependency to `BindsTo` + `StartLimit` retry
semantics. Fleet precedent for boot-order pain: searxng/gluetun ordering,
observability reboot gaps (AGENTS.md).
@@ -0,0 +1,14 @@
---
name: halogen-ctx-pool-resize
description: "Resize a halogen flash-server endpoint's context/KV pool (frank-dragoon pattern): engine knobs, seam band, RAM ledger, re-key sweep, deploy + probe."
---
Two knobs: HALOGEN_CTX (hard per-request ceiling; longer prompt = 400 naming the limit, never truncation) and HALOGEN_KV_POOL_POSITIONS (ONE pool shared behind HALOGEN_KV_SLOTS). Ceiling raises buy a longer conversation; pool raises buy concurrency.
Laws: native window 262,144, beyond it needs HALOGEN_ROPE_YARN (engine 400 text) so never extrapolate for sizing; seam band ctx <= pool <= slots*ctx (pool == slots*ctx => two maximum-length requests coexist, pool == ctx => one fills it); harness contextWindow = ceiling - 45056 (clamp max_tokens = min(maxTokens, window-est-4096), compaction at 85 %, chars/4 under-counts ~3 %) and pool-only changes do NOT move it; ~28 KiB/position measured (200,704 -> 5.5 GiB, 262,144 -> 7.2, 524,288 -> 14.6), quote the ledger reservation not the preflight formula (block scratch + ~115 MiB/slot); the 51.2 GB n-gram table is mmap page cache (Pin::None) so RAM the pool takes is RAM that table loses, re-measure the warm prompt-cache ratio (136x baseline) after a raise; engine ledger vs 121.2 GiB = weights 62.9 + KV + working (3.8 + 8.4*MAX_TOK/16384) + prompt cache + HALOGEN_HOST_RESERVE_GIB=20 + co-tenancy ~10-15.
Procedure: read scripts/frank-dragoon.env and the boot ledger in /home/dank/logs/frank_dragoon.log (not the journal; a printed clamp is the referee); candidate boot via a transient FRANK_DRAGOON_ENV_FILE drop-in sourcing the repo env (never a scratch port), then trash it and prove DropInPaths empty or the repo env silently does not govern; edit env assignment + SHAPE lore and the unit Description; update the seam good_env fixture, -e pass-through asserts and pool-law comment without loosening the band; re-key every quote (deploy-remote.sh service comment, AGENTS.md topology row/engine laws/memory ledger/runbook index, docs index and LLM/index prose and mermaid node text, endpoint runbook shape paragraph plus a dated section, forward pointers in other vault pages, ~/.omp/agent/models.yml comment and contextWindow only if the ceiling moved, live OpenWebUI row in webui.db after backup); ./tests/run.sh; commit explicit paths never -A (subagents share the tree); ./scripts/deploy.sh dankclaw --service frank-dragoon (bare target = fleet drain); verify the new boot ledger and append the ledger table to the runbook plus a log.md worklog entry.
Probes: over-ceiling request -> 400 naming the ceiling in <1 s (preflight rejects before prefill); ~120k admission at max_tokens 45056 accepted, wall time = cold prefill ~1,500 t/s; two maximum-length admissions fired together must BOTH be admitted once pool >= 2*ctx; never report the first request after a boot.
Traps: systemctl mask fails on relinked units, disable forbidden; reservation is prompt + max_tokens so an over-budget request fails preflight even with a free pool; HALOGEN_FLASH_PIN_TRUNK=0 rejected by the v2 loader; rollback = revert the commit and redeploy.
@@ -0,0 +1,48 @@
---
name: halogen-npu-feasibility-census
description: "Census whether halogen-flash-server's NPU small-model path can run on a Strix Halo host: image gate, amdxdna/XRT presence, IOMMU and fabric-clock blockers, enablement deltas."
---
Use when asked whether halogen/halogen-flash-server can use the XDNA2 NPU (embeddings, rerank, moderation/guard, decider, `qwen3.5-2b` generation, `flux2-klein-4b` images, `qwen3-asr-*`) on a Ryzen AI Max / Strix Halo host, or before proposing to enable it.
## What the feature is (not a speed knob)
The Flash trunk stays on the iGPU. `HALOGEN_NPU_MODELS=<names>` starts a second engine in the SAME container and on the SAME API port; the request's `model` field picks the executor. Unset → server identical to before. Each NPU model is a precompiled AIE-ML program (`devices/devices.hnpm`, per-partition `devices/u<N>.elf`, `devices/consts.hnpb`, `<id>.hnpw` weights, `tokenizer/tokenizer.json`), loaded via the HOST's XRT → kernel `amdxdna` → `/dev/accel/accel0`. One pass at a time; `HALOGEN_NPU_QUEUE` 64, overflow queues server-side, then `503 engine_busy`. Entry `entrypoint.sh` validates every file's bytes + sha256 against `/opt/halogen/npu/models.txt` before anything starts. NPU models run only in single-container "all" mode, NOT the `engine`/`api` split.
## Census (all read-only, run from the operator box)
```bash
ssh <host> '
# 1. engine gate + catalog in the pinned image
podman exec <ctr> bash -c "grep -rlo HALOGEN_NPU_MODELS /opt/halogen /usr/local/bin /entrypoint.sh 2>/dev/null | head"
podman exec <ctr> cat /opt/halogen/npu/models.txt | head -40
# 2. is NPU actually wired today? (expect NO env, NO device, NO /dev/accel inside)
podman inspect <ctr> --format "{{json .HostConfig.Devices}}"; podman exec <ctr> ls /dev/accel
# 3. kernel driver + firmware
lsmod | grep -i amdxdna; sudo dmesg | grep -iE "xdna|npu" | tail -5; ls -l /dev/accel
# 4. XRT (distro packages are fine; vendor mounts /opt/xilinx/xrt)
dpkg -l | grep -iE "xrt|xdna"; ls /usr/lib/x86_64-linux-gnu/libxrt* /usr/lib/libxrt* 2>/dev/null
# 5. BLOCKER 1: IOMMU
cat /proc/cmdline; ls /sys/kernel/iommu_groups | wc -l
# 6. BLOCKER 2: fabric clock held?
for c in /sys/class/drm/card*/device; do echo "== $c"; cat $c/power_dpm_force_performance_level 2>&1; cat $c/pp_dpm_fclk 2>&1 | tr "\n" " "; echo; done'
```
Docs (raw fetch, authoritative): `docs/NPU.md`, `docs/npu-small-models.md`, `deploy/host/halogen-fabric-clock` under `peonist-ai/halogen-flash-server` on GitHub. Tag list: `podman search --list-tags ghcr.io/peonist-ai/halogen-flash-server`.
## The two blockers and the enablement delta
- **IOMMU must be on.** `amd_iommu=off` turns the NPU off (no PASID/SVA); `iommu=pt` keeps it. Needs a grub edit + reboot. `iommu_groups == 0` ⇒ off.
- **Fabric clock must be pinned at the top `pp_dpm_fclk` level** (or `power_dpm_force_performance_level=high`); `auto` = unheld. GPU+NPU concurrently HANG Strix Halo machines and corrupt NPU results while that clock moves. Fix: root container with `-v /sys:/host/sys` (server holds it itself), or install `deploy/host/halogen-fabric-clock` + unit (~1 W). If it can't hold, the server FAILS CLOSED and prints the commands.
- Container delta: `--device /dev/accel/accel0` + `HALOGEN_NPU_MODELS`; `--ulimit memlock=-1:-1` and `--group-add keep-groups` usually already present. Distro XRT: mount each of `libxrt_coreutil.so.2`, `libxrt_core.so.2`, `libxrt_driver_xdna.so.2` TWICE (once at `/opt/xilinx/xrt/lib/`, once at its own path) — never mount an `/opt/xilinx/xrt` holding only symlinks.
## Costs to price before proposing
- Upstream A/B: IOMMU translation ON costs **13–16 % prefill** (460 → 385 tok/s at 2,048 tokens, Ryzen AI Max+ 395); decode unaffected. Whether `iommu=pt` specifically costs that on a given box is a local measurement, not a published fact — run the image's own `sweep` after the reboot.
- A few GB host RAM per NPU model (+8 GB FLUX, +1.6/4 GB ASR), and trunk decode drops while the NPU works (shared bus + power).
## Laws / traps
- Never probe the NPU beside the live service: stop + mask first (fleet benchmarking policy); this host class has a precedent of a concurrent GPU experiment needing a power cycle. Distinguish off vs wedged with `ip neigh show <ip>` from a peer.
- `/dev/accel/accel0` existing and `amdxdna` probing cleanly does NOT mean the NPU works — with IOMMU off the failure is at context/PASID creation, not at the device node. Do not trust older notes claiming the node itself is missing.
- `dmesg` lines like `Failed to alloc dev bo memory, ret -28` while large NPU models take turns at NPU memory are benign (vendor-documented); the server recovers.
@@ -0,0 +1,26 @@
---
name: hf-checkpoint-profile
description: "Profile a Hugging Face safetensors checkpoint (sizes, dtypes, tensor-name space, gated/license) with compact HTTP-range reads — safe for subagents on small context slots."
---
# Profile an HF safetensors checkpoint cheaply
Goal: inventory a repo (files/bytes, dtype histogram, tensor-name patterns, per-group GiB, config keys, license/access) without pulling gigabytes and without overflowing a subagent's context slot.
## Endpoints (all public, no token needed even for gated models)
- `https://huggingface.co/api/models/<id>?blobs=true` → `siblings` (name+size), `gated`, `private`, `downloads`, `safetensors` metadata, `tags`.
- `https://huggingface.co/api/models/<id>` for tags/base model; `https://huggingface.co/api/models?author=<x>&limit=100` to find sibling repos (canonical id check: cards often link both `X-1.5-Y` and `X-Y`).
- `https://huggingface.co/api/models/<id>/resolve/main/<file>` for `config.json`, `model.safetensors.index.json`, `chat_template.jinja`, `LICENSE`, `generation_config.json`; `/raw/main/<file>` for text.
## dtype + per-group bytes without downloading shards
Parse the safetensors header over HTTP Range: first 8 bytes = little-endian u64 header length, then that many bytes of JSON (`{name: {dtype, shape, data_offsets}}`). Fetch every shard header with a thread pool (131 shards ≈ seconds), then bytes = prod(shape) × dtype size. Collapse names with `re.sub(r"\.\d+\.", ".N.", k)` (and `\.shard_\d+` → `.shard_N`) to get the pattern space; group-sum by prefix to see where the mass sits (experts / ngram-PLE / MTP / vision / trunk).
Compare the key set against the base model's index to prove "full-weight finetune, identical names" (a zero-diff key set means per-tensor recipes written for the base apply verbatim, and it reveals fused vs per-expert layout, PLE/ngram shards, MTP head, vision tower, absence of `quantization_config`).
## Guardrails
- Run python scripts from a clean directory: a stray `/tmp/inspect.py` shadows stdlib `inspect` and kills `concurrent.futures` imports.
- Never put an HF token on a command line (`/proc/<pid>/cmdline` is world-readable); use `curl -K <mode-600 file>`.
- `curl -f` / `--fail-with-body` and verify byte counts — a 401/403 writes the error body as the "artifact" and exits 0.
## Subagent context budget
Subagents on a 131k-token slot die from `exceed_context_size_error` when told to read whole doc pages. Give grep-only instructions (`| head -40`, `-o`, `-c`), demand a line cap ("report in ≤250 lines, no verbatim dumps"), and prefer `git clone --depth 5` + local grep to web-page reads. A peer scout that overflows returns exit 1 with the 400 in the harness log and NO structured output — assume nothing was learned and redo the fact inline.
@@ -0,0 +1,40 @@
---
name: hf-large-artifact-publication
description: "Publish a large quantized artifact + model card (and an optional anonymized corpus dataset) to HF Hub from dankserver: staging set, HF-style card spine, upload gates, transient-unit launcher, sentinel watching, card-metadata and scrub laws."
---
# HF large-artifact publication (dankserver)
Publish a big model artifact (100+ GiB) + card, and optionally an anonymized corpus dataset, from dankserver under `frank8465`.
## Staging
- Dev box `~/Projects/hf-swift15-upload/` (author card + scripts here), `scp -r` synced to `/home/dank/hf-swift15-upload/` on dankserver. Keep staging + logs + `.rc` sentinels after publication as receipts.
- Payload set: artifact, `README.md` card, `scripts/` (recipe/drivers/probes), `artifacts/` (gate JSONs, per-position `.rows`, manifests, run logs).
- Secret-scan the whole payload before arming: credential regexes (hf_/ghp_/pat, private keys, Bearer values, AKIA/sk-), LAN IPs, hostnames, domains, emails. Verdict each hit — variable *references* and upstream test fixtures are not leaks; check real values in context.
## Card law (HF validator)
- Custom licenses MUST be `license: other` + `license_name:` + `license_link:` (mirror the base model's card trio — fetch `https://huggingface.co/api/models/<base>` and read `cardData`). A raw non-SPDX `license:` value raises `ValueError: Invalid metadata in README.md` inside `upload_file` — AFTER all local gates have run.
- Card spine: what-the-format-is + engine pin digest, recipe table, quality gates (KL/top-1/PPL + per-position data shipped), proof hardware, exact `podman run` line, license chain, manifest table with the artifact's sha256 receipt, "In plain English:" block per section.
- Quote the SHIPPED filename (build name), not a prettified one — a hub-side rename of a 100 GiB LFS file costs a full re-upload.
## Upload mechanics
- venv `/home/dank/venvs/radcorpus` (huggingface_hub 1.28 + hf_xet); `HF_XET_HIGH_PERFORMANCE=1`; token from `~/.cache/huggingface/token` (see defuse-shell-rc-secrets pattern: `~/.config/secrets/<name>` 600 + rc loader, mirrored to the cache file). NEVER token in argv/chat.
- Script gates, in order: sha256 of live artifact vs recorded digest (`gate 1 : MATCH`), `create_repo(..., private=False, exist_ok=True)`, small files first, artifact last, post-upload census — **compare remote sizes to local and FAIL the RC on any mismatch** (this caught a stale card mid-campaign).
- hub 1.28 API traps: `upload_file`/`create_commit` kwargs are keyword-only; no `rename_file`, `hf_hub_url`, `HfApi.commit_api` (use `CommitOperationAdd`/`CommitOperationDelete` from `huggingface_hub` top-level if needed); `whoami` hides `canWrite` for legacy tokens — probe write scope by `create_repo(exist_ok=True)` on the real target.
- zsh-over-ssh: never `echo ===` (triggers `=cmd` expansion); quote it.
## Launcher + watchers
`setsid nohup` over ssh dies at session teardown. Use a transient user unit:
```
systemd-run --user --collect --unit=hf-upload -p SendSIGKILL=no \
-p StandardOutput=append:/home/dank/logs/hf-upload.log \
-p StandardError=append:/home/dank/logs/hf-upload.log \
sh -c "cd <staging> && <venv>/bin/python -u <uploader>.py --go; echo \$? > /home/dank/logs/hf-upload.rc"
```
Sentinel: background ssh loop polling the `.rc` file (60 s period, bounded), tails the log on appearance. Progress bars are `\r`-fed — `tr '\r' '\n' | tail` to read them.
## Corpus dataset variant
- License audit first: read `urls.txt`/manifest, fetch each source card (`license` in cardData). Share-alike sources (CC BY-SA) → publish derived corpus CC BY-SA with attribution; model weights unaffected.
- Fleet-harvested slices = agent-session transcripts: scrub with deterministic substring-class rules (NOT word-boundary: catches `dankclaw_default`, `FRANKBOOK_SVCS`, `franksdank/alloy` image namespace): LAN IPs→10.x, hostnames→server-a/b/client-c, domain→example.com (catch `\.`-escaped forms + bare brand), `/home/<user>`, emails→user@example.com. Residual probes run on OUTPUT; whitelist self-replacements + audited fixture noise.
- Publish original hashes, disclose non-byte-identity, cross-link model card ↔ dataset card, verify via `raw/main/README.md` fetches.
- Dataset repo needs dataset-write scope (fine-grained token; access tokens 403). `repo_type="dataset"` everywhere; 45 MB rides in ~60 s.
@@ -0,0 +1,21 @@
---
name: homepage-smoke-verify-loop
description: "Smoke-verify Dankclaw homepage changes locally before commit/deploy: isolated static tree + fake status + browser census, then deploy gate."
---
# Homepage smoke-verify loop (serverAppConfig)
Verify `shared/homepage/` changes against a real browser BEFORE commit/deploy.
## Procedure
1. `node --check shared/homepage/app.js` and `python3 -c "import json; json.load(open('shared/homepage/services.json'))"`.
2. Isolated tree: `rm -rf /tmp/hp-smokeN && cp -r shared/homepage /tmp/hp-smokeN`.
3. Fabricate runtime files in the tree:
- `status.json`: `{"checked":"x","services":[{"name":<every services.json name>,"state":"up"}]}` (set one model `down` to exercise the red-pulse path).
- `deployed.js`: `window.DEPLOYED_AT="smoke"`.
4. Serve: `(cd /tmp/hp-smokeN && nohup python3 -m http.server 8099 >/dev/null 2>&1 & echo $! > /tmp/.hp-pid)`.
- If reusing port 8099: `pkill -f "http.server 8099"` FIRST — a second server on a busy port fails silently and the OLD tree keeps serving.
5. Browser census via eval tab on `http://127.0.0.1:8099/?v=<epoch>` — ALWAYS cache-bust with `?v=`; python http.server gets heuristically disk-cached by the browser. Assert: broken images (`.app-art,.mi-art` naturalWidth==0), computed styles (`getComputedStyle` for palette/fonts/clipPath), presence of new classes, `document.querySelectorAll('.docs section').map(s=>s.id)` for doc order, regex text census for banned words.
6. Screenshot full page + `selector` shots; inspect visually.
7. `./tests/run.sh` (expect only the known frank-primary MAX_SEQS failure), commit, `./scripts/deploy.sh dankclaw --service homepage` (runs Playwright e2e gate), then repeat the census against `https://www.franksdank.cloud/?v=<epoch>`.
8. Cleanup: kill server, move `/tmp/hp-smokeN` to `~/.AGENTTRASH/YYYYMMDD-homepage-smoke/` (never `rm`).
@@ -0,0 +1,15 @@
---
name: homepage-theme-smoke
description: "Locally smoke-test the Dankclaw homepage theme (style.css/app.js/docs.html) in the managed headless browser, with the stale-server traps that fake the result."
---
Goal: prove a homepage theme change on a throwaway tree before commit/deploy.
Protocol:
1. Stage a fresh copy: `cp -r shared/homepage/. /tmp/hp-smoke-<rand>/`, drop `.AGENTTRASH` dirs, and write a fake `status.json` (`{"generated_at":"x","services":[{"name":...,"state":"up"}]}`) — app.js fetches services.json + status.json and renders NOTHING without both.
2. Serve: `cd <tree> && nohup python3 -m http.server 8099 --bind 127.0.0.1 &`.
3. STALE-LISTENER TRAP (fakes every census): a survivor server from an older wave keeps answering on 8099 — `ss -ltn | grep 8099` proves a listener exists, not WHICH tree it serves. After every (re)start verify identity: `for p in $(pgrep -f "http.server 8099"); do readlink /proc/$p/cwd; done` must print the NEW tree; then `curl -s localhost:8099/<file> | grep -c <new-marker>`. `kill` the stale pid — never trust a `||`-guarded restart. Also: `pkill -f "http.server 8099"` from a shell whose own command line contains that string kills the shell; kill by pid.
4. Browser census: navigate with `?v=<epoch>` cache-bust (browser disk-caches http.server responses); `waitForSelector('.mi-card', ...)` before evaluating — rendering is async (fetch). Geometry checks: rect-gap to 1px for bottom-pinned chips; screenshot for visual proof. `screenshot({path})` may not write — use the returned omp-sshots artifact.
5. In headless pages `new Audio()` is detached: prove voice playback via `tab.requests()` (206 on the /sfx file) + the `mi-speaking` class, not a DOM `<audio>` query.
6. Live-domain check after deploy: the homepage is at `/`, the docs page ONLY at `/docs/` — `/docs.html` falls through the SPA fallback and serves the homepage shell. Anubis adds a challenge: `waitForSelector` with 30-45 s timeout.
7. Deploy loop: `./tests/run.sh` → commit/push → `./scripts/deploy.sh dankclaw --service homepage` (runs the Playwright e2e gate) → live census with fresh `?v=`.
@@ -0,0 +1,48 @@
---
name: html-report-ground-truth-audit
description: "Audit a self-generated HTML report: ground every numeric/code/diagram claim against the repo, fix, and prove structure + render"
---
Use after generating a large HTML report (e.g. via `repo-change-html-report`) whose claims were partly reconstructed from session memory. Fabricated numbers, invented code blocks, and silently-swallowed sections are the failure modes; the audit makes each traceable to the repo, a live command, or a recorded run.
## Procedure
### 1. Enumerate every checkable claim
Grep the report for numerics, file paths, symbol names, and code blocks:
- line counts (`wc -l` the real file)
- test counts (`grep -c '@test' tests/<f>.bats` — NOT suite-run totals: conditional skips make raw counts differ; run-observed totals are legitimate run history, keep them labeled as such)
- diff stats (`git diff --shortstat main..HEAD` — the footer's files/insertions/deletions)
- commit hashes (`git log --oneline`)
- "verbatim" code blocks (see 3)
- diagram node lists and captions (see 4)
### 2. Ground numeric claims with one command each
Fix every mismatch in place. Record the was/now pair — the audit table IS the evidence.
### 3. Verbatim code blocks: prove them
For a block labeled verbatim, the exact string must exist:
- live code: `grep -n '<distinctive line>' <file>`
- historical: `git show <sha> -- <file> | grep -E '^[+-].*(<token1>|<token2>)'`
If the block was fabricated (plausible-looking pseudo-code), replace it with the real excerpt. If condensed, the block header must say "condensed" and every shown line must be real.
### 4. Diagram claims: run the engine
Behavioral diagrams (plan traces, closure graphs) must be re-derived by running the actual CLI:
`python3 scripts/service-map.py plan --host <h> <path>` — then edit node labels, captions, and the prose count ("four services") to match output. Cross-check derived-set diagrams against the source relation (e.g. `grep -rl 'Network=gluetun.container' */**/*.container` for who rides a tunnel).
### 5. Check for swallowed sections
Large prior-session PUT ranges silently overwrite neighbors. Grep for headings/captions the plan promised; re-insert any that vanished, anchored on a fresh `read` of the region.
### 6. Structural balance
Python tag-balance pass over the file:
- per-tag open/close counts (`div`, `pre`, `figure`, `svg`, `table`, `span`, `figcaption`)
- running-div-depth scan printing per-section balance to localize stray closers
Common defect: `.ln`/`.nt` pair emitted without its `.cg` wrapper → stray `</div>`; fix by opening `<div class="cg">` before the pair.
### 7. Render proof
`file://` is blocked on the shared browser — serve the dir: `bash` service `python3 -m http.server <port> --bind 127.0.0.1 --directory <dir>`, ready on that port. Then:
- `browser.open({name, url: 'http://127.0.0.1:<port>/<file>'})`
- DOM assertions via `tab.evaluate(() => …)` — NOT `tab.run` (that runs in Bun; no `document`). Sections are `<h2 id>` + siblings: scope queries to `document.body`/`document.querySelectorAll`, not `#ch5 …`.
- screenshot the repaired regions (scroll via `tab.evaluate(el.scrollIntoView())`), then `tab.close()` and kill the server proc (`write proc://<name>/kill`).
## Deliver
A was/now table of every fix with its ground command, plus the list of claims verified intact. State honestly which numbers are run-history and not recomputable.
@@ -0,0 +1,26 @@
---
name: judge-empty-content-parse-repair
description: "Diagnose and repair LLM-judge \"unparseable verdict\" storms: judge-fail rows store no raw → reproduce live on the failing instance; root causes include empty model response (runaway reasoning chains, finish=length + content:\"\") and judge prose-form verdicts when JSON scaffold breaks; fix parser fallback + prove via restart rejudge."
---
Use when LLM-judge grades show an elevated "unparseable verdict" rate (baseline ~1%; storm = ≥5-10%).
## Law
Judge rows with `ok:false` store NO raw judge output (meta empty) — you are blind in the ledger. NEVER guess the cause from the ledger alone; reproduce the exact failing instance live.
## Procedure
1. **Census the rate**: filter ledger grade rows since a timestamp; rate = bad/total. Compare against the eval's baseline (≈1%). HLE with a 180B bare-JSON judge showed 11.8%.
2. **Pull the failing instance**: pick the newest failing grade row; get its `instance` id and the latest `gen` row for that instance (latest-wins; ledger is append-only).
3. **Reproduce live**: one-shot `bun -` (or equivalent) script: load registry host by judge name, call the judge function with that instance's exact question/answer/response, print `raw` head+tail. Judge raw text is ground truth.
4. **Root-cause A — empty model response**: if `resp_len: 0`, the judge is fine; the MODEL produced no answer content. Census gens: `text` empty + `finish=length` + `completion_tokens == max_tokens` ⇒ **runaway reasoning chain**: the model wrote the whole budget into `reasoning` and never emitted `content`. Loop proof: 60-char block Counter over the reasoning tail (49 identical blocks = degeneration, e.g. `w4 w? w4 w?…`).
5. **Root-cause B — prose verdict**: judges (esp. bare-JSON 180B class) drop the JSON scaffold when judging degenerate inputs (empty response) and emit plain `correct: no\nconfidence: 100`. Parser knowing only quoted `"correct": "no"` forms reports null = "unparseable" while the verdict is perfectly valid. Fix = add prose fallback regex `(?:^|\n)\s*correct\s*:\s*(yes|no)\b` after the JSON/quoted attempts, never before (JSON must win).
6. **Baseline comparison before blaming the engine change**: for any model-behavior rate (runaway %, empty-answer %), census the previous (voided/`.AGENTTRASH/`) ledger the same way. Pre/post identical (18.5% vs 18.0%) ⇒ pre-existing model behavior, not the quantization/config under investigation. This keeps causal claims honest.
7. **Verify the fix live**: restart the supervisor (poison law); pending `ok:false` grade rows re-grade on resume — the previously-blind rows flipping to real `fail` verdicts in the log is the proof.
## Empty-content scoring law
Runaway-chain empties score 0 honestly (AA-style truncating harness). Keep the score, publish the runaway census beside every number. Sampling changes (repetition_penalty, num_predict cap) deviate from AA posture and void+restart the eval — user decision, not an assistant fix.
## Pitfalls
- `podman logs 2>/dev/null`-style stderr discards make failure rows text-empty — check the capture path before inventing model-side causes (sandbox.ts fixed to `2>&1`).
- `export RID=...` for python heredocs (plain `RID=` is invisible to `os.environ`).
- Never `cd x && setsid ... & disown` expecting the cd to persist: `&` backgrounds the whole compound; foreground cwd is unchanged.
@@ -0,0 +1,62 @@
---
name: kernel-flag-claim-audit
description: "Audit a kernel/boot-parameter performance claim before it justifies a host decision (census feature presence, read the patch series verbatim, then publish the corrected plan to a Gitea issue without leaking the token)."
---
Use when a decision rests on a kernel/boot-line performance claim (`iommu=off` vs `pt`, a module param, a new engine flag) and the question is "does an upstream change retire this trade?", or when the answer must land as a Gitea issue comment.
## 1. Census before theory (read-only, no reboot)
```bash
uname -r; cat /proc/cmdline
grep -c . /dev/null; ls /sys/kernel/iommu_groups | wc -l # or the relevant sysfs knob
nm -b /boot/System.map-$(uname -r) 2>/dev/null | grep -i <feature_symbol> | head
apt-cache policy <meta-packages> # what the distro can actually install
grep -E 'CONFIG_(...)' /boot/config-$(uname -r)
ls -l /dev/<device>; id -nG # container-access question
```
- Symbol absence in `System.map` + archive pinning = the claim is **not actionable today**, regardless of merge status. Say that plainly.
- Answer the container/device question from node mode + group membership; it never needs a reboot.
## 2. Primary source beats press
Read the **patch series itself** (cover letter + the implementation patch), not the news write-up. The press number is a symptom; the patch text carries the *preconditions* that decide applicability.
Rules that only surface in the patch text (2026-10-18 AMD PerfOpt case):
- enable path "rehomes devices **already on the identity domain**" → the feature is a companion to `iommu=pt`, never an alternative;
- the enable path *disables* ATS/PRI/PASID/GCR3 (`skip_caps` in `attach_device`) → identity mode still pays a 1:1 page walk until this arms; a passthrough boot is therefore "most of the fix", not all of it;
- a per-IOMMU bit is refcounted / re-armed on resume → matters for suspend users.
Also check the driver-side consumer's gate condition (e.g. `amdgpu_device_use_perfopt()` requires `AMD_IS_APU` **and** the identity domain) — the feature and the endpoint you care about can live on independent paths (amdxdna binds its own SVA domain regardless of default domain type), which is what decouples "perf lever" from "device works again".
Never inherit a vendor/press delta as a local cost: record it as *vendor-claimed, never measured here*, and make the procedure demand a local A/B in the maintenance window. Flag stale baselines by quoting the current measured figure.
## 3. Correct every carrier, in one cutover
Decision-record page (new dated section + front-matter `description` re-key), the retired-endpoint runbook that carries the now-dead law, `AGENTS.md` key-lesson bullet, worklog. Delete the dead law rather than annotating it.
## 4. Publish to Gitea without leaking the token
Token NEVER in argv (`curl -H "Authorization: …"` is world-readable in `/proc/<pid>/cmdline`). Reuse the git credential store, write a 0600 curl config, delete it after:
```python
line = next(l for l in (~/.config/secrets/gitea).read_text().splitlines() if HOST in line)
user, tok = urllib.parse.unquote(...) # store file is percent-encoded
cfg = 0o600 temp: user = "USER:TOKEN"
subprocess.run(["curl","-sS","-K",cfg,"-o",resp,"-w","%{http_code}", ...])
os.unlink(cfg)
```
- New comment: `POST /api/v1/repos/{o}/{r}/issues/{n}/comments`, body `{"body": markdown}` → 201.
- **Correction: `PATCH /api/v1/repos/{o}/{r}/issues/comments/{id}` — edit in place, do NOT post a follow-up.** A wrong claim published is fixed by editing it; leaving the wrong text up for future readers is the failure mode.
- Verify: anonymous `GET .../issues/{n}/comments` then assert the corrected marker is present AND the retracted phrase is absent.
## 5. Verification
`./tests/run.sh` (docs completeness + vault link integrity cover the doc edits), commit, push. Secrets scratch files: unlink them; do not leave a token-bearing temp behind.
## Gotchas
- Markdown bullet lines inserted through the `edit` tool: an escape like `+- item` came out **verbatim** (file kept `+- `), twice. When a list block must be rewritten, `write` the whole file instead of guessing escape syntax.
- A reader-mode `read` of a news page can return the ad-blocker JS blob; use the mailing-list/patch archive (native method) for primary sources.
@@ -0,0 +1,27 @@
---
name: key-chord-source-audit
description: "Answer \"what does chord X do\" on Dank workstations: identify which layer (omp TUI, browser tool, niri, terminal) owns the chord before answering"
---
# Key-chord source audit
Frank asks "what does mod+r / ctrl+X do?" — the chord lives in ONE layer of the stack; answering from the wrong layer is wrong. Check layers in order, in ONE pass.
## Layers, in Frank's stack (most of his "mod" questions are niri)
1. **niri compositor** (Mod = Super). Keybinds are NOT in `~/.config/niri/config.kdl` — it only `include`s `cfg/*.kdl`. Grep `~/.config/niri/cfg/keybinds.kdl` directly. Notation: `Mod+R`, `Mod+CTRL+Alt+H`. Preset-width cycle (Mod+R) uses `preset-column-widths` from `cfg/rules.kdl`; no rule → built-in proportions 3/4, 1/3, 1/2, 2/3, 9/10, 1.
2. **omp TUI**: defaults in `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js` — grep `defaultKeys:\["…"` (array form) and `defaultKeys:"…"` (string form; needs a second pattern). Recognized modifiers: ctrl/shift/alt/super only; no `meta`/`mod` alias. User remaps: `~/.omp/agent/keybindings.yml` (check existence first; absent = no remaps). Docs: `omp://keybindings.md`.
3. **omp browser tool**: `Mod+Key` is Playwright notation (`Mod` → Ctrl on Linux, Cmd on macOS), used by `tab.press()`; e.g. `Mod+R` = page reload. Tern backend `ternKey()` throws on literal `Mod`; puppeteer path passes names through.
4. **Terminals/tmux**: `~/.config/ghostty/config.ghostty` + `ghostty/auto/`, `foot.ini`, `kitty.conf`, `~/.tmux.conf` — keybind/bind/map lines.
## Procedure
1. Classify the notation the user used: `Mod+…` → niri/Playwright; `Ctrl+…` → TUI/terminal; tmux prefix → tmux.
2. Grep that layer's config FIRST, expanding includes (config.kdl → cfg/*.kdl is the trap that caused a wrong answer).
3. For omp bundle greps: two `defaultKeys` forms exist; `super+r` pattern also matches `super+right` — anchor with word boundary.
4. Quote the exact config line as evidence; state the layer that owns the chord.
## Verification
- niri keybinds: `grep -n "Mod+R" ~/.config/niri/cfg/keybinds.kdl` (not config.kdl).
- omp: `grep -o '"app[^"]*":{defaultKeys:[^}]*' cli.js` for app actions; `grep -rn "defaultKeys" pi-tui/src/keybindings.ts` for editor actions.
@@ -0,0 +1,50 @@
---
name: llama-server-arch-verify
description: Verify llama.cpp binary supports a model architecture without relying on binary grep
---
# llama-server Arch Verification
## Problem
`grep -ac <arch> llama-server` returns 0 for ALL architecture names (including ones the binary is actively serving). The arch strings are stored in a packed/constructed form that grep can't catch.
## Solution
Verify arch support via the source registration table, not binary grep:
1. **Check the source registration** (`src/llama-arch.cpp` or `src/llmarch.h`):
```bash
grep -n "gemma4\|qwen3_5\|<arch>" src/llama-arch.cpp
```
If the arch is registered in the source at the commit the binary was built from, it's supported.
2. **Verify the binary is current**:
```bash
stat -c "%y" /path/to/llama-server
git log -1 --format=%cd <commit>
```
The binary mtime should be after the commit date.
3. **Check for model-specific source files**:
```bash
ls src/models/<arch>.cpp src/models/<arch>-*.cpp 2>/dev/null
```
4. **Check for spec-type support** (for MTP/draft models):
```bash
/path/to/llama-server --help 2>&1 | grep "spec-type\|model-draft"
```
## KV Cache Types
The build's allowed `--cache-type-k` / `--cache-type-v` values:
`f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1`
**No true fp8 KV dtype** — `q8_0` is the "FP8 class" (1.0625 B/elem, 8.5 effective bits).
## Separate Draft Model (MTP assistant)
For models where the MTP head is a SEPARATE file (not in-file like Swift):
```bash
--spec-type draft-mtp \
--spec-draft-n-max 3 \
--spec-draft-model /path/to/mtp-assistant.gguf
```
The `--spec-draft-model` flag (aka `-md`, `--model-draft`) loads the separate draft file.
@@ -0,0 +1,47 @@
---
name: llamacpp-candidate-arm-census
description: "Measure a candidate GGUF/KV-shape on a live dank-fleet llama.cpp endpoint (frank-zealot pattern) in ONE service-stop window: pre-flight flag check, per-arm env overrides, boot ledger + nvidia-smi census, behavior probes, trap-guarded restore."
---
Measure candidate shapes on a production llama.cpp endpoint without cutting anything over. One outage window, many arms, guaranteed restore.
## 0. Pre-flight (no GPU needed — do all of it before stopping anything)
- `ssh <host> <candidate-bin> --list-devices` — the candidate must see the card (backend `.so` releases resolve via `$ORIGIN`, so run from the extracted asset dir).
- Flag-compat gate: loop the exact long options the serve script emits and grep the candidate's `--help`. A single unknown flag = a dead boot in the middle of the window. Remote shells may be **zsh**: `$flag[ ,<]` is parsed as an array subscript ("bad math expression") — pipe the check through `ssh <host> 'bash -s' <<'CHK' … CHK` and use `grep -qE "(^|[[:space:]])$f([[:space:],<]|$)"`.
- Pause any concurrent compile (`pkill -f "[r]un-<build>"` — the bracket stops `pkill -f` matching your own command line; `pkill -x ninja`). Ninja is incremental: the build resumes where it stopped, and CPU contention otherwise skews every rate.
## 1. Runner shape
Write one script to `/tmp` on the target via `ssh <host> 'cat >/tmp/….sh' <<'EOF'`. Never edit the repo checkout on the target; drive the shipped serve script with a **per-arm env file** and `FRANK_<SVC>_ENV_FILE` (the serve scripts already honor it).
```bash
SRV=""
used(){ nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits; }
restore(){ [[ -n "$SRV" ]] && kill "$SRV" 2>/dev/null; pkill -x llama-server
for i in $(seq 1 90); do [[ "$(used)" -lt 1500 ]] && break; sleep 2; done
sudo systemctl start <svc>
for i in $(seq 1 150); do [[ "$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:<port>/health)" == 200 ]] && break; sleep 2; done
echo "RESTORE health=$(curl -s -o /dev/null -w %{http_code} …/health) used=$(used) state=$(systemctl is-active <svc>)"; }
trap restore EXIT
```
- `trap restore EXIT` is mandatory, not polite: it also fires on the failure paths, so a script bug cannot leave the endpoint down.
- Preserve the production log first (`cp -f /home/dank/logs/<svc>.log …prod-<ts>.log`) — the serve script truncates it.
- Health-poll cap ~120×2 s. 300 polls turns an expected-allocation-failure arm (the "wall" arms) into 10 wasted minutes.
- Arm = `sed` the base env (CTX_SIZE / PARALLEL / KV_CACHE_TYPE / LLAMA_BIN / LOG_FILE), boot with `setsid nohup env FRANK_<SVC>_ENV_FILE=… bash <serve-script>`, then capture: `nvidia-smi` used+free, ledger grep (`type |model size|n_ctx|flash|KV buffer|buffer size|slots|error|exceed`), `/slots` (per-slot `n_ctx` is the harness contract), `/v1/models`.
- **Point LLAMA_BIN at a binary that exists.** Take 1 of this procedure died in 8 s because the trial env named a still-compiling source build; the trap saved the endpoint.
## 2. Probes that actually discriminate
- chat: `content` vs `reasoning_content` (channel differs per runtime) + `finish_reason`.
- tools: real `tools` schema, assert `tool_calls[0].function.arguments` parses as JSON — empty/`null` arguments is a known fork bug.
- vision: 512×512 **solid color** PNG built with `zlib`+`struct` (no PIL), sent as an OpenAI `image_url` content part; expect the exact color word. The legacy top-level `images` field hallucinates a plausible answer.
- rate: `/completion` (not `/v1/chat/completions`) — it returns `timings` (prefill/decode). ≥128 tokens.
- long-context fill: ~190k tokens through `/completion` proves the per-slot window AND tests the "quantized KV near 200K" stability reports.
Skip `llama-bench` for shape work: it has no `-c`/`-np`, and its `-mmp` is `--mmap` (deprecated), not mmproj. Booting the server per arm measures the real shape and prints the ledger, which is the referee.
## 3. Report
Per arm: ledger KV-buffer MiB, used/free at settle, FITS / tight / NO FIT vs the card's `used+free` ceiling from settled boots (not the spec sheet). Then restore, and state the downtime.
@@ -0,0 +1,33 @@
---
name: llamacpp-endpoint-model-swap
description: "Swap the model on a dank-fleet llama.cpp endpoint: fetch, runtime-door census, env edit, deploy through git, rewrite the bats seam, sweep every live-path straggler, verify VRAM with the engine's own numbers."
---
# llama.cpp endpoint model swap (dank fleet)
Use when replacing the model/shape on a llama.cpp endpoint (frank-zealot on the RTX 3090). The engine's boot numbers are the verdict — do NOT commit to a feasibility verdict from head math; boot it and read `nvidia-smi`.
## Sequence
1. **Runtime door BEFORE anything else.** Histogram the GGUF's tensor types and metadata (one python range-read of the KV section): a custom type (e.g. PQ2_0 = GGML type 142 + `prism.hadamard.*`) means the vendor/fork build is FORCED, not chosen — stock llama.cpp refuses it at load. Record the build identity (path, build number, commit, sha256) in the env comment; a `--help` census on that binary is what settles flag questions (this fleet's builds have NO fp8 KV; q8_0 is the "FP8 class"). NEVER `grep -c <archname>` on the packed binary to check arch support — it returns 0 for every arch.
2. **Geometry first, from the loaded model's own log** (`--log-verbosity 4`, or the HF `config.json` under `text_config` for multimodal): layers, `n_head_kv` (an ARRAY — per-layer), head_dim, `sliding_window_pattern`, `n_swa`, `attention_k_eq_v`. KV scales only with full-attention layers; sliding layers cost a fixed ring; recurrent/linear layers (Gated DeltaNet) bill a fixed state per SLOT (~150 MiB measured), invisible to pool arithmetic. Single-value HF fields lie about per-layer heterogeneity.
3. **Fit the KV rate law to the engine, not to arithmetic.** One boot at `-lv 4` prints `llama_kv_cache` / `sched_reserve` MiB lines. Master 5fc4f3c law: bytes/elem = (K + V) **x2** when the file declares `attention_k_eq_v`. Full cache = n_full x n_head_kv x head_dim x rate x ctx. If your formula disagrees with the printed MiB, your law is wrong.
4. **Price the KV dtype by BOOTING the arms, at the target window.** f16 may simply OOM (`cudaMalloc failed` after the engine's own fit line names the MiB it wanted); q8_0 may be the only pair that funds 2 x 200,704; q4_0 is the fallback rung. Record every arm's used/free. **The mmproj lands AFTER the engine's fit projection** (projected fit < settled VRAM by ~1.5x the projector file size) — quote both numbers.
5. **Fetch with `-f` + byte-count + sha256 check** into `/home/dank/models/<name>/`; `--verify-only` mode for re-census.
6. **Edit the repo trio** (`scripts/serve-<svc>.sh`, `scripts/<svc>.env`, `~/.omp/agent/models.yml`). **Use the serve script's OWN variable names** (`CACHE_TYPE_K`/`CACHE_TYPE_V`, `BIND_ADDR`, `NGL`) — an invented name is silently swallowed by `: "${VAR:=default}"` and the arm boots the DEFAULT (cost a whole trial window: a `KV_CACHE_TYPE` scratch env booted f16 and OOMed while reading as a model fault). Sweep the script's DEFAULTS to the new payload too: a defaulted run must land on the shipped shape, never the retired one. Spec flags move as a SET (`SPEC_TYPE=none` strips `--spec-type` AND `--spec-draft-*`); MTP is a property of the FILE (no `nextn` tensors = no drafter, ever). `models.yml` `contextWindow` = per-slot window (`CTX_SIZE/PARALLEL`); recompute `maxTokens` for the compaction clamp `min(maxTokens, ctx - estimate - 4096)`. Check the payload's own template: if it reads `reasoning_effort`, the `reasoningEffortMap` fold becomes LOAD-BEARING (an unmapped level is a 500 naming Jinja, not the model) and quote the file's `general.sampling.*` trio in `extraBody`.
7. **Rewrite the bats seam in the same commit** (`tests/<svc>_unit.bats`): payload trio paths, slot geometry, spec state, batch pair, verbosity, mmproj opt-out, reasoning flags — argv-asserted against the stub engine; where the contract is a repo value (batch pair, verbosity, **KV dtype**) assert argv == env value so a retune moves one file and a wrong-name env edit cannot pass silently.
8. **Trial window protocol** (when the shape is unproven on the card): stop the unit (trap-guarded `systemctl start` restore in the scratch script), run the arm census + behavior probes as a DETACHED script writing to `/home/dank/logs/<name>.out`, then before touching production CENSUS the live state: `systemctl is-active`, `ss -ltnp | grep <port>`, `pgrep -x llama-server`, `nvidia-smi`. A scratch server orphaned by a client-side timeout holds the whole card and will OOM the real boot — kill by PID (never `pkill -f llama-server`). A trap restore can race your manual kill; read the script's own `RESTORE` log line before assuming who did what.
9. **Deploy through git** (no direct remote edits): commit atomically on the worktree branch, `git fetch && git rebase origin/main` (log.md conflicts at EOF are two waves appending — keep both), push branch (`--force-with-lease` after a rebase), then in the MAIN checkout `git pull --ff-only` + `git merge --ff-only <branch>` + `git push origin main`, then `scripts/deploy.sh <host> --service <svc>` (`--service` is REPEATABLE — name the homepage too when cards changed, or the target ships the model without its docs). A `deploy.sh` with no `--service` is a fleet-wide drain: never.
10. **Verify the engine, not your math**: `systemctl is-active/is-enabled` + the new `Description`, the journal fit/KV/buffer ledger (`projected to use N`, `KV buffer size`, `n_ctx_seq`, `n_slots`), `/v1/models` served alias (radiance-style echo does not apply to llama.cpp, but the alias is still the honest check), `nvidia-smi` settled, then live probes through the REAL request shape the harness sends (exact-instruction chat + `reasoning_content`, `tool_calls` JSON args, solid-color `image_url` >=256 px expecting the exact word, `timings.predicted_per_second`). Never report the FIRST request after a boot (warmup artifact). Push, and record numbers in the env comment + runbook.
11. **Cutover every consumer, same branch**: AGENTS.md (topology row, `### <svc>` section, Model Config note, runbook index ACTIVE + RETIRED rows); the new docs page MUST be linked from the TOP-LEVEL `serverAppConfigs-docs/index.md` (the `docs_completeness` seam fails otherwise, not from `LLM/index.md`); `shared/homepage/services.json` + `docs.html` (model link, spec line, rate, and the WARNING — a censored base breaks the "common uncensored text" equality in `homepage_config.bats`, so re-key the test's premise too); `serverAppConfigs-docs/Dankclaw/OpenWebUI.md` + the live `model` row `meta.description` (live DB state: snapshot `webui.db.bak-<date><letter>`, UPDATE via host python3 over ssh; do NOT restart `open-webui` for a cosmetic string — it logs every family session out).
## Gotchas
- A "we can rebuild the fork ourselves for provenance" claim needs the source tree AT the vendor build's commit — check `git -C <src> log --oneline -1` first. A fork checkout at branch HEAD (`prism` at `0082ac2`) is NOT the vendor build (`2459f68b5`), so the rebuild produces a different binary, not the verified one. Also: if the vendor binary already demonstrates the requirement (KV dtype buffer math in the ledger + live probes), a rebuild buys no behavior change — offer it explicitly as provenance hardening, never do it silently.
- A systemd restart reporting `inactive` in the first seconds is usually model-load time; check `journalctl -u <svc>` for `model done`/`server is listening` before declaring failure.
- Rollback = the previous payload stays on disk; one `MODEL_FILE` + shape edit back. Record a refusal-freedom fallback the same way (an official censored base ↔ a CRACK re-export with byte-identical header geometry is a one-line swap; prove the identity with the tensor histogram, not the card).
- A thinking model can consume the whole token budget in `reasoning_content` (empty `content`, `finish_reason: length`) — probe with a budget that clears thinking; a short-budget vision probe reads as "the model is blind".
- `/completion` at `-lv 4` prints `failed to parse reasoning content ...` on raw prompts closing with an end-of-turn marker: cosmetic (chat path parses correctly). At ~26k+ a bare `/completion` can return 0 tokens with no error — raw context has no role frame; chat is unaffected.
- **Bats vacuity trap**: `serve_argv` REWRITES `$STAGE/test.env` from the shipped env, so a test that mutates the file then calls `serve_argv` asserts on regenerated content — a tautology. Tests that mutate must exec via a `run_stage` helper running the file as-is.
- `env NAME=value cmd` needs valid identifiers; a flag-like token is only legal as an `env` ARGUMENT, not as a shell assignment prefix.
- Gitea `git push` on a rebased branch is rejected non-fast-forward: `--force-with-lease` (your own branch), never on main.
@@ -0,0 +1,27 @@
---
name: llamacpp-serve-unit-boot-loop
description: "Diagnose a Dank-fleet LLM systemd unit that boot-loops: classify 203/EXEC vs model-load OOM vs payload gate, using journal signatures and nvidia-smi/rocm-smi."
---
# llama.cpp / fleet serve unit boot loop
A fleet LLM unit (`frank-large`, `frank-secondary`, `frank-primary`) restarts every `RestartSec`. Read the journal FIRST, act second:
```
ssh <host> 'sudo systemctl status <svc> --no-pager | head -12; sudo journalctl -u <svc> --since "-6 min" -o cat --no-pager | tail -40'
```
## Classify by signature
| Signature | Cause | Fix |
|---|---|---|
| `status=203/EXEC` + `Failed at step EXEC spawning` + `Mem peak: ~1.9M` + GPU flat at 1 MiB | the `ExecStart` script never ran: missing shebang, lost exec bit, or path moved by the deploy (git pull mid-restart) | `head -1 <script>` must be `#!/usr/bin/env bash`; `git ls-files -s <script>` must show `100755`; commit the fix, redeploy |
| `status=203/EXEC` right after a deploy, then self-heals | git replaced the file mid-checkout while the restart loop ran | ignore if the newest attempts succeed; verify `podman ps --format '{{.CreatedAt}}'` identity |
| Engine prints weights loading, then dies | VRAM/disk budget, engine flags | read the engine's own ledger (verbosity 4) for KV/compute-buffer MiB; price from two boots, never a formula |
| Health 503, no error, minutes of silence | normal cold load (big PLE/mmap) | do not kill; the listener binds after the full load |
## Rules
- Judge the newest container/attempt only: `podman ps -a --format '{{.ID}} {{.CreatedAt}}'`, `journalctl --since`; a mid-stop container still serves old logs.
- `systemctl is-active` is `failed` after every intentional stop on foreground-podman units (exit 143) — cosmetic.
- The unit's `Description` is the last-deployed text, not the repo's. A stale Description is not an incident; the next `relink_unit` fixes it — never restart a serving endpoint for cosmetics.
- Fixing the script = commit + `./scripts/deploy.sh <host> --service <svc>`; never edit on the host (a direct `scp` leaves drift that blocks the next deploy's ff-only pull with "local changes would be overwritten"; repair with `git checkout -- <file>`).
@@ -0,0 +1,74 @@
---
name: llamacpp-trial-boot-census
description: "Measure whether a candidate GGUF fits a live llama.cpp endpoint's card: verify serve-script env names first, run a dtype/slot arm matrix with trap-guarded production restore, read the engine fit ledger."
---
# llama.cpp trial-boot census (does candidate X fit this card?)
Paper arithmetic on KV rates is a hypothesis. The engine's own fit ledger plus a
real boot are the proof. This protocol ran on frank-zealot (RTX 3090,
Qwen3.5-9B Q6_K → prism-ml Ternary-Bonsai-2-27B PQ2_0, 2026-10-17).
## 0. Gate: env variable names, not invented ones
A trial env written from memory silently falls back to the serve script's
defaults. `KV_CACHE_TYPE=q8_0` when the script reads `CACHE_TYPE_K`/`CACHE_TYPE_V`
boots **f16** and every later number is wrong (this wasted a whole outage; two
arms printed identical allocations, which is the tell).
```bash
grep -nE ': "\$\{[A-Z_]+[:=-]' scripts/serve-<svc>.sh # the real names + defaults
grep -nE '\-\-[a-z-]+ "\$' scripts/serve-<svc>.sh # what actually reaches argv
```
Never invent: KV dtype, bind addr, n-gpu-layers, log-file (units redirect it).
## 1. Runner skeleton
One script on the host, `set -u`, `trap restore EXIT`:
```bash
restore(){ pkill -x llama-server; for i in $(seq 1 90); do [[ "$(used)" -lt 1500 ]] && break; sleep 2; done
sudo systemctl start <svc>; for i in $(seq 1 150); do [[ "$(curl -s -o /dev/null -w %{http_code} $BASE/health)" == 200 ]] && break; sleep 2; done; }
trap restore EXIT
sudo systemctl stop <svc> # benching law: stop first, mask fails on relinked units
setsid nohup env FRANK_<SVC>_ENV_FILE=/tmp/armA.env bash scripts/serve-<svc>.sh >$LOG 2>&1 &
```
Launch with `setsid nohup … &`, wait with ONE async watcher (`grep -q DONE`),
never poll. Per-arm drop-in envs via `sed` on a base env — one variable per arm.
## 2. Arm matrix (cheap, each ≈ boot + probes)
| Arm | Purpose |
|---|---|
| target shape × target KV dtype | the verdict + behavior probes |
| target shape × f16 KV | proves the wall (or funds the shape) |
| 1 × native-window | answers "can we serve the full context" |
| target shape × q4_0 | the roomy fallback rung |
## 3. Read the ledger, not `nvidia-smi` alone
```bash
grep -a -A8 "memory breakdown" $LOG # total = free + (model + context + compute)
grep -aE "KV buffer size|model buffer size|compute buffer size|estimated worst-case memory usage of mmproj|projected to use|cannot meet free memory target|cudaMalloc failed" $LOG
```
Laws learned the hard way:
- `projected to use N MiB vs M MiB of free` + `cannot meet free memory target of R` ⇒ requirement is `projected + R ≤ free`. With `--n-gpu-layers` pinned by the user the fitter **aborts** instead of shrinking — the boot then dies at the KV `cudaMalloc`.
- `context` bills **more than the KV pool**: hybrid models add a fixed recurrent state per slot (~150 MiB/slot for qwen35's 48 DeltaNet layers). Pool math alone under-prices.
- **The projector's `estimated worst-case memory usage of mmproj is X MiB` is NOT inside the fit projection.** It lands after the fit check, so real headroom = paper headroom − X. This is the "boots fine, then dies / dark" class.
- q8_0 K+V bills exactly `2 × (layers_kv × heads × dim / 32 blocks × 34 B)`/token — check the reported KV MiB against that to prove the dtype actually took.
## 4. Probe traps
- **First request after boot is warm-up garbage**: it paid 2.6 t/s prefill / 37.7 t/s decode where the warm run gave 1,258 t/s. Never bench the first request; discard it or warm with a throwaway.
- Reasoning-on chat probes need **≥512 `max_tokens`**: at 64 the answer was empty (`finish=length` mid-`reasoning_content`) while vision actually worked. Judge `content` only at a budget the thinking can fit.
- Raw `/completion` (no chat template) returns `content` only — thinking is invisible; `timings.prompt_n` / `predicted_per_second` are the real keys, not `prompt_tokens`.
- Vision probe: solid-color PNG ≥256 px, expect an exact color word.
- Concurrency honesty: fire two identical requests in parallel; per-slot rates from each response's `timings`.
## 5. Restore proof
`health=200` **and** `systemctl is-active` — an `inactive` unit behind a 200 means
a stray trial process holds the port. Kill by pid; never `pkill -f llama-server`.
@@ -0,0 +1,24 @@
---
name: llm-bench-harness-fault-census
description: "Diagnose implausible LLM benchmark scores in frank-intelligence-bench: failure-signature census, harness-fault signatures (target NameError, scicode ModuleNotFound, markdown-fence SyntaxError), and official-protocol grading law."
---
Use when benchmark scores look impossible (0% across the board, sudden cliffs) in frank-intelligence-bench.
# Law
Scores are untrusted until `bun run src/cli.ts analyze <run-id>` shows a failure-signature census with ZERO harness suspects. Model-side signatures (AssertionError on real values, AxisError, model's own NameErrors) are fine; harness signatures indict the engine.
# Census
- Reads `runs/<id>/ledger.jsonl` grade rows, extracts last `*Error: name 'X'` / `No module named 'X'` per failing row, aggregates per eval.
- `harnessSuspects()` flags: `NameError: target(s)` (gold target binding missing), `ModuleNotFoundError: scicode` (pinned pkg absent from sandbox image), >50% shared signature ≥10 samples (systematic fault), >50% gen failures (endpoint fault).
# Known harness faults (friction-earned, 2026-10-10)
1. SciCode gold values live ONLY in `test_data.h5` (authors' Drive folder `1W5GZW6_bdiDAiipuFMqdUhvUaHIj6-pR`); tests reference bare `target`. Fix = `stageH5()` (sha256-gated upload to Dankclaw, mounted `/w-data/test_data.h5:ro`) + `process_hdf5_to_tuple(step, n_cases, h5py_file=...)` per official `eval/scripts/test_generated_code.py`.
2. Sandbox image must contain `h5py` + `scicode @ git+https://github.com/scicode-bench/SciCode.git@<pin>` (+ `git` in python:3.12-slim for pip-git). Image tag carries the fix epoch: `frank-bench-py:3.12-scicode1`.
3. Official extractor `get_function_from_code` `exec`s raw submission text → markdown fences are guaranteed SyntaxError. Official pipelines store CLEAN code: strip fences (`stripFences()`: join all ``` blocks) before extraction, for submissions AND prior-step context.
4. Gold CODE ships only for the dev split (50 steps) on HF `AI-SCODE/SciCode`; `problems_test.jsonl` gold=None; GitHub pinned tree ships no gold jsonl; scicode pip package ships no data. Leaderboard (AA) SciCode uses reference-solution context — compare only against the gold-context slice, never the self-cumulative test split.
5. Quarantine poisoned ledgers to `.AGENTTRASH/` (never blend void rows into reports), reset `runs/queue.json` to `{"active": null, "runs": []}` (active must be `null`, not `""`).
# Verification gates
- Real-infra proof: `FIB_INTEGRATION=1 bun test tests/integration.test.ts` (gold solution must PASS official protocol; `process_hdf5_to_tuple("77.1", 3)` → 3 targets). Integration files must `delete process.env.DRY_RUN` and point `FIB_DATA_CACHE` at the real cache (preload isolates it to TMP fixtures).
- After a protocol fix, fresh live run must show mixed PASS/FAIL with model-side-only signatures before any score is reported.
@@ -0,0 +1,27 @@
---
name: llm-bench-noise-discipline
description: "Noise-floor discipline for single-run LLM endpoint A/B benchmarks: the ±3% law, replication-before-keeping, and bracket-confirmation of tuned values."
---
# LLM Bench Noise Discipline
Single-run A/B benchmarks against an LLM endpoint's own timings API have a much larger run-to-run noise floor than the metric's instantaneous precision suggests. On frank-primary (radiance, 256-token greedy decode at 43.6k-token prompt depth), three runs of the IDENTICAL config measured 33.06 / 32.03 / 30.90 t/s — a ~3% spread.
## The ±3% law
- Treat any single-run delta under ~3% as noise, regardless of the harness's reported confidence multiplier.
- Before believing a "keep", run the same config a second time (no edit, pure replication). If the replication drops below the noise band, flag the lucky draw as suspect (`flag_runs`) so best-metric math stops trusting it.
- Noise sources: TP all-reduce wire loss (wht6), page-cache warm/cold of the 51 GiB n-gram table, container boot ordering, expert-pool hit-rate warm-up.
## Bracket before trusting an optimum
A "tuned" value is only confirmed when BOTH neighboring steps are measured: one side may crash (boot wall) and the other may regress. Example at seqs=3: EXPERT_VS_CACHE 0.78 crash / 0.82 optimum / 0.84 = -9% (0.87 collapses to 0% per runbook); MNBT 4096 crash at seqs=3 while the seqs=2 A/B showed it bootable.
## Boot failures are data, not waste
A scratch arm that never reaches /health (720s timeout) maps the viable envelope. Three crashes at seqs=3 (0.78 weights-share, 4096-token chunks, bf16 wire) prove the production trio wht6/2048/0.82 is exactly what makes the 3-slot shape fit.
## Outside-noise signals found at this endpoint
- SPEC_TOKENS=4: -23% decode, draft acceptance 0.831->0.497 (the 4th draft token is rejected too often to pay its verification).
- SPEC_TOKENS=0: -6.8%; MTP drafting always pays.
- KV shape seqs=4/maxlen=100k (same 400k total): -11%; slot overhead is not paid by freed VRAM.
## Harness pattern that worked
stop prod -> boot scratch container on the SAME two GPUs with candidate argv -> poll /health -> ONE self-contained greedy request built from a fixed-seed repo corpus (deterministic prompt_n) -> parse timings -> tear down -> restore prod via trap (never mask a relinked unit; stop only). Fresh prefix-cache dir per run so the cold prefill is cold.
@@ -0,0 +1,30 @@
---
name: llm-endpoint-behavior-battery-a-b
description: "A/B two boots of a live OpenAI-compatible LLM endpoint (payload swap trial) with one identical behavior+throughput battery: refusals, thinking-token census, tools, vision, needle, decode/prefill rates."
---
Run the SAME battery against both boots of an endpoint swap (candidate vs shipped payload). Never compare across different prompts, never benchmark beside the live service (stop + mask first), never measure the first request.
## Harness
`/tmp/halogen-eval/battery.py <tag> [all|tail]` (stdlib only, `urllib`): appends one JSON line per probe to `/tmp/halogen-eval/battery-<tag>.jsonl`, summary to stderr. `tail` = long-context + throughput only. Stage assets next to it: a corpus text file, a base64 512 px solid-color PNG.
Probe order (each is a gate, all comparable across boots):
1. **warmup** — discarded. First request after boot is a graph-capture artifact (measured 3x spread: 2.6 vs 1258 t/s prefill).
2. **sanity** — exact-answer arithmetic, `temperature=0`.
3. **refusals** — 5 hostile prompts, count refusal-marker regex hits. The abliteration/base-alignment census.
4. **thinking census** — 6 fixed questions, `temperature=1.0`, `max_tokens=4096`; report mean `usage.completion_tokens` + per-item. A removed instruction fine-tune shows here first (measured: mean 784 vs the card's 3271 for the base model).
5. **tools** — 1-turn (named-arg `tool_calls`) then 2-turn (tool result → answer).
6. **vision** — OpenAI `image_url` content part only (legacy top-level `images` is silently ignored); solid color, expect the exact word; budget >=512 tokens or thinking eats it and empty `content` + `finish_reason=length` reads as blindness.
7. **needle** at planted depth 0.85 and 0.10 of a token-budgeted haystack.
8. **decode** — greedy, `max_tokens=512`, long-form prompt: `completion_tokens/secs`.
9. **prefill** — one ~58k-token prompt twice: pass 1 cold, pass 2 cached; the ratio is the prompt-cache proof (measured 136x warm, 1.4x when already cached).
## Laws that bite
- **Token-budget every prompt by measurement, never chars/4.** Measure chars/token on the actual corpus (`prompt_tokens` of one probe ÷ its char count; wikitext-2 ≈ 4.33 for a HF-tokenizer Qwen-class model — chars/4 under-bills ~8 %). Over-window prompts come back HTTP 400 whose message names the exact token count and the ceiling — treat that as ground truth, it is the cheapest tokenizer oracle available.
- **Corpus splitting:** check the separator before trusting a paragraph budget. Wikitext-2 raw blocks separate with `"\n \n"`, and a naive parquet→text dump joins rows with a SINGLE newline: a `"\n\n"`/blank-line split then returns ONE paragraph = the whole file, so every budget silently becomes "everything". Verify with `text.count("\n\n")` and the split's part count before running.
- **Catch `HTTPError` in the POST helper and record the body.** A 400 is a probe result, not a crash — otherwise the battery dies mid-run and you re-run the whole thing against a live GPU.
- **Report decode from a quiet host.** The same shape measured 25.6 / 37.9 / 46.8 t/s while a 62 GB download hammered page cache; only the idle-host number is real.
- **Server-side samplers exist.** If the endpoint pins temperature/top_p/top_k, omit `temperature` for the thinking census (match production) and pin `temperature=0` only for exact-answer probes.
## Verdict framing
Behavior gates (sanity/refusals/tools/vision/needle) are the contract; PPL/KL from the engine's own checkpoint tools is advisory; throughput deltas under ~5 % are noise unless a mechanical control (a prompt shorter than the knob under test) proves the knob was the only variable.
@@ -0,0 +1,35 @@
---
name: model-runtime-support-survey
description: "Survey how every open-source runtime serves a given LLM (vLLM, llama.cpp, SGLang, KTransformers, overlays, vendor GGUFs) before writing a serving/feasibility plan; harvest quant recipes, measured rates, and mapped landmines from PR comments and overlay repos."
---
# Model Runtime Support Survey
Answer "how do other engines run model X, and what did they pay to learn it?" before committing a bring-up plan. Every kernel spec, quant recipe, and landmine found upstream is one you don't pay for twice. Proven on GLM-5.3-Flash (2026-10-14): surfaced a production 8xR9700 vLLM overlay, MTP 2.3x decode numbers, and 4 landmines (fp8-KV scale calibration, prefix-cache 0-hits, KDA autotune nondeterminism, chat-template notation).
## Harvest order
1. **Official model card** (`huggingface.co/<org>/<model>`) — the "Serve Locally" section lists officially supported runtimes; sampling/template contract lives here (e.g. `reasoning_effort` defaults, `clear_thinking`).
2. **Per-runtime PRs** (vLLM, llama.cpp, SGLang) — fetch via the GitHub-API-backed read; **the comments, not the body, are the payload**: field reports (prefix-cache hit censuses, ROCm/AMD enablement branches, extension rebuild recipes, crash repros, free tuning levers via `--override-kv`, nondeterminism root-causes). Get the PR status (merged/open/closed) — it sets what "master supports" means.
3. **Overlay/serving repos on HF** (search model name + your GPU family, e.g. `rocm-r9700`): read the tree (`huggingface.co/api/models/<repo>/tree/main`), then `raw/main/README.md`, `raw/main/OVERLAY_NOTES.md`, `raw/main/run-*.sh`. Launch scripts carry the laws in comments (KV sizing justifications, env ladders, dead knobs).
4. **Vendor GGUF guides** (Unsloth docs): append `.md` to the docs URL for clean markdown; gives quant KLD/top-1 tables, template fixes, hardware floors. Check whether their GGUF carries MTP/nextn tensors — that's often the only public source of draft-head weights.
5. **KTransformers tutorials** (`raw.githubusercontent.com/kvcache-ai/ktransformers/main/doc/en/kt-kernel/<Model>-Tutorial.md`) + **SGLang cookbook** (`docs.sglang.io/.../<Model>.md`): CPU-offload expert shape validation, 1M-ctx recipes.
## Harvest checklist (per runtime)
- Measured decode/prefill + hardware shape (cards, TP) — anchor, not decoration; per-card effective GB/s is the cross-engine comparison unit.
- Quant recipe: bits-per-weight split by module class (experts/trunk/embed) — convergent recipes cross-validate your planned conversion.
- Spec-decode: acceptance length, per-position accept, when it pays (CPU-offload) vs loses (VRAM-resident).
- Landmines: KV-scale calibration reading uninitialized linear-attn state; prefix-cache semantics on hybrid KDA/DSA models; kernel autotune nondeterminism (KLD across launches); fp8 flavour bugs (FNUZ vs e4m3fn scale); template notation engines can't parse; import gates per gfx/arch.
- Harness contract: reasoning-effort defaults (16k+ hidden thinking tokens), sampling params, tool/reasoning parser names.
## Output
A runtime matrix table (engine | arch status | hardware | artifact | measured) + a numbered "laws we inherit" list; fold every law into the plan's phases and risks with § cross-refs.
## Gotchas
- Read PR **comments** — the highest-value data (crash repros, censuses) is comment-only.
- HF fetches: `api/models/<repo>/tree/main` for listing, `raw/main/<path>` for files; the HTML card view renders README inline too.
- `browser.open` targets the harness's headless browser, not the user's — refresh user-facing pages with `xdg-open`.
- Companion skill for the artifact side: `radiance-model-feasibility-scout` (GGUF header census).
@@ -0,0 +1,44 @@
---
name: niri-all-monitor-screenshot-fix
description: "Fix flameshot screenshot failures on niri with 2+ outputs: confirm portal outputs.len()==1 census, retire flameshot binds for niri-native equivalents, absolute script paths (niri spawn-sh PATH lacks ~/.local/bin)"
---
# Niri all-monitor screenshot fix (flameshot retirement)
## Symptom fingerprints
- "Bunch of flameshot errors after a delay" when docked (2+ outputs); fine on one output.
- Journal census (ground truth): `journalctl --user -u niri --since today | grep -i screenshot`
- FAIL: `WARN niri::niri: error taking a screenshot: Condition failed: outputs.len() == 1 (2 vs 1)` — one press of flameshot full emits 3 WARNs (composited screen + per-output portal requests).
- SUCCESS: `DEBUG niri::niri: saving screenshot to ...` — per-file lines.
- Timeline: count WARNs before/after a config reload to attribute presses to config states.
## Root cause
Flameshot 14 on Wayland captures via xdg-desktop-portal → xdg-desktop-portal-gnome → niri's GNOME-Screenshot compatibility D-Bus API → `screenshot_all_outputs` → `ensure!(outputs.len() == 1)` (FIXME upstream). Upstream: niri#4313 (closed duplicate, unfixed in 26.04/8ed0da4), flameshot#4449.
- NOT just `full`: live-verified docked, `flameshot full` AND `flameshot screen` both hang ~15 s (exit 124 under timeout), write no file. Assume ALL flameshot capture modes dead with 2+ outputs.
## Fix: retire flameshot binds; niri-native replacements (blankpad layout)
| Chord | Action |
|---|---|
| Print / Ctrl+Shift+1 / Mod+Shift+S | `spawn-sh "/home/blank/.omp/scripts/screenshot-desk.sh"` — per-monitor PNGs |
| Alt+Print / Ctrl+Shift+2 | `screenshot-screen;` — focused monitor → screenshot-path + clipboard |
| Ctrl+Print / Ctrl+Shift+3 | `screenshot;` — interactive UI (Space saves, Esc cancels; single-output selection by design) |
- Misc: `screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"` at CONFIG ROOT (it is `config.screenshot_path`; a KDL `output { }` block REQUIRES a name arg — bare `output {}` is a parse error).
- KDL comments are `//`, never `#` (parses as identifiers → cryptic "identifiers cannot be used as arguments").
- niri 26.04 screenshot UI: "allows only single-output selections for now" (src/ui/screenshot_ui.rs) — no built-in all-monitor composite; script needed.
## All-monitor script (scripts/screenshot-desk.sh)
Per active output (`niri msg --json outputs | jq -r .[].name`): `niri msg action focus-monitor <m>` → sleep 0.15 → `niri msg action screenshot-screen --path "<abs>/… <connector>.png"` → restore prior focused monitor (`niri msg --json focused-output | jq -r .name`). No grim, no portal. `--path` must be absolute.
## THE SPAWN-SH PATH TRAP
niri's spawn-sh PATH = system dirs only; `~/.local/bin` ABSENT. Proof:
`tr '\0' '\n' < /proc/$(pgrep -x niri | head -1)/environ | grep ^PATH`
→ any bare user-script name in a keybind fails SILENTLY (no journal error, no file). Rule: keybinds use absolute script paths (`/home/blank/.omp/scripts/...`). Audit existing binds for bare names (desk.sh was dead too).
## Verifying a keybind end-to-end without a keyboard
Reproduce niri's child env exactly:
`env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl HOME=$HOME USER=$USER LOGNAME=$LOGNAME XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-1 NIRI_SOCKET=/run/user/$(id -u)/niri.$WAYLAND_DISPLAY.$(pgrep -x niri | head -1).sock sh -c "/abs/path/script"`
NIRI_SOCKET is NOT in niri's own environ (it is set on niri's children); socket path convention: `$XDG_RUNTIME_DIR/niri.<wl-display>.<niri-pid>.sock`.
## Deploy mechanics (blankpad command-center)
Live niri config = symlinks into the ~/.omp main checkout ("the checkout is the deployment"). Merge to main → niri auto-reloads (~1 s later journal `DEBUG niri_config: loaded config from ...`); verify that DEBUG line as the reload proof. Manual fallback: Mod+Shift+/. `niri msg` has NO keybinds subcommand — keybind proof = overlay (Mod+Shift+Escape) + journal census.
@@ -0,0 +1,43 @@
---
name: niri-config-edit-validate
description: "Locate, edit, validate, and commit niri compositor config on Frank's workstations: desk/display profiles, EDID-keyed outputs, layout, keybind grepping, dotfiles mirror, noctalia.kdl last-include color override, forced hot-reload."
---
# Editing niri config on Dank workstations
## Layout of the config
- Live config: `~/.config/niri/config.kdl` is a thin file of `include "./cfg/*.kdl"` lines. **Never conclude "not bound" from grepping only `config.kdl`** — keybinds live in `cfg/keybinds.kdl`, rules in `cfg/rules.kdl`, layout in `cfg/layout.kdl`, etc. Grep `~/.config/niri/cfg/` recursively.
- **Include order wins:** `config.kdl` ends with `include "noctalia.kdl"` — a hand-written accent overlay at `~/.config/niri/noctalia.kdl` that re-declares `layout { focus-ring/border/tab-indicator/insert-hint }` + `recent-windows.highlight` colors and BEATS every `cfg/layout.kdl` color edit (observed 2026-10-18: focus-ring color edits to layout.kdl appeared to "not reload" while this file silently repainted them red). Before changing any accent/outline color, grep the WHOLE `~/.config/niri/` tree (not just `cfg/`) for the property and fix the LAST includer. Tracked in repo at `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl`; live file is NOT a symlink — sync by `cp` and `diff` like the rest.
- niri notation: `Mod` = Super/Win key. Chords like `Mod+R { switch-preset-column-width; }`.
- `~/.config/niri` is a REAL directory, not a symlink; the version-controlled copies are a partial mirror under `~/.omp/dotfiles/zz-blankpad/niri/.config/niri/`. Trees can drift: diff them before/after edits. (Hardlink wiring is FRAGILE — editor/agent atomic-rename writes silently break hardlinks; verify live↔mirror by CONTENT diff, never by link trust.) On blankpad the `cfg/*.kdl` entries are symlinks into the repo (`dotfiles/niri/...` base + `zz-blankpad` overrides) — edit through them freely; `noctalia.kdl` and `config.kdl` are the plain-file exceptions.
## Desk/display profiles (monitor arrangement)
- Profiles: `cfg/desk-<n>-<name>.kdl` (e.g. `desk-1-home.kdl`, `desk-2-portable.kdl`); ACTIVE profile is symlinked as `cfg/display.kdl` — `readlink` it to know which. `scripts/desk.sh` manages switching; `desk.sh capture` prints connected outputs' EDID long names.
- Panels are keyed by **EDID long name** (`output "Lenovo Group Limited 0x40A9 Unknown"` = the laptop eDP-1 panel), not connector — connector names reshuffle between desks. Long-name blocks BEAT connector-keyed blocks when both match one output (legacy connector blocks like `output "eDP-1"` linger as duplicates; niri applies the EDID block's `position`).
- Position math: x,y = top-left of each output in the logical layout; right edge = x + mode width. To place panel A left of panel B: `A.x = B.x - A.width`; bottom-align: `A.y = B.y + (B.height - A.height)`.
- Verify: `niri validate` → hot-reload → `niri msg outputs | grep -iE 'Output |Position'` (prints `Logical position: x, y`).
## Schema gotchas (niri 26.04)
- `preset-column-widths` AND `preset-window-heights` are children of `layout { }` as SIBLING blocks — wiki wording misleads; nesting or `window-rule` placement fails `niri validate` with `expected one of proportion, fixed` / `unexpected node`.
- Preset list order = cycle order for `Mod+R` (`switch-preset-column-width`); `Mod+Shift+R` cycles backwards. WITH custom preset lists the cycle contains ONLY the configured presets — no implicit full/maximize tail (verified: [0.25, 0.5] cycles period 2). Maximize = separate `Mod+F` `maximize-column`. Built-in defaults without the node: 1/3, 1/2, 2/3.
- WITHOUT `preset-window-heights`, `switch-preset-window-height` FALLS BACK to the width presets — user sees width-fractions as heights; define the node explicitly to stop it.
- Ground truth for node placement: the binary's embedded template — `grep -a -B4 -A8 "preset-column-widths {" $(which niri)`.
- Window outlines: base geometry (`width`, `off`) lives in `cfg/layout.kdl` `focus-ring`/`border`; COLORS effectively live in `noctalia.kdl` (see include-order law). `border { off }` + focus-ring active = the visible outline.
## Workflow
1. Edit the live file under `~/.config/niri/cfg/` (symlinked = repo file; except `noctalia.kdl`, edit live then `cp` to mirror).
2. `niri validate` (exit 0 + "config is valid") — mandatory before done.
3. **Hot-reload is NOT reliable after harness edits** (atomic-rename writes are missed by niri's watcher; user then tests against the OLD config and reports "no change"). ALWAYS follow with `niri msg action load-config-file` (NOT `reload-config`).
4. Sync changed file into the mirror: `cp ~/.config/niri/cfg/<f>.kdl ~/.omp/dotfiles/zz-blankpad/niri/.config/niri/cfg/<f>.kdl` (repo root is `~/.omp`, branch `main`, remote = dankclaw Gitea `dank/command-center`; push there is pre-authorized). Verify by `diff`, not by cp success.
5. Untracked mirror files: `config.kdl`, `noctalia.kdl`, `animation.kdl`, `input.kdl`, `display.kdl` (symlink target) — flag drift, don't silently add; when a live-only file turns out to be load-bearing (like `noctalia.kdl` was), commit the first durable copy into `dotfiles/zz-blankpad/niri/.config/niri/`.
6. Mirror files often carry pre-existing uncommitted drift (desk.sh captures); a commit of your edit folds it in — say so in the report/commit body.
## Live-behavior verification without disturbing Frank's windows
- `niri msg --json <subcmd>` — the `--json` flag goes BEFORE the subcmd.
- Targeting differs per action: WINDOW-scoped actions take `--id <window-id>` (NOT `--window`) — e.g. `switch-preset-window-height --id N`. COLUMN-scoped actions (`switch-preset-column-width`, `set-column-width`, `maximize-column`) have NO id — they hit the FOCUSED column; only `set-column-width <px|±N%>` positional CHANGE, no flags.
- Spawned test apps do NOT reliably take focus, so blind `spawn + action` cycles resize whatever Frank is looking at. For column actions you must `focus-window --id N` first — and that STEALS focus; if Frank is actively testing chords, your probes and his keystrokes interleave and corrupt both. STOP probing, deliver config + validate, and have him press the chord himself.
- `niri msg --json focused-window` can report a different window than assumed (focus moves); for per-window measurement read `niri msg --json windows` and filter by `id`. Present height of proportion p with gaps g over work-area H renders ≈ p*(H+g) − g. NOTE (measured 2026-10-18): this build's `windows`/`focused-window` JSON returns `x/y/width/height = null` — pixel-sampling the outline color from screenshots is not viable; ask Frank to eyeball after `load-config-file` instead of burning turns on capture forensics.
- If Frank reports "still seeing old values" after a config edit: run `load-config-file` first, then verify empirically before believing anything else — and check `noctalia.kdl` include-order override BEFORE suspecting the reload.
## "What does chord X do?" triage
For "mod+<key>" questions, check in order: `~/.config/niri/cfg/keybinds.kdl` (compositor wins first), terminal configs (`~/.config/ghostty`, `foot`, `kitty`), tmux, then omp TUI chords (`omp://keybindings.md`; modifiers are ctrl/shift/alt/super only, no `Mod`; `Ctrl+R`=history search, `Alt+R`=app.retry).
@@ -0,0 +1,43 @@
---
name: niri-multi-monitor-screenshot-capture
description: Diagnose flameshot screenshot failures on niri with 2+ outputs (portal single-output limit) and capture all monitors via niri-native per-output actions
---
# Niri multi-monitor screenshot capture
Flameshot works on a single niri output, dies the moment a second output connects. Diagnose via compositor journal, capture full desk with niri-native per-output actions — not grim, not portal.
## Diagnose (one pass)
1. `journalctl --user -u niri --since today | grep -i screenshot`
- `WARN niri::niri: error taking a screenshot: Condition failed: outputs.len() == 1 (2 vs 1)` → flameshot's portal path (xdg-desktop-portal-gnome → niri GNOME-Screenshot D-Bus → `screenshot_all_outputs` `ensure!(outputs.len()==1)`). Upstream niri#4313, unfixed in 26.04. Symptoms: error bubbles appearing **after a delay** (portal round-trip), count = number of hotkey presses.
2. `niri msg outputs` — count active outputs. 1 output = flameshot fine; the bug is dock-state-dependent (portable works, docked breaks).
3. Per-mode status: `flameshot screen` (focused output) works docked; `flameshot gui` region works (mixed-res scaling bug = flameshot#4449); `flameshot full` dead on 2+.
## Capture all monitors (niri-native)
niri 26.04 has NO all-output capture: screenshot UI is single-output by design (`screenshot_ui.rs`: "allows only single-output selections for now"); D-Bus path hard-limited. Script pattern:
```bash
prev=$(niri msg --json focused-output | jq -r .name)
while IFS= read -r mon; do
[ "$mon" != "$prev" ] && niri msg action focus-monitor "$mon"
sleep 0.15 # let compositor render a frame after focus move
niri msg action screenshot-screen --path "$out/Screenshot from $ts $mon.png"
done < <(niri msg --json outputs | jq -r '.[].name')
# restore focus: niri msg action focus-monitor "$prev"
```
CLI facts (`niri msg action screenshot-screen --help`): `--path` must be ABSOLUTE; `-d/--write-to-disk` default true (also saves per `screenshot-path`); `-p/--show-pointer` default true.
## KDL gotchas (bit me twice; bats `niri validate` catches them)
- KDL line comments are `//`, NEVER `#` — `#` = parse storm "identifiers cannot be used as arguments".
- `screenshot-path` is a ROOT-level config node (`config.screenshot_path` in config.rs); do NOT nest it in `output {}` — root-level bare node validates and works; `output { ... }` without a name/match arg fails: "additional argument `name` is required".
- Root-level `prefer-no-csd` is a silent no-op (output-section property) — root unknown nodes only warn.
## Verify
- Run the script live; `file` each PNG (valid dimensions per monitor); `niri msg --json focused-output` equals pre-run monitor.
- `bats tests/` incl. niri-config assembly validate on every desk profile before commit.
- Deploy: symlink script into `~/.local/bin` (repo `scripts/` convention, like `desk.sh`); bind `spawn-sh "name"` in keybinds.kdl; niri hot-reloads config on file change.
@@ -0,0 +1,50 @@
---
name: noctalia-bar-layout-edit
description: "Edit/verify Noctalia v5 bar layout and clock widgets (multi-timezone instances, capsule groups); GUI→repo propagation chain, settings.toml symlink audit, and clobber-recovery via dangling stashes on Frank's dotfiles workstations"
---
# Noctalia v5 bar layout edit
Bar widget layout on Frank's noctalia v5 workstations is GUI-override state, not config.toml.
## Where
- Layout file: `~/.local/state/noctalia/settings.toml` → `[bar.<name>]` keys `start` / `center` / `end` (arrays of widget ids; `[widget.<id>]` subtables hold per-widget settings).
- On Frank's boxes this file MUST be a **symlink into the command-center repo** (e.g. `~/.omp/dotfiles/noctalia/.local/state/noctalia/settings.toml`) — edit the repo file, not the link path (atomic-rename writes through the link path would replace the symlink).
- Layer order: built-in defaults < `~/.config/noctalia/config.toml` (+`[include]`) < `settings.toml` (wins). Noctalia writes through symlinks and keeps them.
- Defaults when a key is absent: `start = ["launcher","wallpaper","workspaces"]`, `center = ["clock"]`.
- **Noctalia rewrites this file itself** (GUI actions + normalizes keys/format, e.g. strips `{:%...}` braces, reorders widget lists). Always re-read the exact lines immediately before editing; stale line anchors WILL drift between reads.
## GUI→repo propagation (checkout-is-deployment)
- `~/.config/noctalia/config.toml` is symlinked into the repo → GUI settings writes land in the repo working tree automatically.
- `~/.local/state/noctalia/settings.toml` can SILENTLY become a plain file (observed 2026-10-17; hardlink/symlink wiring decays) → GUI bar-layout changes then diverge from the repo with no signal. AUDIT: `readlink ~/.local/state/noctalia/settings.toml` must print the repo path; if it's a plain file and repo copy is content-identical, re-arm: `ln -sfn <repo-path> <live-path>` + `noctalia config validate`.
- Landing in the working tree ≠ propagated: other boxes only see it after commit + push to Gitea main. `live-sync.path` (PR #15; DirectoryNotEmpty on `dotfiles/`) commits dotfiles-tier writes within ~2 s once deployed — until it is active on a box, the hourly heartbeat leaves up to 59 min of exposure, and on 2026-10-18 exactly that window let a trunk-sync stash destroy months of GUI layout work. Until verified active (`systemctl --user status live-sync.path`), commit GUI layout edits the same session yourself. Noctalia's runtime churn (wallpaper path, nightlight, panel positions) rides along; normal.
- niri has NO settings GUI; niri "GUI changes" don't exist, and `niri msg output ...` runtime overrides are not persisted. niri cfg mirror↔live links also decay (all inodes decoupled once) → audit by CONTENT: `diff` every `~/.config/niri/cfg/*.kdl` against its `dotfiles/zz-blankpad/niri/...` mirror, never trust links.
## If the bar got wiped (recovery)
- The working-tree file is often the ONLY copy of GUI work; a `git checkout`/stash/merge clobber is survivable because git objects outlive stash entries: `git fsck --lost-found`, inspect `git log -1 --format='%ci %s' <dangling>` (newest `WIP on` stash commits hold the last good settings.toml), dump with `git show <c>:dotfiles/noctalia/.local/state/noctalia/settings.toml`.
- Before restoring: back up the clobbered file to `.AGENTTRASH/`; validate the candidate in a sandbox (`XDG_CONFIG_HOME=… XDG_STATE_HOME=… noctalia config validate` — plugin-widget "unrecognized type" warnings there are offline-CLI blindness, not blockers); restore via `cp` onto the repo path (cp writes through the symlink; `mv` would replace it); prove visually with `noctalia msg screenshot-fullscreen`; commit immediately.
- Cross-check the recovered layout against Frank's old screenshots in `~/Pictures/screenshot_*.png` (crop top ~7% with magick) — they are ground truth for what the bar should look like.
## Apply/verify loop (no restart, no reload IPC)
1. Edit the repo `settings.toml`.
2. `noctalia config validate` (checks merged layers; also `noctalia config export [full] | yq -p toml` to inspect effective values).
3. Both layers are file-watched → hot-reload. Prove visually: `noctalia msg screenshot-fullscreen` saves a PNG to `~/Pictures`; read the image and inspect the bar.
4. **Screenshot race:** the `ok` return precedes the PNG write. Never chain `noctalia msg screenshot-fullscreen && magick "$(ls -t ~/Pictures/screenshot_*.png | head -1)" ...` in one command — you crop the PREVIOUS capture. List `ls -t` in a separate later call and crop the named file.
5. Crop for reading: `magick <png> -crop 100%x8%+0+0 +repage /tmp/strip.png`, then read the strip; cross-check clocks against `date -u` / `date`.
## Clock widgets
- `[widget.clock]` settings: `format`, `vertical_format`, `tooltip_format`, `timezone` (IANA id, e.g. `"UTC"`, `""` = system local). Tokens are strftime-compatible (`%H` 24h, `%I:%M %p` 12h, `%F` ISO date); `{:%...}` brace fields optional; `\n` = second line. Docs: docs.noctalia.dev → bar/widgets/clock + configuration/date-format-tokens.
- **One timezone per widget; multiple instances allowed:** any `[widget.<id>]` needs `type = "clock"` for custom ids (bare `clock` id infers it). Each instance converts independently — e.g. local 12h + `timezone = "UTC"` 24h side by side.
- Placement: add ids to `[bar.<name>]` start/center/end, or to a capsule group: `[[bar.default.capsule_group]]` with `id = "g1"` + `members = [...]`, referenced from `start` as `"group:g1"` (member order = left-to-right inside the capsule).
- Local date + local 12h fit in ONE widget (same tz); the UTC 24h clock must be a second widget.
- Expect the date widget (local) and UTC clock to disagree on the day after ~5 PM Seattle — that's correct UTC, not a bug; offer `%F` inside the UTC widget if it irks.
## Taskbar recipes
- Move window indicator beside workspaces: put `"taskbar"` in `start` after `"workspaces"`.
- Combine workspaces+windows into ONE widget: `[widget.taskbar] group_by_workspace = true` — per-workspace capsules with disc labels (focused/occupied/empty/urgent colors); `workspace_group_content = "icons"|"count"|"dots"` (count/dots: left-click activates the workspace); `hide_empty_workspaces`, `show_workspace_label`, `minimal`, `workspace_label_placement = corner|centered|inside`. Requires compositor per-window workspace assignment (niri: yes; LabWC: no → stays flat).
- The `workspaces` widget itself has no window display; styles are regular/minimal/focus_hint only.
## Law
- Full widget/setting tables: https://docs.noctalia.dev/noctalia/bar/ and .../bar/widgets/<name>/ . Commit bar changes atomically to the command-center repo (checkout is the deployment).
- **Ask before applying.** Frank asks "how do I X" questions about the live bar; answer with the exact snippet + file, apply ONLY on explicit go. A settings.toml edit is a live-deployment change, not a demo. (Exception: restoring Frank's own layout after data loss, on his ask.)
- Git `M` on settings.toml usually carries noctalia's own runtime churn (wallpaper path, nightlight, panel positions) alongside your edit — mention it when committing.
@@ -0,0 +1,26 @@
---
name: omp-bundle-claim-audit
description: "Verify or discover omp TUI behavior facts from the minified dist/cli.js bundle, with live niri-screenshot matrix confirmation; use when auditing docs claims or answering \"how does omp actually do X\"."
---
# omp bundle claim audit
Ground omp behavior claims in `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js` (minified single line), not guesswork.
## Extraction
- `grep -oE` too weak for long windows; use python: read whole file, `re.search(r'ANCHOR.{0,900}', s)` windows around stable anchors.
- Reliable anchors: settings ids (`id:"statusLine.contextLine"`), renderer ids (`id:"path",render\(e\)\{`), registry arrays (`l9e=\[`, `Uxe=\{` preset table), descriptions (`"Label",description:"([^"]+)"`).
- Minifier renames symbols per release — anchor on string literals, never on `#Xt`/`zG` names.
- Enum values live in a var: find `id:"KEY"` → `values:VAR` → `re.search(r'VAR=\[[^\]]+\]')`.
- Some segments are factory-generated (`VDt("token_in",...)`) — literal id scans miss them; check the catalog array (`l9e`) and preset tables for referenced ids.
- Beware gallery/preview code faking production behavior (e.g. `N4r` synthesizes a status bar for the settings gallery) — confirm the window you cite is the production render path.
## Live confirmation (only where pixels decide)
1. Edit `~/.omp/agent/config.yml` (file-watched, repaints without restart).
2. `sleep 2 && niri msg action screenshot-screen --path /tmp/x.png --show-pointer false; sleep 1.5` (flag is `--path`, NOT `-o`; the write lands slightly late — sleep before PIL).
3. Crop + 1.3x LANCZOS via PIL, `read` the crop.
- Multi-window screens make bar crops ambiguous: prefer settling the question in code (the layout gate `layout!=="box"&&layout!=="band"` style booleans) over pixel forensics; scan rule rows by dominant-color spans only for single-color questions.
- ALWAYS restore config edits afterwards (track exact line anchors; the edit tool demands a matching snapshot header).
## Audit output
Correct false claims in-place, keep an "Audit log" section listing each ~~claim~~ → truth with code/live evidence; mirror corrections into `omp-docs/log.md`.
@@ -0,0 +1,27 @@
---
name: omp-context-file-discovery
description: Where omp loads context/instruction files from (and why ancestor-directory AGENTS.md never loads) — the discovery map for scoping a rule to a set of projects; includes the before_agent_start cwd-gate pattern and quote-probe verification.
---
# Scoping omp rules: context-file discovery map
## When
Asked to make a rule apply "everywhere", "all repos", or "only projects under <dir>" in the omp harness.
## Discovery facts (bundle-verified 2026-10-18, omp v18.8.7, dist/cli.js)
- Project context loaders are **first-hit at-or-below repo-root**: AGENTS.md walk-up stops with `if(dir===repoRoot)break`; the `.omp/`-dir provider (`EGs`) returns the single nearest dir containing `.omp` (skips home); CLAUDE.md provider loads one user file + one project file. **Consequence: an AGENTS.md placed in an ancestor directory like `~/Projects/` is never loaded by descendant repo sessions.** No ancestor-scope mechanism exists.
- User/global slots: `~/.omp/agent/AGENTS.md` (user-level context, if present), `~/.omp/agent/APPEND_SYSTEM.md` (system-prompt append, project variant checked first, then user; broadcasts to ALL sessions — no path scoping), rules dirs `~/.agents|~/.agent` + `<ancestor>/.agents|/.agent` rules via walk-up (again repo-root bounded). Extensions load from `<settings-dir>/extensions/` — user-level = `~/.omp/agent/extensions/`.
- Settings ids to grep for: `statusLine.*`, `composer.*`, `instructions` (= Copilot-style file-glob instructions package, not a path list).
## The only cwd-scoped mechanism
`before_agent_start` extension handler: event carries `{prompt, images, systemPrompt}` (string | string[]); return `{systemPrompt}` to replace (handlers chain; undefined = untouched). Gate on `ctx.cwd.startsWith(target + "/")`. Constant appended text keeps prompt caching intact; subagents inherit the session cwd, so they get the rule too. Registering extensions under a git-tracked dir that omp also reads (e.g. command-center `agent/extensions/`) propagates them fleet-wide via git pull.
## Verification recipe (quote probe)
Non-interactive one-shot from each side of the boundary, cheap model:
`cd <in-scope-dir> && omp --model <alias> "Look at your system prompt. If it contains a heading starting with '<marker>', reply with exactly the text of its bullet about <x>. Otherwise reply exactly MISSING."`
`--model @role` aliases do NOT resolve in one-shot; use a model alias like `frank-zealot`. Expect the verbatim bullet in scope, `MISSING` out of scope.
## Traps
- A rejected implementation still needs its live copy unshipped (move to `.AGENTTRASH/`) — extension files in `agent/extensions/` take effect for new sessions immediately, merged or not.
- Amend the changelog entry of the abandoned route to state closed-unmerged + the durable findings, so future agents don't resurrect it.
- Prefer direct per-repo AGENTS.md edits when the operator wants auditability over mechanism (command-center 2026-10-18: extension route closed unmerged in favor of a PR in each target repo).
@@ -0,0 +1,40 @@
---
name: omp-cwd-scoped-rules
description: "Deliver a rule to omp sessions only under a directory subtree (e.g. ~/Projects/) — context-file loaders can't; use a before_agent_start extension + quote-probe verification."
---
# Cwd-scoped rules for omp sessions
## Why context files fail for subtree scoping
Verified against `@oh-my-pi/pi-coding-agent/dist/cli.js`: every context provider resolves **at or below the repo root** — the project walker breaks at `repoRoot`, `.omp/`-dir and CLAUDE.md/AGENTS.md providers load exactly one project file (first hit). A file in an ancestor dir like `~/Projects/AGENTS.md` is NEVER loaded by descendant repos. `agent/APPEND_SYSTEM.md` (user level, `gs()`=`Nn.agentDir`) loads for ALL sessions everywhere — broadcast, not scoped.
## Mechanism: before_agent_start extension
File: `~/.omp/agent/extensions/<name>.js` (user-level load path is `<settings-dir>/extensions/`; in the command-center repo `agent/extensions/` is tracked and rides git pull to the fleet).
```js
export default function (pi) {
const ROOT = "/home/blank/Projects/";
const RULES = "\n\n# <heading> ..."; // constant text -> prompt cache unaffected
pi.on("before_agent_start", async (event, ctx) => {
const cwd = (ctx && ctx.cwd) || "";
if (cwd !== ROOT.slice(0, -1) && !cwd.startsWith(ROOT)) return; // undefined = untouched
const base = event.systemPrompt; // string | string[]
return { systemPrompt: Array.isArray(base) ? [...base, RULES] : (base || "") + RULES };
});
}
```
Contract (bundle-verified): handler payload `{prompt, images, systemPrompt}`; returning `{systemPrompt}` replaces the base; handlers chain; fires for subagent sessions too (shared cwd) — usually desired.
## Verification recipe (quote-the-prompt probe)
No print flag; positional arg = one-shot. Role aliases (`--model @sonic`) do NOT resolve in one-shot — use a model id.
```bash
P="If your system prompt contains a heading starting with '# <heading>', quote its <X> bullet; else reply exactly MISSING."
cd /home/blank/Projects/<repo> && omp --model <id> "$P" # expect verbatim bullet
cd /tmp && omp --model <id> "$P" # expect MISSING
```
Run both positive and negative probes; stderr swallows fast failures — keep 2>/dev/null off the first try.
## Gotchas
- Declare precedence in the injected text if a repo's own AGENTS.md contradicts it (models see both).
- A running session does not hot-load new extensions — only new sessions get them.
- `~/.omp/agent/APPEND_SYSTEM.md` precedence: project-level file first, then user-level (first hit wins).
@@ -0,0 +1,24 @@
---
name: omp-harness-window-fit-audit
description: "Audit an omp harness model entry against an engine's prompt+max_tokens fit check; explains and fixes 400 \"max_tokens does not fit\" at 60-85 % of the declared window."
---
Use when a local endpoint returns 400 `max_tokens N does not fit: prompt is P tokens and the context is C, leaving room for R` on omp, typically at 65-85 % of the declared window.
## The three numbers that must agree
1. Engine hard ceiling: prompt_tokens + max_tokens ≤ window (halogen `HALOGEN_CTX`/slot, llama.cpp per-slot `ctx_size/parallel`, radiance `--max-model-len`). A 400, never truncation.
2. Harness clamp: `max_tokens = min(maxTokens, contextWindow − est − 4096)`, est = chars/4 over the conversation+tools. The dump in `~/.omp/logs/http-400-requests/*.json` proves it: est = contextWindow − 4096 − max_completion_tokens.
3. Compaction fires only at `compaction.thresholdPercent` × contextWindow (config.yml; ignore stale reserveTokens/70 % comments).
## The trap
chars/4 under-counts the rendered chat template by ~3 % on code/agent traffic. If contextWindow == engine ceiling, requests with est > 4096/0.03 ≈ 137k are guaranteed 400s below the compaction threshold (65-85 % of a 200k window). maxTokens ≤ 16,384 hides this; 45,056 exposes it.
## Fix procedure
1. Compute required slack: maxTokens + 0.10 × contextWindow (robust to est error).
2. Raise the engine ceiling to ≥ contextWindow + slack (native window is the natural choice: Qwen3.8-Flash-Next 262,144). Halogen: pool must stay slots × CTX (`HALOGEN_KV_POOL_POSITIONS`, ~28 KiB/position); check memory ledger.
3. Keep harness contextWindow below the ceiling by ≥ slack (217,088 = 212×1024 for a 262,144 ceiling with maxTokens 45,056).
4. Verify compaction itself fits its target (remoteEndpoint slot ≥ thresholdPercent × contextWindow − keepRecentTokens(20k) + overhead).
5. Update env + seam test + models.yml + AGENTS.md together; a single-file change re-creates the drift.
## Verification
Probe a prompt at ≥ 180k tokens with max_tokens = the clamped value; expect completion, not 400.
@@ -0,0 +1,25 @@
---
name: omp-keybinding-inspection
description: "Find what a key chord does in omp: doc table, live bundle defaultKeys inspection, modifier names, remap file"
---
Answer "what does chord X do in omp" with evidence from docs + installed bundle, not memory.
## Steps
1. Read `omp://keybindings.md` — common action IDs and defaults (`Ctrl+R` history search, `Alt+R`/`F5` retry, `Alt+A` agent hub, ...). Docs are not a complete inventory.
2. Locate the install: `readlink -f $(which omp)` → usually `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js` (bundled).
3. Extract live default chords from the bundle (authoritative for installed version):
```bash
cd <dist dir> && grep -o '"app[^"]*":{defaultKeys:[^}]*' cli.js
```
Contextual duplicates exist: same chord can map to different actions in different views (e.g. `ctrl+r` = `app.history.search` in composer, `app.session.rename` in resume picker).
4. Modifier names: `pi-tui/dist/types/keys.d.ts` → `ModifierName = "ctrl" | "shift" | "alt" | "super"`. "mod"/Super binds as `super+`; only shipped super chords are `super+alt+backspace|delete|d`. Most Linux terminals swallow plain Super chords (needs kitty keyboard protocol / CSI u: kitty, foot forward them).
5. Remaps live in `~/.omp/agent/keybindings.yml` (NOT config.yml): `action.id: chord` or array; `[]` disables. Profile-aware: default-profile file loads first, active profile overrides per action.
6. In-session live reference: `/hotkeys`.
## Pitfalls
- `grep pi-tui/dist/*.js` fails (no top-level js; types only under `dist/types/`).
- Chord notation is case-insensitive, UI-style (`Ctrl+P`, `Alt+Shift+P`).
- Old unqualified action names auto-migrate; new configs use namespaced ids (`app.*`, `tui.editor.*`).
@@ -0,0 +1,62 @@
---
name: omp-model-id-rename-cutover
description: Rename an OMP fleet model id in models.yml and prove every reference and endpoint still resolves before commit/push.
---
# OMP model id rename cutover
Renaming a model id in `~/.omp/agent/models.yml` touches three layers: the registry, every
reference in `agent/config.yml`, and the wire contract with the serving endpoint.
## 1. Find every reference
Provider names are labels; ids are referenced from `config.yml` (`modelRoles.*`,
`task.agentModelOverrides.*`, `compaction.modes`), from other models' `compactionModel`,
and from docs/memory. Grep the tracked set:
```bash
git ls-files -z | xargs -0 grep -ln 'OLD_ID'
```
`agent/memories/**` and `omp-docs/log.md` are historical records: leave them alone.
`agent/models.yml` comments that name the *service* (e.g. `frank-large.env`) stay — the
systemd unit names do not change with the harness id.
## 2. Prove reference integrity (throwaway, do not commit)
Parse both YAMLs and cross-check. The provider label contains a space and parentheses
(`llamacpp (dankserver)`), so use an explicit pattern; a `split(/\s+/)` yields false
unresolved hits like `(dankclaw)/frank-dragoon`.
```python
pat = re.compile(r"llamacpp \([^)]+\)/[A-Za-z0-9._-]+")
known = {f"{n}/{m['id']}" for n, pv in models["providers"].items() for m in pv["models"]}
# walk cfg.modelRoles / cfg.task / compactionModel, flag tokens not in known
```
Expect: zero unresolved refs, and every `compactionModel` present in `known`.
## 3. Prove the endpoint accepts the new id
`/v1/models` reports the SERVED name (radiance echoes the request id, halogen and
llama.cpp echo their own). Then send a 4-token completion with the NEW id against each
provider `baseUrl`:
```bash
curl -s http://HOST:PORT/v1/chat/completions -H 'content-type: application/json' \
-d '{"model":"NEW_ID","messages":[{"role":"user","content":"say OK"}],"max_tokens":4}'
```
200 + `reasoning_content` = the rename is harness-side only. All three engines on the
Dank fleet ignore the request `model` field.
## 4. Fit check against the served window
Read the served window (`/v1/models` → `context_length` / `meta.n_ctx`) and confirm the
declared `contextWindow + maxTokens` fits the slot/pool the engine admits against.
Compaction fires at `compaction.thresholdPercent` of `contextWindow`; the clamp is
`max_tokens = min(maxTokens, contextWindow − est − 4096)` and the chars/4 estimate
under-runs ~3 % on code.
## 5. Commit shape
One commit for `models.yml` (ids + windows + sampling), one for `config.yml` (role map,
compaction endpoint). Note in the message which wire names stay unchanged, and record
the rename in `omp-docs/log.md` (newest section first). Run `scripts/kb-lint.sh` before
committing docs. The `.githooks/pre-commit` secret gate runs automatically; never
`--no-verify`.
@@ -0,0 +1,71 @@
---
name: omp-models-yml-runner-api-placement
description: Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml without disabling every custom provider
---
# Scope
`~/.omp/agent/models.yml` (or `models.yaml`) on a workstation: adding a **runner**
model row (TTS / image / embedding / rerank / video) alongside chat providers.
# The landmine
Provider-level `api` accepts **only chat/judge transports**:
`openai-completions`, `openai-responses`, `openai-codex-responses`,
`azure-openai-responses`, `anthropic-messages`, `bedrock-converse-stream`,
`google-generative-ai`, `google-gemini-cli`, `google-vertex`,
`openrouter-decisions`, `typesafe`.
Naming a runner API (`openai-speech`, `openai-images`, `openai-embeddings`, …)
at **provider** level fails whole-file validation, and the failure mode is not
scoped to your provider:
```
Warning: models.yml validation failed — custom providers disabled
Failed to load config file models, Schema error: providers.<id>.api: must be "openai-completions", …
```
Every custom provider disappears from the registry (fleet chat endpoints stop
resolving) and `omp models` lists only built-in locals. Measured 2026-10-18.
# Correct shape
Runner api goes on the **model entry only**; `auth: none` satisfies the
credential requirement; omit `contextWindow`/`maxTokens` (the positivity check
fires only when they are provided — `omp models` still prints filled defaults,
which the runner transport never reads).
```yaml
"frank-tts (dankclaw)":
baseUrl: http://192.168.0.143:7772/v1 # no `api:` here
auth: none
models:
- id: frank-tts
name: "frank-tts (dankclaw) — cloned voices"
api: openai-speech # runner api on the model
input: [text]
```
Roles live in `config.yml` (`modelRoles`), not in `models.yml`. Kind roles
(`image`, `web`, `speech`, `dictation`, `judge`) take no `:thinking` suffix.
# Verify after every edit (never assume the file loaded)
```bash
omp models --kind tts --no-extensions # the new row must appear
omp models --no-extensions | grep -E '<each fleet chat model>' # and still be listed
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.
# Speech transport contract (`openai-speech`)
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
only when explicitly set; `sample_rate`/`bit_rate` **throw**
(`openai-speech does not support sampleRate`). Codec comes from the
`output_path` suffix; 15 000-character cap. An unknown `voice` name is a server
error, not a fallback — and `auth: none` makes a dead endpoint read as
*available*, so point `modelRoles.speech` at such a provider only after it has
answered a real synthesis.
@@ -0,0 +1,67 @@
---
name: omp-system-prompt-retrieval
description: "Retrieve the exact system prompt an omp session is running: /dump while live, RPC get_state re-render, and the shipped template/override source paths."
---
# Retrieve omp's live system prompt
Three routes, in fidelity order. Verified on omp 18.4.10 (linux/x64).
## 1. `/dump` — the only exact capture of a LIVE session
TUI slash command: "Copy session transcript to clipboard (and write LLM request JSON to tmp)".
The request JSON is the wire-true payload (system blocks + tool schemas). Path is printed in
the `/dump` output; then:
```bash
jq -r '.system[] | strings // .system' <file> # anthropic shape
jq -r '.messages[] | select(.role=="system") | .content' <file> # openai shape
```
**The prompt is never persisted in the session JSONL.** Session entries are only
`title, session, model_change, thinking_level_change, message, custom(tool_execution_start),
model_usage`. So a past session's prompt is unrecoverable from disk — capture live, or re-render.
## 2. RPC `get_state` — re-render without a model call
`omp --mode rpc` answers `{"type":"get_state"}` with `data.systemPrompt: [blocks]` and
`data.dumpTools` (spec: `omp://rpc.md` "get_state payload"). Drive it over stdio:
```python
cmds = json.dumps({"id":"p1","type":"negotiate_protocol","protocolVersion":2}) + "\n" \
+ json.dumps({"id":"s1","type":"get_state"}) + "\n"
p = subprocess.run(["omp","--mode","rpc","--no-session","--no-pty"],
cwd=TARGET_CWD, input=cmds, capture_output=True, text=True, timeout=280)
```
Closing stdin shuts it down cleanly (exit 0), no model call, no session file written.
- Parse stdout line-by-line. Protocol v2 wraps logical frames >1 MiB in `rpc_chunk` frames:
key on `chunkId`, require all `count` indices, `base64.b64decode` and concatenate in `index`
order, then `json.loads` — otherwise a large prompt is silently lost.
- Shape measured on a dotfiles repo cwd: 2 blocks, ~45 KB / ~391 lines —
block 0 = static instruction block (RFC 2119 line, `§ Role`, Engineering/Tone/Escalation,
tool inventory, `xd://` catalog), block 1 = footer opening `<project-context>` +
`<critical>` + the cwd's `AGENTS.md`.
- `--no-extensions` changes nothing in the blocks; tool count rides in `dumpTools`, not the prompt.
- `cwd` matters: the footer is cwd-derived (workstation block, context files, skills, rules).
## 3. Sources (edit here, not in a dump)
| Layer | Path |
|---|---|
| Default instruction template (Handlebars, block 0) | `<pkg>/src/prompts/system/system-prompt.md` |
| Footer | `<pkg>/src/prompts/system/project-prompt.md` |
| Plain-custom template | `<pkg>/src/prompts/system/custom-system-prompt.md` |
| Repo rules (into footer) | the cwd's `AGENTS.md` |
| Overrides | `--system-prompt`, `--system-prompt-template <path>`, `--append-system-prompt`; discovered `SYSTEM.md`, `SYSTEM_TEMPLATE.md`, `APPEND_SYSTEM.md`, `PERSONALITY.md`, `TITLE_SYSTEM.md` |
`<pkg>` = `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent` (resolve via
`readlink -f $(command -v omp)`). Docs: `omp://system-prompt-customization.md`.
Precedence: explicit CLI flag > project `SYSTEM.md` > project `SYSTEM_TEMPLATE.md` >
user `SYSTEM.md` > user `SYSTEM_TEMPLATE.md`. A same-scope literal always beats a discovered
template, so a `SYSTEM_TEMPLATE.md` is inert while a `SYSTEM.md` sits beside it.
## Trap
NEVER paste a rendered `/dump` into `SYSTEM_TEMPLATE.md`: a render freezes settings, tool
catalogs, and mounted-device data. Copy the Handlebars **source** and diff against the shipped
path after every omp upgrade. Plain files (`SYSTEM.md`, `APPEND_SYSTEM.md`) are inserted
verbatim — `{{cwd}}` and friends stay literal there; only the template route compiles Handlebars.
@@ -0,0 +1,23 @@
---
name: omp-tui-live-visual-verify
description: "Verify omp TUI config changes (status line, composer, theme) live: edit watched config, niri screenshot, PIL crop, restore — with the flag and race pitfalls."
---
# Live visual verification of omp TUI config
## When
Changing `~/.omp/agent/config.yml` (statusLine.*, composer.*, theme) on a Frank workstation and needing PROOF of how it renders — not inference from the bundle. The status-line/composer knobs are file-watched: edits repaint the RUNNING session; no restart.
## Loop (per trial)
1. Read the live config line(s) first; edit with `edit` (never batch trials — one state per screenshot).
2. Capture: `sleep 2 && niri msg action screenshot-screen --path /tmp/x.png --show-pointer false` — flag is `--path` (`-o` is invalid); the write lands asynchronously, so `sleep 1.5` before touching the file.
3. Crop+scale with PIL to the bar/composer region (known good crop for blankpad right monitor: `(2267,1240,3440,1440)`, 1.3x LANCZOS), then `read` the PNG absolute path.
4. For pixel facts (rule colors, meter lengths) don't eyeball alone: scan rows with `Image.load()` counting dominant colors; sample every 2px and lower thresholds before concluding "nothing found".
5. After the matrix, RESTORE the original values and re-screenshot the baseline.
## Pitfalls
- Read a downscaled FULL-screen shot first: Frank's screens carry multiple terminals + a browser; a fixed crop can silently capture a different window's bars (happened during the contextLine audit — two omp windows on one screen).
- Session-accent rules/borders share colors with meters; color presence alone proves nothing. Compare states against each other, and settle ambiguous pixels from the bundle code path (grep `dist/cli.js` for the render function).
- Live config drifts (other sessions own files there): never `git add -A` the main checkout to promote a trial; commit from a separate `git worktree add` off the target ref.
- `read` the absolute `/tmp/...` path for screenshots.
- Multi-value enums: one screenshot per value; identical renders (e.g. claude shape `contextLine: off` vs `embedded`) are themselves a finding — record them as such.
@@ -0,0 +1,49 @@
---
name: omp-worktree-path-gotchas
description: "Use in ~/.omp (or any multi-worktree repo) when an edit lands in the wrong checkout, git checkout is policy-refused, or an omp session id is needed — relative-path tool resolution, shared-worktree branch law, session-id lookup, edit auto-prefix trap."
---
# Multi-worktree gotchas in ~/.omp-style repos
Measured 2026-10-18 building PR #12/#14 in `dank/command-center` worktrees.
## 1. Relative paths resolve to the SESSION worktree — always edit by absolute path
`edit`/`read`/`write` with a relative `path` resolve against the session CWD
(the harness worktree you were spawned in), NEVER the worktree you `cd`'d into
via bash. Identical file content in sibling worktrees yields identical
`[path#tag]` hashes, so an edit can "succeed" against the WRONG checkout with
no warning; only a rejected stale-hash edit reveals it.
- When working in a second worktree, ALL file-tool paths: absolute.
- Symptom seen: edit rejected "hash #X is not from this session" while the
bash-side `git status` showed the file clean — two different files, two
worktrees. Recovery: revert the stray edit (CUT the inserted lines) in the
wrong tree, re-read the right tree, redo with absolute header.
## 2. Branching: shared worktrees refuse git checkout
Workspace policy blocks `git checkout` (branch switches AND `--` discards) in
harness-managed worktrees — concurrent agents depend on them. To start a
branch off fresh trunk:
```bash
git -C ~/.omp worktree add -q -b <type>/<issue#>-<slug> ~/.omp/wt/<name> origin/main
```
Never force past the refusal; the new-worktree route is strictly safer.
Custom worktrees not matching `wt/wt-*` are invisible to the live-sync reaper —
prune them by hand after merge (`git worktree remove`, `git branch -d`).
## 3. omp session id for attribution footers
Fleet law requires `session:` in issue/PR bodies. Authoritative source:
`~/.omp/agent/sessions/<cwd-slug>/<timestamp>_<UUID>.jsonl` — the `<UUID>` is
the session id. Pick the newest file whose first `{"type":"title"}` line names
this conversation to prove the id is yours. The `wt/<ts>-<hash>` dir suffix is
NOT the session id (it persists across sessions per cwd).
## 4. edit-tool auto-prefix trap (recurring)
A body row written bare (intending it as the "original line" anchor or as
context) is AUTO-PREFIXED with `+` and INSERTED — leaving the old line plus a
duplicate. `PUT n.=n:` therefore behaves like insert, not replace, whenever
the anchor row lacks an explicit marker. After any `PUT` where the response
warns "Auto-prefixed bare body row(s)", immediately `CUT` the stale line number
shown in the response (descending order for multiple CUTs).
@@ -0,0 +1,27 @@
---
name: one-decision-per-ask-question
description: "Use when putting decisions to Frank via the ask tool or a grilling round — exactly one work item per question, rounds may group many single-item questions."
---
# One Decision, One Work Item, One Question
Friction-earned rule (corrected twice on 2026-10-18, during the voice/TTS planning).
## Rules
- Each question addresses **exactly one work item**. A round may contain many questions covering different work items; a question may not.
- Never merge two artefacts into one option because they share an implementation dependency. Sharing an endpoint, a service, or a build step is NOT grounds for one question. (`frank-tts` serving both a podcast and an audiobook generator produced exactly this bug.)
- Never offer something Frank did not ask for inside another question's option text. If an idea is mine, it gets its own question or stays out of the plan; it never rides along as "…and also X" in an option label or preview.
- Do not derive option wording from my own scaffolding (todo text, my table row labels, my runbook headings). Derive it from his stated goal.
- Mark the recommended option; a bare "yes" must accept it.
- Facts are mine to fetch (subagent/scout), never a question for him. Only decisions go to him.
- A question whose answer depends on an unsettled fact or another open question belongs to a **later round**, not this one.
## Anti-patterns that triggered this
- One option: "Audiobook/podcast factory — open-notebook + EPUB→narration builder." Two products, one shared dependency, zero requests for the second one. He answered "I never asked for audiobook generation."
- One question: "Is the deliverable the podcast, or the platform (harness voice + OpenWebUI + alerts)?" Four work items in one question. Rejected: "You're lumping multiple asks into one question. Ask me per item."
## Check before sending
For each question: can I name the single artefact it decides? If two names come to mind, split it.
@@ -0,0 +1,30 @@
---
name: openwebui-picker-acl-repair
description: "Add, enable, or repair a model in the Dankclaw OpenWebUI 0.6 picker: connection enable + model row + access_grant trio, verified via /api/models as admin and role=user."
---
# OpenWebUI 0.6 picker: add / enable / repair a model (Dankclaw)
Symptoms: non-admin sees "No results found"/"Model not found" while admin sees the model (repair); endpoint live, model_ids correct, model absent from picker (add).
## Law — a picker entry needs ALL legs, keyed by the EXACT upstream served id
1. openai.api_configs (config table, JSON map by idx): that connection has enable: true and model_ids containing the upstream id. Non-empty model_ids = no upstream fetch (llama.cpp's Ollama-shaped /v1/models is invisible to discovery; halogen/radiance are OpenAI-shaped, model_ids stays authoritative).
2. model table row: id == upstream id (alias rows with base_model_id set render in the picker but chat fails "Model not found"), base_model_id NULL, is_active 1, user_id = admin uid, params {"context":{"length": PER-SLOT window}}, meta {"description": "..."}.
3. access_grant row: resource_type=model, resource_id=<id>, principal_type=user, principal_id='*', permission=read. Without it only admins see the model (get_filtered_models drops info-less rows for role=user).
URLs: on-host endpoints via host.containers.internal:<port> (bridge cannot hairpin the host LAN IP); other hosts via LAN IP.
## Steps
1. Snapshot: cp -a webui.db webui.db.bak-<date><letter>.
2. Apply with host python3 over ssh (no sqlite3 CLI on Dankclaw) on /home/dank/Containers/open-webui/data/webui.db: read-modify-write the whole openai.api_configs JSON value; insert model row (epoch-ms created_at/updated_at); insert access_grant (uuid4 id).
3. Restart: systemctl --user restart open-webui.service (rootless quadlet) to drop the cached model list. Restart kills browser JWTs: expect one SSO re-round-trip per tab; kill stale tabs first so OAuth state cookies don't race.
4. Verify (mint tokens in the container; create_token is in open_webui.utils.auth; export WEBUI_SECRET_KEY from /app/backend/.webui_secret_key):
- GET /api/models (key data) lists the id as admin AND as a role=user token (e.g. qa-e2e-admin uid). This is the picker truth.
- POST /api/chat/completions, prompt "Reply with exactly one word: pong", max_tokens 1500 (thinking models give empty content + finish_reason length on small budgets).
- NEVER gate on GET /api/v1/models/list (key items): on 0.6.x it lists workspace models only, [] for connection-backed models (measured 2026-10-09).
## Related knobs
- User API keys need user.permissions JSON features.api_keys=true in config (default false).
- SSO signups land role pending (ui.default_user_role=pending): UPDATE "user" SET role='user' WHERE email=... (table user, singular, no active column); row exists only after first login.
- Anubis gates ai.franksdank.cloud for headless browsers; drive direct :3005.
- The trio is DB-only, lost on a fresh volume. Record changes in serverAppConfigs-docs/Dankclaw/OpenWebUI.md (Facts, backends table, History): the model id is a foreign key across three tables, so any endpoint alias rename must ship the same re-key.
@@ -0,0 +1,68 @@
---
name: parallel-worktree-path-hazard
description: "\"Use when several agents edit the same repo in different git worktrees at once, or when an edit/read may have landed in the wrong checkout: which path forms bind where, how to detect with git -C status, and the repair protocol (cp + git show HEAD blob write-back) because git checkout/restore is policy-blocked in a shared workspace.\""
---
# Concurrent-worktree path hazard (shared repo, many agents)
Measured 2026-10-18 on `serverAppConfigs` with two agents live at once. The
failure is **silent**: the tool reports success, the diff looks right, and the
change lives in a *different* worktree.
## Which path form binds where
- **Parent/top-level session**: a **relative** `path` resolves against the
session CWD — its own worktree — never against the worktree named in the
prompt. This is how a change meant for `wt-tts-trial-docs` landed in
`wt-20261017-…`.
- **Subagent**: an **absolute** path to its own assigned worktree can resolve
into the parent's worktree when the same *relative* path exists there
(`edit` twice onto `dankclaw/.env.example`). Its `write` with the same
absolute path was fine — the bug is `edit`/`read` path resolution, not `write`.
Rules that hold:
1. A subagent passes paths as `../<its-worktree>/<rel>` (relative to the parent
worktree) for `read` **and** `edit`; the parent passes **absolute** paths to
any worktree that is not its own CWD.
2. After every edit batch, prove the landing site, do not trust the tool's echo:
`git -C <intended-worktree> status --short` must list the file. A file you
edited that is missing from that list went somewhere else.
3. Keep each work item on its own branch/worktree, and keep ONE integration
owner (the parent) for shared files — `scripts/deploy-remote.sh`,
`scripts/deploy-plan.py`, `tests/`, `serverAppConfigs-docs/index.md`,
`AGENTS.md` are the collision hot spots in this repo. Never rebase a
sibling's branch.
## Repair protocol (no `git checkout`, no `git restore`)
`git checkout -- <path>` and `git restore <path>` are **policy-blocked** in a
shared workspace ("overwriting working-tree files … discards changes other
concurrent agents rely on"). Unstaging (`git reset [<path>]`,
`git restore --staged <path>`) is allowed but does not undo content. So:
```bash
F="path/with spaces if any"
# 1. move the change to the worktree that should own it (both at the same base commit)
cp "<wrong-wt>/$F" "<right-wt>/$F"
# 2. undo in the wrong worktree by writing the committed blob back
git -C "<wrong-wt>" show "HEAD:$F" | tee "<wrong-wt>/$F" > /dev/null
# 3. prove both sides
md5sum <(git -C "<wrong-wt>" show "HEAD:$F") "<wrong-wt>/$F" "<right-wt>/$F"
git -C "<wrong-wt>" status --short # must be empty for that file
git -C "<right-wt>" diff --stat # must show the intended delta
```
Never fix it blind: if the wrong worktree had its own uncommitted edits to that
file, `cp`/blob-write-back would destroy them — check
`git -C <wrong-wt> status --short` FIRST and reconcile content, don't overwrite.
## Tidy after a cancelled sibling
A killed subagent can leave a worktree plus an empty branch. Removing them is
safe only when verified empty: `git -C <main> log --oneline -3 <branch>` (no
unique commits) and `git -C <wt> status --short` (clean), then
`git worktree remove <wt>` + `git branch -D <branch>`. An orphan worktree at a
stale commit is the same class of trap as a leftover bench drop-in: the next
agent reads it as unfinished work.
## Report it
File the misresolution to `xd://report_issue` (false positives are fine) and
record the incident in the repo worklog — a repair nobody wrote down looks like
a hijack later.
@@ -0,0 +1,36 @@
---
name: podman-one-shot-hang-proofing
description: "Run remote podman one-shot jobs hang-proof: container-as-unit timeout/reap protocol; diagnosing stalls where ssh never returns and containers outlive timeout"
---
Use when running one-shot container jobs over SSH (grading sandboxes, CI executors) and a caller stalls, or `timeout N podman run ...` demonstrably fails to kill the workload.
## Stall signatures (all three = this bug)
- SSH client process alive for hours, no established-socket data flowing
- `podman ps` shows the container `Up` far past the timeout; `podman top <c> -o pid,stat,etime,args` shows `R`/`S` workload
- endpoint/queue reports idle (`in_flight: 0`) while the caller awaits
## The three laws
1. **A `timeout`/SIGTERM on an attached `podman run` client kills only the client.** The container is conmon-detached: it survives, inherits the ssh stdout pipe, ssh never sees EOF, the awaiting parent blocks forever.
2. **`podman wait` has NO `-t` flag in podman 5.x** (docker-ism) → every exec silently dies with rc 125. Bound the waiter with coreutils `timeout -k 5 <T> podman wait <ctr>` — killing a waiter is harmless.
3. **`podman rm -f` is the only true container kill** (escalates SIGTERM → SIGKILL on the container itself).
## Hang-proof exec protocol (single ssh script)
```bash
cname=job-<unique>
podman run -d --name "$cname" --network none --memory 4g --cpus 2 <image> python /w/entry >/dev/null
timeout -k 5 "$T" podman wait "$cname" >/dev/null 2>&1; rc=$?
code=$(podman inspect -f '{{.State.ExitCode}}' "$cname" 2>/dev/null || echo -1)
podman logs "$cname" 2>/dev/null; echo "EXIT:${rc}:${code}"
podman rm -f "$cname" >/dev/null 2>&1
```
- `rc=0, code=0` → success; `rc=124` → timed out (container reaped); harvest output via `podman logs`.
- Wrap the local ssh spawn in a **hard timeout of T + 180 s** (kills transport half-hangs too).
- Unique `--name` per submission; the trailing `rm -f` is also the leak reaper.
## Recovery of a live stall
1. `ssh host podman rm -f <ctr>` (warning "resorting to SIGKILL" confirms the theory).
2. Kill the hung local ssh + supervisor; queue/ledger resume (append-only ledger + takeover by heartbeat-pid liveness) resumes mid-eval; a hung step re-graded under the fixed protocol records a clean FAIL exit 124.
## Test to keep (regression)
`execPython("while True: time.sleep(1)", timeout=20)` → expect `exitCode 124`, completion < 180 s, then `podman ps --format {{.Names}} | grep -c <prefix>` == 0.
@@ -1,29 +0,0 @@
---
name: quadlet-v1-generator-quirks
description: "When writing or deploying Quadlet .container units on podman 5.x (v1 generator, e.g. Dankclaw), avoid keys the v1 generator hard-fails on and verify generated ExecStart before starting; generated units cannot be enabled."
---
# Quadlet v1 generator quirks (podman 5.x)
The podman 5.x user generator (`/usr/lib/systemd/user-generators/podman-user-generator`, quadlet **v1**) behaves differently from the v4 docs. Verified live on Dankclaw 2026-08-28 while deploying the authentik trio.
## Hard-fail keys (unit silently fails to generate — `systemctl --user cat <unit>` shows "could not be found")
- Any dash-prefixed option line in `[Container]`: `--shm-size=512m` etc. → generator logs `unsupported key '--shm-size' in group 'Container'` and skips the whole unit. Omit them; accept podman defaults.
- `Command=` is unsupported — use `Exec=` (that is what this generator accepts).
## HealthCmd space-splitting
The v1 generator splits `HealthCmd` on spaces into the healthcheck argv array (`podman inspect --format {{.Config.Healthcheck.Test}} <ctr>` shows `[CMD-SHELL pg_isready -d ... ]`). A quoted probe like `/bin/sh -c "pg_isready ..."` lands as argv token `"pg_isready` → command-not-found → container reads **unhealthy while the DB is actually up**. Rule: HealthCmd must contain only space-free argv tokens, e.g. `HealthCmd=pg_isready -d authentik -U authentik` or `python /manage.py check --database default`.
## Generated units cannot be enabled
`systemctl --user enable <generated-unit>` fails with "unit is transient or generated". Boot persistence comes from `[Install] WantedBy=default.target` inside the `.container` file, honored by the generator at user-manager start; linger must stay on for the deploy user. Deploy handlers: install unit → `systemctl --user daemon-reload` → `systemctl --user start`; no `enable`.
## Probe a new key before shipping it
Drop a throwaway `probe.container` into `~/.config/containers/systemd/`, `systemctl --user daemon-reload`, then check the generator verdict:
```bash
journalctl --user --since "1 min ago" -o cat | grep -i "unsupported key"
systemctl --user cat probe.service # shows the generated ExecStart verbatim
rm ~/.config/containers/systemd/probe.container && systemctl --user daemon-reload
```
If a healthcheck ever reads unhealthy but logs look fine, suspect this quirk first: compare the baked `Healthcheck.Test` array against the intended command before believing the status.
@@ -0,0 +1,32 @@
---
name: radiance-frank-primary-benching
description: "Bench frank-primary (radiance) decode/prefill via the scratch-container harness: method, noise floor, and the MNBT/HOST_POOL/envelope laws to respect before proposing any flag change."
---
# Benching frank-primary (radiance)
## Method (works as of 2026-10-06)
- Entry point: `bash autoresearch.sh` (dev box) → scp's `scripts/frank-primary.env` + `scripts/autoresearch-frank-primary.sh` to dankserver → the bench script ON dankserver: stops frank-primary (relinked unit: `stop`, never `mask`), boots scratch container `arbench` on the candidate env, polls /health (binds after full load, ~110 s cold), runs ONE greedy completion (fixed ~140k-char repo-corpus prompt, seed 1234 → 43,627 prompt tokens, 256 decode tokens, temp 0), parses the engine `timings` object, tears down, and restores prod via a trap.
- Metrics from `timings`: `prompt_per_second` (cold prefill), `predicted_per_second` (decode at depth = (predicted_n-1)/predicted_ms), `prompt_ms` (TTFT), `draft_n`/`draft_n_accepted` (acceptance).
- Fresh prefix-cache tier per run (`/data/kvcache/arbench`) so cold prefill is cold; prod's tier untouched.
- Bench locally on dankserver (127.0.0.1) — no LAN jitter. Run-to-run ~4-5 min.
## Noise floor (measurement law)
- Decode: ~±3% on IDENTICAL config (33.06 / 32.03 / 30.90 observed). Single-run deltas <3% are not evidence; re-run before keeping.
- Prefill: ~±2% (453/448/457 identical config).
## Laws at the seqs=3 / maxlen=200k / fp8-KV / MTP-on shape (2×R9700, TP=2)
- **MNBT is a sharp resonance at 2048**: 2560 → decode 2.6-7.7 t/s (acceptance stays normal, verify stalls ~130 ms/step, generation truncates); 1536 → decode 24.7 + 22% prefill loss + truncation; 4096 → boot failure at seqs=3 (boots at seqs=2, -2.6% only). DO NOT move MNBT off 2048.
- **HOST_POOL_MIB 40960 is the load-bearing floor**: 36864 fails boot; 49152 boots but decode-flat.
- **EXPERT_VS_CACHE 0.82 is bracket-confirmed**: 0.78 fails boot; 0.84 costs 8.9% (toward the documented 0.87 collapse cliff).
- **TP_WIRE wht6 is mandatory** at seqs=3: bf16 (lossless) fails boot.
- **SPEC_TOKENS**: depths 1-3 statistically tied on decode; 0 = -6.8% (real), 4 = -22.5% (acceptance collapses to 0.50). Depth 2 nominal peak.
- MAX_MODEL_LEN 262144 (native) costs 2.5% — the larger KV pool steals resident-weights VRAM at expert_tiered.
- GPU_HEADROOM 48 vs 96: no effect.
## Hazard: abandoned bench contamination
- If a prior bench run is skipped/abandoned mid-flight, the next scratch boot may race the leftover engine and produce garbage numbers. Before trusting any collapse measurement: `podman ps -a` on dankserver — confirm the previous arbench is gone and frank-primary is active — then reproduce the collapse on a clean boot before writing it into the record.
## State
- HEAD on the autoresearch branch: spec=2, seqs=3, maxlen=200k (df6b5bb6e807). All spec 1-3 × seqs 2-3 configs decode within 30.9-33.1 — statistically tied. Frank's decision pending: keep spec=2/seqs=3 or revert to his spec=3/seqs=2 (83% acceptance, likely better for real multi-turn traffic).
- Production verified healthy (health 200) after every run via the harness trap.
@@ -0,0 +1,111 @@
---
name: radiance-kv-cache-resize
description: "Resize the KV cache / VRAM pair on a radiance (StillDeadcode) endpoint: engine-budget-table ceiling, pool contract arithmetic, slab-slope law, ledger-first verification, free token-count probes"
---
# Radiance KV / VRAM pair resize
Use when changing `--vram-kv-mib`, `--vram-weights-mib`, `--max-num-seqs`,
`--max-model-len` or `--kv-cache-dtype` on a radiance endpoint
(frank-archon pattern), or when asked "what can we do with the free VRAM".
## 1. The ceiling is the engine's, never a hand-picked reserve
`rocm-smi` free space is NOT spare capacity. The boot prints a `VRAM budget,
per card` table; read it:
```
card total 31.86 GiB
vram_weights 25.00 GiB stated by --vram-weights-mib
vram_kv 5.00 GiB stated by --vram-kv-mib
unclaimed 477.30 MiB the headroom, the unusable above, rounding
W VRAM budgets total 30.00 GiB of the card's 31.86 GiB, so 1.86 GiB is neither
budgeted nor accounted. The driver's context, the activation arena and
--gpu-headroom-mib come out of it; anything past those is throughput on the floor
W card 0 settled at 2.01 GiB free against 209.19 MiB (the 96.00 MiB
--gpu-headroom-mib and 113.19 MiB kept for the static region)
```
`ceiling = (settled_free_MiB - engine_floor_MiB) + current_pair_sum`.
Worked (dankserver, 2026-10-13): 29,120 + (2,058 − 209) = **30,969 MiB/card**.
A pair at the ceiling settles ~330 MiB free at worst case. If a bound cannot be
traced to a ledger line, it is a personal reserve — label it and put it in the
bats gate as such.
## 2. Pool contract (the engine enforces it at REQUEST time, not boot)
`pool_mib >= seqs*max_model_len*rate + 110*seqs + 448`
- rate/token/rank: **7,488 B fp8**, **14,144 B bf16** (1.89×, not 2× — the QSA
block key is 3.25 KiB at BOTH dtypes; it is an index, not attention state).
- +110 MiB a sequence of recurrent state (`kv_gdn_state` 54 MiB × 2 copies +
conv/tail/ple) and 448 MiB of prefix checkpoints, dtype-independent.
A pool that fails this **boots, serves short chats, then 400s every long
request** as `does not fit the 200000 token context, which leaves nothing to
generate into` — radiance refuses, never truncates. Encode it as a bats gate.
`--kv-cache-dtype` takes ONLY `bf16|fp8`, one dtype for the whole cache (no
llama.cpp-style exact-K/quantized-V pair).
## 3. Slab and KV are one budget; the slab is a SLOPE
- Idle KV VRAM is the expert cache's spillover: ledger
`mover: N slots can be lent out of the KV cache (X GiB)`. A bigger pool steals
residency twice over.
- Slab MiB ↔ resident experts is linear (~1,300 slots per 1,792 MiB measured);
the pinned host pool hands back the same slot count as the slab grows
(16,235→17,548 slab with 8,893→7,580 pool). The pinned RAM path is the slow
path, so slab MiB = throughput.
- Measured law (fp8, 43.5k single-stream, Qwen3.8-Flash-Next w4):
22,208 → 3,397, 24,000 → 3,290, 25,600 → **3,584**, 26,071 → **4,379 t/s**.
Two points inside the ±3 % noise band are NOT a plateau — never infer a
threshold from two arms.
- **Decode is a separate axis.** It follows KV dtype/bandwidth and draft
acceptance, not the slab: fp8/slab 26,071 decoded 124.1 t/s cold where
bf16/slab 22,208 decoded 94.1 (512 tokens, 51.5k). Slot count moves the
batch-shared per-stream floor (3 concurrent ≈ 6 t/s; solo ≈ 90–120).
## 4. Verify: ledger first, then live probes (no scratch server)
1. Deploy the service; read `podman logs <svc>`: `backs N sequences all at
M tokens at once` is the capacity referee; check slab slots, `unclaimed`,
`sampler: ... R rows` (= seqs × (1+spec), must stay inside the 25-row wht6
exactness bound), `settled at … free against … floor`.
2. Boundary probe: N concurrent prompts sized to `max_model_len - gen`, each
with a unique marker at the head; expect N/N marker retrieval.
3. Prefill/decode probe: repo `scripts/probe-frank-archon-kv.py` (reads the
shape from the deployed env, so it self-resizes).
### Free token-count calibration (no prefill burned)
`/v1/completions` refuses when **prompt tokens alone** exceed the window —
`prompt: 202,777 prompt tokens does not fit the 200000 token context` — and the
refusal names the count BEFORE any prefill. It does NOT fire on
prompt + max_tokens. Corpus ratio measured: **2.7617 chars/token** (2.5714 for
the fox filler), so a 200,000-token window ≈ 514,286–552,286 chars. Calibrate
with a probe ABOVE the window; below it the request is accepted and pays real
prefill (~22 s).
### Probe traps that produced false results
- Generate **≥128 tokens** (512 preferred) for any decode number; at 8–9 tokens
`inter_token` is first-token noise — rates like 92.7 vs 85.8 are noise.
- Radiance `timings` can carry `null` rates — `(t.get(k) or 0)`, and
`j.get("timings") or {}`.
- The disk prefix cache (64 GiB) survives restarts; put a random nonce at the
HEAD of a "cold" prompt. A warm 197k prefill returns in ~10 s and looks like
a 60,000 t/s miracle.
- With thinking enabled, a small `max_tokens` yields empty `content` +
`finish_reason: stop` on a healthy server; set
`chat_template_kwargs.enable_thinking=false` in probes.
- `echo ===` in an ssh'd zsh dies on `=`-expansion; it silently truncates the
command chain.
## 5. Ship
`scripts/<svc>.env` pair + header, serve-script header, bats gates (pair ≤
ceiling; pool ≥ contract), docs (runbook, `AGENTS.md` fleet row, LLM index,
log.md), `./tests/run.sh`, `deploy.sh <host> --service <svc>`, then the probes.
Record every prediction as falsifiable in the env comment and write the verdict
there when the probe lands — including when it kills the prediction.
@@ -0,0 +1,39 @@
---
name: radiance-model-feasibility-scout
description: "Scout whether a new GGUF model can run on radiance (R9700 fleet): GGUF header census, REUSE/ADAPT/NEW gap matrix vs radiance docs, ledgers (traffic/KV/RAM/disk), KLD-gate substitution, HTML report delivery."
---
# Radiance Model Feasibility Scout
Answer "can model X serve on radiance (dankserver R9700 TP=2), and what's the bill?" Produce an HTML report. Complements `gguf-header-vram-fit-analysis` (fit math), `radiance-w4-convert-playbook` (conversion mechanics), `radiance-quantize-and-score`.
## 1. GGUF header census (trust header, not the card)
- Range-parse the header: tensor count, per-type census (type → tensor count / params / bytes), layer pattern from tensor names, `general.architecture`, `llm_type`, rope dim, expert counts, `mtp` declaration.
- MTP: presence is decided by the **tensor-name census** (`nextn.*`/`mtp` tensors), never by metadata.
- Confirm tokenizer + `chat_template` KV entries ship in the file; mmproj = separate BF16 tower file.
- Cross-check total bytes vs the published file size.
## 2. Gap matrix vs radiance docs
Read `PLUGIN.md` (arch-plugin contract, quantizer registry, core/mm image-pipeline law), `OPS.md` (op catalog: attention tiles h256 gqa2/4/8 + h512 gqa8, GDN, QSA, MoE, MTP), `QSA.md` (indexer decomposition bounds), `GUIDE.md` + `gguf.cpp` (dtype table 0–44: all standard GGUF grids readable — quantization happens in rad-convert, not the loader). Classify every component REUSE / ADAPT / NEW with the specific op named. The live `qwen4exp` plugin (~2k lines, QSA+MoE+mHC+GDN) is the template and the shared-header reuse list.
## 3. Laws checklist (recurring traps)
- GGUF `ssm_a` = −exp(A_log); undo at convert/plugin time.
- One-table-one-encoding: a routed-expert table takes ONE rad encoding; mixed GGUF grids (q2/q3/q4 per layer) re-quantize to w4 GPTQ + whole-expert BF16 protected set.
- int8 trunk + int8 lm_head laws; swiglu clamp carries from the arch family; rope-free layers need no RoPE path.
- KDA ≠ GDN: per-channel decay + 3 convs + L2-normed q/k + lower-bound gate → new kernel family (FLA `chunk_kda` is the oracle); `kkt_solve`/`chunk_scan` is the critical path.
- MLA: latent-cache absorbed form (KV = latent width × dtype), needs its own attention tile (e.g. h512 gqa-64) + per-head v bmm.
- llama.cpp merged PR for the arch = the ground-truth formula oracle (sign conventions, APE layout, Sinkhorn input).
## 4. Ledgers (all in the report)
- Per-token weight traffic GB (trunk int8 + active experts w4 + lm_head) → decode anchored on frank-archon's measured 99 t/s @ ~6.3 GB/token; state [INFERENCE].
- KV: per-token bytes per pool + per-seq state terms; cross-check vs any prior fleet runbook that served the same model.
- Pinned RAM: host pool + prefix vs `LimitMEMLOCK`/`ttm-preflight.sh` law.
- Disk peak = GGUF + `.rad` (~187 GiB-class for a 300B MoE, no PLE) vs free space; options: retire rollback payload / external convert host / `--in-place`.
## 5. KLD gate feasibility
BF16 teacher = GGUF params × 2 B; if unservable (626 GB-class), the substitute gate = llama.cpp (arch PR merged in master) serving the GGUF on the Dankclaw iGPU — GTT aperture ~114 GiB — stop+mask frank-dragoon for the window, KL + top-1 census vs radiance `.rad`. Document as a gate deviation.
## 6. Deliver as self-contained HTML
- Single file, inline CSS with `prefers-color-scheme` vars, section anchors + TOC, census/gap/risk/plan tables, `[INFERENCE]` labels on arithmetic.
- Write in **chunks**: one `write` ≈ ≤8 KB (output-token limit), end each chunk with `<!--MORE-->`, then `edit` with `PUT N.=N:` where N = marker line — **prefix every body row with `+`** (bare rows get auto-prefixed; register lines like `@name` land as literal content).
- Validate: python `html.parser` tag-balance script + grep for stray markers; `xdg-open` the file.
@@ -0,0 +1,31 @@
---
name: radiance-precision-selection
description: "Choose tensor precisions for a radiance (StillDeadcode) rad-convert recipe on RDNA4: what is native, what is supported, what the hardware rewards."
---
# Radiance precision selection
Use when writing/choosing a `rad-convert` recipe for radiance on RDNA4 (gfx1201), or when arguing why a model was converted a particular way.
## Project map (which repo is authoritative)
- `codeberg.org/StillDeadcode/radiance` — standalone engine (C++/HIP). Recipes: `data/recipes/*.recipe`; docs: `docs/{GUIDE,MOE-W4,KERNELS,PLUGIN,TOOLS,OPS,ARCHITECTURES,DOCKER}.md`. Prefer the RAW form of Codeberg URLs when reading: `/raw/branch/main/<path>`.
- `codeberg.org/StillDeadcode/libr4d` — kernel library; entry points encode exact geometry and reject mismatch (`attn_decode_h256_gqa6_fp8kv`, `gdn_chunk_scan_k128_v128_c64_bf16`, `ar_oneshot_2rank_exact`). Kernel names are the ground truth for which dtypes have a fast path.
- `codeberg.org/StillDeadcode/vllm-radiance` — deprecated vLLM wrapper; README still carries the best prose on the FP8 dispatcher and kernel list.
## Precision ladder
| Precision | Role |
|---|---|
| block-FP8 (E4M3 + bf16 block scales) | native tuned GEMM/attention/KV — "preshuffled block-FP8 dispatcher" |
| MXFP4 (OCP group-32) / W4A8 | supported weight packing; activations dynamically quantized to FP8 E4M3, compute is FP8 WMMA |
| bf16 | KV (`auto`), skinny GEMMs, Gated-DeltaNet kernels |
| int8 / int4 | draft head and integer GEMM paths |
## Decision rules
- Prefer block-FP8 for anything the engine qualifies: generic upstream AITER linear in place of the FP8 dispatcher measured **9–11 % slower**; all upstream-qualified models are FP8 (Qwen3.8-27B-FP8, Qwen3.6-27B-FP8, Qwen3.6-35B-A3B-FP8, Gemma-4-31B-it-FP8).
- Weight packing is a *storage* choice, activation dtype is the *compute* choice: MXFP4 buys bytes, not an FP4 compute path.
- The matrix core is integer-biased: f16 WMMA is half the int8 rate and a quarter of the int4 rate ⇒ int4 > int8 > f16. FP8/INT8 kernels must fuse double-K WMMA to saturate the 128-bit memory interface (GPUOpen WMMA guide part 2) — a "slow" quantized kernel usually means a missing K-fused variant, not an inherent dtype cost.
- Keep low-bit tricks on the draft head (int4/int8) where verification tolerates them; keep lm_head and hard-selection tensors (QSA indexer top-k) at high precision — the shipped recipes quantify the cost (int8 lm_head beats E4M3 by ~0.6 top-1 points).
## Verification
- Read the recipe's own header comments: radiance recipes state measured KL / top-1 / prefill-decode deltas for their choice, and name the `--reuse` inputs.
- Confirm the kernel exists before promising a dtype: grep libr4d entry points for the dtype suffix (`fp8kv`, `bf16`, `exact`).
@@ -0,0 +1,26 @@
---
name: radiance-quantize-and-score
description: "Design a rad-convert recipe for a radiance (StillDeadcode) MoE checkpoint, calibrate it, and score it with the KL mode — the procedure plus the servable-format law."
---
# Radiance: quantize, calibrate, score
For a HuggingFace checkpoint → `.rad` on the radiance engine (Codeberg StillDeadcode/radiance).
## 1. Know the servable formats before writing a rule
The block/plugin serves a fixed list; the converter refuses an encoding no kernel reads (`docs/TOOLS.md:184`). For qwen4exp MoE experts: `fp8a8, w4a8, w4a8h, bf16, w4nla8h, w4nl64a8h, w4nl32a8h, w5nl64a8h` (`arch/common/rad_block_moe_fp8.h` `kServed`); dense: `fp8a8, i8a8, fp8a16, mxfp4a8, bf16`, plus the `u2` draft head. `fp6_*`/`u6` encode but never serve. Read `kServed` in source — never trust a code-name list from docs prose.
## 2. Recipe file
Header `#` lines are stored in the container; first matching rule wins, so put `cast dtype=bf16` protected-expert rules and projection-specific rules **ahead of** the general `blk.*.ffn_*_exps.*.weight` rule. Unmatched weights are served as the checkpoint holds them (bf16). Verify names against radiance's internal stems (`ffn_gate_up_exps`, `ffn_down_exps`, `attn_qg`, `ssm_inz`, `ple_ngram`, `output.weight`), not the HF names.
## 3. Calibrate (GPTQ/awq/autoround/paroquant need `$CALIB`)
Bootstrap: `rad-convert` with plain `rtn` → serve with `RADIANCE_CALIB_DIR` (+`RADIANCE_CALIB_TOKENS`) and drive prefill → re-convert with `--recipe … --reuse <bootstrap.rad>` (`--reuse` copies every weight whose encoding did not change). A BF16 checkpoint may be calibrated as it ships: convert with no recipe + `--max-ctx 4096`, serve `--placement expert_tiered --weights-disk-tier`, MNBT large (a step costs the same at 1 token as at 32K). Corpus must look like production traffic; score on a held-out slice.
## 4. Score
`radiance --model BF16.rad … --kld-record DIR --kld-corpus C.jsonl` (exact wire, bf16 KV) once, then `--kld-ref DIR --kld-out R.json` per candidate. Report = mean/median/p99/p99.9/p99.99 KL, top-1 agreement, both perplexities, **per `source`** — label the corpus's `source` field to get per-turn-type scopes. KLD corpus disjoint from calibration. Keep `--max-num-batched-tokens ≤ 8192` (the logits buffer is ~2 GiB/rank per 4096 tokens).
## 5. Set the gate from the published ladder, not from a wish
Compare against `docs/MOE-W4.md`'s table for the arch (served tier / heavier tier / bf16 floor). A threshold below the bf16 floor rejects every quantized build by construction. Escalation ladder reuses the same Grams (Hessians are grid-independent) → escalate by re-converting only.
## 6. Verify before serving
`rad-tune` at the served shape (TP, spec depth) with a persistent cache mount; read the boot ledger for residency; run the fleet probes.
@@ -0,0 +1,44 @@
---
name: radiance-requant-source-audit
description: "Answer \"can we re-quantize a radiance model from what's on disk / what must we re-fetch\" before planning a heavier-or-different quant campaign: irreversibility law, live asset census, base-bound reuse rules, and the expert-mass pool-fit ceiling."
---
# Radiance re-quant source audit (dank-fleet)
Decide whether a new/heavier radiance `.rad` quant is buildable from local state, and what must be re-fetched. Run this BEFORE costing any campaign.
## Irreversibility law (never re-litigate)
- Quantization is one-way: a `.rad` payload on disk is a lossy ENDPOINT. Re-encoding it upward only re-wraps its own KL error.
- `rad-convert` takes a RAW HF checkpoint directory (safetensors) — never a `.rad`. No dequant→requant path exists in the engine (see `scripts/convert-swift15-w4dn8.sh` mount line).
- Therefore any genuine rung change needs the high-precision checkpoint on disk. Even "more bf16-protected experts instead of wider codes" needs the bf16 tensors at convert time.
## Census (live, on the engine host — do not trust docs)
```bash
ssh <host> 'df -h /home/dank | tail -1; du -sh /home/dank/models/* ; \
du -sh /home/dank/corpora/calib /home/dank/corpora/kld-ref /home/dank/corpora/out; \
cat /sys/module/ttm/parameters/pages_limit; \
podman image exists "stilldeadcode/radiance@sha256:<serve-pin>" && echo PIN-PRESENT'
```
- BF16 Swift1.5 source + fp8-native `.rad` were DELETED 2026-10-15; only the serving iq4r + w4dn8 rollback `.rad`s remain under `/home/dank/models/rad/`.
- Survivors (verified 2026-10-17): grams `/home/dank/corpora/calib` (41 GiB), KLD ref `/home/dank/corpora/kld-ref` (24 GiB), corpora JSONL `/home/dank/corpora/out`, HF staging receipts, engine pin image local, free disk ~544 GiB.
## Reuse rules (what each survivor is bound to)
| Asset | Reusable for |
|---|---|
| Calibration grams (RADGRAM2, down-input Hessians) | ONLY the checkpoint they were recorded from (Swift1.5) — tiers-independent, so valid for ANY new recipe tier on that same base |
| KLD reference log-probs | ONLY same-base candidates |
| Corpora JSONL | any base (recalibrate if base changes) |
| Derived protected-expert list | same base only (routing is finetune-specific) |
Reusing grams+KLD ref skips ~8 h of GPU-serve windows (calibration + KLD record).
## Heavier-rung pool-fit ceiling
Expert tables for Qwen3.8-Flash-Next bill ~56 GiB (w4) → ~75 (w5/w6 class) → ~112 GiB (int8/fp8) across both ranks (from the recipe's own mass lines: gate_up 150 + down 75 GiB bf16). The one-table-one-encoding law moves the whole expert set together. Pinned host pool 43,928 MiB/rank ⇒ **only w5/w6-class rungs keep the no-streaming shape**; int8/fp8 experts overflow it and re-open the NVMe 3.5 GB/s streaming era. FP8-official-checkpoint rebuild (172.8 GB, ungated, no-recipe copy) is the cheap half-download "heavier" but carries those streaming economics.
## Known scored comparisons (don't re-pay for them)
Upstream w5dn64-vs-iq4r on the official checkpoint: mean KL 0.0789→0.0717, top-1 93.34 %, decode +4 %, prefill −19 %. A custom heavier official rung reproduces this; a custom heavier Swift1.5 rung reuses the fleet's own grams/KLD assets instead.
## Fetch scripts (dankserver)
- Swift1.5 BF16 335.3 GiB: `scripts/download-frank-archon-swift15.sh`
- Official FP8 172.8 GB: `scripts/download-frank-archon.sh` (NOT bf16 — a bf16-official fetch needs a fork of it)
- iq4r container: `scripts/download-frank-archon-iq4r.sh`
Disk gate arithmetic: checkpoint + output both live on the volume; the campaign convert wanted ≥146 GiB free AFTER the checkpoint lands.
@@ -0,0 +1,37 @@
---
name: radiance-shape-resize-verification
description: "Verify a radiance (frank-archon) KV/slab shape change end-to-end: TTM preflight, boot ledger, boundary probe, decode ladder — with the laws that govern each axis."
---
Use when changing `FRANK_ARCHON_KV_DTYPE`, `FRANK_ARCHON_MAX_SEQS`, `FRANK_ARCHON_VRAM_WEIGHTS_MIB`, or `FRANK_ARCHON_VRAM_KV_MIB` in `scripts/frank-archon.env`, and when asked why a shape change did or did not move throughput.
## Sequence
1. Pool sizing first: `pool_mib >= seqs*maxlen*rate + 110*seqs + 448`, rate = 7.3125 KiB/token fp8, 13.8125 KiB/token bf16 (measured page sizes 26.00 vs 52.00 KiB + 3.25 KiB dtype-independent QSA key).
2. Pair ceiling: `weights + kv <= 30,969` MiB/card (card 32,624 − 3,425 engine-side fixed: 2,048 activation arena, 209.19 floor, 960 driver/kernel, 224 NCCL/IPC, 96 headroom, 600 unaccounted). Settle floor is 209.19 MiB, NOT the hand-picked 1 GiB reserve.
3. Update bats seams: pair ceiling (`FRANK_ARCHON_VRAM_CEILING_MIB`), slab floor, KV-pool contract; keep the ceiling seam owning only pair arithmetic.
4. `./tests/run.sh` (needs bats on PATH: `/home/blank/go/bin`), commit, merge `origin/wt/<branch>` ff-only into main at **/home/blank/Projects/serverAppConfig**, push main.
5. Deploy: `ssh dankserver 'cd /home/dank/serverAppConfigs && git pull --ff-only && ./scripts/deploy.sh dankserver --service frank-archon'` — boot 3–4 min; `--dry-run` first to confirm the plan selects exactly one service.
6. Read the boot ledger: `journalctl -u frank-archon | grep -E "kv cache|budgeted|expert slab|resident experts|unclaimed|settled at|sampler"` — engine prints backed tokens, slab slots, host-pool slots, settle, rows.
7. Verify contract: `ssh dankserver 'python3 /home/dank/serverAppConfigs/scripts/probe-frank-archon-kv.py'` — legs A (decode at depth), B (N×window capacity), C (prefix restore), D (N-stream decode).
## Laws (measured; trust over recalculation)
- Slab is a THRESHOLD: 22,208 → 3,397, 24,000 → 3,290, 25,600 → 3,290 t/s at 43.5k; cliff at ~26 GiB (26,071 → 4,379).
- Decode is nearly window-free: 99.0 t/s at 43.5k, 93.3 at 197k.
- Slots are paid in decode CONCURRENCY: 3 streams = 18.4 t/s aggregate, per-stream a fused-step lottery (113.9/43.3/10.9).
- Idle KV VRAM lends to the expert slab (fp8 pool 6,912 lent 6.08 GiB vs 2.69 at 3,543).
- Bigger pool can LOWER prefill: more resident experts = less pinned host-pool traffic.
- Oversized prompt is FREE and refused by name — counter `frank_archon_prompt_too_large_total` counts; refusal fires on PROMPT tokens alone ("leaves nothing to generate into"); radiance NEVER truncates.
- TTM: serve script raises `ttm.pages_limit` to `FRANK_ARCHON_TTM_TARGET_MIB` itself (reboot resets to half RAM); unit ExecStartPre hook.
- Refusal signature: `activating (auto-restart)`, exit 1 in ~50 ms, no container, no engine log.
## Probe law
- 512 generated tokens minimum (8–24-token probes report decode noise as rate); greedy; cold prompts (unique nonce) to defeat the disk prefix cache; acceptance ~0.66–0.74 on repo corpus, repetitive filler 0.93–0.97 (never compare across corpora).
- Radiance routes thinking to `reasoning_content`: set `chat_template_kwargs.enable_thinking=false`, read content + reasoning_content.
- `finish_reason: "stop"` with `completion_tokens: 1` on a capacity request = accepted, not failed.
## Traps
- Python heredoc over ssh: `str.maketrans` with quoted numeric literals collapses (`'0123456789'` → `'06789'`) — build alphabets with `chr(48+i)`/`chr(97+i)`.
- `FRANK_SUDO_PW` in `/home/dank/.profile` has a CRLF tail; strip `\r` for systemd credentials (`tr -d '\r' | sudo systemd-credential ...`).
- Deployer reports DEPLOY INCOMPLETE on a stray port-holder (`ss -ltnp | grep :7770`); never kill a root process over ssh without sudo.
- The main checkout on the dev workstation is `/home/blank/Projects/serverAppConfig`, NOT `/home/dank/serverAppConfigs` (that path is on the servers).
- `systemctl mask` fails on the relinked unit; `deploy.sh <host>` with no `--service` is a fleet-wide drain.
@@ -0,0 +1,18 @@
---
name: radiance-w4-campaign
description: "Run a radiance GPTQ-w4 quantization campaign on dankserver: calibration → protected experts → convert → KLD gate → deploy, with the engine-pin and MNBT laws."
---
Pipeline for quantizing a BF16 Qwen3.8-Flash-Next checkpoint into a servable `qwen4exp` `.rad` on dankserver (frank-primary). Reference run: `serverAppConfigs-docs/LLM/frank-primary-radiance-swift15-w4dn8.md`; scripts in `scripts/` (`run-swift15-bf16.sh`, `calib-push.py`, `derive-protected-experts.py`, `apply-protected-cast.sh`, `convert-swift15-w4dn8.sh`, `kld-gate-swift15.sh`, `fleet-probes-swift15.sh`).
Sequence:
1. **Fetch** checkpoint with per-shard LFS sha256 verification (`-f`, byte counts, no token on argv).
2. **BF16 container**: `rad-convert` no-recipe (pure copy) — serves as the KLD reference engine.
3. **KLD reference**: record logits on the SERVE pin (1.0.13) at MNBT 32768, `--max-model-len 16384`, corpus ≤16k prompts. Writes atomically at completion (`rows:` line = receipt). Never scp a running bash script (lazy-read law).
4. **Calibration**: engine **1.2.0** (only pin with `RADIANCE_CALIB_TOKENS`); push ~10M tokens of the model's own distribution; per-expert down-proj Grams land as RADGRAM2 files (`gram.r<rank>.blk.L.ffn_down_exps.<e>.bin`). Grams are engine-invariant across pins — the version boundary is by design.
5. **Protected experts**: derive from gram energy (share ≥3 % AND ≥3× routing share), splice between `# PROTECTED-BEGIN/END` markers of the recipe. Plugin law: ONE encoding per expert table; only whole bf16 experts legal beside it — no gate_up/down split.
6. **Final convert** on the SERVE pin (CPU GPTQ, hours; disk gate ≥146 GiB free first).
7. **KLD gate** on the serve pin: **MNBT ≤ 2048** (w4 streams in place only there; 32768 → `DID NOT FIT ... stored form is not the container's`), pool 40960, fp8 KV matching the production KV dtype. Gate: mean KL ≤ 0.03, top-1 ≥ 95 %.
8. **Deploy** `./scripts/deploy.sh dankserver --service frank-primary`; verify `/v1/models` id = served-model-name (alias independent of file — harness models.yml untouched), boot ledger, streamed ≥128-token decode probe, fleet probes (vision needs ≥256-token budget + `reasoning_content` fallback).
Laws: stop frank-primary for every GPU phase (relinked unit: stop, mask fails); verify docs against `podman inspect <name> --format {{join .Config.Cmd " "}}` (rootless, as dank) before writing shapes; ff-pull fails on untracked files the merge tracks — trash first.
@@ -0,0 +1,29 @@
---
name: radiance-w4-convert-playbook
description: "Convert a BF16 Qwen3.8-Flash-Next checkpoint to a GPTQ-w4 radiance .rad: recipe law, calibration tap, derive-protected-experts, convert gates, KLD gate shape and flags."
---
# Radiance w4 convert playbook (dankserver)
End-to-end recipe for rad-convert GPTQ w4 on the qwen4exp arch (engine pin `stilldeadcode/radiance@sha256:4c3f48...` = 1.0.13 for convert/KLD/serve; 1.2.0 `sha256:665775...` ONLY for the calibration tap).
## Pipeline
1. **Calibration corpus** ~10M tokens (`make-calib-corpus.py`, seed-pinned; keep `kld.jsonl` for the gate — regenerate deterministically if lost).
2. **KLD reference on the BF16 container** (no-recipe convert, `--kld-record`), same engine as scoring.
3. **Calib serve on 1.2.0**: `RADIANCE_CALIB_DIR` + `RADIANCE_CALIB_TOKENS=10000000`; tap in `qwen4exp_fp8.so` writes `gram.r<rank>.blk.L.ffn_down_exps.<e>.bin` (RADGRAM2, 32 B header + k×k f32); DONE mark `calibration wrote`.
4. **Derive protected experts** from the Grams: energy share ≥3 % AND ≥3× routing share, even count per rank half.
5. **Recipe law**: ONE encoding per expert table — all routed experts through GPTQ w4 (`libquant: GPTQ quantises 'blk.N.ffn_gate_up_exps' against M calibration file(s)`); PROTECTED section = whole bf16 experts ONLY, spliced between `# PROTECTED-BEGIN/END` markers. The $CALIB env var is expanded by rad-convert.
6. **Convert** host-backend (no GPU; prod-compatible), ~6 min/layer × 49, CPU-saturating. In-container paths must include subdirs (mount prefix strip). Gate: rad-info shows gptq rule; size floor from geometry, not by analogy.
## KLD gate shape (1.0.13)
`--placement expert_tiered --weights-disk-tier --host-pool-mib 40960 --deterministic --gpu-headroom-mib 512 --num-speculative-tokens 0 --kv-cache-dtype fp8 --max-model-len 16384 --ngram-placement ram --kld-ref DIR --kld-out FILE --vram-kv-mib 2048 --max-num-seqs 4 --max-num-batched-tokens 3072`.
- `--kld-ref` takes NO `--kld-corpus` (rejects the pair).
- w4 arena ≈ 262.5 MiB per 256 tokens of MNBT — 32768 needs 33 GiB of VRAM.
- w4 gate_up experts can't disk-tier stream (stored ≠ served form) — pool must hold ~12 GiB/rank.
- Gate: mean KL ≤ 0.03 AND top-1 ≥ 95 % vs reference.
## Pitfalls
- Editing a running bash script corrupts its execution (lazy byte-offset reads) — sync after the run.
- `pkill -f <pattern>` kills your own ssh watcher if the pattern matches the ssh command line.
- zsh on dankserver eats nested quoting — write scripts locally, scp, `sh /tmp/x.sh`.
- Radiance image has no bash/strings: `--entrypoint sh`, `grep -ac` with positive control.
@@ -0,0 +1,28 @@
---
name: recover-subagent-yield-payload
description: "Recover a subagent's full structured report when it exited without yielding (SYSTEM WARNING) — extract aborted yield payloads from the session JSONL, including harness double-encode and split-JSON quirks."
---
Use when a subagent ends with "exited without yielding" yet the transcript shows it repeatedly calling `yield` (each reply "…[+N]" — payload too big for display). The research is usually COMPLETE; only the yield serialization failed. Known harness quirk: the yield `arguments` get wrapped as `{"data": "<json string>"}`, so an object payload is validated as a string → "expected object, received string" retry loop until the agent dies.
## 1. Locate the session JSONL
Transcript header prints the path: `~/.omp/agent/sessions/<cwd-slug>/<session-id>/<AgentName>.jsonl` (worktrees: cwd slug like `-.omp-wt-wt-...`).
## 2. Extract every aborted yield payload (Python via eval — shell quoting will mangle these)
- Parse each line with `json.loads` guarded by try/except (lines may be non-JSON).
- Keep `message.role == "assistant"` messages; in `content`, keep entries with `type == "toolCall"` and `name == "yield"`; collect their `arguments`.
- Unwrap: `arguments` may be a dict containing `data` (a JSON string) or a plain string; `json.loads` it.
## 3. Tolerate broken JSON
Naive `json.loads` on `data` can fail "Extra data: line 1 column ~4600" — the payload is a complete object followed by `, "architecture": ..., "report": ...}` (agent emitted a structure whose early brace-closure ended the first object). Recover with `json.JSONDecoder().raw_decode`:
1. `part1, end = dec.raw_decode(data.strip())`
2. `rest = data[end:].strip()` — if it starts with `,`, parse `part2 = dec.raw_decode('{' + rest.lstrip(', '))[0]`
3. `merged = {**part1, **part2}`
Do this for ALL yields and keep the largest merged dict (attempts evolve; keys seen: `summary`, `files`, `report`, `architecture`).
## 4. Persist and deliver
Write the recovered `report`/`summary` to a durable file (memory dir or `local://`), then continue synthesis — never re-run the research the dead scout already did.
## Prevention (do this first next time)
- While the agent is still alive and looping: write to `agent://<Name>` steering it to "yield immediately, wrap in minimal object or raw markdown, schema-invalid yield acceptable — I read the transcript anyway." It often still can't (the failure is in the harness wrapper, not the agent) → let it be killed and use the extraction above.
- Scout default schema wants objects; terse yields from `scout` agents hit this most. Keep expected reports slice-sized.
@@ -0,0 +1,39 @@
---
name: repo-change-html-report
description: "Build a self-contained HTML report of a branch's changes (per-file detail grouped by atomic change, inline SVG diagrams) and open it in the user's browser"
---
# Repo change → HTML report in browser
Produce a standalone HTML report covering `git log main..HEAD`, grouped into atomic tasks (one task may span commits; one commit may serve several tasks), per-file `what`/`why` tables, inline SVG diagrams, then `xdg-open file://…`.
## 1. Gather facts first (never from memory)
```
git log --reverse --format="COMMIT %h %s" main..HEAD
for c in $(git log --reverse --format=%h main..HEAD); do
echo "== $c $(git log -1 --format=%s $c)"; git show --format="" --stat=200 "$c"; done
```
`--stat=short` is invalid; use `--stat=200`. Supplement with the session's per-file notes.
## 2. Write to a file OUTSIDE the repo
`~/reports/<repo>-<branch-date>.html` — keeps the worktree clean.
## 3. Chunk the writes (payload limits are real)
- First chunk: `write` (head + CSS + hero cards + nav).
- Later chunks: `edit` with `PUT >$` (append), ~6–8 KB each. A single giant `write` gets truncated mid-arguments.
- **No appended body line may begin with `-`** (e.g. a wrapped `--host H`); the edit parser rejects it as a deletion row. Keep flag-like tokens inline.
- Never end a PUT mid-sentence; if a sentence spills, include ALL its lines in the same PUT, then verify the seam region reads cleanly.
## 4. Structure that worked (7-task, 13-commit branch)
- Stat cards (commits, tasks, tests green, key counts) + sticky anchor nav.
- Commit-arc SVG (dots on a timeline, colored by task; dashed edge for a task spanning two commits).
- One section per atomic task: `why` callout → SVG before/after diagram → per-commit `File | What | Why` tables.
- Footer: deploy-day notes, trash/retirement pending approvals.
- CSS: only theme vars (`--fg --muted --surface --border --accent --success --error --c1..--c6`) with a `prefers-color-scheme: dark` block; SVGs style via `currentColor`/vars; no external assets, no scripts.
## 5. Validate before opening
Python `html.parser` pass: tag stack empty, zero mismatches (treat `path rect circle line marker` + void HTML tags as self-closing); assert every nav anchor id exists; print table/figure counts. Then:
```
xdg-open "file:///home/blank/reports/<file>.html"
```
`xdg-open` may block in foreground — background it, don't poll.
@@ -0,0 +1,23 @@
---
name: repo-wide-terminology-sweep
description: "Rewrite or retire a phrase/term across every comment in a repo (e.g. jargon ban, de-law comments) without breaking tests or leaving ragged prose seams"
---
Use when asked to eliminate/replace a word or phrase in comments across a whole repo (jargon bans, style cutovers, retiring event-narrative lore).
## Procedure
1. **Census with word boundaries.** `git ls-files -z | xargs -0 grep -inwE "term(s)?"` — plain substring grep drowns the list in false positives (e.g. `law` matches inside `alloy`-adjacent noise, `download`, `flaw`). Exclude docs explicitly if scope is comments only (`grep -v "^docs/\|^AGENTS.md:"`).
2. **Dump all hits with line numbers** in one pass, then batch-read each site's surrounding block (`read file:start-end`). Comments span multiple lines; a single hit line rarely carries the full claim.
3. **Check tests pin comment text BEFORE rewriting.** Suites sometimes grep exact comment strings; those seams need rewiring (test the contract, not the prose), found by running affected bats/pytest files after each file's edit, not at the end.
4. **Rewrite around the mechanism, not the incident.** Replace dated narratives and jargon with: the constraint, the mechanism enforcing it, the consequence of violating it. Keep project-specific parameters (paths, flags, sizes) — they are the subject; generalize the *reasoning* so it transfers.
5. **Edit multi-line blocks as whole blocks.** When replacing the middle line of a wrapped sentence, include ALL continuation lines in the replacement body. Partial bodies leave seams: sentences ending mid-clause, duplicated lines, orphaned fragments.
6. **Seam hunt after editing.** Review `git diff -U0` added lines for the block mid-sentence heuristic (`grep -E "(the|a|of|and|at|per|by|is|in|to|for)$"`), then re-read every site where the diff boundary fell inside a paragraph; verify the full sentence reads coherently.
7. **Verify behavior-identical.** `bash -n` every touched shell file, `py_compile` touched Python, parse touched YAML, run the FULL suite — comment sweeps must exit 0 with the same test count as before. A green suite does not prove prose is intact; step 6 does.
8. **One atomic commit**; state the rewrite law in the message with a few old→new example pairs.
## Pitfalls
- Jargon often escapes into identifiers and log strings (`scale_law` variable, refusal messages) — grep word-boundary matches in code, rename locals/messages only if no test pins the string.
- Line anchors drift between files edited in the same batch; on a stale-anchor error the tool shows real content — re-target, don't re-read blindly.
- `.AGENTTRASH/` and retired `old/` trees: exclude from scope, they are archives.

Some files were not shown because too many files have changed in this diff Show More