diff --git a/agent/managed-skills/dankvoice-behavior-scenario-authoring/SKILL.md b/agent/managed-skills/dankvoice-behavior-scenario-authoring/SKILL.md new file mode 100644 index 0000000..2972aa2 --- /dev/null +++ b/agent/managed-skills/dankvoice-behavior-scenario-authoring/SKILL.md @@ -0,0 +1,76 @@ +--- +name: dankvoice-behavior-scenario-authoring +description: "Author or extend a dankvoice behavior.bats scenario: driver env knobs (DV_REWRITE/DV_FAIL/DV_CUT_MS/DV_TAG_LOG), the SPOKEN heard-words census, private-TMPDIR leak assertions, and the arm-the-precondition-before-absence trap." +--- + +# Writing a dankvoice behavior test + +`tests/behavior.bats` drives the **built bundle** through a generated harness. Each `@test` +is: set env → run a named *scenario* → assert on printed census lines. Never assert on +internals; the observable vocabulary is `CALLS`, `PLAYS`, `PLAYORDER`, `SPOKEN`, `NOTIFY`, +`CALL input=… auth=… at=…`, `PLAYER`, `KILLED`. + +The driver lives in a heredoc inside `setup()`; find and read it before adding anything: + +```bash +sed -n '/cat > "$DV_DRIVER" ":` in that switch that calls the extension's own handlers +(`say.handler("on", ctx)`, `messageEnd?.(said("…"), ctx)`, `agentEnd?.(answer("…"), ctx)`). +Prefer delivering narration through the harness (`agent_end`/`message_end`) over a typed +`/say`: the humanizer, mood, heckle, framing and the frequency hold govern **unsolicited** +narration only, so a typed line cannot exercise them. + +## Fixture knobs (set per test, all read by the driver) + +| Knob | Effect | +|---|---| +| `DV_REWRITE=off\|echo\|prose\|reasoning\|stall` | what the chat (humanizer) route answers; unset ⇒ it 404s and the rewrite falls back to mechanical text | +| `DV_STALL_MS` | delay the chat answer (timeout behaviour) | +| `DV_FAIL=` | the first N `/audio/speech` requests answer HTTP 500 JSON — an endpoint outage a test can watch the queue weather | +| `DV_HOLD=` | how long the player stub holds each clip; sets the playback windows | +| `DV_CUT_MS=` | in `stagecut`, when another session cuts the line | +| `DV_TAG_LOG`, `DV_TIMING_LOG` | per-clip tag census (`PLAYORDER`, `SPOKEN`) and player START/END ms | +| `DV_CT` | content type of the answer (non-WAV routing) | +| `DV_KEYS` | key replayer for the settings modal | +| `TMPDIR="$WORK/scratch-tmp"` | scope synthesis temp dirs so a leak assertion is yours alone | + +Always pass `DANKVOICE_STAGE_DIR="$WORK/stage"` when the test touches the stage, and use the +`say_run ` helper (it wraps `run env …` so `$output` is captured); a bare +`scenario_name` invocation prints without capturing. + +## Two traps that have each hidden a real bug + +**Assert absence only with the producer armed.** `DV_REWRITE` unset ⇒ the chat route 404s, +so `[[ "$output" != *"/chat/completions"* ]]` and "payload equals the source text" pass even +when rewriting is happening. Nine tests pinned the opposite of the repo's law this way. Pin +with `DV_REWRITE=prose` (the fixture answers with its *own* words) and assert the payload you +expect — then survival is evidence, not absence. + +**Count what was HEARD, not what was requested.** A segment whose clip was killed mid-play, +or aborted mid-synthesis, is legitimately synthesized again on resume. Laws about framing +("one intro per narration") are about hearing, so count `SPOKEN` lines: the driver maps each +played tag back to the `/audio/speech` request that produced that clip. `CALLS` counts +requests and will over-report across a line-cut. + +Temp-dir leak assertions mirror this: `TMPDIR` private, then +`find "$TMPDIR" -maxdepth 1 -type d -name 'dankvoice-*' | wc -l`. + +## Pinning timing + +Derive windows from `DV_HOLD`: with 0.5 s clips, clips occupy ~[10,310], [310,610], [610,910]; +choose `DV_CUT_MS` so the cut lands strictly inside the clip you mean to interrupt, leaving +margin for the 250 ms stage poll and synthesis time. Default scenario timings are env-overridable +(`DV_CUT_MS ?? 600`) so a new test never edits an existing one's schedule. + +## Red-proof and gate (project invariants) + +- Prove a new assertion can fail: in a **private worktree**, `git show HEAD:src/ > src/`, + `bun run build`, run the filter, restore the file and rebuild. Never while another suite runs + (`setup()` rebuilds the bundle from `src/`). See `dankvoice-bats-serialization`, + `dankvoice-bundle-swap-red-proof`. +- `bun run build` before staging: `tests/packaging.bats` byte-compares committed `dist/` with a + fresh build, so `dist/index.js` must be in the same commit as the `src/` change. +- Gate with the whole suite (`bats tests/`), not the filter; ~5 min, so run it unbounded/async and + never write to `src/` while it runs. diff --git a/agent/managed-skills/plex-metadata-title-cache-read/SKILL.md b/agent/managed-skills/plex-metadata-title-cache-read/SKILL.md new file mode 100644 index 0000000..8202e14 --- /dev/null +++ b/agent/managed-skills/plex-metadata-title-cache-read/SKILL.md @@ -0,0 +1,73 @@ +--- +name: plex-metadata-title-cache-read +description: "Read the live Plex library DB read-only for clean titles/file paths (no sqlite3 binary, server running), and use it as the title authority over filename parsing." +--- + +Plex's metadata cache already holds a *clean* title for every file it matched +(`Alien`, not `Alien.Directors.Cut.1979.1080p.BRrip.x265-RARBG`). Read it instead +of parsing filenames, and name folders / group duplicates from it. + +## Where it lives + +Host package: data dir `/var/lib/plexmediaserver`, DB at + +``` +/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db +``` + +(`Resources/com.plexapp.plugins.library.db` under `/usr/lib/plexmediaserver` is a +blank template — not the library.) The dir is `dank:dank`, so no sudo needed. +Containers: check `podman ps` first; on Dankserver Plex runs as a host process. + +## Read it while Plex is RUNNING + +No `sqlite3` binary on the fleet hosts — use `python3 -c` with `sqlite3`. Open +immutable read-only so no WAL/SHM is touched and no lock is taken: + +```python +con = sqlite3.connect(f"file:{db}?mode=ro&immutable=1", uri=True) +``` + +A long local `find /` can hit the tool's own deadline; scope `find` with +`-maxdepth` or use the path above directly. + +## Schema traps + +* `library_sections` → column is **`section_type`**, not `type`. Movies = 1, + TV = 2, Music = 8. +* **`metadata_titles` does not exist** (empty name in the table list). Titles are + on `metadata_items`: `title`, `original_title`, `edition_title`, `year`, + `metadata_type`, `parent_id`, `deleted_at`. +* File path is `media_parts.file`; join + `media_parts → media_items (id = media_item_id) → metadata_items + (id = metadata_item_id)`, and `LEFT JOIN metadata_items par ON par.id = + mi.parent_id` for the show/movie parent. +* **Movies use the LEAF title** (`mi.title`); the parent row is empty. TV uses + the parent. Filter `p.deleted_at IS NULL AND mi.deleted_at IS NULL` or deleted + scans pollute the census. + +```sql +select p.file, mi.title, mi.original_title, mi.edition_title, mi.year, m.container, + m.video_codec, m.audio_codec, m.width, m.height +from media_parts p +join media_items m on m.id = p.media_item_id +join metadata_items mi on mi.id = m.metadata_item_id +where p.file like '/mnt/entropy/Movies/%' + and p.deleted_at is null and mi.deleted_at is null +``` + +`media_items` also carries codec/resolution already — often no ffprobe pass is +needed for matched files. + +## Use it as the title authority + +Reject a Plex title only if it looks like a filename +(`2160p|1080p|x26[45]|hevc|bluray|web-?dl`) — a healthy cache scores **0** junk +titles. Coverage expectation measured on `/mnt/entropy/Movies`: 3,754 of 4,698 +media files matched; the unmatched remainder is extras, BDMV disc trees, TV +episodes misfiled into Movies, and collection box sets — exactly the classes a +filename parser must still handle. + +Propagate one match to every copy of a film (a group of versions) so sidecars, +`.nfo` and media files share one stem; fall back to the filename parser only +when no row exists. diff --git a/agent/managed-skills/plex-title-authority-for-relayout/SKILL.md b/agent/managed-skills/plex-title-authority-for-relayout/SKILL.md new file mode 100644 index 0000000..d3d940d --- /dev/null +++ b/agent/managed-skills/plex-title-authority-for-relayout/SKILL.md @@ -0,0 +1,54 @@ +--- +name: plex-title-authority-for-relayout +description: "Read Plex's metadata cache as the clean-title authority (ziggurat schema, immutable read, no sqlite3 CLI) when naming or regrouping a media library from scripts instead of filename parsing." +--- + +Plex has already matched most of a library to real titles. When a script must name folders/files (Jellyfin relayout, dedup reports, library census), **read Plex's DB and let it override filename parsing** — `Alien.Directors.Cut.1979.1080p.BRrip.x265-RARBG` → `Alien`, `1917.2019.2160p…` → `1917`. Measured on dankserver's Movies section: 3,773 parts, **0** filename-shaped titles, 80% of media files matched (unmatched = extras, BDMV disc trees, TV). + +## Locate and open the DB (no sqlite3 CLI needed) + +``` +DB="/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db" +# data dir: /var/lib/plexmediaserver (host install) or the container's volume +``` + +```python +con = sqlite3.connect(f"file:{DB}?mode=ro&immutable=1", uri=True) # no copy, no wal, no lock +``` + +`immutable=1` is the whole trick: the DB was 1.5 GiB and Plex was live — no copy, no `sqlite3` binary, no risk of touching the service. Set `busy_timeout`; expect occasional inconsistency, acceptable for a read-only census. + +## Schema trap: Plex renamed the tables ("ziggurat") + +The old `sections` table **does not exist** — querying it throws, which reads like "wrong DB". Real names: + +| table | join | key columns | +|---|---|---| +| `library_sections` | — | `id`, `name`, `section_type` (`movie`/`show`), `agent`, `scanner` | +| `section_locations` | `library_section_id` | `root_path`, `available` | +| `metadata_items` | `parent_id` → own `id` | `metadata_type`, `title`, `original_title`, `edition_title`, `year`, `indexed`, `deleted_at` | +| `media_items` | `metadata_item_id` | `container`, `video_codec`, `audio_codec`, `width`, `height` | +| `media_parts` | `media_item_id` | **`file`** (full path), `size`, `duration`, `deleted_at` | + +Census section coverage first (`count(*) where file like '/%'`), then dump: + +```sql +select p.file, mi.title, mi.original_title, mi.edition_title, mi.year, m.video_codec, m.width, m.height +from media_parts p +join media_items m on m.id = p.media_item_id +join metadata_items mi on mi.id = m.metadata_item_id +where p.file like '/%' and p.deleted_at is null and mi.deleted_at is null +``` + +For a movie library the leaf *is* the film; `parent_id` is NULL, so a `left join` on parent returns the same title. `metadata_type`: 1 = movie, 2 = show (a show section riding the same root means episodes live in the MOVIES folder — worth reporting). + +## Using it in a renamer + +- **One film, one name:** match any copy → apply that title to the whole group. Keyed by `file` path, so the map is exact. +- **Override, don't replace:** the parser still names the unmatched (extras, disc trees, TV, unscanned). Keep it as a fallback tier. +- **Bucket on the old key, name with Plex:** group/score sidecars with the parser's words (a `.srt` matches on the year token), format the folder from Plex: `f"{title} ({year})"`. +- **Sanitize for the FS:** Plex titles carry `:` and `&` (`2001: A Space Odyssey`, `Rock & Rule`) — those must go through the same sanitizer as filename-derived names. + +## Cross-check before trusting a parser + +Score the parser against the cache: count titles matching `2160p|1080p|x26[45]|bluray|hevc|10bit` — Plex returned **0**. Every parser mismatch is a real bug class: year-echo pseudo-titles (`1917.2019.2160p…`), two-year titles (`2001…1968`), numeric titles eaten as track indexes (`12 Monkeys`, `007 Skyfall`, `000 Miles to Graceland`, `0 - First Dates` — keep the number unless it's 1–2 digits glued to the words with a year present, i.e. `01. Bye Bye Baby`). diff --git a/agent/memories/sharpshooter/serverAppConfig-eu5lc7uqff33/state.json b/agent/memories/sharpshooter/serverAppConfig-eu5lc7uqff33/state.json index ce7bcd2..e35771d 100644 --- a/agent/memories/sharpshooter/serverAppConfig-eu5lc7uqff33/state.json +++ b/agent/memories/sharpshooter/serverAppConfig-eu5lc7uqff33/state.json @@ -1,4 +1,4 @@ { "v": 1, - "lastConsolidatedAt": 1793403526285 + "lastConsolidatedAt": 1793408441828 } diff --git a/agent/memories/sharpshooter/wt-20261027-142335-6d1fc1f-1hpiehxnq73s0/state.json b/agent/memories/sharpshooter/wt-20261027-142335-6d1fc1f-1hpiehxnq73s0/state.json index 9f94ea0..fa9bb55 100644 --- a/agent/memories/sharpshooter/wt-20261027-142335-6d1fc1f-1hpiehxnq73s0/state.json +++ b/agent/memories/sharpshooter/wt-20261027-142335-6d1fc1f-1hpiehxnq73s0/state.json @@ -1,6 +1,6 @@ { "v": 1, - "lastConsolidatedAt": 1793405934807, + "lastConsolidatedAt": 1793409834839, "lastResult": { "at": 1793403514528, "sessions": 1, diff --git a/agent/memories/sharpshooter/wt-20261030-163847-6d1fc1f-p5mp3j807mvf/state.json b/agent/memories/sharpshooter/wt-20261030-163847-6d1fc1f-p5mp3j807mvf/state.json index 791da38..9704ca9 100644 --- a/agent/memories/sharpshooter/wt-20261030-163847-6d1fc1f-p5mp3j807mvf/state.json +++ b/agent/memories/sharpshooter/wt-20261030-163847-6d1fc1f-p5mp3j807mvf/state.json @@ -1,6 +1,6 @@ { "v": 1, - "lastConsolidatedAt": 1793405994161, + "lastConsolidatedAt": 1793409947332, "lastResult": { "at": 1793405994161, "sessions": 1, diff --git a/agent/memories/sharpshooter/wt-20261030-164316-1b9ba40-gg5su7w1ks2p/state.json b/agent/memories/sharpshooter/wt-20261030-164316-1b9ba40-gg5su7w1ks2p/state.json index bde332c..f71e7d0 100644 --- a/agent/memories/sharpshooter/wt-20261030-164316-1b9ba40-gg5su7w1ks2p/state.json +++ b/agent/memories/sharpshooter/wt-20261030-164316-1b9ba40-gg5su7w1ks2p/state.json @@ -1,4 +1,4 @@ { "v": 1, - "lastConsolidatedAt": 1793406016281 + "lastConsolidatedAt": 1793409916309 } diff --git a/agent/memories/sharpshooter/wt-20261030-180042-6d1fc1f-3s0rhbwqcda42/state.json b/agent/memories/sharpshooter/wt-20261030-180042-6d1fc1f-3s0rhbwqcda42/state.json new file mode 100644 index 0000000..b1890b0 --- /dev/null +++ b/agent/memories/sharpshooter/wt-20261030-180042-6d1fc1f-3s0rhbwqcda42/state.json @@ -0,0 +1,4 @@ +{ + "v": 1, + "lastConsolidatedAt": 1793410002774 +} diff --git a/dotfiles/noctalia/.local/state/noctalia/settings.toml b/dotfiles/noctalia/.local/state/noctalia/settings.toml index 633c2fa..c17ee93 100644 --- a/dotfiles/noctalia/.local/state/noctalia/settings.toml +++ b/dotfiles/noctalia/.local/state/noctalia/settings.toml @@ -279,22 +279,22 @@ transition = [ "fade" ] interval_seconds = 1200 [wallpaper.default] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png" [wallpaper.last] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png" [wallpaper.monitors.DP-1] path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png" [wallpaper.monitors.DP-2] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png" [wallpaper.monitors.HDMI-A-1] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png" [wallpaper.monitors.eDP-1] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gw26v3.png" [weather] unit = "imperial"