From 802014fea9ae8b24a5744a9d0bde6dc40f4f7bf3 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 19 Oct 2026 17:29:41 -0700 Subject: [PATCH] live: heartbeat sync 2026-10-19T17:29:41-07:00 (Refs #11) --- .../dankvoice-bundle-swap-red-proof/SKILL.md | 94 ++++++++++--------- .../dankvoice-menu-knob-workflow/SKILL.md | 38 ++++++++ .../SKILL.md | 31 ++++++ .../state.json | 2 +- .../state.json | 2 +- .../.local/state/noctalia/settings.toml | 8 +- 6 files changed, 123 insertions(+), 52 deletions(-) create mode 100644 agent/managed-skills/dankvoice-menu-knob-workflow/SKILL.md create mode 100644 agent/managed-skills/tts-endpoint-parameter-capability-audit/SKILL.md diff --git a/agent/managed-skills/dankvoice-bundle-swap-red-proof/SKILL.md b/agent/managed-skills/dankvoice-bundle-swap-red-proof/SKILL.md index b686a48..ce3d630 100644 --- a/agent/managed-skills/dankvoice-bundle-swap-red-proof/SKILL.md +++ b/agent/managed-skills/dankvoice-bundle-swap-red-proof/SKILL.md @@ -1,56 +1,58 @@ --- name: dankvoice-bundle-swap-red-proof -description: "Prove a dankvoice behavior test is red-before/green-after by pointing DV_EXT at the previously committed dist bundle, with a pinned scratch fixture dir." +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." --- -Use when a new black-box `tests/behavior.bats` assertion in dankvoice must be shown to be red-capable (fails on the pre-change code) without `git stash`/worktree juggling, or when the old bundle must be compared to the new one. +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 -The bats suite drives `dist/index.js` only (nothing imports `src/`), and the bundle is self-contained. So the *previous commit's* behavior is one `git show` away: write it to a file, point `DV_EXT` at it, run the same driver, and read the same `PLAYS n` / `CALL` / `NOTIFY` lines the test asserts. -## Procedure -1. Build the new bundle and export the old one: - ```bash - cd ~/Projects/ - bun run build >/dev/null 2>&1 - git show HEAD:dist/index.js > /tmp/old-dist.js - ``` -2. Stand up the fixtures in a **pinned** scratch dir — never `$(ls -dt /tmp/tmp.* | head -1)`: other live sessions on the box create `tmp.*` dirs continuously, and a glob picks *theirs*, which is how a red-proof silently becomes "Module not found" and then a false `PLAYS 0`. - ```bash - W=/tmp/dv-redproof; rm -rf "$W"; mkdir -p "$W/bin" - ``` -3. Copy the **real** player stub, not a simplification. The driver counts plays from the stub log; a stub that only `sleep`s records nothing and reports `PLAYS 0` for every build, red and green alike. Mirror `setup()` in `tests/behavior.bats`: - ```bash - 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" # PATH fallback player - 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" - sed -n '86,408p' tests/behavior.bats > "$W/driver.ts" # the inline heredoc driver - ``` - (Re-check those line bounds against the current file; the driver lives inside `setup()`.) -4. Export the env the driver expects and run old vs new against the SAME scenario: - ```bash - export 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 - for ext in /tmp/old-dist.js "$PWD/dist/index.js"; do - rm -f "$W/player.log" - DV_EXT="$ext" DV_PLAYER_BIN="$W/no-such-player" bun "$W/driver.ts" speak | grep -E '^PLAYS|^NOTIFY' - done - ``` - The driver sets its own fixture server URL and prepends `dirname "$DV_STUB")/bin` to `PATH` (see its `DANKVOICE_*` block), so the discovered-player fallback has somewhere real to land. -5. Add a **control**: same scenario with a valid player path. Old-vs-new differences that the control also shows are harness bugs, not behavior changes. +`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" 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 -- old `PLAYS 0` + new `PLAYS 1` + control `PLAYS 1` → the assertion is red-capable and the fix is real. -- identical results old vs new → the harness is lying (stub too thin, wrong scratch dir, or the scenario never reaches the changed path). Fix the harness before believing any green. -- File-mode assertions: pre-seed the file with the *old* mode (`printf … > file; chmod 644 file`) — a fresh write with `mode:` in `writeFileSync` only applies at creation, so a fresh file can look green while the old code is equally green. -## Gotchas -- Never poll the bats suite while doing this; `setup()` runs `bun run build`, so a concurrent `src/` edit gets compiled into someone else's run (see `dankvoice-bats-serialization`). -- `bun run build` does not typecheck; a stale identifier reference survives bundling and only fails at runtime — the suite, not the build, is the gate. +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 `, `NOTIFY`, `SETTINGS`). `CALLS` counts `/audio/speech` calls only; the rewrite call appears as its own `CALL /…/chat/completions … sys=` 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. diff --git a/agent/managed-skills/dankvoice-menu-knob-workflow/SKILL.md b/agent/managed-skills/dankvoice-menu-knob-workflow/SKILL.md new file mode 100644 index 0000000..a786f10 --- /dev/null +++ b/agent/managed-skills/dankvoice-menu-knob-workflow/SKILL.md @@ -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 ` 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 <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. diff --git a/agent/managed-skills/tts-endpoint-parameter-capability-audit/SKILL.md b/agent/managed-skills/tts-endpoint-parameter-capability-audit/SKILL.md new file mode 100644 index 0000000..4f2a2ff --- /dev/null +++ b/agent/managed-skills/tts-endpoint-parameter-capability-audit/SKILL.md @@ -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. diff --git a/agent/memories/sharpshooter/wt-20261019-020309-6d1fc1f-8knxmfcrfa1k/state.json b/agent/memories/sharpshooter/wt-20261019-020309-6d1fc1f-8knxmfcrfa1k/state.json index 251816d..591cf64 100644 --- a/agent/memories/sharpshooter/wt-20261019-020309-6d1fc1f-8knxmfcrfa1k/state.json +++ b/agent/memories/sharpshooter/wt-20261019-020309-6d1fc1f-8knxmfcrfa1k/state.json @@ -1,6 +1,6 @@ { "v": 1, - "lastConsolidatedAt": 1792452174894, + "lastConsolidatedAt": 1792456074901, "lastResult": { "at": 1792433353188, "sessions": 1, diff --git a/agent/memories/sharpshooter/wt-20261019-125509-7c8a99d-me8scwe1t2nf/state.json b/agent/memories/sharpshooter/wt-20261019-125509-7c8a99d-me8scwe1t2nf/state.json index ece4f14..613a655 100644 --- a/agent/memories/sharpshooter/wt-20261019-125509-7c8a99d-me8scwe1t2nf/state.json +++ b/agent/memories/sharpshooter/wt-20261019-125509-7c8a99d-me8scwe1t2nf/state.json @@ -1,6 +1,6 @@ { "v": 1, - "lastConsolidatedAt": 1792452243238, + "lastConsolidatedAt": 1792455940767, "lastResult": { "at": 1792452243238, "sessions": 1, diff --git a/dotfiles/noctalia/.local/state/noctalia/settings.toml b/dotfiles/noctalia/.local/state/noctalia/settings.toml index 2b190a8..ceaae0a 100644 --- a/dotfiles/noctalia/.local/state/noctalia/settings.toml +++ b/dotfiles/noctalia/.local/state/noctalia/settings.toml @@ -275,13 +275,13 @@ transition = [ "fade" ] interval_seconds = 1200 [wallpaper.default] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_y8ewzk.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_l38ljr.jpg" [wallpaper.last] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_y8ewzk.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_l38ljr.jpg" [wallpaper.monitors.DP-1] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_y8ewzk.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_l38ljr.jpg" [wallpaper.monitors.DP-2] path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg" @@ -290,7 +290,7 @@ transition = [ "fade" ] path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg" [wallpaper.monitors.eDP-1] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_y8ewzk.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_l38ljr.jpg" [weather] unit = "imperial"