Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3022277167 | ||
|
|
689ada428f | ||
|
|
9ebf5fe85f | ||
|
|
86280608e5 | ||
|
|
411b32ff59 | ||
|
|
04f93133fc | ||
|
|
7da5e9dfa8 | ||
|
|
0d43eb0948 | ||
|
|
5110060c24 | ||
|
|
6f1f55345a | ||
|
|
6af3714bd0 | ||
|
|
495e2229f5 | ||
|
|
37ef4897ae | ||
|
|
06e549cb27 | ||
|
|
c96741794a | ||
|
|
e14d0906ea | ||
|
|
d5be16127b | ||
|
|
b7aef40266 | ||
|
|
fafdaa2617 | ||
|
|
7213068d91 | ||
|
|
05dbe8c5c1 | ||
|
|
806352f8f6 | ||
|
|
34a7af2847 | ||
|
|
635c64d773 | ||
|
|
64d9c54657 | ||
|
|
aec7f0f068 | ||
|
|
4023b21365 | ||
|
|
e14fc884a3 | ||
|
|
b3a84c5bb8 | ||
|
|
9e6254108a | ||
|
|
ce87958b77 | ||
|
|
802014fea9 | ||
|
|
e22acf4913 | ||
|
|
c14c64d6c0 | ||
|
|
6e3e5594eb | ||
|
|
36ff3742b4 | ||
|
|
fd6a07bb58 | ||
|
|
1921d76319 | ||
|
|
d302151a8d | ||
|
|
5b6f4381b0 | ||
|
|
cae49fcd09 | ||
|
|
ee292ffd10 | ||
|
|
dd4713fa81 | ||
|
|
a8727ddf0b | ||
|
|
2faeab665a | ||
|
|
62b3b92e69 | ||
|
|
ec6a707fe4 | ||
|
|
ac5b377750 |
+87
-10
@@ -1,25 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Secret gate: block any commit whose staged diff contains live-secret shapes.
|
||||
# This repo is pushed to a Gitea remote; anything committed is instantly visible
|
||||
# Secret gate: block any commit whose staged diff contains live-secret shapes or
|
||||
# credential assignments. This repo IS the deployment and every workstation
|
||||
# pulls it, so a committed secret is instantly in every agent's context window
|
||||
# on the fleet. Deliberate override: ALLOW_SECRETS=1 git commit ...
|
||||
#
|
||||
# Two rules, deliberately different in scope:
|
||||
#
|
||||
# 1. Token SHAPES are scanned across the ENTIRE staged diff, whatever the file
|
||||
# type. The first version of this gate whitelisted extensions
|
||||
# (*.md *.yml *.yaml *.json *.toml *.sh *.ts *.js), so a `postgres.env`, a
|
||||
# `.container` quadlet, an `.ini`, or an extension-less dotfile — exactly
|
||||
# the shapes the dotfiles tier is made of — was never read at all.
|
||||
# 2. Credential ASSIGNMENTS (PASSWORD=…, secret_key: …) are scanned only in
|
||||
# non-prose files. The auto-learned memory tier is prose ABOUT credentials,
|
||||
# and a gate that fires there gets overridden until it means nothing; the
|
||||
# 2026-10-20 audit measured 7 such prose hits on the tracked tree. Markdown
|
||||
# keeps shape detection only, which is what it had before.
|
||||
#
|
||||
# Bcrypt hashes and Basic-auth headers joined the shape list on 2026-10-20
|
||||
# (issue #40) after a live Caddy basicauth hash plus its plaintext password —
|
||||
# and the Pi-hole internal password — were found in a tracked memory file.
|
||||
set -uo pipefail
|
||||
|
||||
staged=$(git diff --cached --diff-filter=ACM -U0 -- '*.md' '*.yml' '*.yaml' '*.json' '*.toml' '*.sh' '*.ts' '*.js')
|
||||
[ -z "$staged" ] && exit 0
|
||||
shapes='sk-[A-Za-z0-9]{18,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|glpat-[A-Za-z0-9_-]{20}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|BEGIN [A-Z ]*PRIVATE KEY|aes_[A-Za-z0-9/+]{40}|eyJ[A-Za-z0-9_-]{20,}\.ey|\$2[aby]\$[0-9]{2}\$[./A-Za-z0-9]{20,}|Authorization:[[:space:]]*[Bb]asic[[:space:]]+[A-Za-z0-9+/=]{24,}'
|
||||
|
||||
patterns='sk-[A-Za-z0-9]{18,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|glpat-[A-Za-z0-9_-]{20}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|BEGIN [A-Z ]*PRIVATE KEY|aes_[A-Za-z0-9/+]{40}|eyJ[A-Za-z0-9_-]{20,}\.ey'
|
||||
# `newpass1234`, `secret_key: "rotated-value"` — an assignment with a literal
|
||||
# value. `$VAR`, `${VAULT}`, <placeholders> and empty values cannot match.
|
||||
assign='[A-Za-z0-9_]*(PASSW|SECRET|TOKEN|API_?KEY|ACCESS_KEY|PRIVATE_KEY|CREDENTIAL)[A-Za-z0-9_]*[[:space:]]*[:=][[:space:]]*["'"'"']?[A-Za-z0-9/+._@-]{8,}'
|
||||
placeholders='changeme|change[-_]|placeholder|redacted|example|sample|dummy|nokey|no[-_]key|not[-_]needed|your[-_]|localhost|xxxx|yyyy|\$\{|<[a-z]|\*\*\*|%s|null|false|true'
|
||||
|
||||
# Exemptions: a variable naming a location (`…_FILE=`, `…_PATH=`) or a value
|
||||
# that is a filesystem path is a pointer, not a credential. Flagging those is
|
||||
# how a gate gets permanently overridden.
|
||||
assign_exempt='(_FILE|_PATH|_DIR|_URL|_ENDPOINT|_SOCKET)[[:space:]]*[:=]|[:=][[:space:]]*["'"'"']?(/|\.\./)'
|
||||
# Fail closed: a gate that cannot read the staged diff must not report success.
|
||||
if ! files=$(git diff --cached --diff-filter=ACM --name-only 2>&1); then
|
||||
{
|
||||
echo "SECRET GATE: cannot read the staged diff — refusing to commit."
|
||||
printf ' %s\n' "$files"
|
||||
echo "Override deliberately: ALLOW_SECRETS=1 git commit ..."
|
||||
} >&2
|
||||
[ -n "${ALLOW_SECRETS:-}" ] && exit 0
|
||||
exit 1
|
||||
fi
|
||||
# stderr is captured so the failure branch can quote it; on success it must not
|
||||
# leak a `warning:` line into the file list and have it mistaken for a path.
|
||||
files=$(printf '%s\n' "$files" | grep -vE '^(warning|fatal|error):' || true)
|
||||
[ -z "${files//[[:space:]]/}" ] && exit 0
|
||||
|
||||
report=""
|
||||
exempt=""
|
||||
count=0
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
# Self-exemption, deliberately an exact filename list and not `tests/`: the
|
||||
# gate's fixtures must contain credential-shaped strings or there is nothing to
|
||||
# test against, and this hook documents the shapes it looks for. A secret parked
|
||||
# in any other test file is still caught.
|
||||
case "$f" in
|
||||
.githooks/pre-commit|tests/secret-gate.bats) exempt="$exempt $f"; continue ;;
|
||||
esac
|
||||
diff=$(git diff --cached -U0 -- "$f")
|
||||
[ -z "$diff" ] && continue
|
||||
# Only ADDED lines can leak. Matching removals too blocks the very commit that
|
||||
# deletes a credential — which is exactly what happened on 2026-10-20, when the
|
||||
# gate refused the redaction of the password it exists to catch.
|
||||
added=$(printf '%s\n' "$diff" | grep -E '^\+' | grep -vE '^\+\+\+' | sed 's/^+//' || true)
|
||||
[ -z "$added" ] && continue
|
||||
hits=$(printf '%s\n' "$added" | grep -Ei "$shapes" || true)
|
||||
if [ "${f##*.}" != "md" ]; then
|
||||
assigns=$(printf '%s\n' "$added" | grep -Ei "$assign" | grep -Ev "$assign_exempt" | grep -Eiv "$placeholders" || true)
|
||||
hits=$(printf '%s\n%s\n' "$hits" "$assigns" | sed '/^[[:space:]]*$/d')
|
||||
fi
|
||||
if [ -n "$hits" ]; then
|
||||
n=$(printf '%s\n' "$hits" | wc -l)
|
||||
count=$((count + n))
|
||||
report+=" $f"$'\n'"$(printf '%s\n' "$hits" | cut -c1-100 | sed 's/^/ /')"
|
||||
fi
|
||||
done <<< "$files"
|
||||
|
||||
# Say so out loud: an exemption that prints nothing is how a carve-out becomes
|
||||
# invisible, and invisible is the property a secret wants.
|
||||
if [ -n "$exempt" ]; then
|
||||
printf 'secret-gate: self-test fixtures exempt:%s\n' "$exempt" >&2
|
||||
fi
|
||||
|
||||
hits=$(printf '%s\n' "$staged" | grep -Ei "$patterns" || true)
|
||||
if [ -n "${ALLOW_SECRETS:-}" ]; then
|
||||
[ -n "$hits" ] && printf 'secret-gate: ALLOW_SECRETS set, allowing %s secret-shaped string(s) through.\n' "$(printf '%s\n' "$hits" | wc -l)"
|
||||
[ -n "$report" ] && printf 'secret-gate: ALLOW_SECRETS set, allowing %s secret-shaped string(s) through.\n' "$count"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$hits" ]; then
|
||||
if [ -n "$report" ]; then
|
||||
{
|
||||
echo "SECRET GATE: staged diff contains $(printf '%s\n' "$hits" | wc -l) secret-shaped string(s):"
|
||||
printf '%s\n' "$hits" | sed 's/^/ /'
|
||||
echo "SECRET GATE: staged diff contains $count secret-shaped string(s):"
|
||||
printf '%s\n' "$report"
|
||||
echo "Fix it, or override deliberately: ALLOW_SECRETS=1 git commit ..."
|
||||
echo "A credential that reaches a tracked file is already in history on every"
|
||||
echo "machine that pulls this repo — deleting it later does not un-leak it."
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+24
@@ -26,6 +26,10 @@ auth.json
|
||||
**/auth.json
|
||||
.env
|
||||
**/.env
|
||||
*.env
|
||||
# `*.env` is the rule that matters: `.env` alone does not match `postgres.env`
|
||||
# or `frank-job-hunt.env`, and those are the files that actually carry
|
||||
# POSTGRES_PASSWORD / API keys on this fleet (issue #40).
|
||||
trust.json
|
||||
**/trust.json
|
||||
*.pem
|
||||
@@ -74,3 +78,23 @@ wt/
|
||||
# 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-*
|
||||
|
||||
# --- theme media: content the pipeline consumes, not configuration ---------
|
||||
# 23 wallpapers, 80 MiB. Tracked until 2026-10-20: every clone of a config repo was
|
||||
# carrying a library no diff can review. The live library at
|
||||
# ~/.local/share/noctalia/wallpapers is a real directory of real files (measured
|
||||
# 2026-10-20), so this path in the repo was a duplicate of it, not its source — and
|
||||
# note that untracking deletes the copy in every other checkout regardless of
|
||||
# `git rm --cached` on the branch. A fresh box starts with an empty library; every
|
||||
# filename carries its wallhaven ID, so `curl -L -o <name> https://wallhaven.cc/w/<id>`
|
||||
# puts it back. Law: omp-docs/tools/dotfiles-deploy.md
|
||||
dotfiles/noctalia/.local/share/noctalia/wallpapers/
|
||||
|
||||
# --- generated theme output: machine-written, never reviewable, never deployed ---
|
||||
# noctalia/matugen rewrite these in place; a tracked copy is a stale snapshot that
|
||||
# deploy.sh would push back over a live palette. Patterns match the exclusion list in
|
||||
# scripts/deploy.sh. Law: omp-docs/inventory/home-config-census.md, Refs #42
|
||||
dotfiles/**/themes/noctalia*
|
||||
dotfiles/**/colors/noctalia*
|
||||
dotfiles/**/noctalia.*
|
||||
dotfiles/**/noctalia-*
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -86,15 +86,47 @@ 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).
|
||||
|
||||
## Agent skills
|
||||
|
||||
The engineering skills (`triage`, `to-tickets`, `to-spec`, `research`,
|
||||
`implement`, …) read their per-repo configuration from three pages in the
|
||||
knowledge base. Those pages own their facts; this is only the pointer.
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Work items are Gitea issues on `dank/command-center` (Dankclaw,
|
||||
`192.168.0.143:3000`) — no `gh`, no `tea`. See
|
||||
[`omp-docs/skills/issue-tracker.md`](omp-docs/skills/issue-tracker.md).
|
||||
|
||||
### Triage labels
|
||||
|
||||
The five canonical roles, each label string equal to its name. See
|
||||
[`omp-docs/skills/triage-labels.md`](omp-docs/skills/triage-labels.md).
|
||||
|
||||
### Domain docs
|
||||
|
||||
Single-context, inside the existing knowledge base: `omp-docs/glossary.md` and
|
||||
`omp-docs/decisions/`. See [`omp-docs/skills/domain-docs.md`](omp-docs/skills/domain-docs.md).
|
||||
|
||||
## Secret gate (HARD rule)
|
||||
|
||||
The pre-commit hook (`.githooks/pre-commit`, enabled via
|
||||
`git config core.hooksPath .githooks`) scans every staged diff for live-secret
|
||||
shapes (`sk-…`, `ghp_…`, `github_pat_…`, `glpat-…`, `xox…`, `AKIA…`, PEM headers,
|
||||
JWTs) and blocks the commit. Quarantine list — never tracked, ever: `auth.json`,
|
||||
`.env`, `trust.json`, `*.pem`, `*.key`, `identity*`. Deliberate override:
|
||||
`git config core.hooksPath .githooks`) blocks the commit in two ways. Token
|
||||
**shapes** (`sk-…`, `ghp_…`, `github_pat_…`, `glpat-…`, `xox…`, `AKIA…`, PEM
|
||||
headers, JWTs, bcrypt hashes, `Authorization: Basic` headers) are scanned across
|
||||
the **entire** staged diff — every file type, because the first version of the
|
||||
gate whitelisted extensions and never read a `.env`, `.container`, `.ini`, or
|
||||
extension-less dotfile at all. Credential **assignments** (`PASSWORD=…`,
|
||||
`secret_key: …`) are scanned in non-prose files only; markdown keeps shape
|
||||
detection, because the auto-learned memory tier is prose *about* credentials and a
|
||||
gate that fires there gets overridden until it means nothing. The gate **fails
|
||||
closed**: if it cannot read the staged diff it refuses the commit rather than
|
||||
passing it. Quarantine list — never tracked, ever: `auth.json`, `.env` and
|
||||
`*.env` (`postgres.env`, `frank-job-hunt.env` are the ones that carry real
|
||||
values), `trust.json`, `*.pem`, `*.key`, `identity*`. Deliberate override:
|
||||
`ALLOW_SECRETS=1 git commit …`. Remember why: a secret in a tracked file is a
|
||||
secret in every agent's context window on every machine that pulls this repo.
|
||||
secret in every agent's context window on every machine that pulls this repo, and
|
||||
redacting it later does not un-leak the history.
|
||||
|
||||
## Machine facts (verify before trusting)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: amd-apu-vulkan-tenant-onboarding
|
||||
description: "Add a ggml/Vulkan inference tenant to an AMD APU (gfx1151 / Strix Halo iGPU) that already serves another engine: APU memory census, RADV heap gate, render-node resolution, build-side glslc/ICD deps, co-tenancy measurement."
|
||||
---
|
||||
|
||||
Use when a ggml-based engine (llama.cpp, qwentts.cpp, anything built on ggml) must move from CPU to the **iGPU** of an AMD APU (gfx1151 / Strix Halo / Ryzen AI Max) that **already serves another engine on the same card**. Answers two questions only a booted host can: what gate the second tenant must clear, and which files the build needs to clear it.
|
||||
|
||||
## 1. The usual VRAM reasoning is wrong on an APU
|
||||
|
||||
`/sys/class/drm/cardN/device/mem_info_vram_total` reports a small **carve-out** (2 GiB class), and `rocm-smi`'s "dedicated" figure is not the working set either. The real working set lives in **GTT / system pages**: `/proc/meminfo` → `GTT` and `System pages`. A second GPU tenant therefore consumes host RAM from the same pool the first tenant's weights and the OS use — no supplementary groups, no separate aperture to reserve.
|
||||
|
||||
- Price the tenant from a **booted** footprint, never from `weights + KV + buffers`: on an APU the compute buffers and any projector ride on GTT, which is host RAM; check `free` *after* the load, not before.
|
||||
- Never subtract a "dedicated" figure from it to find headroom.
|
||||
- Unheld fabric clock (`pp_dpm_mech` = `auto`) is a hazard only when the NPU joins too; a pure iGPU pair is unaffected — check it before adding a third tenant.
|
||||
- Swap on a host with GPU tenants protects co-tenants; paging a KV pool is worse than the OOM it prevents.
|
||||
|
||||
## 2. The gate that decides feasibility: the RADV heap a container sees
|
||||
|
||||
`rocm-smi` and `free` cannot see this number; only a container off the **engine's own base image** can:
|
||||
|
||||
```bash
|
||||
# one-off container, no model, no load, no compute — safe beside production
|
||||
podman run --rm --device /dev/dri --group-add keep-groups <engine-base> bash -c \
|
||||
'apt-get update -qq >/dev/null 2>&1; apt-get install -y -qq --no-install-recommends mesa-vulkan-drivers vulkan-tools glslang-tools >/dev/null 2>&1; vulkaninfo --text'
|
||||
```
|
||||
|
||||
Read `VkPhysicalDeviceMemoryProperties` → the `DEVICE_LOCAL` heap size. On a hybrid Strix Halo box Mesa splits the aperture (~1/3 : 2/3), so `DEVICE_LOCAL` is the budget the ggml tool will actually allocate against — measured 77.33 GiB *with the first tenant loaded*, against a 2 GiB DRM carve-out. Two traps, both measured:
|
||||
|
||||
- `vulkaninfo --batch` does not exist in Ubuntu 24.04's `vulkan-tools` (prints help, exits 1). Every `vulkaninfo` parse needs `--text`.
|
||||
- The container may have **no NVIDIA ICD**, so Vulkan index `0` *is* the iGPU — but do not trust ordering by index: `--text` and read `deviceName` + `driverID` (`RADV_GFX1151` vs `NVIDIA`). A hand-picked `/dev/dri/renderDN` is worse than useless on a hybrid host: **`renderD128` there is the NVIDIA card and `renderD129` the amdgpu iGPU**; resolve the node with `readlink -f /sys/class/drm/renderD*/device/driver` and pass `/dev/dri` wholesale, letting the driver pick by name.
|
||||
- A rootless container that drops its supplementary groups enumerates **nothing**. `--group-add keep-groups` (quadlet: `GroupAdd=keep-groups`) plus the host udev rule that makes the render nodes group-writable is the whole requirement; `--device /dev/dri` alone is not enough.
|
||||
|
||||
## 3. Build-side deps: the configure fails before the device ever matters
|
||||
|
||||
ggml's `src/ggml-vulkan/CMakeLists.txt` requires, in order: `find_package(Vulkan COMPONENTS glslc REQUIRED)` then `find_package(SPIRV-Headers CONFIG REQUIRED)`, and it compiles its shaders with **glslc**, never `glslangValidator`. On Ubuntu 24.04 (measured against the digest-pinned `ubuntu:24.04` base, 2026-10-19):
|
||||
|
||||
- `glslang-tools` ships `glslangValidator` and **NOT `glslc`** → `command -v glslc` empty, `find_package(Vulkan … glslc REQUIRED)` fails, and the Vulkan backend silently never exists.
|
||||
- The package is the standalone **`glslc`** (universe, `2023.8-1build1`). Working build-stage set: `libvulkan-dev glslc spirv-headers` — `libvulkan-dev` provides `vulkan/vulkan.h` + the `VulkanHeaders`/`VulkanLoader` CMake configs `find_package(Vulkan)` resolves to, `spirv-headers` ships `/usr/share/cmake/SPIRV-Headers/SPIRV-HeadersConfig.cmake`. A configure probe in the pinned base prints `Found Vulkan: … found components: glslc` then `Including Vulkan backend`.
|
||||
- **The archive's glslc is old.** 2023.8 compiles `GL_KHR_cooperative_matrix` (the path ggml's vectored quantized matmul needs) but reports `cooperative_matrix2`, `integer_dot_product`, `bfloat16`, `float_e2m1/e4m3` as unsupported, so those shader families are compiled out. If a Vulkan prefill reads as inexplicably slow, suspect the **shader compiler before the device**; the fix is a pinned glslang build passed as `-DVulkan_GLSLC_EXECUTABLE=<path>`, not a different runtime.
|
||||
- Runtime stage needs the loader **and an ICD**: `libvulkan1` + `mesa-vulkan-drivers`. Loader-without-ICD enumerates zero devices, and ggml-based servers die in device lookup — often *after* reading the weights, so the journal reads like a model fault.
|
||||
- Ship **no NVIDIA ICD** on purpose: one Vulkan device in the enumeration is what makes the *name* `Vulkan0` safe to write into config. Prefer the name over an index in every flag.
|
||||
|
||||
## 4. Prove enumeration without allocating anything
|
||||
|
||||
Do not boot a scratch second engine beside production to ask which devices exist — a second model load is a real GPU tenant, and fleet law stops that. Instead run the binary with a device name that cannot exist and read its error; it names what it enumerated, with no allocation and no model resident:
|
||||
|
||||
```bash
|
||||
podman run --rm --device /dev/dri --group-add keep-groups --entrypoint /app/tts-server <image> \
|
||||
--model /dev/null --codec /dev/null --device NoSuchDevice 2>&1 | tail -20
|
||||
```
|
||||
|
||||
## 5. The rollback must be a flag, not a rebuild
|
||||
|
||||
Put the device in the env file as a value the entrypoint passes only when set, and make **set-but-empty** mean "pass no flag at all" — the binary's own default (CPU, always built) is the rollback. `: "${VAR:=default}"` is the WRONG form: `:=` fires on an empty value too, so the documented rollback line `VAR=` is silently rewritten back to the GPU and the only way out is a rebuild. Use:
|
||||
|
||||
```bash
|
||||
if [[ ! -v QWENTTS_DEVICE ]]; then QWENTTS_DEVICE=Vulkan0; fi
|
||||
```
|
||||
|
||||
(`podman`'s `--env-file` passes `VAR=` through as set-and-empty, and `source` does the same, so both readers agree.) Pin it in a test: empty device ⇒ the flag is absent from argv.
|
||||
|
||||
## 6. Co-tenancy measurement (only after the tenant is live)
|
||||
|
||||
The gate is the **first tenant's** performance with a real request from the second in flight — not the second tenant's own speed. And price the tenant from a booted footprint, never from weights+KV+buffers: on an APU the compute buffers and any projector ride on GTT, which is host RAM; check `free` after the load, not before.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: command-center-bundle-lint-gate
|
||||
description: "Before committing docs in dank/command-center, lint the real omp-docs bundle directly — the bats suite only lints fixtures, so a green suite is not proof."
|
||||
---
|
||||
|
||||
# The bundle lint gate (`dank/command-center`)
|
||||
|
||||
Adding or editing pages under `omp-docs/`? `bats tests/` going green proves
|
||||
nothing about the pages you just wrote.
|
||||
|
||||
## Why this exists
|
||||
|
||||
`tests/kb-lint.bats` exercises `scripts/kb-lint.sh` against `mktemp -d` fixtures
|
||||
(`"conformant bundle passes"`, `"missing frontmatter is reported"`, …). For years
|
||||
nothing in the suite pointed the linter at the shipped tree, so a page with no
|
||||
frontmatter, an index that stopped listing a file, or a broken relative link
|
||||
committed under a green suite. (Two tests appended 2026-10-20 now run both
|
||||
checkers against `omp-docs/`; verify they are still present before assuming the
|
||||
suite covers it — `grep -n omp-docs tests/kb-lint.bats`.)
|
||||
|
||||
The general law: **a lint suite that tests only the linter does not lint your
|
||||
content.** Check which one a suite runs before calling green a gate.
|
||||
|
||||
## Gate a docs commit
|
||||
|
||||
```bash
|
||||
cd <worktree>
|
||||
bats tests/ ; grep -E '^1\.\.' /tmp/… # suite (count, failures)
|
||||
./scripts/kb-lint.sh omp-docs # want: kb-lint: 0 violation(s)
|
||||
python3 scripts/kb-coverage.py omp-docs # want: index-coverage: 0 issue(s)
|
||||
```
|
||||
|
||||
`kb-lint.sh <kb-dir>` (default `omp-docs`) = OKF v0.1 conformance **plus link
|
||||
integrity**, so it is the check that absolute bundle links like
|
||||
`/tools/gitea/client-access.md` resolve. `kb-coverage.py` = every page listed by
|
||||
its directory `index.md` (repo doctrine: nothing may appear in a directory its
|
||||
`index.md` does not list).
|
||||
|
||||
OKF shape that passes: every concept page carries frontmatter with a `type`
|
||||
(`type: reference` + `title`/`description`/`tags`/`timestamp`/`verified`);
|
||||
`index.md` files carry **no** frontmatter; new pages get an entry in their
|
||||
directory index in the same commit, plus a dated entry in `omp-docs/log.md`
|
||||
(one entry = what changed + the durable lesson, a few lines, never a narrative).
|
||||
|
||||
## Commit-message trap in this harness
|
||||
|
||||
A multi-line message written as a heredoc inside a chained `bash` command
|
||||
(`git commit -q -F - <<'MSG' … && …`) fails with
|
||||
`pi-natives:command: syntax error at end of input`. Write the message to
|
||||
`/tmp/msg-<topic>.txt` with the write tool and `git commit -q -F /tmp/msg-<topic>.txt`.
|
||||
Keep the `&&` chain so a failed gate stops the commit — hooks (secret gate) stay armed,
|
||||
never `--no-verify`.
|
||||
|
||||
## Land and reap
|
||||
|
||||
Merge is form-encoded `do=merge&merge_style=no-ff` and answers `200` with an
|
||||
**empty body**; never read that as ambiguous — verify by landing. Then
|
||||
`git -C ~/.omp merge --ff-only origin/main` (the checkout *is* the deployment;
|
||||
dirty `agent/memories/`, `agent/config.yml`, `APPEND_SYSTEM.md` are machine-tier
|
||||
live state and do not block an ff whose diff misses them), fast-forward the
|
||||
session's `wt/*` scratch branch the same way, then `git worktree remove <build-wt>`
|
||||
and `git branch -d <type>/<issue>-<slug>` (`-d` succeeding is itself proof of
|
||||
merge).
|
||||
|
||||
Stray files whose names start with `-` (e.g. `-c`, `-es` — vim takes a
|
||||
`--startuptime` flag name as the output filename) show as untracked dirt:
|
||||
`mv ./-c .AGENTTRASH/<date>-<meaning>.log`, never `rm`.
|
||||
@@ -1,49 +1,62 @@
|
||||
---
|
||||
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."
|
||||
description: "Add/repair a LAN DNS record for a new *.franksdank.cloud service on Dankclaw's Pi-hole v6 (hosts/custom.list + repo addn-hosts, plus the public Porkbun A record side) — and triage \"LAN names stopped resolving\" including the FTL boot-time record-load failure."
|
||||
---
|
||||
|
||||
Use when a new `*.franksdank.cloud` service resolves publicly/LAN-needs a local address, on Dankclaw (Pi-hole v6 container, quadlet `pihole.container`).
|
||||
# Dankclaw Pi-hole LAN DNS record
|
||||
|
||||
## Ground truth layout (verified 2026-10-11)
|
||||
## What this fixes
|
||||
|
||||
- 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.
|
||||
A new `*.franksdank.cloud` service resolves publicly (via Porkbun → `franksdank.cloud` A record → Caddy on Dankclaw `192.168.0.143`) but **does not resolve on the LAN**. Pi-hole is the LAN DNS server (`dankclaw_default` bridge, published port 53) and serves local records only from files it imports.
|
||||
|
||||
## Procedure (30 s, proven)
|
||||
## Live truth
|
||||
|
||||
```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
|
||||
- **The repo owns the records.** `dankclaw/pihole/addn-hosts` (hosts-style lines: `192.168.0.143 <name>`) is copied by `deploy-remote.sh` to `/home/dank/Containers/pihole/addn-hosts` and bind-mounted **read-only** into the container at `/etc/pihole/hosts/addn-hosts` (`Volume=/home/dank/Containers/pihole/addn-hosts:/etc/pihole/hosts/addn-hosts:ro` in `dankclaw/pihole/quadlet/pihole.container`). Add the name there, in the repo, then deploy — that is the durable path. `tests/pihole.bats` validates every line (IPv4, LAN range, name suffix, Caddyfile cross-check).
|
||||
- `/home/dank/Containers/pihole/etc-pihole/hosts/custom.list` also exists on the host (older mechanism, mode `640` owned by the container uid, root reads it with `sudo`). It is **not** the repo's owner file — do not hand-edit it as the fix.
|
||||
- `pihole.toml` carries a `dns.hosts = [ "192.168.0.143 …", … ]` array too (FTL's own in-config records). Both sources exist; if LAN names fail, neither is being applied.
|
||||
- Pi-hole runs as a rootless quadlet named `pihole` — query it with `podman exec pihole …`, never `sudo podman` (per-user storage law).
|
||||
- `pihole.franksdank.cloud` resolves through Pi-hole itself (loop/edge), so a vhost record pointing at `192.168.0.143` is correct for every Caddy-fronted service.
|
||||
|
||||
## Procedure (repo-first)
|
||||
|
||||
1. Append the line to `dankclaw/pihole/addn-hosts`:
|
||||
```
|
||||
192.168.0.143 <name>.franksdank.cloud
|
||||
```
|
||||
One record per vhost; keep it in sync with the Caddyfile (the bats seam cross-checks the two).
|
||||
2. `./tests/run.sh` (the `pihole` seam validates the file), commit, push.
|
||||
3. Deploy so the copy step runs: `./scripts/deploy.sh dankclaw --service pihole`.
|
||||
4. Verify from the box: `getent hosts <name>.franksdank.cloud` → `192.168.0.143`.
|
||||
5. Public edge record (optional; skip when every subscriber is LAN-only): Porkbun `POST /api/v3/json` with `apikey`/`secretapikey`/`command=add`/`type=A`/`hostname=<name>`/`data=76.49.118.136` (see `dankclaw-porkbun-edge`).
|
||||
|
||||
Client-side proof (avoid the resolver trap):
|
||||
|
||||
```
|
||||
sudo resolvectl flush-caches # systemd-resolved otherwise keeps the negative cache
|
||||
curl -sv --max-time 5 https://<name>.franksdank.cloud 2>&1 | grep 'Connected to'
|
||||
```
|
||||
|
||||
Hosts files are read-only-readable only via sudo (dir owned by uid 100999). `sudo -n` works on dankclaw.
|
||||
Expected: `Connected to <name>.franksdank.cloud (192.168.0.143)` — LAN route, not the public IP.
|
||||
|
||||
## API maze — DON'T re-explore
|
||||
## Triage: every LAN name broke at once (2026-10-19 law)
|
||||
|
||||
- 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.
|
||||
Signature: **all** `*.franksdank.cloud` return NXDOMAIN / Playwright `ERR_NAME_NOT_RESOLVED` while **external names still resolve**. The external-OK/local-fail split is the tell: FTL is answering external names from cache and holds **no local records at all** after a container/host restart — the mounts and files are intact and readable inside the container (`ls -l /etc/pihole/hosts/`, `grep -c . …/addn-hosts`), `dns.hosts` in `pihole.toml` is complete. The mechanism is unestablished; do not go hunting for a lost file.
|
||||
|
||||
## 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\"}"
|
||||
```
|
||||
ssh dankclaw 'getent hosts grafana.franksdank.cloud' # empty => FTL, not the client
|
||||
ssh dankclaw 'podman exec pihole pihole restartdns reload' # fixes it in seconds
|
||||
```
|
||||
|
||||
- `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.
|
||||
Distinguish before acting: a **single** client failing while others work is FTL rate-limiting that client — `podman exec pihole grep Rate-limiting /var/log/pihole/FTL.log`; a `10.0.2.x` client address is a pasta container's DNS forwarder (SNAT), not a machine. Full causal page: `serverAppConfigs-docs/Infrastructure/Pi-hole.md` (**Records** pitfall + Failure modes table).
|
||||
|
||||
## Client-side cache gotchas
|
||||
## 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.
|
||||
- Do NOT insert local records into SQLite. v6's `gravity.db` `domain` table has NO `type=2` custom-DNS rows in this build.
|
||||
- `pihole restartdns reload` re-imports the hosts files **and** `dns.hosts`; that is the remedy for the boot-load failure above. Plain `pihole restartdns` also restarts the resolver — brief LAN-wide DNS blip.
|
||||
- `dig @<ip> <name>` from the workstation bypasses systemd-resolved — use it to check FTL's answer directly (`+short @192.168.0.143`).
|
||||
|
||||
## Cross-reference
|
||||
|
||||
- Fleet DNS flow: `dankclaw/Caddyfile` (Caddy binds `:443` on the host, serves `*.franksdank.cloud` vhosts)
|
||||
- Pi-hole service page: `serverAppConfigs-docs/Infrastructure/Pi-hole.md`, flow page `Infrastructure/LAN-DNS.md`
|
||||
- Public edge: managed skill `dankclaw-porkbun-edge`
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: dankvoice-bats-serialization
|
||||
description: "Run dankvoice bats suites without hangs or false failures: serialize dist rebuilds, never edit src mid-run, and rebuild before committing the tracked bundle."
|
||||
---
|
||||
|
||||
Use when running the dankvoice `bats tests/` suite on the shared box, when a suite run hangs or times out, or when the "committed dist is byte-identical to a fresh build" pin test fails after a commit.
|
||||
|
||||
## The two serialization hazards
|
||||
|
||||
### 1. `setup()` rebuilds `dist/` from `src/`
|
||||
Every test runs `cd "$REPO" && bun run build` in `setup()`. Consequences:
|
||||
|
||||
- **Never edit `src/` while a suite is running.** The next test's `setup()` compiles whatever is on disk at that instant, so an in-flight edit gets attributed to the run and produces a failure with no owner. Finish the gate, then write.
|
||||
- **Two suites on the same checkout race on `dist/`.** Serialize: run one, wait for its report, then start the next. Multiple worktrees of the repo each build their own `dist/` and do not collide.
|
||||
- Suite runtime is ~85 s for 55 tests; a backgrounded job with a 600 s deadline is the right shape (`timeout: 600`, `async: true`). Never poll it — `setup()` output lands in the conversation when the job ends.
|
||||
|
||||
### 2. `dist/` is tracked, and a pin test enforces it
|
||||
One test rebuilds and compares against the committed bundle. Therefore the commit order is:
|
||||
|
||||
```bash
|
||||
bun run build # BEFORE staging
|
||||
git add -A && git commit -m '…'
|
||||
bats tests/ # gate AFTER the commit
|
||||
```
|
||||
|
||||
Committing `src/` changes without rebuilding first fails the pin test (`not ok … byte-identical to a fresh build`) — the tracked bundle is the *previous* state. If that happens on an unpushed branch, fix it by amend rather than a follow-up commit:
|
||||
|
||||
```bash
|
||||
bun run build && git add dist && git commit --amend --no-edit
|
||||
```
|
||||
|
||||
Verify freshness any time the pin is in doubt:
|
||||
|
||||
```bash
|
||||
bun run build >/dev/null 2>&1 && git status --porcelain dist # empty = committed == fresh build
|
||||
```
|
||||
|
||||
## If a backgrounded gate job vanishes
|
||||
A crash can kill the job before its report; `read proc://bg_N` returning "not found" means it is gone, not that it failed. Reconstruct what landed before resuming — `git log --oneline -3`, `git status --porcelain`, then the freshness check above — and rerun the suite; the commit may well have succeeded while the gate never ran.
|
||||
|
||||
## Gotchas
|
||||
- `DV_HOLD` is seconds (`sleep "${DV_HOLD:-0.1}"`), so `DV_HOLD=3` really holds the stub for three seconds.
|
||||
- Isolation tests must not count shared state: any assertion that sweeps `$TMPDIR` or `~/.omp/cache` observes every other live session on the box (each narrating session creates `dankvoice-*` scratch dirs). Point `TMPDIR` at a private directory instead — see the `cutting off mid-utterance leaves no orphaned audio behind` test.
|
||||
- `bun run build` does not typecheck. Run `bunx tsc --noEmit` separately when moving code between modules; the bundler happily emits a bundle that references a deleted identifier.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: dankvoice-bundle-swap-red-proof
|
||||
description: "Prove a dankvoice behavior test is red against the pre-change bundle by pointing DV_EXT at git show HEAD:dist/index.js, with the driver extracted from the bats heredoc into a pinned scratch directory. Use before claiming a new bats assertion is red-capable."
|
||||
---
|
||||
|
||||
Prove a new `tests/behavior.bats` assertion is **red-capable** — that it fails against the *previous* build, not just passes against the new one — without stashing, branching, or editing `src/`.
|
||||
|
||||
## Why
|
||||
|
||||
`tests/behavior.bats` is a black-box suite: the plugin bundle is loaded through `DV_EXT`, so a *different bundle* is the red side. Stashing the fix would break the `dist` byte-identity pin test and the shared worktree.
|
||||
|
||||
## Working recipe
|
||||
|
||||
```bash
|
||||
cd ~/Projects/dankvoice # or the feature worktree: ~/Projects/wt-dv-*
|
||||
bun run build # current src = the green side
|
||||
git show HEAD:dist/index.js > /tmp/old-dist.js # the red side (pre-change bundle)
|
||||
|
||||
W=/tmp/dv-redproof # PINNED name; see traps below
|
||||
rm -rf "$W" && mkdir -p "$W/bin"
|
||||
cat > "$W/player.sh" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >> "$DV_STUB_LOG"
|
||||
for arg in "$@"; do case "$arg" in *.wav) cp "$arg" "$DV_COPY" ;; esac; done
|
||||
sleep "${DV_HOLD:-0.1}"
|
||||
SH
|
||||
chmod +x "$W/player.sh"
|
||||
ln -s "$W/player.sh" "$W/bin/pw-play"
|
||||
printf '#!/usr/bin/env bash\nprintf "116\\talsa_output.usb-KT.0\\taudio/sink\\t*\\n"\n' > "$W/bin/wpctl"
|
||||
chmod +x "$W/bin/wpctl"
|
||||
# The driver lives inside the bats file as a heredoc: extract it verbatim.
|
||||
end=$(grep -n '^TS$' tests/behavior.bats | cut -d: -f1)
|
||||
sed -n "86,$((end - 1))p" tests/behavior.bats > "$W/driver.ts"
|
||||
|
||||
export DV_EXT="$PWD/dist/index.js" DV_STUB="$W/player.sh" DV_STUB_LOG="$W/player.log" \
|
||||
DV_COPY="$W/played.wav" DV_OMPCONFIG=/dev/null DANKVOICE_CONFIG="$W/settings.json" \
|
||||
DV_MODELS="$W/models.yml" DV_WPCTL=x DV_SELECT=""
|
||||
for ext in /tmp/old-dist.js "$PWD/dist/index.js"; do
|
||||
rm -f "$W/player.log" "$DANKVOICE_CONFIG"
|
||||
DV_EXT="$ext" bun "$W/driver.ts" <scenario> 2>&1 | grep -E '^PLAYS|^CALLS|^NOTIFY|^SETTINGS'
|
||||
done
|
||||
```
|
||||
|
||||
The scenario name is the driver's third argv (`speak`, `auto`, `repeat`, `humanizecycle`, `badurl`, …); the bats `-f` test body's env (`DV_PLAYER_BIN`, `DV_SETTINGS_JSON`, `DANKVOICE_VERBOSITY`, …) is passed the same way. `DV_SELECT` holds the menu answers, `|`-separated, consumed in order.
|
||||
|
||||
## Traps, each one cost a turn
|
||||
|
||||
- **The scratch directory must be pinned by name.** `ls -dt /tmp/tmp.*` picks up *other sessions'* temp dirs, and then `player.sh` and `driver.ts` live in different directories — the run silently does the wrong thing.
|
||||
- **The stub must append to `$DV_STUB_LOG`.** The driver counts player invocations from that file; a stub that only `sleep`s reports `PLAYS 0` for *both* bundles and the whole proof reads as "fix does nothing".
|
||||
- **`DV_EXT` is required.** Missing it yields `Cannot find package 'undefined'` from the driver, which looks like a broken driver, not a missing env var.
|
||||
- **`DV_SETTINGS_JSON` supports `__PORT__`** (the driver substitutes the fixture port), so a preseed can name a prefixed route — needed whenever the test is about *which* endpoint got resolved.
|
||||
- **Never run this while a `bats` run is in flight** in the same repo: `setup()` runs `bun run build`, so two runs clobber `dist/` and the pin test.
|
||||
|
||||
## Reading the result
|
||||
|
||||
Red side must show the *defect* (`PLAYS 0`, `644`, a chat `CALL` with the wrong prefix, `Invalid URL`), green side the fix — assert on printed markers only (`PLAYS`, `CALLS`, `CALL <url>`, `NOTIFY`, `SETTINGS`). `CALLS` counts `/audio/speech` calls only; the rewrite call appears as its own `CALL /…/chat/completions … sys=<full system prompt>` line, which is how prompt-level knobs (verbosity, heckle) are observed.
|
||||
|
||||
Prefer this over a synthetic leak: if the check already caught a real leftover/failure once, that event *is* the sensitivity proof — say so in the commit message instead of faking one.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: dankvoice-menu-knob-workflow
|
||||
description: "Add a persisted /say knob to dankvoice and test its menu row through the bats driver (DV_SELECT picks, INPUT stub, dist-pin rebuild gate)."
|
||||
---
|
||||
|
||||
Adding a setting to `dankvoice` (`~/Projects/dankvoice`, omp speech extension) means five touchpoints plus one behavioral test. Skip one and TypeScript or a silent drop catches it; skip the test and the row ships dead.
|
||||
|
||||
## The five touchpoints (order matters)
|
||||
|
||||
1. `src/settings.ts` — field on `Settings` (optional) **and** a type-guarded line in `readSettings`. A field without a guard line is written to disk and never read back; tsc stays clean, so nothing else catches it.
|
||||
2. `src/config.ts` — `Config` field (required, defaulted), resolution line, entry in the returned object, and the knob documented in the file header comment (that header is the README's pointer for "full knob list").
|
||||
- Convention: explicit env wins over settings (`env.X === "1" ? true : env.X === "0" ? false : (settings.x ?? false)`); booleans use `1`/`0`, enumerations use the literal value.
|
||||
3. `src/index.ts` — `persistSettings()` key list (a knob missing there never survives the session) **and** the getter in the `new Speaker({...})` deps object.
|
||||
4. `src/utterance.ts` — getter on `SpeakerSettings` if the Speaker reads it at speak time.
|
||||
5. `src/menu.ts` — the row. Rows carry their own `run`; never dispatch on `choice === options[N]`.
|
||||
|
||||
Gate at the call site, not the prompt, when a mode must not apply to operator-typed text: `Speaker.speak(ctx, text, thinking, auto)` — `auto=true` only from `message_end`/`agent_end`; `/say <text>` passes nothing. Prompt-only gating leaks.
|
||||
|
||||
## Test the row behaviorally, never as a config assertion
|
||||
|
||||
`tests/behavior.bats` drives the built `dist/index.js` against a mock harness. Selects and typed text both come from one pipe-separated queue: `DV_SELECT="row label|sub choice|typed text"`, consumed in call order; a consumed-exhausted `select` returns `options[0]`, so always name the row by its **rendered label** (`"Verbosity: normal (change)"`, `"Intro: none (change)"`).
|
||||
|
||||
- `ui.input(title, placeholder)` in the driver mock logs `INPUT <title> <placeholder>` and takes the next queue entry. If the mock lacks `input`, no typed value was ever testable — add the stub before writing the test.
|
||||
- Scenario pattern that proves a pick works (`case "knobs"`): `say.handler("menu")` → `say.handler("on")` → two `agentEnd(answer(...))` with `settle()` between. Then assert on observable output, not on the settings file:
|
||||
- prompt-shaping knob: `[[ "$output" == *"ONE spoken sentence"* ]]` (directive reached the rewrite request)
|
||||
- framing: `[[ "$output" == *"input=Update, chief!"* ]]`
|
||||
- throttle/framing counting: `[[ "$output" == *"CALLS 1"* ]]`
|
||||
- A guard test whose negative case would pass trivially must also assert its premise, e.g. truncation: assert `CALLS 1` so "no outro" cannot be a green caused by nothing ever being cut.
|
||||
|
||||
Red first, in a batch: write all the new `@test` blocks, run `bats tests/behavior.bats -f "<pattern>"`, confirm the new ones fail, then implement.
|
||||
|
||||
## Gates and gotchas
|
||||
|
||||
- **Two `bats` runs at once corrupt each other**: `setup()` runs `bun run build`, so a concurrent run rebuilds `dist/` mid-suite and the byte-identical pin test fails spuriously. Serialize.
|
||||
- **Edit `src/` → rebuild before commit**: the pin test compares committed `dist/` to a fresh build. Sequence is `bun run build && bunx tsc --noEmit && bats -f <pattern>`, then `git add -A` (dist included). A failure there is normally a stale bundle, not a bad assertion.
|
||||
- `VAR=value some_function args` in bats does reach the child process, so knob tests need no `export`.
|
||||
- `edit` tool: inserting after a line whose content you know but that the current tag never displayed gets refused — the refusal echoes the real line; if it matches intent, re-issue the identical edit instead of re-reading.
|
||||
- Ship: branch `feat/<issue#>-<slug>` off fresh `origin/main`, one commit per knob, PR targets `main`, merge only on Frank's explicit OK (`do=merge`, `merge_style=no-ff`), then `omp plugin upgrade dankvoice` and `cmp` installed `dist/index.js` against the merged one.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: dankvoice-silence-triage
|
||||
description: "Triage silent or late dankvoice auto-speak on the fleet: arming key, /say grammar, endpoint/sink, event granularity, humanizer latency, plus the black-box bats driver."
|
||||
---
|
||||
|
||||
# Triage "dankvoice is silent" on the fleet
|
||||
|
||||
Symptom that recurs: auto-speak only speaks when `/say <text>` is typed
|
||||
explicitly. Work the gates in this order; each has an objective check.
|
||||
|
||||
1. **Armed?** `cat ~/.omp/cache/dankvoice.json | jq .auto` — must be `true`
|
||||
(or `DANKVOICE_AUTO=1`). Arming is read at **startup only** and `/say on`
|
||||
never persists, so a session started before the key was added is silent even
|
||||
though fresh sessions work. `/say start` is *not* a subcommand: unknown
|
||||
arguments are spoken verbatim (it read out the word "start"). `/say on` is
|
||||
the verb; `/say status` must print `auto-speak on`.
|
||||
2. **Endpoint + sink?** `/say status` must show a real endpoint (not
|
||||
`NO ENDPOINT`) and the saved sink must exist:
|
||||
`pw-dump | grep -o 'alsa_output\.[^"]*' | sort -u`.
|
||||
3. **Did it synthesize?** `pgrep -a pw-play` during a turn: one process per
|
||||
segment, `--target <sink>`, wav under `/tmp/dankvoice-*/say-<epochms>.wav`.
|
||||
One temp **directory per utterance chain** — count dirs to count utterances,
|
||||
pids to count segments.
|
||||
4. **Event granularity (the structural cause).** `agent_end` fires once per
|
||||
*prompt* when the loop settles and is skipped when the harness maps a
|
||||
non-terminal settle (`willContinue`) — long runs with background jobs or
|
||||
subagents therefore narrate nothing. Mid-run narration needs `turn_end`
|
||||
(fires per assistant message + tool results). Verify delivery, don't infer
|
||||
it: skill `omp-extension-event-probe`.
|
||||
5. **Latency is not silence.** Narration passes through the humanizer
|
||||
(frank-zealot) before synthesis, so an utterance lands several seconds after
|
||||
its turn; under endpoint load that stretches to tens of seconds.
|
||||
`/say humanize off` = mechanical voice, near-instant. Judge cadence from
|
||||
probe-log timestamps, never from when audio starts.
|
||||
|
||||
## Design rules the plugin now encodes (dank/dankvoice#2→#3)
|
||||
|
||||
- Narrate `turn_end` per turn **plus** `agent_end` for the settled answer;
|
||||
dedupe identical text per run (reset on `agent_start`) so the answer speaks once.
|
||||
- A turn landing while the player is alive is **dropped** — no queue, no
|
||||
interrupting; a tool loop would cut every sentence off after a word.
|
||||
Busy test: `state.child !== undefined && state.child.exitCode === null`.
|
||||
- Tool-only turns (no prose, no narration-flagged thinking) stay silent.
|
||||
- `print`/`json` modes stay silent, so benches never chatter.
|
||||
- Settings writes must **round-trip the file** (`{...onDisk, ...owned keys}`).
|
||||
Rebuilding from owned keys deleted `"auto"` on any menu choice and silently
|
||||
un-armed every later session — the classic cause of "it worked yesterday".
|
||||
|
||||
## Black-box test harness (repo `dank/dankvoice`)
|
||||
|
||||
`tests/behavior.bats` drives the built artifact through an in-process driver:
|
||||
`const say = DV_EXT(ctx, cfg)[0]`, handlers via `const events = new Map();
|
||||
const pi = { on: (n,h)=>events.set(n,h) }`, then `events.get("turn_end")?.({
|
||||
turnIndex:0, message:{role:"assistant",content:[{type:"text",text:"…"}]},
|
||||
toolResults:[]}, ctx)`. Assert on `CALL`/`PLAYS`/`NOTIFY` lines and
|
||||
`SETTINGS <file contents>`; stubs are `DV_PLAYER_CMD` (logs voice+input, honours
|
||||
`DV_HOLD`) and `DV_REWRITE_URL` (404 → falls back to mechanical text). `setup()`
|
||||
runs `bun run build` per test → serialize suites on the shared `dist/`.
|
||||
`tests/packaging.bats` requires `dist/` **committed** (compares to HEAD), so
|
||||
build → commit → the drift pin goes green.
|
||||
@@ -1,36 +1,35 @@
|
||||
---
|
||||
name: docs-dedupe-verify-campaign
|
||||
description: "Run an exhaustive docs dedupe + fact-verification campaign in dank/command-center: scout cluster ledgers, evidence-overridden merges, live fleet probes, log compaction law, and landing via Gitea PR."
|
||||
description: "Run an exhaustive docs dedupe + fact-verification campaign in dank/command-center: scout cluster ledgers, override scouts with evidence, gate every commit, and sweep scaffolding before PR."
|
||||
---
|
||||
|
||||
Campaign procedure for deduping + fact-checking `omp-docs/` in dank/command-center (proven on Refs #29, 2026-10-19: 8 commits, -2,229 lines, 0 lint/coverage/bats failures).
|
||||
# Exhaustive docs dedupe + verification campaign (dank/command-center)
|
||||
|
||||
## 1. Recon
|
||||
- Fresh branch off `origin/main` in a new worktree (`git worktree add ~/.omp/wt/<name> -b docs/<issue>-<slug> origin/main`); the session worktree's `git checkout -b` is policy-blocked.
|
||||
- Run `scripts/kb-lint.sh omp-docs` baseline; write a throwaway index-coverage checker (each dir's `index.md` must link every subdir/file) — mechanical, no scout needed.
|
||||
- Open the Gitea issue first (multi-moving-parts law).
|
||||
## Frame
|
||||
- Read `omp-docs/references/okf-spec.md` + root `omp-docs/index.md` first; inventory with `find omp-docs -name '*.md' | wc -l`.
|
||||
- Run `scripts/kb-lint.sh omp-docs` baseline; index-coverage gate is `scripts/kb-coverage.py omp-docs` (each dir's `index.md` must link every sibling file/subdir).
|
||||
- Branch off fresh `origin/main` in a NEW worktree (`git -C ~/.omp worktree add ~/.omp/wt/<name> -b docs/<issue>-slug origin/main`) — workspace policy blocks `git checkout -b` in session worktrees.
|
||||
- Open the Gitea issue first (multi-moving-parts law); branch name `docs/<issue#>-<slug>`.
|
||||
|
||||
## 2. Scout fan-out (overlap detection)
|
||||
- One `task` batch, `agent: scout`, one task per doc cluster + one cross-cutting sweep (cap 2 concurrent; rest queue).
|
||||
- Each scout writes a ledger to `local://dedupe/<cluster>.md`: inventory table (file, lines, purpose, verdict KEEP/MERGE/DELETE/UPDATE), overlaps, facts-to-keep ledger, inbound links, staleness, claims-needing-verification.
|
||||
- Scouts are LEADS, not truth. Observed failures: fabricated ledger entries (claims about text that does not exist in any file), a DELETE verdict on a generated artifact that is under test (`tools/niri/keybindings.md` is generated by `scripts/gen-keybinds.sh` and tested by `tests/gen-keybinds.bats` — keep), and a KEEP based on misreading a changelog leftover. Re-derive every consequential verdict from the files/repo yourself.
|
||||
## Scout fleet (overlap detection)
|
||||
- One `task` call, `tasks[]` batch, agent `scout`, read-only; one cluster per task (skills trees, llama trees, pi vs packages, niri, sleep/power, core, gitea, gitea+cross-cutting sweep). Cap 2 concurrent; queue rest.
|
||||
- Ledger format per cluster → `local://dedupe/<cluster>.md`: inventory table (file | lines | verdict KEEP|MERGE|DELETE|VENDOR|UPDATE), overlaps, facts-to-keep, inbound links, staleness.
|
||||
- **Scouts lie.** Verified misses in the 2026-10-19 campaign: fabricated ledger entries ("peer-reviewed BrowseComp" claim that exists in no file), a DELETE verdict for a generated artifact that is checked in by design AND under test (`tools/niri/keybindings.md`, see `scripts/gen-keybinds.sh` + `tests/gen-keybinds.bats`), and a KEEP for a leftover the scout misread its own changelog about. NEVER apply a scout verdict without re-checking the file, the tests, and the changelog yourself.
|
||||
|
||||
## 3. Verify fleet facts live (never trust doc dates)
|
||||
- Read-only BatchMode probes: `ssh -o BatchMode=yes dankclaw '...'` (podman ps/inspect, systemctl status, curl localhost, os-release, cpuinfo). `sudo -n` only has systemctl verbs — use `podman exec <ctr> grep/cat` for in-container files.
|
||||
- Update docs with dated drift banners ("probed 2026-10-19") for policy divergence; NEVER silently re-apply a policy — flag as operator decision (e.g. SearXNG engine policy).
|
||||
## Fact verification
|
||||
- Fleet facts: read-only probes over `ssh -o BatchMode=yes dankclaw|dankserver` (podman ps/inspect/top, stat, /etc/os-release, curl localhost APIs, `podman exec <c> <tool> --version`). Live state beats docs even when docs claim recent verification — the 2026-10-18 "audit" still drifted (zealot 27B→9B, build 10754→10777, container census 14→32).
|
||||
- External claims: web_search/web read yourself (no subagents per Frank's rule). Kill fabrications outright (invented distro `FreeToken`, nonexistent `ornith-llama-cpp-setup` tree, unattributable "44.5% peer-reviewed CUA") — delete/drop with an explicit note, never keep "plausible" numbers.
|
||||
- Policy drift (live ≠ tuned docs): flag in the doc + PR body as an operator decision; NEVER silently re-apply policy.
|
||||
|
||||
## 4. Verify external claims via direct web research (no subagents)
|
||||
- Version/protocol/benchmark claims: primary source only (upstream README, raw.githubusercontent, /api/v1/version, arxiv). If a number cannot be re-attributed to a published benchmark, drop it with an explicit note in the doc.
|
||||
- Fabrication hunt: invented distros/projects/model names (found: "FreeToken" distro, ornith-llama-cpp-setup tree, Intel CPU on an AMD box) — delete the artifact, not just the line.
|
||||
## Log compaction (log.md)
|
||||
- Entry law: dated entry = what changed + durable lesson + concept-page pointer; narratives recoverable via `git log --follow`. Record the law at the top of the log. Tag historical-era entries.
|
||||
|
||||
## 5. Log compaction law
|
||||
- Changelog entry = date + what changed + durable lesson + concept-page pointer, few lines. Full narratives live in git (`git log --follow`). Record the law at the top of `log.md`. Never re-expand an entry in place. Keep pi-era entries tagged "(pi era)".
|
||||
## Gates + landing
|
||||
- Gate every commit: `bash scripts/kb-lint.sh omp-docs && python3 scripts/kb-coverage.py omp-docs && bats tests/`. Pre-commit hook prints `error: command not found: caddy` on Caddyfile-touching commits — warning only, commit still lands.
|
||||
- Before opening the PR: `git diff --stat origin/main..HEAD | grep -i 'tmp\|scratch\|\.bak'` — scaffolding merged under a `.tmp-` name in the 2026-10-19 campaign and needed a follow-up PR to promote.
|
||||
- PR via API (skill `gitea-api-pr-operations` for token-extraction + merge payload); `xdg-open` the PR the moment it opens; comment the campaign ledger on the issue.
|
||||
- After Frank's merge OK: propagate deployment `git -C ~/.omp fetch && git merge --ff-only origin/main`; reap campaign worktree (`git worktree remove --force`) + delete branch local/remote; close issue with landing comment.
|
||||
|
||||
## 6. Gates + landing
|
||||
- Gate every commit: `bash scripts/kb-lint.sh omp-docs` + index-coverage + `bats tests/` (67 tests). Pre-commit hook prints `error: command not found: caddy` on Caddyfile-touching commits — warning only, commit still lands.
|
||||
- Gitea API auth: `~/.config/secrets/gitea` is **URL-form** (`http://dank:<token>@host`), not a raw token. Extract: `T=$(sed -n 's|.*://[^:]*:\([^@]*\)@.*|\1|p' ~/.config/secrets/gitea | head -1)`; raw `cat` as bearer token → 401.
|
||||
- PR body ends with `---` + `cwd: <wd> · session: <id>` footer; then `xdg-open <PR-URL>` immediately; comment the campaign ledger summary on the issue. NEVER merge without Frank's explicit OK.
|
||||
|
||||
## Edit-tool field notes (recurring traps)
|
||||
- `PUT N+=:` is invalid syntax — it inserts the literal text. One insert-op per edit call; recompute line numbers per call.
|
||||
- File hash tag changes after every edit — re-`read` before each subsequent edit call to the same file; reusing a stale tag is rejected.
|
||||
## Line-edit op syntax trap (bit three times)
|
||||
- Valid ops: `PUT N.=M:` (body `+text` lines), `CUT N.=M` / `CUT N.=M @reg`, `PUT <N @reg`, `PUT >N:` / `PUT <N:`. There is NO `PUT N+=:` form — it lands in the file as a literal garbage line and shifts the insert to the wrong position. For gap-insert use a lone `PUT >N:` or `PUT <N:` per call; pre-compute line numbers against the file map (ops reference original lines within one call, but a failed op poisons positions).
|
||||
- Recovery recipe when a literal op line lands: read exact bounds, `CUT N.=N` (junk), `CUT A.=B @block` (misplaced section), then `PUT <M @block` at the true anchor, re-read tail, add spacing blank if a `---` now abuts a list line.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: docs-point-of-use-destruction
|
||||
description: "Documentation exists for point of use, not archival. Use before any dedupe/compact/verify-docs job, and whenever a census or \"only copy\" argument is being used to KEEP documentation: apply the purpose test and destroy obsolete measurements. Supersedes the preservation conclusions of vault-one-fact-one-owner-dedupe §3/§12."
|
||||
---
|
||||
|
||||
# Documentation exists for point of use
|
||||
|
||||
**Supersedes** `vault-one-fact-one-owner-dedupe` §3 ("port before cut") and §12
|
||||
("expect docs flat") wherever those conclude a page must be *kept*. Sole custody of a
|
||||
useless fact is still useless.
|
||||
|
||||
## The test, before you write and again before you keep
|
||||
|
||||
- Would the reader, doing a real task tomorrow, **act differently** because of this
|
||||
line? No → delete it.
|
||||
- Is it a measured rate (t/s, tok/s, ms, MiB-of-pool, acceptance %, a bench ladder) for
|
||||
a model / runtime / layout that is **not running now and not being compared now**?
|
||||
Destroy it. Not "file it in an Eras row", not "preserve it for history". AI moves
|
||||
faster than the vault: the number is wrong before anyone reads it, and a wrong number
|
||||
in a runbook gets believed.
|
||||
- Keep the **law** the measurement taught; drop the measurement. Qualitative causal
|
||||
stories survive; numbers do not.
|
||||
- What survives: live config and its exact numbers, procedures, failure signatures,
|
||||
security posture, interface contracts, rollback pointers.
|
||||
- **Losing information is an acceptable, usually correct outcome.** Frank said so
|
||||
explicitly on 2026-10-19 after rejecting a PR that had preserved 53 ledgers.
|
||||
|
||||
## Traps that lead agents to preserve junk
|
||||
|
||||
- **A census cannot decide what to keep.** `port-census.py`-style tools answer "where
|
||||
does this fact live", never "does anyone need it". "14–63 sole-record lines per page"
|
||||
was read as *irreplaceable* when the fact was *obsolete*. Ask usefulness, not custody.
|
||||
- **A census that includes the file under test proves the page is held by itself.**
|
||||
Exclude it, print the corpus size, assert absence; treat `0 unheld` on a long page as
|
||||
a tool bug until proven otherwise.
|
||||
- **"One fact, one owner" is a placement rule, not a preservation rule.** It says where
|
||||
a surviving fact lives; it never says the fact must survive.
|
||||
- **A change diary (`log.md`) is the largest archive in most vaults** (this one: 2,380
|
||||
lines, ~8 % of the vault). Git holds the chronology; a causal law belongs on the page
|
||||
that owns the thing it governs. Retire the diary.
|
||||
|
||||
## Execution that worked (2026-10-19, −13.7k lines)
|
||||
|
||||
1. Write the kill list as a reviewed artifact, one path per line, grouped by *why it is
|
||||
dead* — that list becomes the PR's argument and the trash manifest.
|
||||
2. Destroy whole pages first (`scripts/destroy-vault.py <list>`): move to
|
||||
`.AGENTTRASH/<date>-destruction/`, then delete every reference from survivors, whole
|
||||
line included when the dead link was its only content. Do not compare link targets
|
||||
with `pathlib.Path.stem` — it strips real suffixes (`sd.cpp`).
|
||||
3. Then strip numbers from what remains (`scripts/axe-measurements.py`): delete ledger
|
||||
sections (noun-LEDS the heading, nothing instructional after it), blank any table cell
|
||||
reporting a rate, fold Eras older than the current one into a single row pointing at
|
||||
git. Never touch a shape number or a Laws/Wire/Rollback section.
|
||||
4. Sweep indices (generator must exclude the trash dir, drop rows whose page cell became
|
||||
empty), re-lint, re-check links.
|
||||
5. Rewrite the governing policy in the same PR, or the next agent re-grows the vault:
|
||||
project `AGENTS.md` *Documentation Policy* + the global agent settings.
|
||||
|
||||
## Report honestly, per area
|
||||
|
||||
docs / scripts / operator-doc separately, measured against the merge-base. The tools are
|
||||
a cost that must be named, not hidden inside a headline "net" number — that is what got
|
||||
the previous pass rejected.
|
||||
|
||||
## Verify the repair tools themselves
|
||||
|
||||
One repair pass matched "line ends with a comma" and deleted the trailing comma from 660
|
||||
wrapped prose lines. Restore by matching lines that are byte-identical to `HEAD` except
|
||||
for trailing punctuation (`restore-punctuation.py`), keep the destructive rule out of the
|
||||
tool, and record the incident in its docstring.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: fixture-delay-window-race-test
|
||||
description: Pin a race/abort bug in a fixture-server test by delaying only the Nth request so the interruption lands inside the in-flight window; assert via a driver-installed unhandledRejection reporter.
|
||||
---
|
||||
|
||||
# Reproducing a timing-window bug against a fixture server
|
||||
|
||||
Use when a defect only appears in a window — a promise created before it is awaited, a kill/abort landing mid-flight, a prefetch interrupted — and a fixture HTTP server answers instantly, so the window never opens. Earned in `dankvoice` issue #7 (`[Unhandled Rejection] AbortError` from `/say stop` during a pipelined synthesis).
|
||||
|
||||
## Why naive attempts pass vacuously
|
||||
|
||||
1. **Uniform delay on the fixture route** (`DV_LATENCY_MS` for every request) makes *all* requests resolve at the same time, so by the time the interruption fires nothing is in flight. The test goes green against the broken code — worse than no test.
|
||||
2. Asserting on a log line count that also counts interruption noise: `dankvoice`'s driver prints `PLAYS <n>` as the number of **player log lines**, and the stub appends `KILLED` on SIGTERM — so a single playback with one interrupt reports `PLAYS 2`. Count the payload lines instead (`grep -c 'PLAYER .*\.wav$'`).
|
||||
|
||||
## The shape that works
|
||||
|
||||
- **Delay only requests after the first.** The first response gets the pipeline into playback; the second stays in flight, which is the window:
|
||||
```ts
|
||||
const spoken = received.filter((c) => c.url?.endsWith("/audio/speech")).length; // already pushed
|
||||
const delay = spoken > 1 ? Number(process.env.DV_SYNTH_MS ?? 0) : 0;
|
||||
if (delay > 0) setTimeout(answer, delay); else answer();
|
||||
```
|
||||
- **Make the interruption land inside it.** Player holds long (`DV_HOLD=2`), interruption at a fixed offset (`DV_STOP_AT=400`) with the delayed response far later (`DV_SYNTH_MS=1200`): margins wide enough that the abort is neither before the request nor after the response.
|
||||
- **Turn unhandled rejections into observable output** in the test driver instead of letting the runtime crash on them — that is also exactly what the real harness does to the user:
|
||||
```ts
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.log(`UNHANDLED ${reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason)}`);
|
||||
});
|
||||
```
|
||||
Then assert `[[ "$output" != *"UNHANDLED"* ]]` plus the concrete behavior (segments actually played, player killed).
|
||||
- **Leak checks measure a delta, not an absolute:** count `dankvoice-*` temp dirs before and after the run and compare; an absolute `== 0` fails on another test's leftovers.
|
||||
|
||||
## Order
|
||||
|
||||
Write the scenario first and watch it fail **on the rejection assertion specifically** — if it fails on some other assertion (a count, a kill line), the window is wrong, not the bug. Then fix, then confirm red→green on that one assertion before running the full suite.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: fleet-status-daily-report
|
||||
description: "Answer \"status update / what happened yesterday\" for the dank fleet: git-window record on origin/main plus a live per-host census (endpoints, units, RAM, DNS), with the reboot-attribution check that stops false incident causes."
|
||||
---
|
||||
|
||||
# Fleet status report (Dankclaw + Dankserver)
|
||||
|
||||
Use for "what's the state of the fleet", "what happened yesterday", or a post-incident sweep. Read-only.
|
||||
For one service's live state, prefer `fleet-service-runtime-census`; this is the fleet-wide sweep.
|
||||
|
||||
## 1. The day's record (repo is the ledger)
|
||||
|
||||
```bash
|
||||
git fetch origin --quiet
|
||||
git log --no-merges --since="YYYY-MM-DD 00:00" --until="YYYY-MM-DD+1 00:00" \
|
||||
--date=format:'%H:%M' --pretty='%ad %h %s' origin/main
|
||||
git log --merges --since=... --pretty='%ad %h %s' --date=format:'%H:%M' origin/main # PR numbers
|
||||
```
|
||||
Merge lines carry the PR number — that is the review trail; the non-merge log is what actually changed.
|
||||
Group commits into streams (endpoint names lead the subject) rather than listing 20 subjects.
|
||||
|
||||
## 2. Live census (parallel: one call per host + one endpoint sweep)
|
||||
|
||||
Endpoint sweep from any LAN box — `/health` is honest on all four (archon binds only after full load):
|
||||
|
||||
```bash
|
||||
for u in 192.168.0.143:7770 192.168.0.143:7771 192.168.0.235:7770 192.168.0.143:7772; do
|
||||
printf "%s %s\n" "$u" "$(curl -s -m 12 -w ' [%{http_code}]' http://$u/health | head -c 160)"; done
|
||||
curl -s http://192.168.0.143:7770/v1/models # dragoon lists EVERY engine it serves, incl. NPU-lane models
|
||||
```
|
||||
|
||||
Per host: `uptime -s; uptime -p`; `systemctl show <llm units> -p ActiveEnterTimestamp -p NRestarts -p DropInPaths`
|
||||
(`sudo` for the root LLM units, `--user` for the quadlets); `list-units --failed`; `free -h`;
|
||||
`journalctl -k --since "<evening>" -p err`; the host's own engine log tail.
|
||||
|
||||
## 3. Attribution rules (where reports go wrong)
|
||||
|
||||
- **Never call a host reboot from a container's start timestamp.** Check `uptime -s` first; if uptime spans the
|
||||
event, the trigger was a **container restart** — find it with
|
||||
`journalctl --user -u <name>.service --since … --until …` + `podman inspect <c> --format '{{.State.StartedAt}}'`.
|
||||
(This exact mistake mislabelled a 2026-10-19 Pi-hole incident as boot-caused; the law's remedy belongs after
|
||||
*any* restart, which is a different action item.)
|
||||
- `podman stats` is a 28-container wall — sort/limit it; RSS of the LLM tenants is what matters.
|
||||
- This podman's inspect template has **no `.Config.PodName`, no `.Config.GroupAdd`, and `.HostConfig.Devices`
|
||||
can read `[]` even when a quadlet `AddDevice=` worked**. Prove GPU use from the engine's own log
|
||||
(`ggml_vulkan: Found 1 Vulkan devices: … Radeon 8060S (RADV GFX1151)`), not from inspect.
|
||||
- Kernel lines: **count** occurrences (`journalctl -k --since '7 days' | grep -c "NETDEV WATCHDOG"`) before
|
||||
calling a one-off a trend.
|
||||
- Failed units: `systemctl show <u> -p ExecMainExitTimestamp -p ExecMainStatus --value` separates
|
||||
"broken since boot" from "broke yesterday" — and a nightly failure (e.g. logrotate) is a live risk, not noise.
|
||||
|
||||
## 4. Report shape
|
||||
|
||||
Stream table (what landed) → live-state table (what is true now) → **corrections to my own earlier record** →
|
||||
open items that need Frank's decision, each with the evidence line and the one action. Flag anything that would
|
||||
touch host config outside the repo as a decision, never a silent fix.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: franksign-worktree-db-cred-trap
|
||||
description: "Diagnose franksign e2e seam 5 / health 503 db:down in a fresh worktree: setup-dev.sh minted a password that mismatches the shared franksign-db container."
|
||||
---
|
||||
|
||||
# franksign fresh-worktree DB credential trap
|
||||
|
||||
## Symptom
|
||||
`tests/run.sh` in a fresh franksign worktree fails seam 5 ("e2e server did not
|
||||
become healthy") while the server log says `franksign-api listening on :3100`.
|
||||
Manual probe shows the server IS up:
|
||||
|
||||
```bash
|
||||
PORT=3100 node --env-file=.env apps/api/dist/cli.js >/tmp/manual-e2e.log 2>&1 &
|
||||
sleep 2; curl -s http://127.0.0.1:3100/health
|
||||
# -> 503 {"status":"degraded","db":"down",...}
|
||||
```
|
||||
|
||||
Not a regression, not a :3100 port collision (check `ss -tlnp | grep 3100` to rule that out).
|
||||
|
||||
## Root cause
|
||||
`infra/setup-dev.sh` mints a fresh `POSTGRES_PASSWORD` per checkout
|
||||
(`if [ ! -f infra/postgres.env ]`), but `franksign-db` + `franksign-pgdata`
|
||||
are shared box state owned by the main checkout
|
||||
(`~/Projects/franksign`). The new worktree's `.env` DATABASE_URL then fails
|
||||
TCP auth against the initialized volume.
|
||||
|
||||
The setup script masks it: its readiness gate is
|
||||
`podman exec franksign-db pg_isready`, which uses the in-container local
|
||||
socket and passes regardless of password — so setup prints "postgres ready"
|
||||
while the checkout is dead on arrival.
|
||||
|
||||
## Fix (git-ignored files, no commit needed)
|
||||
Copy the shared credentials from the owning checkout:
|
||||
|
||||
```bash
|
||||
grep DATABASE_URL /home/blank/Projects/franksign/.env # source of truth
|
||||
# PUT that exact DATABASE_URL line into this worktree's .env
|
||||
# PUT the matching POSTGRES_PASSWORD into this worktree's infra/postgres.env
|
||||
```
|
||||
|
||||
Gotcha: the shared password may contain literal `$$`. Keep it byte-for-byte —
|
||||
systemd `EnvironmentFile` and `node --env-file` both read values literally,
|
||||
so `$$` in the file IS `$$` in the password. No escaping, no de-escaping.
|
||||
|
||||
## Verify
|
||||
```bash
|
||||
PORT=3100 node --env-file=.env apps/api/dist/cli.js & # then
|
||||
curl -fsS http://127.0.0.1:3100/health # -> {"status":"ok","db":"up"}
|
||||
tests/run.sh # 14/14
|
||||
```
|
||||
|
||||
## Permanent fix (proposed, not yet landed)
|
||||
`setup-dev.sh` should reuse `~/.config/containers/systemd/postgres.env`
|
||||
(symlink to the owning checkout) when present instead of minting new creds.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: gguf-lora-adapter-fit-census
|
||||
description: "Read-only census before applying a GGUF LoRA/adapter to a llama.cpp (or fork) endpoint: Hub metadata, declared base provenance, runtime flag support, and the behavioural probe that proves the effect."
|
||||
---
|
||||
|
||||
# GGUF LoRA / adapter fit census (read-only, before you promise a file to a payload)
|
||||
|
||||
A 3 MB adapter is a payload change: it edits the compute graph at load. Do the census
|
||||
before editing an endpoint's trio, and prove the EFFECT behaviourally — no metadata,
|
||||
card table, or health gate can see it.
|
||||
|
||||
## 1. Census the Hub repo first (no auth for public repos)
|
||||
|
||||
```bash
|
||||
curl -sS "https://huggingface.co/api/models/<org>/<repo>?blobs=true" | python3 -c "
|
||||
import json,sys; d=json.load(sys.stdin)
|
||||
print(d.get('pipeline_tag'), d.get('library_name'))
|
||||
print([t for t in d.get('tags',[])][:20]) # base_model / base_model:adapter rows matter
|
||||
for s in d['siblings']: print(s['rfilename'], s.get('size'), (s.get('lfs') or {}).get('oid'))"
|
||||
curl -sS "https://huggingface.co/<org>/<repo>/raw/main/README.md" > /tmp/card.md # grep for --lora flags
|
||||
```
|
||||
|
||||
- `base_model` / `base_model_relation: adapter` name the pack the adapter was built for.
|
||||
- **A file with `lfs.oid = None` is a plain git object**: pin the sha256 of the Hub's bytes,
|
||||
not an LFS oid. Fetcher law still applies: `curl -fsSL --fail-with-body`, byte count,
|
||||
sha256, `--verify-only`, no resume.
|
||||
|
||||
## 2. Read the adapter's own GGUF KV section (settles provenance in one read)
|
||||
|
||||
Small python read of the KV pairs — no torch, no download beyond the adapter itself:
|
||||
magic `GGUF`, `u32 ver`, `u64 n_tensors`, `u64 n_kv`; key = `u64 len` + bytes; type `u32`
|
||||
(0 u8,1 i8,2 u16,3 i16,4 u32,5 i32,6 f32,7 bool,8 str,9 array,10 u64,11 i64,12 f64);
|
||||
array = `u32 elem_type` + `u64 count`.
|
||||
|
||||
Keys worth printing: `general.type` (`adapter`), `adapter.type` (`lora`),
|
||||
`adapter.lora_alpha`, and whatever provenance the publisher wrote — e.g. PrismML/abliteration
|
||||
packs carry `prism.abliterate.base_gguf`, `base_sha256`, `direction_row`, `target_layers`,
|
||||
`target_embedding`, `sites`, `basis`.
|
||||
|
||||
**Provenance is metadata, not a gate.** The loader matches **tensor names, not hashes**, so an
|
||||
adapter fit against a sibling quantization of the same model still applies — which is a
|
||||
decision to surface to the operator, not a failure to hide. Compare `base_sha256` against the
|
||||
sha the endpoint's own fetcher pins; if they differ, say so in the PR body, the env header and
|
||||
the endpoint page, and prove the effect behaviourally instead.
|
||||
|
||||
Two publisher facts that decide whether a fork can carry the file at all: the build it was
|
||||
verified on, and what it deliberately does NOT touch (an embedding rank is the usual crash —
|
||||
`token_embd` left alone is a graph constraint, not an omission).
|
||||
|
||||
## 3. Ask the runtime binary, not your memory
|
||||
|
||||
```bash
|
||||
<llama-server> --help | grep -iE "lora|adapter"
|
||||
```
|
||||
Expect `--lora FNAME`, `--lora-scaled FNAME:SCALE,...`, `--lora-init-without-apply`
|
||||
(+ `POST /lora-adapters` for runtime scale). Prefer `--lora-scaled <file>:<scale>` as ONE
|
||||
argv token so a scale can never reach the engine without its file, and follow the endpoint's
|
||||
existing opt-out convention (the `MMPROJ_FILE=` shape: `:`-less default so an explicitly empty
|
||||
value means "off, on purpose"). Gate the deployer on the file's existence with the adapter's
|
||||
OWN fetcher named in the error text.
|
||||
|
||||
## 4. Prove the effect: refusal/behaviour probe, before AND after
|
||||
|
||||
The probe is not a bench, so the no-bench law does not block it — and the **before** half must
|
||||
be captured while the old shape is still live.
|
||||
|
||||
- Same prompts, `temperature` low, both before and after; classify with a refusal-marker regex
|
||||
and print only the opening characters, never the generated body.
|
||||
- **Budget ≥ 2 048 tokens on a thinking model.** A 600-token request can be swallowed whole by
|
||||
the reasoning channel: `content` comes back empty with `finish_reason: length` and
|
||||
`reasoning_content` full — which reads exactly like a refusal that never happened.
|
||||
- A leading `⚠️ Disclaimer`/hedge after the adapter is style, not the circuit: judge the marker
|
||||
sentence, not the tone.
|
||||
|
||||
## 5. Post-deploy reads (the ledger is the instrument)
|
||||
|
||||
- Boot log shows `llama_adapter_lora_init_impl: loading lora adapter from '<file>'` and then
|
||||
**dumps the adapter's KV block** — the provenance mismatch is printed at every start, so the
|
||||
runbook should point at the journal instead of restating it.
|
||||
- Confirm the flag on argv: `tr '\0' ' ' < /proc/$(systemctl show <svc> -p MainPID --value)/cmdline`;
|
||||
confirm `systemctl show <svc> -p DropInPaths --value` is empty (a leftover bench drop-in outranks
|
||||
everything you just deployed).
|
||||
- Shape must be untouched by an adapter: `n_slots` / `n_ctx_slot` / KV lines unchanged, and
|
||||
`/metrics` still answering (a llama.cpp endpoint is invisible to Prometheus without `--metrics`).
|
||||
- Root units log to the journal; rootless podman storage is read WITHOUT sudo (`sudo podman
|
||||
inspect` sees a different storage namespace and reports "no such object").
|
||||
|
||||
## Yields
|
||||
|
||||
One sentence per question: does the file exist and at what size/sha; which pack it declares as
|
||||
its base and whether that matches the served payload; which flag the runtime actually offers;
|
||||
what the before/after probe classified. Anything else is a decision for the operator, and the
|
||||
rollback should be one env line on a file the endpoint already loads.
|
||||
@@ -1,21 +1,27 @@
|
||||
---
|
||||
name: gitea-api-pr-operations
|
||||
description: "\"Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea API (192.168.0.143:3000, repo dank/command-center) with the token stored in ~/.config/secrets/gitea — incl. the required merge payload, where review comments actually live on 1.27.3, and how to recover the session id for the mandatory body footer.\""
|
||||
description: "\"Open, list, merge, and diagnose issues/PRs and manage labels on the Dankclaw Gitea API (192.168.0.143:3000) with the token stored in ~/.config/secrets/gitea — incl. the token-only write auth (Basic reads but 422s on POST/PATCH), the required merge payload, where review comments actually live on 1.27.3, and how to recover the session id for the mandatory body footer.\""
|
||||
---
|
||||
|
||||
# Gitea API PR operations (Dankclaw `dank/command-center`)
|
||||
# Gitea API operations (Dankclaw `dank/command-center`, `dank/serverAppConfigs`)
|
||||
|
||||
## Where
|
||||
- Token: `~/.config/secrets/gitea` (`http://user:TOKEN@192.168.0.143` single line).
|
||||
- Token: `~/.config/secrets/gitea` (`http://user:TOKEN@192.168.0.143` single line — git-credential URL form, NOT `username=`/`password=` lines).
|
||||
Extract WITHOUT echoing:
|
||||
`tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)`
|
||||
- Build a 600-perm temp curl config and use `curl -K`:
|
||||
`cfg=$(mktemp) && chmod 600 "$cfg" && printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"`
|
||||
Delete `$cfg` after use. NEVER pass `-H "Authorization: token $tok"` (the literal
|
||||
header lands in transcript + logs). NEVER echo the token.
|
||||
- Base URL: `http://192.168.0.143:3000/api/v1/repos/dank/command-center`.
|
||||
- Base URL: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>`.
|
||||
|
||||
## Recipes (all verified live 2026-10-18)
|
||||
## Auth: reads tolerate Basic, writes DO NOT (measured 2026-10-20)
|
||||
GETs succeed with an HTTP Basic header built from the same credential, but every
|
||||
POST/PATCH/DELETE with Basic returns `407/422 {"message":"token is required"}`.
|
||||
Always use the `Authorization: token <tok>` header for everything — never debug
|
||||
"permissions" when the header shape is the fault.
|
||||
|
||||
## Recipes (all verified live)
|
||||
- **Open PR** (token only needed for private repos; this one reads publicly but
|
||||
POST needs auth): `POST /pulls` JSON `{title, head, base:"main", body}`.
|
||||
- **PATCH PR title/body**: `PATCH /pulls/<n>` with JSON body `{title}` and/or
|
||||
@@ -36,6 +42,12 @@ description: "\"Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea
|
||||
merge --ff-only origin/main` propagates the live deployment.
|
||||
- **Issues**: same shape at `/issues` (`POST {title, body}`), `/issues/<n>/comments`
|
||||
(`POST {body}`, `PATCH .../comments/<id>`) — both work with the temp-cfg auth.
|
||||
- **Labels**: `GET /labels` (list incl. numeric `id`); `POST /labels`
|
||||
`{"name":"<name>","color":"#rrggbb"}` to create; attach by ID with
|
||||
`POST /issues/<n>/labels {"labels":[<id>,...]}` (names are rejected);
|
||||
detach with `DELETE /issues/<n>/labels/<id>`. serverAppConfigs gained
|
||||
needs-triage / needs-info / ready-for-agent / ready-for-human / wontfix
|
||||
(2026-10-20) alongside the older `human-only`.
|
||||
|
||||
## Review comments: the API lies on 1.27.3 (learned 2026-10-18)
|
||||
- `GET /pulls/<n>/comments` (inline review comments) → **404 `not found` on every
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: gitea-merge-form-encoded
|
||||
description: "Use when a Gitea API merge on Dankclaw (192.168.0.143:3000) returns HTTP 422 \"[Do]: Required\" or 405 \"Please try again later\" — the merge endpoint needs form-encoded fields with the lowercase field name do, and a behind-main branch must be merged forward before it will merge."
|
||||
---
|
||||
|
||||
Gitea on Dankclaw (192.168.0.143:3000) — merge quirks, measured 2026-10-19 on `dank/serverAppConfigs` and again on `dank/dankvoice`, re-earned the same day on serverAppConfigs PR #27.
|
||||
|
||||
## 1. The payload: form-encoded, lowercase `do`
|
||||
|
||||
Creating a PR accepts a JSON body. **Merging does not**: `POST /api/v1/repos/{owner}/{repo}/pulls/{n}/merge` with a JSON body (`{"merge_style":"no-ff", ...}`) or with those values as request headers returns `422 {"message":"[Do]: Required"}` — four variants of that all fail identically, so do not "fix" it by changing the style name or moving the headers around.
|
||||
|
||||
**The field name is lowercase `do`.** `--data-urlencode 'Do=merge'` (capital D, which the error message spells) returns the same `422 [Do]: Required`. Do not retry with case variants — the web-UI form field is `do`.
|
||||
|
||||
Working call (form-encoded):
|
||||
|
||||
```bash
|
||||
tok=$(sed -n 's|^https\{0,1\}://[^:]*:\([^@]*\)@192\.168\.0\.143.*$|\1|p' ~/.config/secrets/gitea | head -1) # git credential.helper store file
|
||||
curl -sS -X POST -w 'http %{http_code}\n' -K <(printf 'header = "Authorization: token %s"\n' "$tok") \
|
||||
--data-urlencode 'do=merge' \
|
||||
--data-urlencode 'merge_style=no-ff' \
|
||||
http://192.168.0.143:3000/api/v1/repos/dank/<repo>/pulls/<N>/merge
|
||||
# 200 with an empty body == merged
|
||||
```
|
||||
|
||||
- `merge_style` values are the dashed names: `no-ff`, `merge`, `rebase`, `squash`.
|
||||
- Keep the token out of argv: `-K <(printf …)` feeds the `Authorization` header through stdin (a `-H "Authorization: token …"` lands in `/proc/<pid>/cmdline`, world-readable — the 2026-09-16 HF-token leak class).
|
||||
- Verify by fast-forwarding a checkout (`git fetch && git merge --ff-only origin/main`) rather than re-reading the PR.
|
||||
- Fleet law still applies: never merge without an explicit current OK from Frank, and open the PR URL in his browser (`xdg-open`) the moment it exists.
|
||||
|
||||
## 2. `405 {"message":"Please try again later"}` = the branch is behind main
|
||||
|
||||
A correctly formed merge call returns **405**, not 422, when Gitea cannot test-merge the head into the base. Read it as "merge main forward", not "retry later" — no sleep-retry ever clears it.
|
||||
|
||||
Confirm first: `GET /pulls/{n}` shows `"mergeable": false` (and `merged: false`, `state: open`) for exactly this state.
|
||||
|
||||
The fix is the repo's own trunk-based law — merge forward, never rebase:
|
||||
|
||||
```bash
|
||||
git worktree add ../wt-prN -B prN <head-branch>
|
||||
cd ../wt-prN && git merge --no-edit origin/main # resolve, see below
|
||||
./tests/run.sh # the seam is the arbiter, run it on the RESOLVED tree
|
||||
git push -q origin prN:refs/heads/<head-branch> # same branch name; Gitea reuses the open PR
|
||||
# then re-POST the merge — 200 this time
|
||||
```
|
||||
|
||||
Notes earned on PR #27 (2026-10-19):
|
||||
|
||||
- A `git worktree` per PR keeps the deployed checkout untouched; clean up with `git worktree remove --force ../wt-prN && git branch -D prN`.
|
||||
- `git status --short` during a merge lists every incoming file as staged — that is the merge commit's content, not a dirty tree.
|
||||
- Push a local branch to the PR's branch name with an explicit refspec (`prN:refs/heads/docs/…`); the push output prints `Visit the existing pull request`, which is how you confirm you hit the right head.
|
||||
- Gitea needs a few seconds after the push to recompute mergeability; a `sleep 3` before re-POSTing avoids a second 405.
|
||||
|
||||
## 3. Resolving vault conflicts against a compaction wave
|
||||
|
||||
When main has run a docs compaction (page deletions, description shortening), two conflict shapes recur:
|
||||
|
||||
- **`description:`** — `scripts/check-okf.py` enforces **DESC: ≤ 120 chars**. A branch's longer description must be shortened, not argued for; keep main's shape and fold the new topic into the wording.
|
||||
- **A `[[Page]]` link whose target page main destroyed** — `git ls-tree -r --name-only origin/main -- serverAppConfigs-docs | grep -i '<page>'` settles it. Drop the dead link; do not resurrect a page. Compaction left link-stripped stub bullets behind (`- the compile fix for …`, dash + two spaces): `git grep -nE '^- [A-Z]' origin/main -- 'serverAppConfigs-docs/**/*.md'` counts them, and the seam cannot see them because a stub is no longer a link to validate.
|
||||
|
||||
After any frontmatter change, regenerate: `./scripts/gen-vault-index.py serverAppConfigs-docs` and commit the index rows — the `vault indices are generated, not hand-carried` seam diffs regeneration against the checkout. The sub-index sync can emit one extra space after `|` for a quoted description; commit what the generator emits, since the seam compares against the generator.
|
||||
|
||||
## Credential-store token extraction (same file, adjacent trap)
|
||||
|
||||
`~/.config/secrets/gitea` holds one line shaped `http://user:token@192.168.0.143:3000`. Splitting it with `awk -F'/'` puts the host in `$3`, not `$2`, and `awk -F'/' '{print $NF}'` yields the host+path, not the token — an empty/garbage token makes **private repos answer `404 not found`** (Gitea hides them from anonymous), which reads like a wrong URL rather than an auth failure. Extract with:
|
||||
|
||||
```bash
|
||||
tok=$(sed -n 's|^https\{0,1\}://[^:]*:\([^@]*\)@192\.168\.0\.143.*$|\1|p' ~/.config/secrets/gitea | head -1)
|
||||
echo "token length: ${#tok}" # 40 — print the length, never the value
|
||||
```
|
||||
|
||||
Other API quirks measured the same day:
|
||||
|
||||
- `GET /pulls?state=open` can return an error object where `state=all` returns the list; `state=all` list entries may omit `head`/`base` — fetch `/pulls/{n}` for those.
|
||||
- POST `/pulls` for a head/base pair that already has a PR → `409` naming `issue_id: N`; the PR number is that `issue_id`, so PATCH it instead of POSTing again.
|
||||
- Issues and PRs share the issue index per repo, so `#36`-style numbers are repo-scoped: a footer fix aimed at `dank/command-center#36` 404s when pointed at `dank/dankvoice`.
|
||||
@@ -1,29 +1,53 @@
|
||||
---
|
||||
name: gitea-pr-api-cycle
|
||||
description: "Open/update/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000, dank/command-center) with the token-safe curl+jq sequence, including the {\"Do\":\"merge\"} merge payload and post-merge live-checkout fast-forward."
|
||||
description: "\"Open/update/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000) with the token-safe sequence, the form-encoded merge field, the empty-token-returns-404 trap, and post-merge live-checkout fast-forward plus the plugin-package-name upgrade gotcha.\""
|
||||
---
|
||||
|
||||
# Gitea PR/issue cycle — dank/command-center
|
||||
# Gitea PR/issue cycle — Dankclaw API
|
||||
|
||||
Friction-earned 2026-10-18 across PRs #15–#20 and issue #18: three separate fumbles (jq trailer flag, 422 merge payload, silent -o/-w swallowing) codified once.
|
||||
Friction-earned 2026-10-18 (PRs #15–#20, issue #18) and re-earned 2026-10-19 on `dank/dankvoice` PRs #4–#6: the merge payload claim below was wrong, and an empty token masqueraded as a missing repo.
|
||||
|
||||
## Where
|
||||
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/command-center/{pulls,issues}`
|
||||
- Token: never inline, never echoed. Extract from the git credential file into a 600-perm temp curl config:
|
||||
`tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)`
|
||||
`cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"` → `curl -K "$cfg"`; `rm -f "$cfg"` when done.
|
||||
- Unauthenticated GET works for state reads (`GET /pulls/N` → `jq -c '{state,merged,merge_commit_sha}'`).
|
||||
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>/{pulls,issues}` — repo varies (`command-center`, `dankvoice`, `serverAppConfigs`); numbering is per-repo, so a footer citing `#36` means nothing without the repo.
|
||||
- Token: never inline, never in argv (`-H "Authorization: token …"` lands in `/proc/<pid>/cmdline`, world-readable). Extract into a 600-perm curl config and hand it to curl with `-K`:
|
||||
```bash
|
||||
tok=$(sed -nE 's#^https?://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
|
||||
[ "${#tok}" -ge 40 ] || { echo "token extraction failed"; exit 1; }
|
||||
cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
|
||||
curl -sS -K "$cfg" … ; shred -u "$cfg"
|
||||
```
|
||||
- **Empty token answers `404 {"message":"not found"}`, NOT 401** — Gitea hides the private repo. A 404 on a path you have used before means auth, not a typo'd repo. The trap that produces the empty token: `awk -F'/' … '{print $NF}'` on the store line (`scheme://user:token@host/`) — the host is in `$3`, the match never fires, you get a silent empty variable. Always assert the length before POSTing.
|
||||
- Multi-line bodies: build JSON in python (`json.dumps({"title":…, "body":open(f).read(),…})`) or `jq -n --arg b "$(cat b.txt)"`. NEVER pass `---` as a bare jq argument (parsed as a flag). Escaped quotes inside `python3 -c '…'` break on `\"` — write the script to a file or use a heredoc.
|
||||
- Always capture status: `-o r.json -w '%{http_code}'`; a bare `-o` hides failures.
|
||||
|
||||
## Body construction (the two traps)
|
||||
- Build JSON with `jq -n --arg t <title> --arg b "$(cat b.txt)" '{title:$t, head:<branch>, base:"main", body:$b}'`; write the body to a temp file first (`printf '%s' … > b.txt`). NEVER pass `---` as a bare jq argument — jq parses it as a flag (`unknown flag: ---`). The AGENTS.md attribution trailer (`---` + `cwd: … · session: …`) goes INSIDE the body string.
|
||||
- Always capture status explicitly: `-o r.json -w '%{http_code}'` and print it; a bare `-o` hides failures (a 422 looked like success-with-no-output once).
|
||||
## Create
|
||||
`POST /pulls` with `{title, body, base:"main", head:<branch>}` — JSON is accepted here (unlike merge).
|
||||
- **409 `pull request already exists for these targets [issue_id: N, …]`** — a PR for that head/base exists already. Common cause: the push message printed `/pulls/new/<branch>` and someone (Frank, or an `xdg-open` you ran) clicked Create. `GET /pulls?state=all`, find it, then `PATCH /pulls/N` with your title+body instead of re-posting.
|
||||
- `GET /pulls?state=all` on this Gitea omits `head`/`base` per entry — read `/pulls/N` for those, or key off `number`/`user`.
|
||||
- Issues: same shape, `POST /issues` `{title, body}`; `PATCH /issues/N` fixes a body retroactively (works for PRs too, since a PR is an issue).
|
||||
|
||||
## Merge (the 422 trap)
|
||||
`POST /pulls/N/merge` requires body `{"Do":"merge"}` — `Do` is the Required field on this Gitea. `{}`, `{"DoNotMerge":false}`, `{"merge_style":…}` all return 422 (`"[Do]: Required"`). Success may return an EMPTY body — verify by GET afterwards (`state=closed, merged=true`), not by the POST response.
|
||||
- Merge only with Frank's explicit per-PR OK (repo law).
|
||||
- Post-merge the live deployment must follow: `git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main` (the checkout IS the deployment; branch is `main`).
|
||||
## Merge (the 422 trap) — corrected
|
||||
`POST /pulls/N/merge` needs **form-encoded** fields; the merge form field name is lowercase **`do`**:
|
||||
|
||||
## PR etiquette per AGENTS.md
|
||||
- Immediately after opening: `xdg-open <PR-URL>` lands it in Frank's Firefox.
|
||||
- Redesign mid-review: amend + `git push -f` the branch, then PATCH the PR body (`/pulls/N`, `{"body":…}`) so title/body match the net diff — verify `{number,state,mergeable}` in the response.
|
||||
- Issue creation identical shape: `POST /issues` with `{title, body}`, expect 201; PATCH same endpoint `/issues/N` to fix bodies retroactively, then GET-readback verify.
|
||||
```bash
|
||||
curl -sS -X POST -K "$cfg" \
|
||||
--data-urlencode 'do=merge' --data-urlencode 'merge_style=no-ff' \
|
||||
http://192.168.0.143:3000/api/v1/repos/dank/<repo>/pulls/N/merge
|
||||
```
|
||||
|
||||
JSON `{"Do":"merge"}` (what this skill used to claim) and header variants all return `422 {"message":"[Do]: Required"}`. `merge_style` ∈ `no-ff|merge|rebase|squash`. Success returns **200 with an empty body**; verify by landing a checkout rather than re-reading the PR. Merge only with Frank's explicit per-PR OK.
|
||||
|
||||
## Post-merge landing
|
||||
- Command-center: the checkout IS the deployment — `git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main`.
|
||||
- Package repos (`~/Projects/<pkg>`): same `--ff-only`, then reinstall (`omp plugin upgrade <name>`) and `cmp` the installed `dist/index.js` against the repo `dist/index.js` to prove the live artifact is the merged one. Check `ls -ld node_modules` after landing anything that touched it.
|
||||
- **`omp plugin upgrade` takes the npm package name, not the short name.** `omp plugin upgrade dankvoice` fails with `Failed to upgrade dankvoice: dankvoice is not installed` while the plugin is plainly installed; the package is `omp-dankvoice`. Find the truth with `ls ~/.omp/plugins/node_modules` or `find ~/.omp -path '*<name>/dist/index.js' -not -path '*/Projects/*'` — the installed tree is unscoped (`~/.omp/plugins/node_modules/omp-dankvoice/dist/index.js`), not `@dank/dankvoice`.
|
||||
- **`cmp` exiting 1 BEFORE the upgrade is the expected state** (installed bundle is the pre-merge build, diff reported at some char/line). Do not treat it as a failed deploy and do not "fix" the build; run the upgrade, then `cmp` again for byte identity, and grep the new bundle for a string only the merged code contains (e.g. `grep -c "Heckle instead of summarizing"`) as a second, semantic confirmation.
|
||||
- Prune: `git worktree remove --force <path>` (force: untracked deps link), `git branch -D`, `git push origin --delete <branch>` (allowed: local Gitea).
|
||||
|
||||
## Attribution footer (AGENTS.md)
|
||||
Every issue/PR body ends with `---`, then `cwd: <session cwd> · session: <session id>`. Read the real values, never invent an id.
|
||||
- **The session id is the UUID suffix of the LIVE transcript `.jsonl`, not the session directory name.** Transcript dirs are keyed by cwd (e.g. `-.omp-wt-wt-20261019-020309-6d1fc1f`) and ONE cwd can hold several sessions (resume / continuation forks), so `ls -t ~/.omp/agent/sessions/` cannot tell you which is yours and `ls -t <dir>` is equally blind. Pick the file being appended to right now: `ls -l --time-style=+%H:%M:%S <dir>/*.jsonl` beside `date +%H:%M:%S` — the mtime that equals the current second is the live session. The id is the part after `_` in `<timestamp>_<uuid>.jsonl`.
|
||||
- A `history.db` query for `session_id` filtered by `cwd` usually agrees, but it is not authoritative for WHICH of several sessions in that cwd is live — use the jsonl mtime test.
|
||||
- `local/` does NOT disambiguate: a forked transcript dir carries copies of the same `local://` artifacts, so matching artifact names proves nothing about which session is live.
|
||||
- `cwd` is that session's own working directory (the path the harness states), **not** the build worktree you committed in — when they differ, name the build worktree inside the body and keep `cwd` the session's.
|
||||
- Resolve `local://<name>.md` with `read local://<name>.md` (its header prints the absolute path) instead of guessing the directory.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: gitea-token-from-http-credential-store
|
||||
description: "Extract the Dankclaw Gitea token from ~/.config/secrets/gitea for API calls to PRIVATE repos, and diagnose the empty-token 404 that looks like a missing repo."
|
||||
---
|
||||
|
||||
The Gitea credential store line on Frank's boxes is **http** (not https) with a percent-encoded port:
|
||||
|
||||
```
|
||||
http://dank:<40-char-token>@192.168.0.143%3a3000
|
||||
```
|
||||
|
||||
## Extraction that actually works
|
||||
|
||||
```bash
|
||||
tok=$(sed -nE 's#^[a-z]+://[^:]+:([^@]+)@.*#\1#p' ~/.config/secrets/gitea | head -1)
|
||||
curl -s -H "Authorization: token $tok" http://192.168.0.143:3000/api/v1/<path>
|
||||
```
|
||||
|
||||
Common wrong pattern (silent failure): an `awk`/`grep` anchored on `https://` or on `@192.168.0.143:` matches nothing, leaving `tok=""` — verify with `echo "tok len ${#tok}"` before believing any API result.
|
||||
|
||||
## The 404 trap
|
||||
|
||||
`dank/command-center` is **public**: an empty token still returns real issue data. `dank/dankvoice` is **private**: the same empty token returns `404` on `/api/v1/repos/<owner>/<repo>` and `{}`-shaped bodies (`title: undefined`) on issues. A 404 with a zero-length token is an auth problem, NOT a missing repo — confirm by printing the token length first, then re-check.
|
||||
|
||||
## Other notes
|
||||
|
||||
- `git credential fill` fails headless here (`could not read Username … terminal prompts disabled`); read the store file directly instead.
|
||||
- Never print the token: mask when inspecting the file —
|
||||
`sed -E 's#(://[^:]+:)[^@]+@#\1REDACTED@#' ~/.config/secrets/gitea`
|
||||
- Issue/PR numbers are **per repo**: `dank/dankvoice` #8–#11 (refactor, hardening, AbortError fix, menu defects) are unrelated to `dank/command-center` #8–#11 (SDLC, trunk-law). Read the body from the repo the work targets before referencing an issue in a commit message.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: gitignore-trailing-slash-symlink-contamination
|
||||
description: "Trailing-slash gitignore rules don't match symlinks, so worktree dependency symlinks get committed; detect with git ls-tree mode 120000, fix with git rm --cached."
|
||||
---
|
||||
|
||||
A `.gitignore` entry written with a **trailing slash matches directories only**. A symlink of the same name is not a directory, so it slips past the rule, and `git add -A` in a `git worktree` checkout silently **tracks** it. Cost measured 2026-10-19: a machine-specific symlink (`node_modules -> /home/blank/Projects/dankvoice/node_modules`) landed in `main` with a merged PR.
|
||||
|
||||
Why worktrees are the trigger: a worktree has no `node_modules` (or other installed-deps dir) of its own, so the cheap way to make build/test work there is a symlink into the primary checkout. With `node_modules/` ignored, nothing complains.
|
||||
|
||||
## Detect
|
||||
|
||||
```bash
|
||||
git ls-tree HEAD -- node_modules # mode 120000 == tracked symlink
|
||||
git ls-files -s | awk '$1==120000' # every tracked symlink in the index
|
||||
```
|
||||
|
||||
## Fix (branch, never on main directly)
|
||||
|
||||
```bash
|
||||
git rm -q --cached node_modules # untrack; leaves the file on disk
|
||||
```
|
||||
|
||||
plus relax the ignore rule to the slash-free form, with the reason inline so nobody re-adds the slash:
|
||||
|
||||
```
|
||||
# Trailing slash would only match directories: a node_modules symlink (the
|
||||
# shape `git worktree` checkouts end up with) slips past it and gets tracked.
|
||||
node_modules
|
||||
```
|
||||
|
||||
Then `git check-ignore -v node_modules` must print the rule, and the suite runs after (the symlink still resolves, it is just ignored).
|
||||
|
||||
## Prevent
|
||||
|
||||
In a fresh worktree, create the deps symlink **after** confirming the ignore rule has no trailing slash:
|
||||
|
||||
```bash
|
||||
git check-ignore -v node_modules || sed -i 's|^node_modules/$|node_modules|' .gitignore
|
||||
```
|
||||
|
||||
Same class for any ignored-by-convention dir that a worktree might provide as a link: `node_modules`, `vendor`, `.venv`, `target`, `dist` when built artifacts are ignored.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: land-branch-without-checkout
|
||||
description: "Open a PR from a shared worktree when workspace policy blocks git checkout/restore: push a named branch ref from HEAD without switching branches."
|
||||
---
|
||||
|
||||
# Landing a branch when `git checkout` is policy-blocked
|
||||
|
||||
Shared-workspace policy blocks `git checkout` and `git restore` (working-tree
|
||||
switches could strand concurrent agents' uncommitted work). You still must ship
|
||||
the commit on the current worktree branch as a named `chore|feat|fix/<issue>-<slug>`
|
||||
branch. Never work around the block — go around it via refs.
|
||||
|
||||
## Sequence
|
||||
|
||||
```bash
|
||||
# 1. commit only your files (atomic); leave other dirty paths alone
|
||||
git add <your-files> && git commit -m "..."
|
||||
|
||||
# 2. branch ref at HEAD — no working-tree change, stays on wt/<...>
|
||||
git branch <type>/<issue>-<slug> HEAD
|
||||
git push origin <type>/<issue>-<slug> # pre-push hooks still run
|
||||
|
||||
# 3. PR via tracker API with head = pushed ref (see gitea-pr-api-cycle / gh)
|
||||
```
|
||||
|
||||
- The worktree stays checked out on `wt/<timestamp>`; the new branch exists
|
||||
only as the pushed ref (plus a local ref that merges cleanly later).
|
||||
- `-u` upstream tracking on the wt branch is misleading — skip it; set
|
||||
`git config branch.<name>.remote origin` + `.merge` only if you will
|
||||
later check the branch out.
|
||||
- Dirty regenerated fixtures left behind (e.g. `gen:sample` PDFs rewritten
|
||||
by setup/gate runs): `git restore` is also blocked — do NOT edit-forward
|
||||
to revert them; report them. They are regenerated on every bootstrap, so
|
||||
dirt is expected and harmless.
|
||||
|
||||
## Verify
|
||||
|
||||
- `git ls-remote --heads origin <branch>` prints the same sha as `git rev-parse HEAD`.
|
||||
- PR API returns 201 with `head.sha` equal to that sha.
|
||||
|
||||
## Do NOT
|
||||
|
||||
- `git checkout -b`, `git stash`, `git restore`, `git reset --hard` here —
|
||||
all working-tree mutations the policy can block, and stashes in a shared
|
||||
checkout are a cross-agent footgun.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: land-merged-pr-with-blocked-tree-ops
|
||||
description: "Use when a merged PR must land in a checkout whose working tree is dirty and workspace policy blocks git switch/restore/stash/reset-to-commit: prove identity, commit forward, merge, verify."
|
||||
---
|
||||
|
||||
# Landing a merged PR into a checkout with blocked tree operations
|
||||
|
||||
Shared-workspace policy can block `git switch`, `git checkout <branch>`, `git restore`, `git stash`, and `git reset <commit>` ("uncommitted changes other concurrent agents rely on"). A `git pull --ff-only` then aborts with *"Your local changes to the following files would be overwritten by merge"* — **even when the local content is byte-identical to the incoming commit**, because git compares the index, not intent. Observed twice on 2026-10-19 (dank/command-center, dank/dankvoice).
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Prove identity first — never assume it.**
|
||||
`git fetch && git diff --stat origin/main -- <the dirty paths>` → **empty output** means the working tree already equals the merged result, so committing it forward destroys nothing. If it is not empty, stop: there is real local work; do not proceed.
|
||||
2. **Commit forward** the identical paths (message says exactly that, e.g. *"land merged #N content; policy blocks restore/stash, so the identical copies are committed rather than discarded"*). Hooks still run.
|
||||
3. **Merge, don't ff:** `git merge --no-edit origin/main` (ort resolves identical changes on both sides cleanly). `--ff-only` is impossible once step 2 created a commit.
|
||||
4. **Untracked-collision case:** if the abort says *"Please move or remove them before you merge"*, an untracked local file occupies a path the merge adds. `mv` it aside, merge, then `cmp` the restored file against the copy — identical → drop the copy (if the copy came from `.AGENTTRASH`, delete it and `rmdir`, since the file is now tracked).
|
||||
5. **Verify:** repo lint/tests, `git status --porcelain -- <paths>` empty, and `git rev-parse --short HEAD origin/main` equal **after a fresh `git fetch`**.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `git merge … | tail -3` masks the exit status — `tail` always exits 0 and an `&&` chain then continues past an aborted merge. Run the merge unmasked, or check `${PIPESTATUS[0]}`.
|
||||
- Immediately after a push, `git rev-list --count main..origin/main` can report phantom commits-behind from a stale remote-tracking snapshot. Re-`fetch` and compare `rev-parse` before believing any drift.
|
||||
- Do not push redundant commits into a *deployment* checkout without checking the repo's tier law: machine/live tiers commit straight to `main`, human/agent prose goes by PR.
|
||||
|
||||
## Never
|
||||
|
||||
No `--no-verify`, no `reset --hard`, no `checkout -- <path>`, no "temporary" stash: those are exactly the operations the policy blocks because they discard other agents' work.
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: llamacpp-adapter-swap
|
||||
description: "Add or roll back a GGUF LoRA adapter on a dank-fleet llama.cpp endpoint (e.g. frank-zealot): provenance census, fork-flag gate, fetcher+env+deployer surface, seam tests, and the behavioural before/after probe that is the only real proof."
|
||||
---
|
||||
|
||||
# Applying a GGUF LoRA adapter to a fleet endpoint
|
||||
|
||||
An adapter is **payload, not shape**: it changes what the endpoint answers and nothing in
|
||||
the KV/budget equation. Fleet law still applies — no benchmarks unless Frank orders one,
|
||||
rollback is one env line, `./tests/run.sh` before commit, deployer health gate + e2e gate.
|
||||
|
||||
## 1. Census the artifact BEFORE writing any code
|
||||
|
||||
```bash
|
||||
curl -sS "https://huggingface.co/api/models/<repo>?blobs=true" # size + LFS oid (None => plain git object)
|
||||
curl -sSL -o /tmp/a.gguf "https://huggingface.co/<repo>/resolve/main/<file>" && sha256sum /tmp/a.gguf
|
||||
```
|
||||
|
||||
Parse the GGUF KV section (15 lines of struct.unpack) and read **provenance**: abliteration
|
||||
adapters carry `prism.abliterate.base_gguf` / `base_sha256`, `adapter.type`, `sites`,
|
||||
`target_layers`, `target_embedding`. Compare the declared base against the file the endpoint
|
||||
actually serves (its sha is in the endpoint's own fetcher script). A mismatch is normal for
|
||||
sibling quantizations — the loader matches **tensor names, not hashes** — but it must be
|
||||
stated in the docs, because it means the card's measured numbers are not this endpoint's
|
||||
numbers.
|
||||
|
||||
## 2. Gate the runtime, from the binary's own `--help`
|
||||
|
||||
On the host: `/home/dank/GitPackages/<build>/llama-server --help | grep -iE 'lora|adapter'`.
|
||||
Confirm `--lora-scaled FNAME:SCALE` exists (and `--lora-init-without-apply` + `POST
|
||||
/lora-adapters` if a runtime toggle is ever wanted). Vendor forks gate adapters by metadata
|
||||
version too — e.g. PrismML builds older than `prism-b10777` refuse packs with `unsupported
|
||||
prism.hadamard.version: 2`. Read the card for the build it verified on and put that in the
|
||||
fetcher header.
|
||||
|
||||
## 3. Surface (five files, copy the mmproj convention)
|
||||
|
||||
- `scripts/<svc>.env`: `LORA_FILE=<path>` + `LORA_SCALE=1`; document that **`LORA_FILE=` (empty)**
|
||||
is the rollback and `LORA_SCALE=0` is the inert-but-present mode. Use `${VAR=default}` (no
|
||||
colon) so an explicitly empty value survives — same reason `MMPROJ_FILE`/`PRESENCE_PENALTY` do.
|
||||
- `scripts/serve-<svc>.sh`: `EXTRA_ARGS+=(--lora-scaled "${LORA_FILE}:${LORA_SCALE}")` — **one
|
||||
argv token**, so a scale can never arrive without its file; refuse a missing file by name
|
||||
(`Restart=always` turns an unfinished fetch into a crash loop that reads like a runtime fault).
|
||||
- `scripts/download-<svc>-<adapter>.sh`: copy the sibling fetcher (`-fsSL --fail-with-body`, byte
|
||||
count, sha256, `--verify-only`, idempotent). No LFS oid => pin the content sha. Header carries
|
||||
the provenance finding.
|
||||
- `scripts/deploy-remote.sh`: `LLM_<SVC>_LORA="${LLM_<SVC>_LORA:-<path>}"` + a check whose error
|
||||
names **this** fetcher, not the payload's.
|
||||
- `scripts/deploy-plan.py` needs nothing: fetchers are host-side state and select no service.
|
||||
|
||||
## 4. Seam tests (argv is the contract)
|
||||
|
||||
Stub-engine harness: create the adapter stub from the **shipped env's basename**, emit
|
||||
`LORA_FILE`/`LORA_SCALE` in the generated test env, then assert on output only —
|
||||
`--lora-scaled` value equals `<path>:<scale>`; a distinctive `LORA_SCALE` (0, 0.75) changes the
|
||||
token; `LORA_FILE=` yields **absence** of the flag (`! grep -qxF -- '--lora-scaled'`); a missing
|
||||
file exits non-zero naming that path; extend the env↔deployer anti-drift pair list.
|
||||
|
||||
## 5. Deploy and prove it behaviourally
|
||||
|
||||
Capture the **before** state on the live endpoint first, then: merge → on host `git pull` +
|
||||
run the fetcher → `./scripts/deploy.sh <host> --service <svc>`.
|
||||
|
||||
```bash
|
||||
sudo systemctl show <svc> -p DropInPaths --value # must be empty (bench-drop-in law)
|
||||
sudo tr '\0' ' ' < /proc/$(sudo systemctl show <svc> -p MainPID --value)/cmdline
|
||||
sudo journalctl -u <svc> --since <boot> | grep -iE 'adapter|lora' # llama_adapter_lora_init_impl prints prism.* keys
|
||||
```
|
||||
|
||||
Refusal probe (the only proof of an abliteration adapter): same prompts, `temperature 0.1`,
|
||||
**`max_tokens >= 2048`** — on thinking models a short budget is consumed by
|
||||
`reasoning_content` and `content` comes back empty with `finish_reason: length`, which reads
|
||||
as a refusal that never happened. Classify with a marker regex and report refusal-vs-complied;
|
||||
do not paste generated harmful content into chat. Also run the standing trio: exact-phrase
|
||||
chat, `tool_calls` with valid JSON, and a >=256 px solid-colour vision probe. A residual
|
||||
`⚠️ Important Disclaimer` heading after the swap is style, not the refusal circuit.
|
||||
|
||||
## Docs
|
||||
|
||||
Endpoint page (`serverAppConfigs-docs/LLM/<svc>.md`): Adapter row, Fetch row, Flags row, one
|
||||
Era row whose rollback is the env line, and a measured before/after paragraph. Laws worth
|
||||
adding: provenance is metadata not a gate; an `token_embd` rank would crash, so an adapter is
|
||||
not inert; the runtime pin may gain a second reason. Never restate the card's benchmark table.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: monolith-split-under-dist-pin
|
||||
description: "Use when cutting a monolithic source file into modules behind a black-box test suite whose committed build artifact (dist) must stay byte-identical to src; one verified commit per cut, red-proof against the previous bundle. Not for feature work or doc-only edits."
|
||||
---
|
||||
|
||||
# Monolith split under a committed-bundle pin
|
||||
|
||||
Procedure that landed a 1354-line `index.ts` as 9 modules + 301 lines of wiring across 9 commits with zero red commits. Assumes: black-box test suite (tests drive the built bundle, not internals) and a pin test asserting the committed `dist/` equals a fresh build of `src/`.
|
||||
|
||||
## Per-cut loop (one commit per cut, always in this order)
|
||||
|
||||
1. `bun run build` (or project equivalent) **before** staging — commit `dist/` in the same commit as the moved `src/`, otherwise the pin test fails ("committed dist is byte-identical to a fresh build") and you must amend. Amending is fine only while the branch is unpushed.
|
||||
2. `tsc --noEmit`, then run the full suite async (`bats tests/ > /tmp/x.log 2>&1; grep -E '^1\.\.'`).
|
||||
3. **Never write to `src/` while the suite runs.** `setup()` rebuilds the bundle from `src/`, so a concurrent edit attributes someone else's half-written state to your run. Read-only prep (planning the next cut's exact line ranges) is the correct thing to do while waiting.
|
||||
4. Gate green → next cut. Gate red → fix, rebuild, amend, re-gate.
|
||||
|
||||
## Cutting mechanics
|
||||
|
||||
- Cut leaf/pure modules first (grammar, transcript rules, URL resolution, pure segmenter), then stateful ones (config, IO, pipeline, UI). Never let a module import `index` — if the new module needs something from `index`, that thing is a leaf and must move too (this is what forced `segment.ts` out early).
|
||||
- Extract in **verbatim** bodies: read the exact ranges, write them into the new file, then delete the same ranges. Only module docs, export keywords, and injected dependencies change. Keeps each commit reviewable as a move.
|
||||
- Edit tools that require "displayed lines" from the latest read snapshot: re-read the exact ranges under the current tag, then issue one edit call with all ops listed **bottom-up** (original line numbers throughout). Adjacent ranges merge into one `CUT a=b`; leaving a doubled blank line is the usual scar — one extra `CUT` next turn.
|
||||
- Mutable config must reach a long-lived object as **getters**, not a construction-time snapshot: menu handlers mutate `cfg` after construction, and a snapshot freezes the settings the UI exists to change.
|
||||
- After moving code, grep the old file for the moved names: stale call sites compile under bundlers that skip typechecking (`stop()` vs `speaker.stop()` was caught only by `tsc`, not by `bun build`).
|
||||
|
||||
## Proving a new assertion is red-capable (before trusting it green)
|
||||
|
||||
Build the old bundle and point the test harness at it — no stash, no worktree juggling:
|
||||
|
||||
```bash
|
||||
git show HEAD:dist/index.js > /tmp/old-dist.js # bundle is self-contained
|
||||
# rebuild the throwaway harness by extracting the driver the suite writes
|
||||
sed -n "<heredoc-body-range>p" tests/behavior.bats > "$W/driver.ts"
|
||||
DV_EXT=/tmp/old-dist.js <same env as the test> bun "$W/driver.ts" <scenario> # expect RED
|
||||
DV_EXT="$PWD/dist/index.js" <same env> bun "$W/driver.ts" <scenario> # expect GREEN
|
||||
```
|
||||
|
||||
Copy the project's real player/server stub verbatim — a simplified stub (e.g. one that skips writing the log file the driver counts) prints a false `PLAYS 0` and looks like a failing fix. Pin the scratch dir to a fixed path; `ls -dt /tmp/tmp.*` picks up another session's directory.
|
||||
|
||||
## Black-box test hygiene learned the hard way
|
||||
|
||||
- A test that counts artifacts in the **shared** temp directory observes every other process on the box (live agent sessions synthesize into the same `dankvoice-*` dirs). Give the run a private `TMPDIR="$WORK/scratch-tmp"` and assert there; the assertion's sensitivity is proven by the fact it caught a real leftover while scoped globally.
|
||||
- Let preseeds reference the ephemeral fixture port with a placeholder (`__PORT__`) substituted by the driver, so a test can preseed a URL pointing at its own fixture server.
|
||||
- Refactor-only commits get **no new test**: re-asserting unchanged labels/order after a commit whose point is that they are unchanged is a tautology; the existing suite is the gate. New tests belong to behavior changes, red-first.
|
||||
- Tests must observe **output**, never internals: assert `PLAYS`/`CALLS`/`CALL <url>`/`NOTIFY <text>`, not that a variable was set.
|
||||
|
||||
## Docs after a split (same branch, own commit)
|
||||
|
||||
Move each explanation to the file that now owns it (knob list → the module that reads env; state-machine prose → the module that implements it) and leave the entry point a one-line-per-module map. Then grep for pointers into the old file — a README saying "see header of `src/index.ts`" becomes a lie the moment the knobs move.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: omp-extension-event-probe
|
||||
description: "Prove which lifecycle events the omp harness actually dispatches to extensions, with a throwaway timestamping probe plugin, instead of inferring delivery from side effects."
|
||||
---
|
||||
|
||||
# Prove harness→extension event delivery with a probe plugin
|
||||
|
||||
Use when an extension handler seems never to fire, or when you must know *when* an
|
||||
event arrives relative to another (mid-run vs settle). Never infer dispatch from
|
||||
the feature's side effects: async work in the handler (LLM rewrites, synthesis,
|
||||
network) lags seconds and destroys the timing signal.
|
||||
|
||||
## Shape (no build step needed)
|
||||
|
||||
`/tmp/probe/package.json` — the installer validates the declared entry on disk:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "event-probe",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": "./index.js",
|
||||
"pi": { "extensions": ["./index.js"] }
|
||||
}
|
||||
```
|
||||
|
||||
`/tmp/probe/index.js` — append arrival times, not counts:
|
||||
|
||||
```js
|
||||
import { appendFileSync } from "node:fs";
|
||||
const LOG = "/tmp/probe.log";
|
||||
const stamp = (line) => appendFileSync(LOG, `${new Date().toISOString().slice(11, 23)} ${line}\n`);
|
||||
export default function probe(pi) {
|
||||
pi.on("agent_start", () => stamp("agent_start"));
|
||||
pi.on("turn_end", (e) => stamp(`turn_end turn=${e.turnIndex} toolResults=${(e.toolResults ?? []).length}`));
|
||||
pi.on("agent_end", (e) => stamp(`agent_end messages=${(e.messages ?? []).length} willContinue=${String(e.willContinue)}`));
|
||||
}
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
1. `cd ~/.omp/plugins && omp plugin link /tmp/probe` (link, never edit
|
||||
`node_modules`), then `omp plugin doctor` → expect a `✔ plugin:event-probe` line.
|
||||
2. Launch a **fresh** TUI session in a pty (`omp --cwd /tmp/probe-run`, `pty: true`).
|
||||
Extensions load at startup only — an already-running session never sees the probe.
|
||||
3. PTY gotcha: input sent before the splash clears is swallowed. Read the screen
|
||||
first, then submit text and finish with a separate write containing a bare
|
||||
`\r` (LF alone left the text sitting in the composer).
|
||||
4. Drive a task with ≥2 tool rounds so turns and settles separate in time
|
||||
(sequential reads work and need no approval; ask for steps "strictly one
|
||||
after the other" or the model parallelises them into one turn).
|
||||
5. Wait for a terminal line with an async bounded loop (`grep -q agent_end`,
|
||||
`sleep 1` inside one backgrounded bash call) — never poll across turns.
|
||||
|
||||
## Read
|
||||
|
||||
`turn_end` timestamps strictly earlier than `agent_end` ⇒ per-turn delivery
|
||||
works. `willContinue=true` on `agent_end` marks a non-terminal settle (harness
|
||||
maps `isTerminal === false` to it) — handlers that `return` on it never speak.
|
||||
|
||||
## Teardown (mandatory)
|
||||
|
||||
`omp plugin uninstall event-probe`; `omp plugin doctor`; delete `/tmp/probe*`.
|
||||
The link writes into `~/.omp/plugins/package.json` (tracked, live tier) —
|
||||
uninstall restores it; confirm with `grep` for the probe name returning nothing.
|
||||
|
||||
## Ground truth read for free
|
||||
|
||||
Event union + payloads: `~/.omp/plugins/node_modules/@oh-my-pi/pi-coding-agent/dist/types/extensibility/shared-events.d.ts`;
|
||||
the session→extension mapping lives in `src/extensibility/extensions/lifecycle-mirror.ts`
|
||||
(`extensionEventFromSessionEvent`). Extension-level `turn_end` carries
|
||||
`turnIndex`/`message`/`toolResults` but **no `willContinue`** — that field exists
|
||||
only on `agent_end`, so "is this the last turn" cannot be decided from `turn_end`.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: omp-extension-stale-module-audit
|
||||
description: "Diagnose recurring omp extension handler timeouts/silence when the on-disk fix is already deployed: harness has no extension hot-reload, so running sessions freeze the module at session start (zombie modules)."
|
||||
---
|
||||
|
||||
Use when a fix for an omp extension is merged/deployed yet the SAME errors persist ("handler timed out after 30000ms", silence, wrong behavior) in running sessions.
|
||||
|
||||
**Core fact:** the harness loads extension `.ts` modules at session start and never reloads them (no `/reload` for extensions; bundle `reload` hits are LSP/MCP/browser only). A session older than the fix keeps executing stale code regardless of the file on disk.
|
||||
|
||||
**Procedure:**
|
||||
1. Census the errors from logs, not guesses — timeout lines carry the culprit pids:
|
||||
`grep -h "handler timed out" ~/.omp/logs/omp.<today>*.log | python3 -c 'import sys,json; rows=[json.loads(l) for l in sys.stdin]; rows.sort(key=lambda r:r["timestamp"]); [print(r["timestamp"], r["event"], r["pid"]) for r in rows]'`
|
||||
(`~/.omp/logs/omp.<date>.<pid>.log` — the pid in the filename is the session.)
|
||||
2. Session start times vs deployed file versions:
|
||||
`ps -eo pid,lstart,args | grep '[o]mp'`
|
||||
`git log --format='%h %ad %s' --date=format:'%m-%d %H:%M' -- <extension path>`
|
||||
`git reflog --format='%gd %ad %gs' --date=format:'%m-%d %H:%M' | grep merge` (live-sync pull times = when the file content changed).
|
||||
Any session whose start time precedes the fix's arrival has the stale module.
|
||||
3. Confirm the stale revision actually contains the bad pattern — read what that session loaded, not HEAD:
|
||||
`git show <old-rev>:<path> | grep -n 'await speak\|async (event'`
|
||||
(e.g. an `async agent_end` awaiting a >30 s chain is exactly what the harness's per-handler `Kzc` 30 s race logs; fire-and-forget `void` is fine — detached promises are NOT tracked by the dispatch window.)
|
||||
4. Optional empirical proof when static analysis says "impossible": probe extensions through the real harness:
|
||||
`omp -p --no-extensions -e /tmp/probe.ts "Reply OK"` with a probe that logs handler start/return and `void`s a 40 s detached promise + spawns a long child. Fast exit + no timeout log ⇒ detachment is untracked ⇒ blame stale modules, not the harness.
|
||||
5. Remediation: restart the affected sessions (kill the listed pids or their terminals — the operator session itself may be one of the zombies; say so explicitly). There is no other fix.
|
||||
|
||||
**Gotchas:**
|
||||
- Per-handler attribution in the log is accurate per dispatch; don't blame other extensions before checking the pid's start time.
|
||||
- Endpoints healthy + silence persisting usually means TWO stacked bugs (e.g. stale awaiting module in old sessions AND a mute bug in current code) — diagnose each pid cohort separately.
|
||||
- Extension chain code must use self-owned `AbortSignal.timeout(...)`, never a ctx-scoped signal: harness dispatch signals die when the dispatch window closes.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: omp-lifecycle-event-selection
|
||||
description: "Pick the right omp harness lifecycle event for narration/telemetry: agent_end is a settle, turn_end waits for tool results, message_end is earliest with content, after_provider_response carries no content."
|
||||
---
|
||||
|
||||
# Choosing an omp lifecycle event (narration, telemetry, hooks)
|
||||
|
||||
Re-derived twice from the installed typings/sources (`@oh-my-pi/pi-coding-agent`, `@oh-my-pi/pi-agent-core`) while fixing dankvoice. Read this before wiring a handler; verify against the installed version's `src/extensibility/extensions/types.ts` and `pi-agent-core/src/agent-loop.ts` when precision matters.
|
||||
|
||||
| event | payload you can use | when it fires | traps |
|
||||
|---|---|---|---|
|
||||
| `after_provider_response` | `status`, `headers`, `requestId`, `metadata` **only** | response headers arrive, **before the stream body is consumed** | No message content at all: useless for anything that speaks or summarizes text. Right for latency telemetry, non-200/abort detection, `requestId` correlation. |
|
||||
| `message_end` | full `message` (assistant, user, tool-result, gate-stop, error) | the moment that message is built (`agent-loop.ts`, assistant case) | Earliest event carrying assistant text. Fires for non-assistant messages too → gate `message.role === "assistant"`. Payload is cloned for extensions. |
|
||||
| `turn_end` | `turnIndex`, `message`, `toolResults` | after that turn's **tool results** come back | Stale by the tool duration: a 14 s tool delays a `turn_end` trigger 14 s. Extension event has **no `willContinue`** (the `turnIndex` is numbered by the lifecycle mirror), so you cannot tell the terminal turn from here. |
|
||||
| `agent_end` | `messages`, `willContinue`, `awaitingAsyncWork` | once per user prompt, when the loop settles | `willContinue: true` = the harness scheduled a continuation (auto-retry / empty-stop), not a user-visible settle — narrating it speaks an answer the model is about to redo. Silence-prone on long-running sessions. |
|
||||
| `agent_start` | — | run begin | Use to reset per-run dedupe state, so a later answer with identical text is not swallowed. |
|
||||
|
||||
## Narration cadence pattern (what dankvoice ships)
|
||||
|
||||
- `turn_end` (or `message_end` if the tool-time lag matters) for mid-run commentary; `agent_end` only for the settled answer.
|
||||
- Drop a mid-run trigger while audio/IO is busy — never queue, never interrupt.
|
||||
- Dedupe the terminal text per run (`agent_start` resets), because the last turn and the settle carry the same string.
|
||||
- Skip `ctx.mode === "print" | "json"` so benches stay silent.
|
||||
|
||||
## Prove delivery, don't infer it
|
||||
|
||||
Audio/UI timing cannot show which events arrive. Link a throwaway extension that appends timestamps per event and run one real session: `omp plugin link /tmp/probe` (package.json with `"pi": { "extensions": ["./index.js"] }`), prompt a task with a slow tool, read the log, then `omp plugin uninstall probe`. Confirmed live 2026-10-19: extensions receive `agent_start`, `turn_end`, `agent_end`; `turn_end` landed seconds before `agent_end`.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: omp-local-artifact-path
|
||||
description: Resolve a local:// artifact to a real filesystem path (for scripts/curl) without guessing the session-slug directory.
|
||||
---
|
||||
|
||||
`local://<name>.md` artifacts live in a per-cwd, per-session directory whose name you cannot guess. Three invented paths failed before this worked (2026-10-19).
|
||||
|
||||
Actual layout:
|
||||
|
||||
```
|
||||
~/.omp/agent/sessions/<cwd-slug>/<timestamp>Z_<session-id>/local/<name>.md
|
||||
```
|
||||
|
||||
`<cwd-slug>` is the working directory with `/` replaced by `-`, **keeping the leading dash** — `/home/blank/.omp/wt/wt-20261019-125509-7c8a99d` under `/home/blank` becomes `-.omp-wt-wt-20261019-125509-7c8a99d`. Dropping the leading `-.` is the mistake that produces `FileNotFoundError`.
|
||||
|
||||
## Get the real filesystem path
|
||||
|
||||
Ask the tool, don't reconstruct it — a ranged read of the artifact reports the resolved path in its header:
|
||||
|
||||
```
|
||||
read local://pr-body-msgend.md:1-1 -> [~/.omp/agent/sessions/-.../local/pr-body-msgend.md#TAG]
|
||||
```
|
||||
|
||||
Or locate it by basename (needs enough depth; `maxdepth 5` misses it — the file sits 6–7 levels down):
|
||||
|
||||
```bash
|
||||
f=$(find ~/.omp/agent/sessions -maxdepth 7 -name 'pr-body-msgend.md' | head -1)
|
||||
cp "$f" /tmp/prbody.md # stable path for the python/curl step
|
||||
```
|
||||
|
||||
Copy to `/tmp` before handing the content to a script: one cheap command, and every later tool call stops depending on session-scoped paths.
|
||||
|
||||
## Related lookups in the same directory tree
|
||||
|
||||
- Session id for the current cwd — `~/.omp/agent/history.db` (prompts table has `session_id`, `cwd`; the column is `prompt`, **not** `text`, and raw `?q=` queries cannot be combined with a table selector or pagination):
|
||||
```
|
||||
read history.db?q=SELECT session_id, cwd, substr(prompt,1,50) AS p FROM history WHERE cwd LIKE '%wt-20261019%' ORDER BY rowid DESC LIMIT 3
|
||||
```
|
||||
Use the real id in issue/PR attribution footers; never reuse an id remembered from a compacted transcript.
|
||||
- Transcript dir for the same session sits beside `local/`.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: open-notebook-provision-model-rest
|
||||
description: "Provision or repair an open-notebook model (LLM/TTS/embedding) over its REST API on Dankclaw: credential→model→defaults order, the defaults-store-the-ID trap, the /api/embed 500-that-succeeds, and the two-note ranking proof for embeddings."
|
||||
---
|
||||
|
||||
# open-notebook model provisioning over REST
|
||||
|
||||
Use when adding/repairing a model row (embedding, LLM, TTS) in open-notebook on Dankclaw, when semantic
|
||||
search is dead, or when an embed error appears in the worker log. Config lives in the **database**
|
||||
(encrypted with `OPEN_NOTEBOOK_ENCRYPTION_KEY`) — no repo file mirrors it, REST or the UI are the only doors.
|
||||
|
||||
## Access (never put the bearer in argv)
|
||||
|
||||
The API bearer IS `OPEN_NOTEBOOK_PASSWORD`; the API publishes on loopback `127.0.0.1:5056` only, so run
|
||||
from Dankclaw over ssh. `ssh dankclaw 'curl -H "Authorization: Bearer …"'` leaks the secret into
|
||||
`/proc/<pid>/cmdline` — use a mode-600 curl config:
|
||||
|
||||
```bash
|
||||
PW=$(grep -m1 '^OPEN_NOTEBOOK_PASSWORD=' /home/dank/Containers/open-notebook/env | cut -d= -f2- | tr -d '"')
|
||||
cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: Bearer %s"\n' "$PW" > "$cfg"
|
||||
B=http://127.0.0.1:5056 # not 5055: seerr holds the host's 5055
|
||||
```
|
||||
|
||||
## Order, and the address rule
|
||||
|
||||
`POST /api/credentials` → `POST /api/models` (`credential: <credential id>`) → `PUT /api/models/defaults`
|
||||
→ any profile that names a model (speaker `voice_model`, episode `speaker_config`/`outline_llm`).
|
||||
|
||||
- Credential provider for any in-house OpenAI-compatible server: **`openai_compatible`** (not `openai`),
|
||||
with a dummy `api_key` when the upstream does not authenticate.
|
||||
- The app's URL guard refuses base URLs resolving in `169.254.0.0/16`, so `host.containers.internal`
|
||||
cannot be stored; a **container name on `dankclaw_default`** (e.g. `http://frank-dragoon-halogen:8731/v1`)
|
||||
or another machine's RFC1918 IP both pass.
|
||||
|
||||
## The two traps that eat the most time
|
||||
|
||||
1. **`PUT /api/models/defaults` takes the model ID, never the name.** `"default_embedding_model":
|
||||
"qwen3-embedding-0.6b"` is accepted and echoed back, then every job dies in the worker with
|
||||
`Model with ID qwen3-embedding-0.6b not found`. Use the row's `model:…` id (GET defaults first, change
|
||||
one key, PUT the whole object back — the other keys in that response are ids and must be preserved).
|
||||
2. **`POST /api/embed` answers 500 *after* the work succeeds.** Upstream `EmbedResponse.command_id`
|
||||
declares `str` and the worker returns a `RecordID`, so FastAPI raises in serialization only. The truth is
|
||||
`podman logs open-notebook | grep "Successfully embedded"`, never the HTTP code. Note saves
|
||||
(`POST /api/notes`) submit `embed_note` themselves; `/api/embed` is only for backfills.
|
||||
|
||||
## Proof it actually works
|
||||
|
||||
- Per row: `POST /api/models/<id>/test` → `Embedding dimensions: 1024` (embeddings) or a real completion.
|
||||
- Embeddings, discriminating test: create **two** unrelated notes, then
|
||||
`POST /api/search` `{"query": "...", "type": "vector", "search_notes": true, "limit": 5}` — the enum is
|
||||
`text|vector` (not `semantic`) and the score field is `similarity`. Each query must rank its own note
|
||||
high and the other low (measured 0.659/0.292 vs 0.906/0.318). One note proves nothing: `total_count` is 1
|
||||
either way. Delete the probe notes afterwards (`DELETE /api/notes/<id>`).
|
||||
- Confirm which engine served it: `podman logs <engine container> | grep "POST /v1/embeddings"`.
|
||||
|
||||
Owner pages: `serverAppConfigs-docs/Dankclaw/Open Notebook.md` (app-side shapes), and the endpoint's own
|
||||
page for whatever serves the model.
|
||||
@@ -1,68 +1,34 @@
|
||||
---
|
||||
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.\""
|
||||
description: "Use when several agents edit the same repo in different git worktrees at once, or when an edit lands in the wrong checkout: relative tool paths resolve to the session cwd, not the shell cwd."
|
||||
---
|
||||
|
||||
# Concurrent-worktree path hazard (shared repo, many agents)
|
||||
Use when several agents (or you across two checkouts) edit the same repo in different git worktrees at once, or when an edit/query lands in the wrong checkout.
|
||||
|
||||
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.
|
||||
## The silent failure: relative tool paths resolve to the SESSION cwd
|
||||
|
||||
## 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`.
|
||||
`read` / `edit` / `write` / `grep` resolve a **relative** `path` against the **session working directory** — never against the `cd` inside a neighboring `bash` call, and not against a `../other-worktree/...` prefix either (that gets normalized back into the session tree). So after `cd ../wt-B && grep …` you can `read path="docs/page.md"` and get **wt-A's** copy.
|
||||
|
||||
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.
|
||||
Why it survives "I just read it to check": the returned path is printed workspace-relative and the file name is identical in every checkout, so the output looks like a correct read of the tree you meant. Only content that differs between the checkouts betrays it.
|
||||
|
||||
## 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:
|
||||
Rules:
|
||||
- **Absolute paths for every tool call that targets a worktree other than the session's.** No exceptions for quick reads.
|
||||
- Confirm the checkout from content you know differs (a heading, a status line), not from the echoed path.
|
||||
- A `read` error like "Did you mean .AGENTTRASH/<name>" is a signal the relative path resolved into the session tree — re-issue absolute.
|
||||
- Same trap for `bash`: it starts each call in the session cwd, so `cd` twice per call or use absolute paths there too.
|
||||
|
||||
```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
|
||||
```
|
||||
## Landing a change when the session worktree is stale
|
||||
|
||||
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.
|
||||
`git diff --name-only HEAD origin/main` first. If main moved a file you are editing, your edit's base is wrong even though the file "looks fine" — do not commit on the stale base.
|
||||
|
||||
## 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.
|
||||
1. `git worktree add ../wt-clean -b <type>/<slug> origin/main`; copy the new files in.
|
||||
2. Re-apply the edit against the clean tree's version **with absolute paths**, then read it back absolutely to prove which tree changed.
|
||||
3. Run the suite inside the clean worktree; commit there.
|
||||
4. Remove your own duplicate edit from the session worktree by editing it away (`CUT`/`PUT`). `git checkout -- <path>` and branch switches are policy-blocked in shared workspaces and should stay blocked — they discard edits other agents rely on.
|
||||
5. Never carry the same uncommitted edit in two worktrees at once; if you must keep both trees, keep a task that reconciles them.
|
||||
|
||||
## 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.
|
||||
## Multi-agent hazards in one shared worktree
|
||||
|
||||
- Another agent can stage, commit, or check out files under you; re-read a file immediately before editing it if any tool result hinted at intervening change.
|
||||
- Untracked scratch you drop in a shared tree (reports, notes, `.AGENTTRASH/` entries) is visible to everyone and can be committed by them. Put throwaway artifacts outside the checkout.
|
||||
- Never `git clean -f`, `rm -rf`, or branch switching to "get a clean state" in a tree another agent is using.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: plugin-git-install-committed-artifact
|
||||
description: "Use when shipping or landing an omp/pi plugin (omp plugin install/upgrade git): install validation fails on a missing build artifact, a stale bun git-mirror cache is reused, the live bundle needs proof it is the merged build, or upgrade says \"not installed\"."
|
||||
---
|
||||
|
||||
# Plugin install from git: committed artifact + fresh mirror
|
||||
|
||||
`omp plugin install git:<url>` (and `pi install git:`) **clone the repo and validate the manifest's declared extension entry WITHOUT running a build step**. Two failure classes seen live (dank/dankvoice, 2026-10-19):
|
||||
|
||||
## 1. Declared entry missing from the clone
|
||||
|
||||
Symptom: `Plugin <name> extension validation failed: ./dist/index.js: declared extension entry not found on disk`.
|
||||
|
||||
Cause: build output (`dist/`) is gitignored; the manifest points at it; the clone has only `src/`.
|
||||
|
||||
Fix — ship the artifact:
|
||||
- Remove `dist/` from `.gitignore`, `bun run build`, commit `dist/`.
|
||||
- Prove build determinism first: build twice, `cmp` the outputs (bun esm builds are byte-reproducible without sourcemaps).
|
||||
- Pin the contract in a small bats suite so it can't rot:
|
||||
1. the manifest's declared entry is tracked in git (`git ls-files -- "${entry#./}"` non-empty — this is the installer's exact contract, not a config assertion);
|
||||
2. committed bytes equal a fresh build (`bun run build` then `git diff --name-only -- dist` is empty → red on drift).
|
||||
|
||||
## 2. Stale bun git-mirror cache
|
||||
|
||||
Symptom: fix pushed, retry **still** fails identically, and the retry is suspiciously fast (seconds).
|
||||
|
||||
Cause: bun keeps bare mirrors in `~/.bun/install/cache/<hash>.git` and may reuse them at an old commit — even after the fix is pushed and the default branch is correct.
|
||||
|
||||
Diagnose + fix:
|
||||
```bash
|
||||
find ~/.bun/install/cache -maxdepth 2 -iname "*<repo>*"
|
||||
git --git-dir ~/.bun/install/cache/<hash>.git log --oneline -3 --all # stale HEAD?
|
||||
rm -rf ~/.bun/install/cache/<hash>.git # it is a cache; safe
|
||||
```
|
||||
Prove the fetch path independently first (also validates credentials for private Gitea repos):
|
||||
```bash
|
||||
GIT_TERMINAL_PROMPT=0 git clone -q <url> /tmp/x && git -C /tmp/x log --oneline -1 && ls /tmp/x/dist
|
||||
```
|
||||
|
||||
## Order of investigation (don't skip)
|
||||
1. Gitea default branch correct? (`GET /api/v1/repos/<o>/<r>` with token — private repos 404/None unauthenticated.)
|
||||
2. Plain unattended clone contains the entry? If yes, the installer's view is cached → purge mirror.
|
||||
3. Only then suspect the installer itself (validation code lives in the minified bundle; `grep -oE '.{600}<error string>' "$bunfile"` on `readlink -f $(which omp)` shows the exact check).
|
||||
|
||||
## Landing a merged PR on the live box
|
||||
|
||||
After the merge and the `--ff-only` on the dev checkout (`ls -ld node_modules` after landing anything that could have touched it):
|
||||
|
||||
- **`omp plugin upgrade <name>` takes the PACKAGE name, not the install alias.** `omp plugin upgrade dankvoice` fails `dankvoice is not installed` while the live copy sits in `~/.omp/plugins/node_modules/omp-dankvoice/`. Read the directory name (or `package.json` `"name"`) and upgrade that.
|
||||
- Then close the chain of custody with one command — the deployed bytes are the merged bytes:
|
||||
```bash
|
||||
cmp ~/Projects/<repo>/dist/index.js ~/.omp/plugins/node_modules/<pkg>/dist/index.js
|
||||
```
|
||||
If only docs/tests changed, `dist/` is untouched and this should already match; if `src/` changed, the plugin must be upgraded first or this fails with `char <n>` — which is a real finding (stale live bundle), not a test bug.
|
||||
- The session that did the merge keeps the OLD bundle in memory: it is still running pre-merge code. Never conclude "the change does not work" from the merging session.
|
||||
|
||||
## After install — proof without a TUI
|
||||
- `omp plugin doctor` → per-plugin ✔ line.
|
||||
- Audible/behavioral chain when TUI input is unusable (update-banner modal freezes pty submits): run `omp --mode=rpc --no-session` with the feature env armed (dankvoice: `DANKVOICE_AUTO=1`, gate is print/json-only) and watch for objective side effects, e.g. PipeWire: `pw-dump | grep -q "say-<ms>.wav"` proves synth+playback end-to-end.
|
||||
- When pty submits DO work (no modal), the strongest instrument is a stub player plus a clip count, not ears or pixels: `DANKVOICE_PLAYER=/tmp/stub.sh` where the stub appends `$1` to a log and exits 0. Launch the DEPLOYED bundle by path (`omp --no-extensions --no-session -e ~/.omp/plugins/node_modules/<pkg>/dist/index.js`) in a neutral cwd with `DANKVOICE_CONFIG` pointed at a scratch file so the operator's settings are untouched.
|
||||
- **Count clips per narration EVENT, not per turn.** One turn can fire several narrations (dankvoice: `message_end` carries prose+thinking, `agent_end` the settled answer; the dedupe only skips byte-identical text, so thinking-on means both speak). A "too many clips" result is usually correct behaviour — check the harness transcript for the turn count before believing a bug. The player deletes each temp wav, so the log line count is the only surviving evidence: park it in `~/.AGENTTRASH/<date>-<topic>/` if it becomes evidence for an issue.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: red-verify-without-stash
|
||||
description: "Prove a regression test is red-capable against pre-fix code in a shared worktree where git stash/checkout are policy-blocked, via git-show blob swap with md5-verified restore."
|
||||
---
|
||||
|
||||
# Red-verifying tests without `git stash`
|
||||
|
||||
## When
|
||||
You fixed a file and added tests that should have failed before the fix, but workspace policy blocks `git stash` (and `git checkout -- file`) in shared worktrees (e.g. `~/.omp` shared-workspace policy). Needed to prove red-capture instead of asserting it.
|
||||
|
||||
## Recipe
|
||||
One `bash` call — swap the fixed file for the HEAD (pre-fix) blob, run only the fix-relevant tests, restore, verify byte-identity:
|
||||
|
||||
```bash
|
||||
cd "$WT"
|
||||
md5sum path/to/fixed.file > /tmp/file-hash
|
||||
cp path/to/fixed.file /tmp/fixed.file.bak
|
||||
git show HEAD:path/to/fixed.file > path/to/fixed.file # pre-fix content; new tests now exercise old code
|
||||
bats -f "test name A|test name B" tests/suite.bats # expect: not ok for every fix-relevant test
|
||||
cp /tmp/fixed.file.bak path/to/fixed.file
|
||||
md5sum -c /tmp/file-hash && echo RESTORED
|
||||
```
|
||||
|
||||
## Notes
|
||||
- `git show HEAD:path > path` truncates-and-refills the file in place; safe because you hold the md5 and a backup copy, and the restore check proves the fixed bytes are back.
|
||||
- Run only the fix-relevant tests via the runner's name filter (`bats -f` regex); a full suite wastes time and old tests pass on both versions.
|
||||
- Expect exactly the tests that pin the fix's behavior to fail pre-fix; tests unrelated to the change (pure coverage of old behavior) pass on both versions — don't count them as red evidence.
|
||||
- `bats` pipelines mask exit codes without `pipefail`; judge by the `ok`/`not ok` lines, not the shell status.
|
||||
- For non-test proof of endpoint behavior (live A/B of an HTTP request body), replay the exact body with `curl` against the live endpoint instead of speculating; keep both timings.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: repo-agent-handoff-doc
|
||||
description: "Use when asked to write or refresh a standing handoff/status doc so agents landing in a repo can continue work isolated from other directories; sections that code cannot reveal, plus moving untracked docs onto a branch without dirtying a shared checkout."
|
||||
---
|
||||
|
||||
# Standing agent handoff doc for a repo
|
||||
|
||||
"Put a complete handoff on project X in its directory so agents there can work
|
||||
isolated" is not a changelog request. The value is the knowledge the code cannot
|
||||
reveal. Discover the repo's own conventions first, then write one **living** file.
|
||||
|
||||
## 0. Fit the repo, do not invent a layout
|
||||
|
||||
`ls -a` + `grep -n '^##' README.md` + `find docs -maxdepth 2` before writing:
|
||||
- existing `docs/` conventions (dated `docs/decisions/*.md` records vs flat pages),
|
||||
- whether an `AGENTS.md`/`CLAUDE.md` auto-loads (if not, the README is the only
|
||||
entry point → the handoff MUST be linked from it, same commit),
|
||||
- the exact numbers you will quote: per-file `grep -c '^@test'`, `package.json`
|
||||
`scripts`, module list from `ls src/`. Never write a test count or command from
|
||||
memory.
|
||||
|
||||
## 1. Section checklist (point of use — each answers a question the next agent hits)
|
||||
|
||||
1. **State line**: the commit it describes (`main` at `<sha>`, "after PRs #N") +
|
||||
the rule that whoever leaves work updates it in the same commit. A stale
|
||||
handoff is worse than none: it gets believed.
|
||||
2. **Commands** — build/typecheck/test with real runtime, plus concurrency
|
||||
hazards (e.g. a suite that rebuilds `dist/` in `setup()` → two `bats` runs
|
||||
corrupt each other's byte-identical pin).
|
||||
3. **Test seam and how to add a test** — where the harness mock lives, scenario
|
||||
names, how scripted UI answers are consumed, env dials that are *ignored*
|
||||
(a `setup()` that clobbers your override), and the red-first rule with the
|
||||
one exception (a characterization test that says so in a comment).
|
||||
4. **The laws, each with the test that holds it** — table form. Distinguish
|
||||
load-bearing invariants from incidental behaviour; this is the section that
|
||||
stops a confident agent "fixing" a chosen design.
|
||||
5. **Worktree/dev-dir recipe** — symlinked deps, gitignore rule that keeps the
|
||||
symlink untracked, why the shared checkout stays clean.
|
||||
6. **Ship path** — branch → tests → rebuild committed artifact → PR → merge on the
|
||||
owner's OK → land with `--ff-only` → deploy (the gotcha that the upgrade target
|
||||
is the PACKAGE name, not the directory alias) → `cmp` the live artifact against
|
||||
the merged build → prune. Include the auth traps of the forge (token via a
|
||||
600 curl `-K` file, empty token answering 404 not 401, form-encoded merge).
|
||||
7. **Live verification** — the instrument that produces evidence (stub player,
|
||||
log line counts, pty submit semantics), and explicitly what is NOT a check
|
||||
(print-mode invocation, screen reads of transient toasts, single-take audio
|
||||
comparisons).
|
||||
8. **Environment reality not visible from this code** — the live endpoints,
|
||||
measured limits, refusals; crosslink the README if it owns the table.
|
||||
9. **Open items** — numbered, each actionable alone, each saying who decides if a
|
||||
human must; name the repo a fix belongs in when it is NOT this one.
|
||||
10. **History** — one line per issue/PR. No tables of metrics.
|
||||
|
||||
One owner per fact: link the README/decision records instead of restating knob
|
||||
tables or rationale.
|
||||
|
||||
## 2. Tree hygiene (the costly step)
|
||||
|
||||
Writing straight into a shared checkout leaves an untracked file on `main` — a
|
||||
half-deployed state where the checkout is the deployment, and a dirty tree for
|
||||
everyone else. Move it onto a branch without switching anyone's HEAD:
|
||||
|
||||
```bash
|
||||
mv <repo>/docs/handoff.md /tmp/handoff.md
|
||||
git -C <repo> fetch -q origin
|
||||
git -C <repo> worktree add -q -b docs/<slug> ~/Projects/wt-<topic> origin/main
|
||||
ln -s <repo>/node_modules ~/Projects/wt-<topic>/node_modules # if deps are needed
|
||||
mv /tmp/handoff.md ~/Projects/wt-<topic>/docs/handoff.md
|
||||
git -C <repo> status --porcelain # shared checkout clean again
|
||||
```
|
||||
|
||||
README pointer edit happens **in the worktree** (an edit there is a separate
|
||||
checkout; editing the shared one dirties a tracked file). Then commit, run the
|
||||
suite, push, open the PR, and merge only on the owner's explicit OK.
|
||||
|
||||
## 3. Voice rules
|
||||
|
||||
Prose in the repo's own style (match an existing decision record). State reasons
|
||||
and consequences, not bookkeeping. No measured numbers for things that drift;
|
||||
keep the law, drop the figure.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: ryzenai-generic-failure-loader-order
|
||||
description: "Debug AMD Ryzen AI OGA \"Exception during initialization: Generic Failure\" on Linux XDNA2 — the LD_LIBRARY_PATH-vs-activate ordering trap, its proof instruments, and the OGA 0.14 run contract."
|
||||
---
|
||||
|
||||
Symptom (any `onnxruntime_genai` run on a Ryzen AI NPU, container or bare):
|
||||
|
||||
```
|
||||
[E:onnxruntime:, inference_session.cc:2811 operator()] Exception during initialization: Generic Failure
|
||||
```
|
||||
|
||||
Fails ~2 ms after `og.Model()`. No `openat` of `/dev/accel/accel0`, **nothing in the kernel log**, no XRT log (the INI logger is not built into AMD's shipped XRT), no core dump. It is NOT a platform, model-export, or driver-ABI failure.
|
||||
|
||||
## Root cause
|
||||
|
||||
`source <venv>/bin/activate` from `install_ryzen_ai.sh` **prepends** to `LD_LIBRARY_PATH`:
|
||||
|
||||
```
|
||||
site-packages/flexml/flexml_extras/lib : site-packages/onnxruntime/capi : site-packages/voe/lib
|
||||
```
|
||||
|
||||
`voe/lib` is the **compiler-side** bundle: it ships its own `libxrt_coreutil.so.2` and `libryzen_mm.so.1`. Set the path *before* activating and those copies win, XRT's XDNA shim binds against the wrong coreutil, `xrt_core::shim_loader::shim_loader()` → `get_userpf_device()` → `xrt::device(0)` throws, `ryzenai::Allocator::Alloc` throws next, and ORT flattens both into the bare string above.
|
||||
|
||||
**Rule: activate first, THEN set `LD_LIBRARY_PATH`, with `/opt/xilinx/xrt/lib` first.** Passing the var via `podman -e` and sourcing activate afterwards in the same `bash -lc` is the broken shape — that is exactly how a runner script differs from a hand-typed probe, and it is why diagnostics pass while the runner fails. Mounting XRT is not enough either: the tree must be **on** the path.
|
||||
|
||||
## Proof instruments, cheapest first
|
||||
|
||||
1. `ldd <deployment>/lib/libonnxruntime_providers_ryzenai.so | grep -E "libxrt_coreutil|libryzen_mm"` — the resolved path settles it alone: `/opt/xilinx/xrt/lib` = good, `.../voe/lib` = this bug.
|
||||
2. `XILINX_XRT=/opt/xilinx/xrt /opt/xilinx/xrt/bin/xrt-smi examine` → `RyzenAI-npu5 | aie2p | 6x8` + `NPU Firmware Version`. Needs `libboost_filesystem1.83.0`, `libboost_program_options1.83.0`, `libuuid1` (26.04 names; the 24.04 docs' 1.74.0 does not exist). A "No such file or directory" for a file that exists = missing interpreter/dep, not a missing binary.
|
||||
3. `strace -f -e trace=openat` — `/dev/accel/accel0` never opened ⇒ failure is pre-device ⇒ loader-level.
|
||||
4. `gdb -q -batch` with `catch throw` + `bt 10` loop → names `shim_loader` / `Allocator::Alloc`.
|
||||
|
||||
Known dead ends (one run each, then stop): `--ulimit memlock`; XRT INI verbosity; `DD_LOG_LEVEL` / `RYZENAI_TRACE` / `RYZENAI_EP_TRACING` (print nothing before the throw); byte-scanning XRT `.so` for PCI IDs (ambiguous); libboost presence for the OGA path (only `xrt-smi` needs it); flag-by-flag container matrices (`--memory`, `--security-opt`, `--name`, extra mounts — all innocent).
|
||||
|
||||
## Verified facts on this stack
|
||||
|
||||
- Vendor XRT 2.25.37 userspace (`dpkg-deb -x`, never dpkg-installed) works against Ubuntu's in-tree `amdxdna` 0.7.0; no DKMS module needed. PCI `1022:17f0` rev 11 = XDNA2, reported as `aie2p 6x8`.
|
||||
- AMD's `quicktest` passing does **not** prove NPU use (it can silently run on CPU); trust `xrt-smi` + logits, not "it didn't crash".
|
||||
- `/dev/accel/accel0` is `root:render 0660`; rootless podman cannot read it (userns drops the gid; `keep-groups`, `keep-id:gid=990`, `--group-add render` all fail) → rootful container, or a udev rule like the GPU nodes'.
|
||||
- OGA 0.14 (RAI 1.8) contract: `GeneratorParams` has only `set_search_options` (no `input_ids`, no `model.generate`); feed with `Generator.append_tokens(ids)`, loop `generate_next_token()`, read `get_sequence(0)`; `max_length` counts **prompt + generated**. Full Fusion (`*_npu_4K`) caps input+output at 4096.
|
||||
- Validate content, not just exit code: `np.argsort(logits[0])[-8:]`. A real logits tensor after `append_tokens` proves the graph executed (prefill is folded into step 1, so a sub-millisecond first step + a 1-token sequence means it did not); if argmax is the sequence terminator (`
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: ryzenai-npu-oga-linux-run
|
||||
description: "Run an AMD vendor-compiled Ryzen AI (XDNA2) model — OGA/onnxruntime-genai — on a Linux Ryzen AI host with the in-tree amdxdna driver, without installing the vendor driver or touching the host driver; includes the triage for ORT \"Exception during initialization: Generic Failure\"."
|
||||
---
|
||||
|
||||
Run a vendor-compiled Ryzen AI (XDNA2) model on Linux with the **in-tree** amdxdna driver: no vendor kernel module, no `dpkg -i`, host driver untouched.
|
||||
|
||||
## Preconditions
|
||||
- Host: Strix Point / Strix Halo / Krackan (PCI `1022:17f0`), in-tree `amdxdna` bound to the NPU function (`readlink /sys/bus/pci/devices/<bdf>/driver` → `amdxdna`), `/dev/accel/accel0` present.
|
||||
- `ryzen_ai-<ver>.tgz` installed into a venv (`install_ryzen_ai.sh -a yes -p <venv>`; it hard-codes `python3.12` — put a uv-managed 3.12 **bin dir** on PATH, never a symlink: a symlinked shim breaks venv creation with `No module named 'encodings'`).
|
||||
- Model artifact MUST match the RAI version: `<name>_rai_<ver>_npu_<ctx>`. Other versions load and emit garbage.
|
||||
- Vendor XRT userspace: `dpkg-deb -x` the debs from `RAI_<ver>_Linux_NPU_XRT.zip` (base + npu + `xrt_plugin…amdxdna.deb` — the plugin is what ships `libxrt_driver_xdna.so.*`) into a host dir, bind-mount it at `/opt/xilinx/xrt`.
|
||||
|
||||
## Container run line (the part that is easy to get wrong)
|
||||
```
|
||||
--device /dev/accel/accel0 # root:render 0660 → rootful, or a world-writable udev rule
|
||||
-v <xrt-root>/opt/xilinx/xrt:/opt/xilinx/xrt:ro
|
||||
-e RYZEN_AI_INSTALLATION_PATH=<venv> -e XILINX_XRT=/opt/xilinx/xrt
|
||||
-e LD_LIBRARY_PATH=/opt/xilinx/xrt/lib:<venv>/onnxruntime/lib:<venv>/deployment/lib
|
||||
# cwd MUST be <venv>/deployment/lib — the RyzenAI EP is dlopen'd by bare name and the
|
||||
# only place ORT looks is the working directory.
|
||||
# Activate the venv explicitly; a login shell (`bash -lc`) re-sources the profile and
|
||||
# can put a numpy-less base python first on PATH.
|
||||
```
|
||||
|
||||
## LAW: mounting XRT is not enough — `/opt/xilinx/xrt/lib` must be ON `LD_LIBRARY_PATH`
|
||||
The RyzenAI EP reaches the XDNA shim through XRT's own loader. With the tree bind-mounted
|
||||
but off the loader path, `xrt::device(0)` throws in `xrt_core::shim_loader`, the RMM
|
||||
allocator fails next, and ORT flattens all of it into
|
||||
|
||||
```
|
||||
[E:onnxruntime:, inference_session.cc:2811] Exception during initialization: Generic Failure
|
||||
```
|
||||
|
||||
Signature that distinguishes this from a real device/driver fault: fails in ~2 ms, **never
|
||||
opens `/dev/accel/accel0`**, nothing in `dmesg`/`journalctl -k`, no XRT log (the INI logger
|
||||
is not built into the vendor build — `XRT_INI_PATH` + `[Common] verbosity` writes nothing).
|
||||
|
||||
## Triage recipe for "Generic Failure" (in this order — each step is one call)
|
||||
1. `strace -f -e trace=openat` → does it open `/dev/accel/accel0`? If **no**, the failure is pre-device: loader/shim, not the driver. Also note the last file it opens (`genai_config.json` = config parsed, then provider setup fails).
|
||||
2. `gdb -q -batch -x cmds --args python driver.py` with `catch throw` + `bt 10` + `continue`. Frames `xrt_core::shim_loader::shim_loader` / `get_userpf_device` / `xrt::device::device` then `ryzenai::Allocator::Alloc` ⇒ the shim never loaded ⇒ fix `LD_LIBRARY_PATH`.
|
||||
3. Prove it with a two-arm run in ONE container (toggle only the suspect) before writing the law down. Both arms passing ⇒ that variable was never the cause.
|
||||
4. Ground truth on device visibility: `xrt-smi examine` → `RyzenAI-npu5 | aie2p | 6x8` + `NPU Firmware Version`. If xrt-smi dies on `libboost_filesystem.so.1.83.0`, install `libboost_filesystem1.83.0`+`libboost_program_options1.83.0` — **boost is only needed by xrt-smi, not by the OGA/EP path** (verified by purging it and still loading the model).
|
||||
5. `voe` in site-packages is a *compiler* toolkit (passes/patterns), not a runtime — swapping `LD_LIBRARY_PATH` to `voe/lib` fixes nothing.
|
||||
6. Byte-scanning XRT `.so` files for supported PCI IDs is inconclusive (2-byte ID patterns collide constantly) — don't bother.
|
||||
|
||||
## Distrust "it ran" on this stack
|
||||
- Vendor `quicktest` can print success while computing on CPU (missing peano lib ⇒ silent CPU fallback). Prove NPU residency per run: sample `/sys/class/accel/accel0/device/mem_usage` (needs root) during generation, or read the EP's `Actually running on NPU` line.
|
||||
- Never report request #1 after a boot as a rate — first pass includes graph/init cost.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: ryzenai-npu-oga-linux-spike
|
||||
description: "Run an AMD-precompiled AMD Ryzen AI (XDNA2) ONNX-GenAI model on a Linux Strix Halo host from public artifacts: staging, rootless NPU device-access wall, container recipe, and the NuTTS-air speech-token contract (65,536-code vocab, 50 codes/s real-time line)."
|
||||
---
|
||||
|
||||
Run an AMD-precompiled XDNA2 model (OGA / onnxruntime-genai) on a Linux Strix Halo host (Dankclaw-class) without installing the vendor stack on the host.
|
||||
|
||||
## Where the bits actually live (both public, no AMD account)
|
||||
|
||||
- Runtime tarball: `https://download.amd.com/opendownload/RyzenAI/1.8.0/ryzen_ai-1.8.0.tgz` (~7.28 GiB). Verify with a ranged GET — first bytes `1f 8b 08`.
|
||||
- NPU driver/XRT zip: `https://download.amd.com/opendownload/RyzenAI/Driver/RAI_1.8_Linux_NPU_XRT.zip` (90 MB).
|
||||
- Dead ends (404, do not re-probe): `download.amd.com/driver/linux/RAI_1.8_Linux_NPU_XRT.zip`, `.../opendownload/RyzenAI/ryzen_ai-1.8.0.tgz`, `www.xilinx.com/bin/public/openDownload?filename=...` (301 → 404). The login-walled EULA *form* is not the gate; the `opendownload` tree is the real file host.
|
||||
|
||||
Tarball has 30 top-level entries: cp312/linux_x86_64 wheels (`onnxruntime_genai_ryzenai-0.14.0`, `onnxruntime_vitisai-1.27.0`, `vitis_mllib`, `flexml*`, `device_essentials_strx`, `model_generate`, `voe-1.8.0`), plus `deployment.tar.gz` (contains `lib/libonnxruntime-genai.so`, `lib/libonnxruntime_providers_ryzenai.so`, AIE compiler libs), `onnxruntime.tar.gz`, `quicktest.tar.gz` (VitisAI-EP install validator), `LLM.tar.gz` (text-only OGA examples: `run_model.py`, `tokenizer_factory.py`), `install_ryzen_ai.sh`. `voe-4.0-linux_x86_64.tar.gz` is only `vaip_config.json` — no TTS xclbins. **AMD ships no TTS runner and no TTS example.**
|
||||
|
||||
## Host rules
|
||||
|
||||
- **Never `dpkg -i` the vendor XRT.** `dpkg-deb -x <deb> <dir>` and bind-mount `<dir>/opt/xilinx/xrt` at the same path in the container. The plugin deb would install a DKMS `amdxdna` that replaces the distro in-tree driver; the in-tree driver on Ubuntu 26.04 is `amdxdna 0.7.0` and already loads `amdnpu/17f0_11/npu_7.sbin` at boot (`dmesg | grep amdxdna`).
|
||||
- **Rootless containers cannot read `/dev/accel/accel0`** (`root:render 0660`): the host gid never enters the userns gid map, so `--device` yields `nobody:nogroup`. Verified failing 2026-10-19: `--group-add keep-groups`, `--userns=keep-id`, `--userns=keep-id:gid=990` (process gets gid 990, node still unreadable), `--group-add render`, `--group-add gid:990` (both error). Options: run rootful (`sudo podman`, separate storage → rebuild the image rootful, ~1 min), or add a world-writable udev rule for `accel0` (the fleet already does exactly this for `kfd`/`renderD*` in `99-comfyui-gpu-access.rules`, whose header explains why: systemd user-unit spawns carry no supplementary groups).
|
||||
- **Rootless image build trap:** the vendor installer's `tar -xf` preserves recorded uids (95777 etc.) and aborts under `set -e` with `Cannot change ownership ... Invalid argument`, silently skipping the `deployment`/`onnxruntime` extraction. Patch it in the Containerfile: `sed -i 's/tar -xf/tar --no-same-owner -xf/g' install_ryzen_ai.sh`.
|
||||
- Installer usage: `./install_ryzen_ai.sh -a yes -p /opt/rai -c /opt/rai` (python **3.12** required; base `python:3.12-slim`; needs `linux-libc-dev zip pciutils patch libgomp1`). It builds a venv from the local wheels, reinstalls `onnxruntime-vitisai` then `--no-deps onnxruntime-genai-ryzenai`, installs `requirements_extras.txt` (transformers 4.57.6), patches `bin/activate` (sets `RYZEN_AI_INSTALLATION_PATH=$VIRTUAL_ENV` + `LD_LIBRARY_PATH` into site-packages), and untars `LLM/deployment/onnxruntime/voe/quicktest` into the cpp path.
|
||||
- `xrt-smi` in a container needs `libboost-filesystem1.83.0` + `libboost-program-options1.83.0` (**hyphenated** Ubuntu names) and `apt update` first; boost is **only** needed by `xrt-smi`, never by the OGA/EP path.
|
||||
|
||||
## HF artifact staging
|
||||
|
||||
- Gated (401 on API, 403 on git) for a plain account: `neuphonic/neucodec` (the **only** NeuCodec encoder), `neuphonic/neucodec-onnx-decoder`, `neuphonic/neucodec-onnx-decoder-int8`, `neuphonic/neutts-air`. `amd/neutts-air_rai_1.8.0_npu_4K` is public. Public mirrors of the codec exist (`eugenehp/neucodec` has the full 1.16 GB `pytorch_model.bin`) — treat a mirror of a gated release as a licensing decision for the operator, not a workaround to take silently.
|
||||
- Gated-repo loader trap: `HF_HUB_OFFLINE=1` + `HF_HOME` pointed at a cache laid out as `hub/models--<org>--<repo>/snapshots/<rev>/…` works, but `NeuCodec._from_pretrained` **asserts the repo id** is in `["neuphonic/neucodec", "neuphonic/distill-neucodec"]` — a local directory path is refused outright, so pass the repo id and satisfy it from the cache.
|
||||
- `github.com/neuphonic/neutts` is public and ships `samples/{dave,emily,greta,jo}.{pt,txt,wav}` = **pre-encoded reference codes + transcripts** → a spike needs zero gated bytes.
|
||||
- Auth without leaking the token into `ps`: build a 600-perm credential file with a shell builtin and point git at it —
|
||||
`umask 077; printf 'https://oauth2:%s@huggingface.co\n' "$(cat ~/.cache/huggingface/token)" > ~/.git-credentials-hf`
|
||||
then `git -c credential.helper="store --file ~/.git-credentials-hf" clone ...`. Never `git clone https://oauth2:$TOKEN@...`.
|
||||
|
||||
## NuTTS-air on XDNA2 (the model-side contract)
|
||||
|
||||
- The package is an OGA model dir: `genai_config.json` (`type: qwen2`, 24 layers, hidden 896, 14 heads / 2 KV, ctx 32768, `vocab_size` **217,652**, `provider_options: [{"RyzenAI": {"hybrid_opt_token_backend": "npu"}}]`), plus `dd_metastate_*` AIE artifacts and `model.onnx` + `model.onnx.data`.
|
||||
- **Speech tokens live in the text vocab, and the codebook is 65,536 — not 10,001.** `tokenizer.json` `added_tokens` holds exactly 65,536 `<|speech_N|>` entries, ids **151,671–217,206** contiguous, over a base BPE vocab of 151,643; specials are 151,644–151,670 (`<|SPEECH_GENERATION_END|>` = 151,670). The vendor template maps a codec index straight to its id: `"".join(f"<|speech_{i}|>" for i in ref_codes)`. An encoded reference really does contain codes in the 60k range, so a 10k assumption silently truncates the space.
|
||||
- The graph's own `eos_token_id` is `[151645, 151643]` (the **chat turn** stop) and `search.eos_token_ids` is unset — pass 151,670 as an extra eos or speech ends after one token.
|
||||
- **Real-time line = 50 speech codes/s** — NeuCodec `sample_rate 24000 / hop_length 480`, measured exactly (49.96–50.00 across three 20–25 s references). Judge any NPU/iGPU TTS number against 50. (Qwen3-TTS, the fleet's deployed arm, is 12.5 codes/s — do not conflate the two bars.)
|
||||
- Reference encode is cheap and cacheable: `librosa.load(sr=16000, mono=True)` → `[1,1,T]` float tensor → `codec.encode_code(audio_or_path=…)` (mirrors `NeuTTS.encode_reference`); CPU encoder RTF 0.28–0.38 on 8 threads. `neucodec` needs `torchtune` (`RotaryPositionalEmbeddings`) — install both `--no-deps --target <dir>` so a second torch never shadows the venv's.
|
||||
- Build the prompt by *importing* the vendor function, never by copying the template:
|
||||
`sys.path.insert(0, <neutts repo>); from neutts.neutts import NeuTTS`, then
|
||||
`NeuTTS._apply_chat_template(SimpleNamespace(tokenizer=hf_tok, input_format="text"), ref_codes, ref_text, text, None)` — works on a duck-typed `self`, so no backbone/codec load. Template: `user\n<|TEXT_REPLACE|>\nassistant\n<|SPEECH_REPLACE|>`; splice `TEXT_PROMPT_START + encode(f"{ref_text} {text}") + TEXT_PROMPT_END` at TEXT_REPLACE, then truncate at SPEECH_REPLACE and append `[SPEECH_GENERATION_START] + encode("".join(f"<|speech_{i}|>"))`.
|
||||
- Voice identity = reference codes + transcript (request-time conditioning). No trained artifact exists to preserve; packs holding only `reference.wav` require the gated encoder once, offline — **and** a transcript, which a voice pack does not carry (`manifest.json` holds provenance/crop geometry only) and a host without ASR cannot produce.
|
||||
- Full-Fusion input ceiling: 4096 tokens **total**, input + output.
|
||||
|
||||
## Status caveat (2026-10-19)
|
||||
|
||||
Staged artifacts, image build, device-access matrix, prompt contract, reference pre-encode:
|
||||
**done and reproducible.** Token generation on the NPU: **NO-GO, and the loader is the
|
||||
wall.** The device is healthy (`xrt-smi validate` PASSED incl. `throughput`,
|
||||
`RyzenAI-npu5 / aie2p / 6×8`, `pyxrt.device(0)` constructs), and `strace` shows the
|
||||
failing load reading `genai_config.json`, opening `model.onnx`, loading
|
||||
`libxrt_driver_xdna.so.2` and opening `/dev/accel/accel0` with successful GEM ioctls —
|
||||
then the RyzenAI EP **declines the graph before touching any `dd_metastate_*`**, silently:
|
||||
`GENAI_ORT_VERBOSE_LOGGING`, `GENAI_LOG_ORT_LIB`, `set_log_options`,
|
||||
`set_log_callback(callable)`, `RYZENAI_EP_TRACING`, `RYZENAI_LLM_PROFILE_INIT` all emit
|
||||
zero lines. Hardlinked mirror dirs of the model tree were ruled out as shadowing. One run
|
||||
loaded and produced finite logits; byte-identical runs never reproduced it. **No token rate
|
||||
was ever measured, so quote none**, and remember `quicktest` success ≠ NPU residency (that
|
||||
build falls back to CPU silently — prove residency via
|
||||
`/sys/class/accel/accel0/device/mem_usage`).
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: ryzenai-oga-model-dir-and-eos-override
|
||||
description: "Debug OGA on XDNA2 when the RyzenAI EP never claims the If-rooted graph (shadow model dirs, bind-mounted configs, missing device), or a TTS/LLM graph stops on the wrong eos token."
|
||||
---
|
||||
|
||||
# OGA on XDNA2: graph not claimed, or generation stops at once
|
||||
|
||||
Two independent failure families in the AMD `onnxruntime-genai` (OGA) + RyzenAI EP stack on
|
||||
Linux XDNA2 (Strix Halo / Ryzen AI Max). Diagnose them in this order — they are separate
|
||||
gates and each has its own instrument.
|
||||
|
||||
## Gate 0 — is the NPU path alive at all?
|
||||
|
||||
Run the vendor's own instrument before touching anything else:
|
||||
|
||||
```bash
|
||||
cd /opt/rai/quicktest && RYZEN_AI_INSTALLATION_PATH=/opt/rai python quicktest.py
|
||||
# want: "Setting environment for STX/KRK" ... "Session successfully initialized" ... "Test Finished"
|
||||
```
|
||||
|
||||
It finds the device by shelling out to `lspci -nn` and grepping `1022:17f0`, so it also
|
||||
proves `lspci` is present in the container. Pass ⇒ the in-tree `amdxdna` driver and the
|
||||
extracted userspace pair correctly; stop blaming the host.
|
||||
|
||||
Separate **XRT enumeration** from **OGA's device probe** — they are not the same check:
|
||||
|
||||
```python
|
||||
import sys; sys.path.insert(0, "/opt/xilinx/xrt/python")
|
||||
import pyxrt; dev = pyxrt.device(0) # constructs => XRT sees the device
|
||||
```
|
||||
|
||||
`pyxrt.device(0)` succeeding while OGA says `No RyzenAI devices detected` means the
|
||||
refusal is OGA's own predicate, inside `libonnxruntime-genai.so` (the string lives there,
|
||||
next to `RyzenAI`, `max_length_for_kv_cache`, `library_path`). EP-side env levers worth
|
||||
pulling: `RYZENAI_EP_TRACING`, `RYZENAI_EP_PERFORMANCE_COUNTERS`, `RYZENAI_LLM_ASYNC_INIT`,
|
||||
`RYZENAI_LLM_PROFILE_INIT`.
|
||||
|
||||
## Gate 1 — `invalid model … Node (If) cond_full`
|
||||
|
||||
This is **ONNX Runtime verifying the raw graph**, which happens when no provider claimed
|
||||
it: the EP registered nothing and the CPU provider was left holding an `If` whose branches
|
||||
mismatch. It is never a corrupted model.
|
||||
|
||||
```bash
|
||||
LD_DEBUG=files LD_DEBUG_OUTPUT=/tmp/ld python loadprobe.py
|
||||
grep -a "calling init:.*ryzenai\|undefined symbol" /tmp/ld.*
|
||||
# observed: libonnxruntime_providers_ryzenai.so loaded, then
|
||||
# symbol lookup error: undefined symbol: GetProvider (fatal)
|
||||
```
|
||||
|
||||
`GetProvider` is not a dynamic symbol in the ORT cores, the shared-provider stub, or the
|
||||
EP (`nm -D` shows neither defines nor wants it), so the failure is registration, not an
|
||||
ELF mismatch. Confirm the cores are identical before chasing versions: both shipped
|
||||
`libonnxruntime.so.1.27.0` copies hash equal.
|
||||
|
||||
Separate the two gates with an explicit global-scope load before importing OGA:
|
||||
|
||||
```python
|
||||
import ctypes
|
||||
ctypes.CDLL("/opt/rai/deployment/lib/libonnxruntime_providers_ryzenai.so",
|
||||
mode=ctypes.RTLD_GLOBAL)
|
||||
import onnxruntime_genai as og
|
||||
og.Model(MODEL) # registration fixed => error MOVES to "No RyzenAI devices detected"
|
||||
```
|
||||
|
||||
An error that **moves** proves you cleared one gate and exposes the next. Silence in the
|
||||
first form means the provider list was empty.
|
||||
|
||||
### Dead ends (measured, do not retry)
|
||||
|
||||
- **Shadow model dirs never work.** Hardlink mirror, symlink mirror, and even a
|
||||
read-only single-file bind mount of a patched `genai_config.json` over the export's own
|
||||
`genai_config.json` all produce the same `If` rejection. The graph is only claimable
|
||||
when loaded from the export's own directory, and there is no config-path override in
|
||||
`0.14.0-dev`: the only env names in the shipped binaries are `GENAI_LOG_ORT_LIB`,
|
||||
`GENAI_ORT_VERBOSE_LOGGING`, `OG_ORT_LIB`. Carrying a config change therefore means
|
||||
either editing the download in place or a **single-file bind mount over the real dir**
|
||||
(`-v file.json:/spike/<model>/genai_config.json:ro`) — and if the EP still refuses, the
|
||||
gate is not the config.
|
||||
- **Bisect the image before believing "environment drift".** Run the same load probe
|
||||
against every local tag; three tags failing identically rules the image out.
|
||||
- **Rule out host state with instruments, not memory:** `fuser -v /dev/accel/accel0`,
|
||||
`dmesg | grep -i xdna`, `stat -c "%i %y"` on the model files, `df -h`, `free -g`.
|
||||
- `sysfs` class is `accel`, not `misc`: glob `/sys/class/accel*/*`. An empty
|
||||
`/sys/class/misc/accel*` is your glob, not a missing device.
|
||||
- `GENAI_ORT_VERBOSE_LOGGING=1` and `og.set_log_options(...)` produce nothing useful in
|
||||
this build; `set_log_options` is a pybind builtin (no `inspect.signature`) and rejects
|
||||
`severity` as an int. Don't burn turns trying to make it talk — use `LD_DEBUG`.
|
||||
- `strace` is absent from the RAI image and `apt` may have no mirror: `LD_DEBUG=files` is
|
||||
the loader tracer that needs nothing installed.
|
||||
|
||||
## Gate 2 — generation stops after one token
|
||||
|
||||
OGA reads its stop list from the **model** section: `model.eos_token_id`. Chat exports
|
||||
carry the chat-turn-close id (e.g. 151645) there, so the model emits it first and the
|
||||
speech/tool budget is never spent — while `search` has **no** eos key: writing
|
||||
`search.eos_token_id` is a parse refusal (`Unknown value "eos_token_id"`), not a no-op.
|
||||
|
||||
- Fix: rewrite `model.eos_token_id` to only the task's own end token (TTS: the
|
||||
speech-generation-end id), or pass it through `set_search_options` if the build accepts
|
||||
that key name.
|
||||
- A sub-millisecond `generate_next_token()` **with finite, high-confidence logits** means
|
||||
the graph ran and chose a stop token — it is not evidence of a CPU fallback.
|
||||
- Verify a token-range census against the export's `added_tokens.json`, not against a
|
||||
guessed vocabulary size: NeuCodec codes reach 65,536 entries (ids 151671…217206), and a
|
||||
census bounded at 10k silently miscounts every code as a text token.
|
||||
- A hardlinked run dir is a loaded gun: `open(dst, "w")` on a hardlink truncates the
|
||||
**original** through the shared inode. `os.unlink` the link before writing a patched
|
||||
file. Also skip `.git/` and mirror directories when walking an HF checkout.
|
||||
|
||||
## Rate measurement
|
||||
|
||||
Once a load succeeds, measure at ≥300 steps with a host-side `%CPU` sampler beside the
|
||||
container (`ps -C python -o pid=,pcpu=,rss=` every 2 s): a device-bound decode waits on
|
||||
`accel0` and stays under one core, a silent CPU fallback pegs at least one. Never report
|
||||
request #1 after a boot — graph capture makes it a warmup artifact.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: ryzenai-venv-dependency-triangle
|
||||
description: "Debug the torchao/torchtune/transformers import triangle inside an AMD Ryzen AI (ryzen_ai-*.tgz) venv under torch 2.6, including the masked \"cannot import name\" failures and the port-the-one-symbol escape."
|
||||
---
|
||||
|
||||
# Ryzen AI venv dependency triangle (torch 2.6 era)
|
||||
|
||||
Symptom that starts it all — and it never names the culprit:
|
||||
|
||||
```
|
||||
ImportError: cannot import name 'HubertModel' from 'transformers'
|
||||
```
|
||||
|
||||
The named symbol is healthy. `import torchao` alone reproduces the failure; `import transformers` alone does not. transformers imports its **torchao integration** whenever the real modelling path loads, and the failure is re-raised two frames deep by the lazy loader as a missing name.
|
||||
|
||||
## Instruments
|
||||
|
||||
Never conclude from `grep -c Symbol transformers/__init__.py` — 4.5x generates its lazy `__init__`, so an absent literal proves nothing. Get the real exception:
|
||||
|
||||
```python
|
||||
import traceback, torch, transformers
|
||||
try:
|
||||
__import__("transformers.models.hubert.modeling_hubert")
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
```
|
||||
|
||||
That prints the truth, e.g. `AttributeError: module 'torchao.quantization' has no attribute 'Float8WeightOnlyConfig'` from `transformers/modeling_layers.py`.
|
||||
|
||||
Also probe the order matrix (`import X; from transformers import Y` for each suspect): only the poisoned order fails, which isolates the culprit without reading vendor code.
|
||||
|
||||
## The triangle (measured 2026-10-19, ryzen_ai 1.8.0, torch 2.6.0+cpu, transformers 4.57.6)
|
||||
|
||||
| Option | Result |
|
||||
|---|---|
|
||||
| torchao absent | `from transformers import HubertModel` -> OK |
|
||||
| torchao 0.7.0+cpu | `ImportError: Float8WeightOnlyConfig` (masked as missing symbol) |
|
||||
| torchao 0.8.0+cpu | imports (warns `No module named 'triton'`), still lacks `Float8WeightOnlyConfig` |
|
||||
| torchao >= 0.9 | has the symbol, requires torch >= 2.7 -> impossible in this venv |
|
||||
| torchtune, ANY version (0.6.1 … 0.1.1) | guard `torchao not installed`, or `pyarrow requires NumPy 2.0 or newer, found 1.26.4` |
|
||||
|
||||
Two independent dead ends: torchtune's package init pulls `datasets` -> **the pyarrow AMD ships in its own venv refuses that venv's own numpy 1.26.4**. So `import torchtune` is structurally dead, and torchao (the only thing that satisfies its guard) breaks transformers. **Never "just upgrade" torch/numpy: the vendor's ONNX runtime and torch build are what the NPU EP is loaded against.**
|
||||
|
||||
## Escape: port the one symbol, pin both absent
|
||||
|
||||
Ask what the third-party package is actually used for before fighting the resolver. In `neucodec` it was one symbol (`RotaryPositionalEmbeddings`, ~40 lines, copied verbatim from `torchtune/modules/position_embeddings.py`, BSD-3) used by two files. Apply it in the image with a transform that **asserts** each import line exists, so a vendor update fails the build instead of shipping a half-patched package:
|
||||
|
||||
```dockerfile
|
||||
COPY patches/rope_local.py /opt/patch/rope_local.py
|
||||
RUN python - <<'PY'
|
||||
import pathlib
|
||||
from importlib.util import find_spec
|
||||
pkg = pathlib.Path(find_spec("neucodec").origin).parent # find_spec: importing it is what is impossible yet
|
||||
(pkg / "rope_local.py").write_text(pathlib.Path("/opt/patch/rope_local.py").read_text())
|
||||
old, new = "from torchtune.modules import RotaryPositionalEmbeddings", "from .rope_local import RotaryPositionalEmbeddings"
|
||||
for name in ("bs_roformer5.py", "codec_decoder_vocos.py"):
|
||||
p = pkg / name; s = p.read_text()
|
||||
assert old in s, name + ": import line moved - re-read the vendor source"
|
||||
p.write_text(s.replace(old, new))
|
||||
PY
|
||||
```
|
||||
|
||||
Gate both absences at build time so nobody re-adds them and inherits a three-week-later mystery:
|
||||
|
||||
```dockerfile
|
||||
RUN ... import-check \
|
||||
&& python -c "import importlib.util as u; assert u.find_spec('torchao') is None; assert u.find_spec('torchtune') is None"
|
||||
```
|
||||
|
||||
Prove a ported numeric kernel with the vendor's own fixtures, not with faith: `neucodec` ships `samples/jo.wav` *and* its own encoding `samples/jo.pt`, so re-encoding the WAV and comparing integer codes is a pass/fail correctness test of the RoPE port.
|
||||
|
||||
## Rules of thumb
|
||||
|
||||
- `--no-deps` for anything adjacent to torch/onnxruntime/numpy; check the vendor pins by reading them out of the venv (`python -c "import torch; print(torch.__version__)"`), never from docs.
|
||||
- A `--dry-run` install that resolves without touching torch/numpy/onnxruntime is the licence to install *with* deps.
|
||||
- `uv pip install --target DIR` / a nested venv does **not** layer over a parent venv: a venv created from `/opt/rai/bin/python` resolves "system site-packages" to the base interpreter, and uv's resolver ignores `.pth` paths and will pull a second numpy that breaks the vendor C extensions.
|
||||
- Ephemeral `podman run --rm` probes discard everything you installed in them — build a throwaway tag (truncate the Containerfile before the failing step) instead of re-hypothesising.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: sigterm-kill-test-stub
|
||||
description: "Pin \"code kills its child process\" behavior with a bash stub player: make the stub SIGTERM-reactive via sleep&wait, log KILLED via trap; diagnose deferred-trap failures."
|
||||
---
|
||||
|
||||
Use when a test must prove production code sends SIGTERM to a spawned child (player, worker, server stub) and the test asserts the kill happened — or when such a test mysteriously fails with `KILLED` missing and wall time equal to the stub's sleep duration.
|
||||
|
||||
## The trap-during-sleep gotcha
|
||||
|
||||
A bash stub that sleeps in the **foreground** does NOT run its TERM trap when signalled: bash defers traps until the current foreground command completes. The stub dies (or logs) only after the sleep finishes — long after the test read the evidence file.
|
||||
|
||||
Symptoms:
|
||||
- Assertion on a `KILLED` marker line fails although production code demonstrably calls `child.kill("SIGTERM")`.
|
||||
- Test wall time equals the stub's full sleep (the kill "had no effect").
|
||||
|
||||
## Fix: reactive stub skeleton
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >> "$STUB_LOG" # call record
|
||||
trap 'printf "KILLED\n" >> "$STUB_LOG"; exit 143' TERM # BEFORE the sleep
|
||||
for arg in "$@"; do case "$arg" in *.wav) cp "$arg" "$COPY" ;; esac; done
|
||||
sleep "${STUB_HOLD:-0.1}" & # background sleep…
|
||||
wait $! # …wait is interruptible
|
||||
printf 'DONE\n' >> "$STUB_LOG"
|
||||
```
|
||||
|
||||
- `sleep N & wait $!` is the idiom: a signal makes builtin `wait` return immediately, the trap runs at once, `exit` inside the trap prevents fallthrough to `DONE`.
|
||||
- Tune duration per test via the env knob (`STUB_HOLD=5`) so the child is provably still alive when the kill is triggered.
|
||||
- Put the trap BEFORE any long command; a trap registered after the sleep never guards it.
|
||||
|
||||
## Harness wiring (black-box driver pattern)
|
||||
|
||||
- Scenario: start the long work → settle (>spawn latency, <hold) → trigger the killer (shutdown event, second invocation, `/stop`) → settle → read log.
|
||||
- Expose the stub log lines on the driver's stdout (`LOG <line>` per line) so tests grep output, not files: black-box seam stays output-only.
|
||||
- Assert `KILLED` present; for interrupt-semantics also count (`kills -eq 1`) and total call lines (`PLAYS 3` = call, KILLED, call).
|
||||
|
||||
## Orphan hygiene
|
||||
|
||||
Second child still sleeping when the driver exits: spawn with `stdio: "ignore"` so the orphan holds no capture pipe — it SIGPIPEs/quiets at teardown; do not lengthen tests waiting for it.
|
||||
|
||||
## Verify before trusting
|
||||
|
||||
Run the pair separately: kill-tests green while a byte-equality test (`cp` of the payload before sleep) stays green — the reactive rewrite must not drop the copy step (it sits between trap and sleep).
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: strix-halo-npu-readiness-census
|
||||
description: "Engine-agnostic census of whether an AMD Strix Halo / Ryzen AI host can run anything on its XDNA2 NPU: driver bind, firmware, device perms, IOMMU, distro XRT, missing AIE compiler, fabric clock, RAM headroom."
|
||||
description: "Engine-agnostic census of whether an AMD Strix Halo / Ryzen AI host can run anything on its XDNA2 NPU: driver bind, firmware, device perms, IOMMU, distro XRT, missing AIE compiler, fabric clock, RAM headroom — plus the strace test that separates a dead device from a runtime that declines the graph."
|
||||
---
|
||||
|
||||
Use when asked "can we run <engine/model> on the Strix Halo / Ryzen AI NPU?" for a runtime that is **not** halogen (halogen has its own gate + vendor catalog: use `halogen-npu-feasibility-census` for that). This answers only: is the *host* able, and what is missing.
|
||||
|
||||
## Why it is usually the wrong door first
|
||||
|
||||
XDNA2 executes **precompiled AIE partition programs** loaded through XRT → `amdxdna`. There is no generic tensor dispatch: an engine built on ggml/llama.cpp (or any GGUF loader) cannot "enable" the NPU — the port must be a re-export + vendor compile, not a flag. So pair this census with the accelerator the same engine *can* select by build flag (Vulkan/HIP on the gfx1151 iGPU) before proposing NPU work.
|
||||
XDNA2 executes **precompiled AIE partition programs** loaded through XRT → `amdxdna`. There is no generic tensor dispatch: an engine built on ggml/llama.cpp (or any GGUF loader) cannot "enable" the NPU — the port must be a re-export + vendor compile, not a flag. So pair this census with the accelerator the same engine *can* select by build flag (Vulkan/HIP on the gfx1151 iGPU) before proposing NPU work. On Dankclaw the iGPU flag was ours to flip all along (`-DGGML_VULKAN=OFF` written out in the fleet's own engine Containerfile) — check the engine's own build file before treating the NPU as the only hardware door.
|
||||
|
||||
## Census (all read-only, over ssh from the operator box)
|
||||
|
||||
@@ -37,8 +37,20 @@ nproc; free -g | head -2 # RAM headroom + t
|
||||
- No `ryzenai-compile`/ONNX Ryzen AI EP ⇒ nothing on the host can produce a partition program. That is the enablement delta, and it is a toolchain install + a model-specific export/quantize/compile project, not a config change.
|
||||
- `free` headroom matters: NPU partitions want a few GB of host RAM each and can log benign `Failed to alloc dev bo memory, ret -28` under pressure.
|
||||
|
||||
## Device healthy, load still fails? Classify before blaming the hardware
|
||||
|
||||
Two different failures, one call apart. Run the load under `strace -f -e trace=openat`:
|
||||
|
||||
- **Never opens `/dev/accel/accel0`** ⇒ pre-device: loader/shim/env (see `ryzenai-npu-oga-linux-run` — the `Generic Failure` in ~2 ms is this class).
|
||||
- **Opens `/dev/accel/accel0`, GEM ioctls succeed, then the EP declines** ⇒ post-device: the *runtime* rejects the graph. The device is innocent; stop the hardware investigation and fix the last file it opened (`model.onnx` = graph rejected; `dd_metastate_*` = partition artifact problem).
|
||||
|
||||
Ground truth that the device itself is sound, inside the container: `xrt-smi examine` (expect `RyzenAI-npu5 | aie2p | 6x8` + firmware version) then `xrt-smi validate` — `PASSED` incl. `throughput`. Needs `libboost-filesystem1.83.0` + `libboost-program-options1.83.0` (**hyphenated** Ubuntu names, after `apt update`); boost is required by `xrt-smi` only, never by the OGA/EP path. Vendor `quicktest` printing success does **not** prove NPU residency (that build falls back to CPU silently) — sample `/sys/class/accel/accel0/device/mem_usage` during generation instead.
|
||||
|
||||
When the vendor runtime declines silently, exhaust its own levers and then say so: `GENAI_ORT_VERBOSE_LOGGING`, `GENAI_LOG_ORT_LIB`, `set_log_options`, `set_log_callback(callable)`, `RYZENAI_EP_TRACING`, `RYZENAI_LLM_PROFILE_INIT` all produced zero lines on the 2026-10-19 NuTTS-Air decline. Rule out shadowed artifacts by moving hardlinked mirror copies out of the scanned tree (glob-based sibling scans) — if the failure is byte-identical afterwards, that theory is dead. **If a load ever succeeded once and the byte-identical command never reproduces it (files pristine by inode+mtime), that is a NO-GO, not a lead: no rate may be quoted for a run that cannot be repeated.**
|
||||
|
||||
## Laws / traps
|
||||
|
||||
- Read state only. Never probe or load the NPU beside a live inference service — stop + mask the service first (fleet benching policy); this host class has a precedent of a concurrent GPU/NPU experiment needing a power cycle.
|
||||
- `sudo podman ...` finds nothing (per-user rootless storage); run podman as the deploy user.
|
||||
- `/dev/accel/accel0` existing does not mean a context/PASID can be created; with IOMMU off the failure is at context creation, not at the node.
|
||||
- Rootless containers cannot read `accel0` (`root:render 0660`) by any documented flag combination — rootful, or a world-writable udev rule.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: tts-endpoint-parameter-capability-audit
|
||||
description: "Determine which request parameters a fleet TTS endpoint actually honors by probing façade and engine separately, with a nondeterminism control, instead of trusting docs or byte diffs."
|
||||
---
|
||||
|
||||
Use when asked whether a TTS (or other OpenAI-compatible shim) endpoint supports a parameter — mood, tone, instructions, speed, seed — or when a request "seems to" change the audio. Docs and single-request comparisons both lie; audit the layers.
|
||||
|
||||
## Why the naive checks fail
|
||||
- **Byte-size diffs are meaningless.** Synthesis is nondeterministic: three byte-identical requests to frank-tts came back 107564 / 134444 / 111404 bytes (~±15 % length). "The mood file differs from the baseline" proves nothing.
|
||||
- **HTTP 200 proves nothing.** A shim that rebuilds its own payload answers 200 to `speed:"banana"` because it never looks at the field.
|
||||
- **A façade can drop fields the engine supports**, so testing only the published port under-reports capability.
|
||||
|
||||
## Procedure
|
||||
1. **Read the shim's parse block** (which request keys it reads) — not its docstring:
|
||||
`ssh -o BatchMode=yes dankclaw 'podman exec frank-tts grep -nE "body\.get|payload = |json\.loads" /app/app.py'`
|
||||
Whatever keys appear there (plus what it forwards in its own `payload = {...}` dict) is the real published contract.
|
||||
2. **Enumerate the engine's key table from the binary** (works with no `strings` in the image):
|
||||
`podman exec <engine> sh -c 'grep -aoE ".{60}instructions.{60}" /app/tts-server | head -2'`
|
||||
Adjacent tokens come out as a run — that run IS the accepted-key list (`...instructions response_formatspeedseedmax_new_tokensrepetition_penalty subtalker_top_k subtalker_temperature ...`). Also `podman exec <engine> /app/tts-server --help`, and `podman inspect <c> --format "{{json .Config.Entrypoint}}"` to find the binary.
|
||||
3. **Reach the engine directly** when its port is unpublished: `TTS_UPSTREAM` in `podman inspect <shim>` gives the real in-network address (`http://frank-tts-engine:8880`, not `host.containers.internal`). No curl/python on the host? Pipe a script into the container that has one: `podman exec -i frank-tts python3 - <<'PY' ... PY`.
|
||||
4. **Control for nondeterminism before claiming an effect**: repeat the identical request twice as the control; only compare conditions separated by far more than the jitter, and prefer an error response (a 400 naming the unsupported flag) as the decisive evidence.
|
||||
|
||||
## Findings for the dank fleet (verify before trusting — they move)
|
||||
- Published façade `:7772` reads only `model`, `input`, `voice`/`voice_id`, `response_format`; it rebuilds the payload, so `speed` and every unknown field are dropped before the engine.
|
||||
- Engine (`frank-tts-engine`, qwentts.cpp, qwen-talker-1.7b-**base**, Vulkan) accepts `instructions`, `speed`, `seed`, `max_new_tokens`, `repetition_penalty`, `subtalker_top_k/_temperature/_top_p`, but answers `400 "--instruct is not supported for base models"` for `instructions` on the base checkpoint. `seed` does not make output reproducible.
|
||||
- Therefore tone/mood must be produced in the **text layer** (prompt directives), not the TTS request; and `speed` needs a two-line façade fix if tempo control is ever wanted.
|
||||
|
||||
## Verification habits
|
||||
- POST with a wrong *type* (`speed:"banana"`) to learn whether anything validates, and with a bogus `voice` to see the error shape (`available_voices` came from the engine, not the shim).
|
||||
- Plain GET of `/v1/audio/voices` for the roster.
|
||||
- Read-only probes of the fleet are always allowed; synthesis requests are transient GPU work, keep them to a handful.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: typescript-fleet-repo-bootstrap
|
||||
description: "Initialize a TypeScript monorepo (Bun workspaces + Turborepo + Biome + Express/Vite + quadlet dev DB + Storybook/Playwright gates) on Frank's fleet, with the traps that cost hours at franksign init."
|
||||
---
|
||||
|
||||
# TypeScript fleet repo bootstrap
|
||||
|
||||
Bootstrap a fleet-style TS monorepo the way franksign was initialized (2026-10-20):
|
||||
Bun workspaces + Turborepo + Biome + Express 5 on Node + Vite/React SPA + quadlet
|
||||
dev Postgres + Storybook-as-component-test + prod-shaped Playwright e2e, gated by a
|
||||
single `tests/run.sh` seam runner (serverAppConfigs pattern).
|
||||
|
||||
## Shape
|
||||
|
||||
```
|
||||
apps/ homepage (zero-JS), web (Vite+React SPA), api (Express, single origin: /, /app, /api, /health)
|
||||
packages/ domain libs (each: src/*.ts, tests colocated, tsc -p tsconfig.build.json)
|
||||
templates/ data the API serves
|
||||
tests/ run.sh + seams/*.bats + e2e/*.spec.ts + lib/ guards (.mjs)
|
||||
infra/ quadlet/*.container + setup-dev.sh (idempotent bootstrap)
|
||||
<repo>-docs/ OKF v0.2 KB: index.md, log.md, read-only/ (agent-off-limits)
|
||||
AGENTS.md git law (trunk-based, --no-ff, explicit merge OK) + TDD tiers + docs law
|
||||
HANDOFF.md at repo base for worktree-based future sessions
|
||||
```
|
||||
|
||||
## Non-obvious traps (all measured on Frank's box, Node 26 / bun 1.4.3 / playwright 1.64)
|
||||
|
||||
- **Playwright `testDir` resolves against process CWD, not the config file.**
|
||||
Pin the seam to invoke playwright with `cd` into the spec dir.
|
||||
- **A `testMatch` regex with a leading `^` silently matches nothing** — Playwright
|
||||
tests it against prefixed paths; "No tests found", zero diagnostics. Anchor only
|
||||
the trailing `$`. And keep raw `/` out of the pattern if a guard script parses
|
||||
the config literals.
|
||||
- **`tsc` with `noEmit: false` but no `rootDir`/`outDir` emits `.js` strays into
|
||||
`src/`** while package `main` points at `dist/` — the strays resurface on every
|
||||
build. Pin `rootDir: "src", outDir: "dist", declaration: true` in every
|
||||
tsconfig.build.json.
|
||||
- **`bunx <tool>` ignores the workspace install** and pulls its own version (observed
|
||||
with biome and playwright). Git hooks MUST call `./node_modules/.bin/<tool>`.
|
||||
- **Node 26 rejects `--env-file-if-set`** (no "if-set" variant on that build);
|
||||
use `--env-file=<file>` — a shell-set env var still wins over file values, so
|
||||
`PORT=3100 node --env-file=.env dist/cli.js` overrides cleanly.
|
||||
- **Quadlet resolves `EnvironmentFile=` relative to the UNIT directory**, not the
|
||||
repo: `setup-dev.sh` must link the env file beside the unit in
|
||||
`~/.config/containers/systemd/`. Generated quadlet units cannot be
|
||||
`systemctl --user enable --now`'d — the generator's `default.target.wants`
|
||||
symlink IS the enablement; plain `start` is enough.
|
||||
- **`gitignore` rule `.env.*` swallows `.env.example`** — add the `!.env.example`
|
||||
negation when you write the first ignore file.
|
||||
- **Gitea 1.27.3 API create-repo with a JSON body can answer 422 `[Name]: Required`**
|
||||
even when the field is present; probe `/api/v1/repos/<owner>/<name>` first — it
|
||||
may already exist (empty = ready for `git push -u origin main`).
|
||||
- **Biome 2.x**: `biome migrate` needs `--write` (without it, it only prints the
|
||||
plan) and migrates `rules.recommended` → `rules.preset`; `check --write` skips
|
||||
`useTemplate`/`noNonNullAssertion` (unsafe) — prefer rewriting `!` assertions to
|
||||
named lookups so `--unsafe` never touches tests.
|
||||
- **Grafana-grade gate law** (from serverAppConfigs): every e2e spec must be routed
|
||||
by a `projects[].testMatch` or it silently never runs — ship a scaffold-guard
|
||||
script that fails the seam on an orphan spec.
|
||||
|
||||
## Gate wiring
|
||||
|
||||
`tests/run.sh` (bats seams: build, unit, docs-lint, homepage budget, quadlet,
|
||||
storybook build, e2e) ← pre-push hook; pre-commit = secret-regex scan on staged
|
||||
diff + `./node_modules/.bin/biome check` on staged code. e2e seam boots the
|
||||
prod-shaped stack (built assets + Express + quadlet DB on a dedicated port,
|
||||
health-gated with `curl` before Playwright starts).
|
||||
|
||||
## Session close-out
|
||||
|
||||
log.md entry with `(cwd · session)` footer; HANDOFF.md updated; origin on Gitea
|
||||
owner `dank`; PR bodies carry the attribution footer.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: vault-one-fact-one-owner-dedupe
|
||||
description: "\"Collapse per-payload/per-attempt vault pages into one owner page per endpoint without losing a fact: port-before-cut orphan census, doubled-page repair, trash-aware tools, delegated-write verification, index regeneration, and the merge-resolution order.\""
|
||||
description: "\"Collapse per-payload/per-attempt vault pages into one owner page per endpoint without losing a fact: port-before-cut orphan census (corpus must exclude the file under test), doubled-page repair, trash-aware tools, delegated-write verification, honest per-area line accounting, index regeneration, and the merge-resolution order.\""
|
||||
---
|
||||
|
||||
# One fact, one owner: dedupe the vault without losing a fact
|
||||
@@ -37,12 +37,19 @@ file being rewritten. Any line with **no** surviving key is an orphan → port,
|
||||
deliberately and say so. Use `scripts/port-census.py <new-dir> <git-HEAD:path|file>` (numbers
|
||||
with ≥ 4 int digits, decimals, flags, paths, backticked spans, `--`, ALL-CAPS, or ≥ 2 hyphens).
|
||||
|
||||
Two calibration traps:
|
||||
Three calibration traps:
|
||||
- **Prose words are not facts.** A hyphen or trailing period inside a word (`long-standing`,
|
||||
`deployed:`) was counted as a tracked token while real identifiers hid behind it; that noise
|
||||
buried 300 real lines under 979 fake ones. Strip punctuation, require fact shape.
|
||||
- **The corpus decides the question.** Corpus = the new file only → "what did this rewrite
|
||||
drop?" Corpus = whole vault → "is the dropped fact orphaned repo-wide?" Ask one at a time.
|
||||
- **The corpus MUST exclude the file under test, and you must assert it.** A census pointed at
|
||||
the directory that *contains* the page proves the page is held by itself: ten era pages
|
||||
reported `0 sole-record lines`, which reads as "pure duplication, trash it". With the file
|
||||
excluded, the same pages held **14–63 sole-record lines each** — campaign ledgers that exist
|
||||
nowhere else. A tool's own docstring promise ("excluding it") is not a check: print the
|
||||
corpus size, assert the target is absent, and treat `0 unheld` on a long page as a tool bug
|
||||
until proven otherwise. A mechanical census is a scout: its verdict is a lead, not a finding.
|
||||
|
||||
Also grep `AGENTS.md` separately — a fact it carries is not an orphan.
|
||||
|
||||
@@ -52,7 +59,7 @@ Signature: a page whose `wc -l` is ~2× its `HEAD` size, with a **second frontma
|
||||
the body** whose closing `---` is glued onto the end of a body/timestamp line. Cause chain: a
|
||||
frontmatter rewrite that re-reads the file it just wrote (so its own output becomes the body),
|
||||
then a fence-repair pass keyed on `line == "---"` that cannot see a glued fence and inserts a
|
||||
fence mid-page, so the next pass treats the tail as a new document.
|
||||
fence mid-page, so the next page-tail becomes a new document.
|
||||
|
||||
Un-double rule that worked: find the first frontmatter's close (accepting a glued fence), then
|
||||
the first `type:` line after it that has more keys (`title|description|tags|timestamp`) behind
|
||||
@@ -82,9 +89,10 @@ grep -rnoE "\((page-a|page-b|...)\.md\)" --include="*.md" serverAppConfigs-docs
|
||||
|
||||
Both wikilinks and markdown path refs must be repointed (Research `See Also` lists and bench
|
||||
pages are where path refs hide). Then **regenerate** index rows with a script, never by hand.
|
||||
The generator bug to check for: presence tested as a **bare stem substring** — a page whose
|
||||
name prefixes another's (`frank-medium` inside `frank-medium-rocmfpx`) then silently never
|
||||
gets a row. Test the wikilink form `[[<stem>]]`.
|
||||
Generator bugs to check for: presence tested as a **bare stem substring** — a page whose name
|
||||
prefixes another's (`frank-medium` inside `frank-medium-rocmfpx`) then silently never gets a
|
||||
row (test the wikilink form `[[<stem>]]`); and **no dead-row sweep**, so deleting a page leaves
|
||||
an index row that fails the link check — the generator must drop rows for absent pages too.
|
||||
|
||||
## 7. Enforce the law with a seam
|
||||
|
||||
@@ -96,6 +104,12 @@ re-created era page then cannot pass tests. For the length hint, prefer **derive
|
||||
owned by the page that measured it) plus one documented per-page opt-out; never let a size
|
||||
ceiling force a ledger to be split across two owners.
|
||||
|
||||
Splitting an over-long page to satisfy a ceiling is usually line-NEGATIVE: one measured split
|
||||
turned a 700-line page into 488 + 306 across five files (+23 for the folder net of stub
|
||||
deletions), because frontmatter blocks, pointer sections and connective prose are charged per
|
||||
file while the facts move for free. Roll the split back, carry `okf-exempt: length` with the
|
||||
reason, and keep the stub pages deleted.
|
||||
|
||||
## 8. Edit-tool hazard (costs the most time)
|
||||
|
||||
Body rows beginning with `-` or `+` are op markers. The documented `+-` escape **writes a
|
||||
@@ -104,6 +118,10 @@ line untouched and PUT only the indented continuation lines, or convert the note
|
||||
bullet whose body lines start with two/four spaces. If a wrapped `[[Two Word Page]]` shows up
|
||||
as an unresolved link, the target was hard-wrapped across lines — unwrap it.
|
||||
|
||||
A `PUT` that spans a multi-line statement can silently drop neighbouring assignments
|
||||
(auto-repaired boundary echo); after editing a constants block, `grep -n "^[A-Z_]* = "` and
|
||||
run a syntax probe before executing.
|
||||
|
||||
## 9. Delegated writes: verify bytes, never narrative
|
||||
|
||||
Measured in one campaign, all four failure modes live:
|
||||
@@ -146,7 +164,24 @@ matter how concrete they read; and re-read the source bullet before trusting a t
|
||||
- Open the PR through the API with a 600-perm curl config (managed skill
|
||||
`gitea-api-pr-operations`), end the body with the `cwd:` / `session:` footer, then
|
||||
`xdg-open <PR-URL>` the instant it exists. Never merge without Frank's current explicit OK.
|
||||
Push with an explicit refspec (`HEAD:refs/heads/<pr-branch>`) — a worktree's own branch name
|
||||
is not the PR branch, and `git push origin HEAD` opens a stray branch.
|
||||
- Docs-only change ⇒ **no deploy**. Fast-forward the deploy checkout only
|
||||
(`git -C ~/Projects/serverAppConfig merge --ff-only origin/main`).
|
||||
- Say what was NOT done, in the PR body: soft lint tallies left (missing `## Citations`),
|
||||
conflicts left standing, repo config that was never installed on a host.
|
||||
|
||||
## 12. Report line counts per area, never one net
|
||||
|
||||
"Compaction" measured against the merge-base **per area**: docs / `scripts/` / `AGENTS.md`.
|
||||
A single headline number hides that tooling and prose move in opposite directions — one
|
||||
campaign reported "−1,849 vault lines" while the vault was flat and the growth was the two
|
||||
enforcement scripts that made the law mechanical. Expect a mature one-owner vault to land
|
||||
**docs flat**: the reductions available are deletions of *restatement* (duplicated pages,
|
||||
transient deploy chatter in `log.md`, `AGENTS.md` bullets whose numbers an endpoint page owns)
|
||||
and they are roughly cancelled by the cost of the citations the verification pass adds. When
|
||||
asked to shrink further, show the menu — drop the tools, drop the citations, or accept content
|
||||
loss — instead of inventing cuts. `log.md` compaction rule that survived census: drop a line
|
||||
only when it matches a transient shape (`/health`, `deployed`, `restart`, `systemctl start`)
|
||||
**and** every fact token in it is held by an owner page; a token-held line can still be the
|
||||
only record of *when* an incident happened.
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
- Hermes (Dankclaw) model routing as of 2026-09-05: main default = frank-large (Dankclaw 192.168.0.143:7770/v1), small/aux routes = frank-small (Dankserver 192.168.0.235:7770/v1), vision + title_generation route to the exact FLM registry tag gemma4-it:e4b (192.168.0.143:7771/v1). The pre-rename aliases frank-default-large/-small/-sightsound are dead since the 2026-09-03 rename cutover — never route on them. Live config lives at /home/dank/Containers/hermes/data/config.yaml (owned by uid 109999; needs sudo to read/edit); the repo's dankclaw/hermes/config.yaml.example is a sanitized backup and can lag live, so diff live first before editing either. Signal adapter is currently disabled in live config (platforms.signal.enabled: false).
|
||||
- On the guarded franksdank.cloud vhosts (Caddy → Anubis :8923 → Authentik outpost), every unauthenticated request — roots and deep paths alike — is answered with HTTP **200** and an HTML challenge page (titleMaking sure you're not a bot!/title), not a 4xx. Verifying guard health by status code alone is meaningless; assert on leakage markers instead (copyparty UI title, homepage id="grid", services.json "category" JSON shape). Non-JS clients (curl/wget/WebDAV) can never solve the PoW challenge, so they are permanently parked at the challenge page — this is the entire security boundary for copyparty, which serves authenticated traffic with anonymous read perms (-v /w::r:ra,dank). Pinned by tests/e2e/specs/15-unauth-fetch.spec.ts (guest project, empty storage state): any guard-route regression that lets app bytes answer fails loudly; any gate implementation passes.
|
||||
- On Dankclaw's Pi-hole (FTL v6.7, quadlet at /home/dank/Containers/pihole), /etc/pihole/hosts/custom.list (host path /home/dank/Containers/pihole/etc-pihole/hosts/custom.list) is a LIVE-ONLY Custom-DNS source NOT tracked in the repo — it held ~22 A records pointing every *.franksdank.cloud vhost at 192.168.0.143, including names with no public Porkbun record. gravity.db's domainlist table has zero custom-DNS rows on this build, and the pihole CLI's sqlite path (pihole-FTL sqlite3) works but the web API auth was failing at discovery time, so retiring a name from LAN DNS requires editing custom.list directly (sudo sed -i '/name/d') followed by podman exec pihole pihole restartdns reload; deleting only the repo-managed addn-hosts record is not sufficient. Verify with dig +short name @192.168.0.143 → empty answer.
|
||||
- Pi-hole admin panel at pihole.franksdank.cloud has two auth layers: Caddy basicauth (edge, username dankuser + hash) and Pi-hole internal password (CivetWeb, set via pihole setpassword). The Pi-hole internal password was the actual failure point, not the Caddy hash. Pi-hole resolves to 192.168.0.143 via addn-hosts (all 28 names point here), making it a LAN service behind Caddy's public TLS edge. Options to remove public exposure: (1) remove the Caddy vhost entirely — LAN clients hit Pi-hole quadlet directly on :8083; (2) keep Caddy vhost but remove the Porkbun A record so external DNS won't resolve pihole.franksdank.cloud. Current state: Caddy basicauth hash = $2b$14$cJm.hbvDi1dEh7x0XRMnseaIdvIrRyH.AcTL56jGJf3wd54qvEb9K (dankuser, password pihole2026); Pi-hole internal password = pihole.2026 (set via pihole setpassword, survives restart).
|
||||
- Pi-hole admin panel at pihole.franksdank.cloud has two auth layers: Caddy basicauth (edge, username dankuser + hash) and Pi-hole internal password (CivetWeb, set via pihole setpassword). The Pi-hole internal password was the actual failure point, not the Caddy hash. Pi-hole resolves to 192.168.0.143 via addn-hosts (all 28 names point here), making it a LAN service behind Caddy's public TLS edge. Options to remove public exposure: (1) remove the Caddy vhost entirely — LAN clients hit Pi-hole quadlet directly on :8083; (2) keep Caddy vhost but remove the Porkbun A record so external DNS won't resolve pihole.franksdank.cloud. Current state: Caddy basicauth hash and the Pi-hole internal password are BOTH redacted (2026-10-20, issue #40) — they sat here in plaintext, which means they are in git history on every machine that ever pulled this repo, so both need rotation before this note is trustworthy; the live values are the Dankclaw host `.env` (`CADDY_BASICAUTH_HASH`) and `pihole setpassword`, never this file.
|
||||
- Pi-hole admin at pihole.franksdank.cloud has two auth layers: (1) Caddy basicauth edge (username dankuser, hash in rendered Caddyfile from CADDY_BASICAUTH_HASH in .env), (2) Pi-hole internal CivetWeb password (stored in cli_pw as bcrypt hash, set via pihole setpassword CLI). The env file WEBPASSWORD is used at container start but the CLI hash is what the running web server validates. When "I can't get into Pi-hole admin" returns 403 even after Caddy basicauth passes, the Pi-hole internal password is the likely culprit. Fix: podman exec pihole pihole setpassword newpw to update the CLI hash, then verify podman exec pihole cat /etc/pihole/cli_pw shows updated value. Verify by curling with basicauth: curl -sk -H 'Authorization: Basic ZGFua3VzZXI6...' https://pihole.franksdank.cloud/admin/ should return 302 (not 401 or 403).
|
||||
- Pi-hole admin panel at pihole.franksdank.cloud uses Caddy basicauth (not Pi-hole's own CivetWeb login). The hash is shared across services (RomM, Copyparty, Pi-hole) via CADDY_BASICAUTH_HASH in the host .env. The rendered Caddyfile at /home/dank/Containers/caddy/Caddyfile is mounted into the Caddy container — updates are picked up by Caddy's file watcher without restart. Password reset: generate bcrypt hash with python3 + bcrypt (hashpw + gensalt), update the rendered Caddyfile line directly or update .env + deploy. Caddy accepts both $2a$ and $2b$ bcrypt variants. _(context: Pi-hole admin password reset session: discovered Caddy basicauth (not Anubis), could not find matching password in common candidates, reset to pihole2026, updated rendered Caddyfile directly, synced repo .env files, Caddy file watcher handled hot-reload.)_
|
||||
- Pi-hole admin panel at pihole.franksdank.cloud uses Caddy basicauth (not Pi-hole's own CivetWeb login). The hash is shared across services (RomM, Copyparty, Pi-hole) via CADDY_BASICAUTH_HASH in the host .env. The rendered Caddyfile at /home/dank/Containers/caddy/Caddyfile is mounted into the Caddy container — updates are picked up by Caddy's file watcher without restart. Password reset: generate bcrypt hash with python3 + bcrypt (hashpw + gensalt), update the rendered Caddyfile line directly or update .env + deploy. Caddy accepts both $2a$ and $2b$ bcrypt variants. _(context: Pi-hole admin password reset session: discovered Caddy basicauth (not Anubis), could not find matching password in common candidates, reset to the current password [redacted 2026-10-20, issue #40], updated rendered Caddyfile directly, synced repo .env files, Caddy file watcher handled hot-reload.)_
|
||||
- On the FrankBook-Pro *arr fleet (current lsio images: Sonarr v4/Radarr v5 API v3, Lidarr v2/Readarr API v1), the download-client validation endpoints are NOT usable via API: POST /api/{v}/downloadclient/test/{id} and .../test?forceTest=true both return 405, and POST .../downloadclient/action/getTest exists but 400s without a bespoke body. To prove download-client health after changing a qB host/port, skip the API test route and prove behaviorally instead: podman exec arr curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/ (expect 200) plus a real qB login from inside the container (curl --data-urlencode username=admin --data-urlencode password=$QBIT_PW .../api/v2/auth/login, expect 200), then confirm /api/{v}/health shows no DownloadClient-*/wbrrelated warnings. Also: arr APIs redact stored client passwords as ******** (sonarr/radarr/lidarr) — PUT the object back with the sentinel untouched to preserve the stored value; readarr (v1) returns the plaintext. When editing via the edit tool, body rows must be prefixed with +, not : — : prefixes get written literally into the file. _(context: Capture turn after the arr→qB loopback repoint (2026-09-04): app-side download-client test endpoints were unreachable, so client health was proven behaviorally instead.)_
|
||||
- The *arr fleet (Prowlarr, Sonarr, Radarr, Lidarr, Readarr, qBittorrent) runs ONLY on FrankBook-Pro (192.168.0.112, Mac podman machine) since 2026-09-04 — the dankserver compose copy was removed (config roots in ~/.AGENTTRASH/20260904-dankserver-arr-teardown, 22 MB, awaiting purge approval). Fleet invariant, enforced by tests/frankbook_quadlet.bats: ALL six containers share gluetun's netns (Network=gluetun.container, Wants/After gluetun.service) and publish NO ports; the gluetun unit publishes the union (8080, 6881 tcp+udp, 9696, 8989, 7878, 8686, 8787), so all egress exits ProtonVPN by construction (AllowedIPs=0.0.0.0/0; proven: netns egress 51.179.177.14 vs residential 71.227.227.172). deploy-plan.py couples gluetun → all six joiners (a gluetun recreate kills the whole family). Consequences: (1) the old arr→qB addressing hack 10.88.0.1:8080 (podman-bridge gateway) routes INTO the tunnel from inside the netns — download clients must use 127.0.0.1:8080 (done via arr APIs, verified by real qB login from inside sonarr's container); same rule for any future Prowlarr Application rows (baseUrl 127.0.0.1:arr port). (2) Caddy arr vhosts and homepage probes target 192.168.0.112:port (host-side published ports unaffected by netns changes). (3) Known defects as of 2026-09-04: Prowlarr's Applications table is EMPTY (the four rows from 2026-08-30 were lost — prowlarr→arr sync broken until rebuilt); readarr's qB proxy auth defect needs retest post-move; EZTV/1337x "CloudFlare-blocked" parking is stale (egress now VPN — retest before enabling). (4) E2E lesson: playwright specs 11-admin/12-crawl now derive expected open-link counts from services.json (showLink) instead of hardcoded counts — keep new registry entries consistent. Also: deploy.sh --service X dispatches ONLY X on frankbook (path-coupling applies to changed-path planning); gluetun restart self-heals all netns joiners via Restart=always so a single gluetun deploy converges the family. _(context: 2026-09-04 operator decision session: dankserver arr compose copy (booted that morning) removed after Frank chose FrankBook-Pro as the sole arr deployment; netns consolidation + test seam + loopback repoint executed same session. Commits 4c7465d, df4339a.)_
|
||||
- Rootless podman on Dankserver (5.7.0, verified 2026-09-04 via /proc/pid/uid_map across 7 containers): the DEFAULT user namespace maps container uid 0 → host uid 1000 (dank) and container uid N → host 100000+N-1 (e.g. calibre-web PUID=1000 writes as 100999). Consequences: (1) PUID=0 + data roots owned 1000:1000 is the complete fleet recipe — NO UserNS/userns_mode key is needed on quadlets or compose services; the plex quadlet works without one. (2) NEVER add userns_mode: host to a podman-compose service: podman-compose 1.5.0 runs every compose service inside a pod and podman hard-rejects the combo (--userns and --pod cannot be set together), leaving the stack down with no recreate path. (3) Diagnostics: podman exec c id always shows uid=0 inside any mapping — meaningless; ground truth is /proc/container-pid/uid_map, host-side ps -o user= -p pid, or ownership of freshly-written files. (4) *arr boot-crash signature: UnauthorizedAccessException: Access to the path '/config/...' at logger init = data root not writable (e.g. root-owned dir created by sudo mkdir); the app exits before binding its port, so the port probe reads "dead" rather than "crashed" — check podman logs tail for "Press enter to exit" before assuming an outage has an infra cause. (5) NEVER pkill -f rootlessport on these hosts: it kills the port-forward helper of EVERY published container simultaneously (2026-09-03 outage); restart the owning systemd units instead. _(context: Dankserver *arr stack restore session, 2026-09-04: stack committed 2026-08-29 never booted (Sentry crash, dead ports read as outage); userns_mode: host fix was a red herring — rejected by podman-compose pods; real fix was chown 1000:1000 of /opt config dirs. Commit e454159 (revert), 8a63624 (docs).)_
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792536631688
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792441535971
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792441746823
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792443092036
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792440454430
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792532762570
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792398332410,
|
||||
"lastConsolidatedAt": 1792439707623,
|
||||
"lastResult": {
|
||||
"at": 1791599865775,
|
||||
"sessions": 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792401025778
|
||||
"lastConsolidatedAt": 1792526055797
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792398289746
|
||||
"lastConsolidatedAt": 1792469747088
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792400453482,
|
||||
"lastConsolidatedAt": 1792439324791,
|
||||
"lastResult": {
|
||||
"at": 1792398531282,
|
||||
"sessions": 1,
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"v":1,"kind":"product_decision","statement":"Documentation links to @~/Projects/serverAppConfig/serverAppConfigs-docs/","source":"explicit_user","evidence":"correct our documentation to just link to @~/Projects/serverAppConfig/serverAppConfigs-docs/","friction":{"corrective":false,"regression":false,"subtle":false},"sessionId":"01a1531d-3e48-70b0-8ee0-6d48a197cae3","ts":1792397374946}
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792397349857
|
||||
"lastConsolidatedAt": 1792439315327,
|
||||
"lastResult": {
|
||||
"at": 1792439315327,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792426412619,
|
||||
"lastConsolidatedAt": 1792439612626,
|
||||
"lastResult": {
|
||||
"at": 1792401456699,
|
||||
"at": 1792439612626,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792426569902,
|
||||
"lastConsolidatedAt": 1792524715562,
|
||||
"lastResult": {
|
||||
"at": 1792402864677,
|
||||
"at": 1792433353188,
|
||||
"sessions": 1,
|
||||
"deltas": 8,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792426408958,
|
||||
"lastConsolidatedAt": 1792434572397,
|
||||
"lastResult": {
|
||||
"at": 1792401394555,
|
||||
"at": 1792432178813,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"deltas": 2,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## Agent Output Patterns
|
||||
|
||||
- Typed intro: "Update, [randomly generated epithet]!"
|
||||
- Heckle mode option
|
||||
- Typed outro: "More later, over and out!"
|
||||
- Generation frequency control
|
||||
|
||||
## Documentation Flow
|
||||
|
||||
- Put any created docs in the existing knowledgebase instead of creating a new one
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792538189166,
|
||||
"lastResult": {
|
||||
"at": 1792537233005,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
## Handoff Location
|
||||
- put it at ~/Projects
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792538116812,
|
||||
"lastResult": {
|
||||
"at": 1792537164207,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792537985341
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
## Documentation structure
|
||||
|
||||
- Store agent-related docs in `dankvoice-docs`
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792538005214,
|
||||
"lastResult": {
|
||||
"at": 1792537052571,
|
||||
"sessions": 1,
|
||||
"deltas": 2,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792536921764
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
tt.*
|
||||
.tests
|
||||
doc/tags
|
||||
debug
|
||||
.repro
|
||||
foo.*
|
||||
*.log
|
||||
data
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"neodev": {
|
||||
"library": {
|
||||
"enabled": true,
|
||||
"plugins": true
|
||||
}
|
||||
},
|
||||
"neoconf": {
|
||||
"plugins": {
|
||||
"lua_ls": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,4 @@
|
||||
# 💤 LazyVim
|
||||
|
||||
A starter template for [LazyVim](https://github.com/LazyVim/LazyVim).
|
||||
Refer to the [documentation](https://lazyvim.github.io/installation) to get started.
|
||||
@@ -0,0 +1,2 @@
|
||||
-- bootstrap lazy.nvim, LazyVim and your plugins
|
||||
require("config.lazy")
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"LazyVim": { "branch": "main", "commit": "999700997f72227187d49d8b92667183dc7fc809" },
|
||||
"base16-nvim": { "branch": "master", "commit": "7183774e90841f4bd27a47cb6e75cf532fd1192d" },
|
||||
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
|
||||
"bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" },
|
||||
"catppuccin": { "branch": "main", "commit": "edefef779ab08ce1a4a404713e3012b0d202bd35" },
|
||||
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" },
|
||||
"flash.nvim": { "branch": "main", "commit": "5f0f270fdc7c5b0c21d903ee85b9cb06f2ac636a" },
|
||||
"friendly-snippets": { "branch": "main", "commit": "b4d01b0fdf3c9a549961c2f9ffe8dc09be166219" },
|
||||
"gitsigns.nvim": { "branch": "main", "commit": "fd780920613e541cbdfac41bf711583ed49f9249" },
|
||||
"grug-far.nvim": { "branch": "main", "commit": "bbde8ef5ae46ceee2c3bd8b21fc048c5f8ffbfc0" },
|
||||
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
|
||||
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
|
||||
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
|
||||
"mason-lspconfig.nvim": { "branch": "main", "commit": "00b5473cca4db5b410ff4bae724e3b6e21f2687f" },
|
||||
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
|
||||
"mini.ai": { "branch": "main", "commit": "d0c4987a1ead2bcf4eb22cc9815c662d76677c08" },
|
||||
"mini.icons": { "branch": "main", "commit": "f916635dc5ae2f479fbb9ab8184bb9444b62e86c" },
|
||||
"mini.pairs": { "branch": "main", "commit": "03eb1f0062a2cee5314d1ac3939246e4bb21352e" },
|
||||
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
|
||||
"nui.nvim": { "branch": "main", "commit": "10fc361835c856ba4233ef5ea135b919bf3dce97" },
|
||||
"nvim-lint": { "branch": "master", "commit": "f6230a5c06ba59e6059c14637f1dbe29a7978b9b" },
|
||||
"nvim-lspconfig": { "branch": "master", "commit": "45a3a444b710511c39abca3b60bb6e23a21e3de5" },
|
||||
"nvim-treesitter": { "branch": "main", "commit": "29212d826054bb2948fa9d3a99eca9bf6a050eb2" },
|
||||
"nvim-treesitter-textobjects": { "branch": "main", "commit": "5c7b0263797dfd1bd6202f2b219f3b53a80b2187" },
|
||||
"nvim-ts-autotag": { "branch": "main", "commit": "d7220b55e16ed3c04e88086256def5d282429493" },
|
||||
"persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" },
|
||||
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
|
||||
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
|
||||
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },
|
||||
"tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" },
|
||||
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
|
||||
"ts-comments.nvim": { "branch": "main", "commit": "a59d6092213447450191122c9346f309161504cb" },
|
||||
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extras": [
|
||||
|
||||
],
|
||||
"install_version": 8,
|
||||
"news": {
|
||||
"NEWS.md": "11866"
|
||||
},
|
||||
"version": 8
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Autocmds are automatically loaded on the VeryLazy event
|
||||
-- Default autocmds that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/autocmds.lua
|
||||
--
|
||||
-- Add any additional autocmds here
|
||||
-- with `vim.api.nvim_create_autocmd`
|
||||
--
|
||||
-- Or remove existing autocmds by their group name (which is prefixed with `lazyvim_` for the defaults)
|
||||
-- e.g. vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell")
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Keymaps are automatically loaded on the VeryLazy event
|
||||
-- Default keymaps that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua
|
||||
-- Add any additional keymaps here
|
||||
@@ -0,0 +1,53 @@
|
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||
if not (vim.uv or vim.loop).fs_stat(lazypath) then
|
||||
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
|
||||
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
vim.api.nvim_echo({
|
||||
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
|
||||
{ out, "WarningMsg" },
|
||||
{ "\nPress any key to exit..." },
|
||||
}, true, {})
|
||||
vim.fn.getchar()
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
vim.opt.rtp:prepend(lazypath)
|
||||
|
||||
require("lazy").setup({
|
||||
spec = {
|
||||
-- add LazyVim and import its plugins
|
||||
{ "LazyVim/LazyVim", import = "lazyvim.plugins" },
|
||||
-- import/override with your plugins
|
||||
{ import = "plugins" },
|
||||
},
|
||||
defaults = {
|
||||
-- By default, only LazyVim plugins will be lazy-loaded. Your custom plugins will load during startup.
|
||||
-- If you know what you're doing, you can set this to `true` to have all your custom plugins lazy-loaded by default.
|
||||
lazy = false,
|
||||
-- It's recommended to leave version=false for now, since a lot the plugin that support versioning,
|
||||
-- have outdated releases, which may break your Neovim install.
|
||||
version = false, -- always use the latest git commit
|
||||
-- version = "*", -- try installing the latest stable version for plugins that support semver
|
||||
},
|
||||
install = { colorscheme = { "tokyonight", "habamax" } },
|
||||
checker = {
|
||||
enabled = true, -- check for plugin updates periodically
|
||||
notify = false, -- notify on update
|
||||
}, -- automatically check for plugin updates
|
||||
performance = {
|
||||
rtp = {
|
||||
-- disable some rtp plugins
|
||||
disabled_plugins = {
|
||||
"gzip",
|
||||
-- "matchit",
|
||||
-- "matchparen",
|
||||
-- "netrwPlugin",
|
||||
"tarPlugin",
|
||||
"tohtml",
|
||||
"tutor",
|
||||
"zipPlugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Options are automatically loaded before lazy.nvim startup
|
||||
-- Default options that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua
|
||||
-- Add any additional options here
|
||||
@@ -0,0 +1,70 @@
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
require('base16-colorscheme').setup({
|
||||
base00 = '#181818',
|
||||
base01 = '#282828',
|
||||
base02 = '#323232',
|
||||
base03 = '#646464',
|
||||
base04 = '#95a99f',
|
||||
base05 = '#e4e4ef',
|
||||
base06 = '#e4e4ef',
|
||||
base07 = '#e4e4ef',
|
||||
base08 = '#f43841',
|
||||
base09 = '#9e95c7',
|
||||
base0A = '#96a6c8',
|
||||
base0B = '#f43841',
|
||||
base0C = '#a596e9',
|
||||
base0D = '#f8868c',
|
||||
base0E = '#96b0e9',
|
||||
base0F = '#becff4',
|
||||
})
|
||||
|
||||
local hi = function(group, opts)
|
||||
vim.api.nvim_set_hl(0, group, opts)
|
||||
end
|
||||
|
||||
-- telescope.nvim
|
||||
hi('TelescopeNormal', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('TelescopeBorder', { fg = '#646464', bg = '#181818' })
|
||||
hi('TelescopePromptNormal', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('TelescopePromptBorder', { fg = '#646464', bg = '#181818' })
|
||||
hi('TelescopePromptPrefix', { fg = '#f43841', bg = '#181818' })
|
||||
hi('TelescopePromptCounter', { fg = '#95a99f', bg = '#181818' })
|
||||
hi('TelescopePromptTitle', { fg = '#181818', bg = '#f43841' })
|
||||
hi('TelescopePreviewTitle', { fg = '#181818', bg = '#96a6c8' })
|
||||
hi('TelescopeResultsTitle', { fg = '#181818', bg = '#9e95c7' })
|
||||
hi('TelescopeSelection', { fg = '#e4e4ef', bg = '#323232' })
|
||||
hi('TelescopeSelectionCaret', { fg = '#f43841', bg = '#323232' })
|
||||
hi('TelescopeMatching', { fg = '#f43841', bold = true })
|
||||
|
||||
-- mini.pick
|
||||
hi('MiniPickNormal', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('MiniPickBorder', { fg = '#646464', bg = '#181818' })
|
||||
hi('MiniPickPrompt', { fg = '#e4e4ef', bg = '#181818' })
|
||||
hi('MiniPickPromptPrefix', { fg = '#f43841', bg = '#181818' })
|
||||
hi('MiniPickBorderText', { fg = '#181818', bg = '#f43841' })
|
||||
hi('MiniPickMatchCurrent', { fg = '#e4e4ef', bg = '#323232' })
|
||||
hi('MiniPickPromptCaret', { fg = '#f43841', bg = '#323232' })
|
||||
hi('MiniPickMatchRanges', { fg = '#f43841', bold = true })
|
||||
end
|
||||
|
||||
-- Register a signal handler for SIGUSR1 (matugen updates).
|
||||
-- The handler re-requires this module, which re-runs the code below, so the
|
||||
-- previous handle is stopped first; otherwise handlers double on every signal.
|
||||
if _G.__matugen_signal then
|
||||
_G.__matugen_signal:stop()
|
||||
_G.__matugen_signal:close()
|
||||
end
|
||||
|
||||
local signal = vim.uv.new_signal()
|
||||
_G.__matugen_signal = signal
|
||||
signal:start(
|
||||
'sigusr1',
|
||||
vim.schedule_wrap(function()
|
||||
package.loaded['matugen'] = nil
|
||||
require('matugen').setup()
|
||||
end)
|
||||
)
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,197 @@
|
||||
-- since this is just an example spec, don't actually load anything here and return an empty spec
|
||||
-- stylua: ignore
|
||||
if true then return {} end
|
||||
|
||||
-- every spec file under the "plugins" directory will be loaded automatically by lazy.nvim
|
||||
--
|
||||
-- In your plugin files, you can:
|
||||
-- * add extra plugins
|
||||
-- * disable/enabled LazyVim plugins
|
||||
-- * override the configuration of LazyVim plugins
|
||||
return {
|
||||
-- add gruvbox
|
||||
{ "ellisonleao/gruvbox.nvim" },
|
||||
|
||||
-- Configure LazyVim to load gruvbox
|
||||
{
|
||||
"LazyVim/LazyVim",
|
||||
opts = {
|
||||
colorscheme = "gruvbox",
|
||||
},
|
||||
},
|
||||
|
||||
-- change trouble config
|
||||
{
|
||||
"folke/trouble.nvim",
|
||||
-- opts will be merged with the parent spec
|
||||
opts = { use_diagnostic_signs = true },
|
||||
},
|
||||
|
||||
-- disable trouble
|
||||
{ "folke/trouble.nvim", enabled = false },
|
||||
|
||||
-- override nvim-cmp and add cmp-emoji
|
||||
{
|
||||
"hrsh7th/nvim-cmp",
|
||||
dependencies = { "hrsh7th/cmp-emoji" },
|
||||
---@param opts cmp.ConfigSchema
|
||||
opts = function(_, opts)
|
||||
table.insert(opts.sources, { name = "emoji" })
|
||||
end,
|
||||
},
|
||||
|
||||
-- change some telescope options and a keymap to browse plugin files
|
||||
{
|
||||
"nvim-telescope/telescope.nvim",
|
||||
keys = {
|
||||
-- add a keymap to browse plugin files
|
||||
-- stylua: ignore
|
||||
{
|
||||
"<leader>fp",
|
||||
function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end,
|
||||
desc = "Find Plugin File",
|
||||
},
|
||||
},
|
||||
-- change some options
|
||||
opts = {
|
||||
defaults = {
|
||||
layout_strategy = "horizontal",
|
||||
layout_config = { prompt_position = "top" },
|
||||
sorting_strategy = "ascending",
|
||||
winblend = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- add pyright to lspconfig
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
---@class PluginLspOpts
|
||||
opts = {
|
||||
---@type lspconfig.options
|
||||
servers = {
|
||||
-- pyright will be automatically installed with mason and loaded with lspconfig
|
||||
pyright = {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- add tsserver and setup with typescript.nvim instead of lspconfig
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
dependencies = {
|
||||
"jose-elias-alvarez/typescript.nvim",
|
||||
init = function()
|
||||
require("lazyvim.util").lsp.on_attach(function(_, buffer)
|
||||
-- stylua: ignore
|
||||
vim.keymap.set( "n", "<leader>co", "TypescriptOrganizeImports", { buffer = buffer, desc = "Organize Imports" })
|
||||
vim.keymap.set("n", "<leader>cR", "TypescriptRenameFile", { desc = "Rename File", buffer = buffer })
|
||||
end)
|
||||
end,
|
||||
},
|
||||
---@class PluginLspOpts
|
||||
opts = {
|
||||
---@type lspconfig.options
|
||||
servers = {
|
||||
-- tsserver will be automatically installed with mason and loaded with lspconfig
|
||||
tsserver = {},
|
||||
},
|
||||
-- you can do any additional lsp server setup here
|
||||
-- return true if you don't want this server to be setup with lspconfig
|
||||
---@type table<string, fun(server:string, opts:_.lspconfig.options):boolean?>
|
||||
setup = {
|
||||
-- example to setup with typescript.nvim
|
||||
tsserver = function(_, opts)
|
||||
require("typescript").setup({ server = opts })
|
||||
return true
|
||||
end,
|
||||
-- Specify * to use this function as a fallback for any server
|
||||
-- ["*"] = function(server, opts) end,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- for typescript, LazyVim also includes extra specs to properly setup lspconfig,
|
||||
-- treesitter, mason and typescript.nvim. So instead of the above, you can use:
|
||||
{ import = "lazyvim.plugins.extras.lang.typescript" },
|
||||
|
||||
-- add more treesitter parsers
|
||||
{
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
opts = {
|
||||
ensure_installed = {
|
||||
"bash",
|
||||
"html",
|
||||
"javascript",
|
||||
"json",
|
||||
"lua",
|
||||
"markdown",
|
||||
"markdown_inline",
|
||||
"python",
|
||||
"query",
|
||||
"regex",
|
||||
"tsx",
|
||||
"typescript",
|
||||
"vim",
|
||||
"yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-- since `vim.tbl_deep_extend`, can only merge tables and not lists, the code above
|
||||
-- would overwrite `ensure_installed` with the new value.
|
||||
-- If you'd rather extend the default config, use the code below instead:
|
||||
{
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
opts = function(_, opts)
|
||||
-- add tsx and treesitter
|
||||
vim.list_extend(opts.ensure_installed, {
|
||||
"tsx",
|
||||
"typescript",
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- the opts function can also be used to change the default opts:
|
||||
{
|
||||
"nvim-lualine/lualine.nvim",
|
||||
event = "VeryLazy",
|
||||
opts = function(_, opts)
|
||||
table.insert(opts.sections.lualine_x, {
|
||||
function()
|
||||
return "😄"
|
||||
end,
|
||||
})
|
||||
end,
|
||||
},
|
||||
|
||||
-- or you can return new options to override all the defaults
|
||||
{
|
||||
"nvim-lualine/lualine.nvim",
|
||||
event = "VeryLazy",
|
||||
opts = function()
|
||||
return {
|
||||
--[[add your custom lualine config here]]
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
-- use mini.starter instead of alpha
|
||||
{ import = "lazyvim.plugins.extras.ui.mini-starter" },
|
||||
|
||||
-- add jsonls and schemastore packages, and setup treesitter for json, json5 and jsonc
|
||||
{ import = "lazyvim.plugins.extras.lang.json" },
|
||||
|
||||
-- add any tools you want to have installed below
|
||||
{
|
||||
"williamboman/mason.nvim",
|
||||
opts = {
|
||||
ensure_installed = {
|
||||
"stylua",
|
||||
"shellcheck",
|
||||
"shfmt",
|
||||
"flake8",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Applies the hand-kept wallpaper palette (lua/matugen.lua) on top of LazyVim's
|
||||
-- colorscheme instead of living with tokyonight defaults.
|
||||
--
|
||||
-- base16-colorscheme writes highlights directly and never touches v:colors_name,
|
||||
-- so LazyVim keeps thinking tokyonight is loaded and its plugin-specific tweaks
|
||||
-- still run; ours is the last word on the UI groups.
|
||||
return {
|
||||
"RRethy/base16-nvim",
|
||||
lazy = false,
|
||||
priority = 900,
|
||||
config = function()
|
||||
-- LazyVim loads its colorscheme after plugin configs, so re-apply on every
|
||||
-- ColorScheme event: that also makes `:colorscheme foo` land on our palette.
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
desc = "Re-apply the matugen base16 palette after any colorscheme",
|
||||
callback = function()
|
||||
require("matugen").setup()
|
||||
end,
|
||||
})
|
||||
require("matugen").setup()
|
||||
end,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
column_width = 120
|
||||
@@ -1,21 +0,0 @@
|
||||
// ────────────── Output Configuration ──────────────
|
||||
output "Samsung Electric Company Odyssey G75F HNTY601056" {
|
||||
mode "3840x2160@60"
|
||||
scale 1
|
||||
transform "normal"
|
||||
position x=0 y=0
|
||||
}
|
||||
|
||||
output "DP-1" {
|
||||
transform "90"
|
||||
position x=0 y=0
|
||||
}
|
||||
|
||||
output "DP-3" {
|
||||
transform "270"
|
||||
position x=1200 y=0
|
||||
}
|
||||
|
||||
output "eDP-1" {
|
||||
// no position — auto-placed to the right of DP-* outputs
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user