fix(617): make the cross-repo skills real symlinks and reconcile the ersatztv skill #639

Merged
timothy merged 3 commits from fix/617-skill-symlinks into main 2026-07-26 02:13:29 +02:00
Owner

fixes #617

What was wrong

.claude/skills/jellyfin/ was a real directory holding a stale fork of the canonical server-management copy — not the symlink CLAUDE.md describes. It presented X-Emby-Token as the auth pattern (v12 only accepts it via EnableLegacyAuthorization, slated for removal in v13) and omitted the DELETE /Items/{id} data-loss warning that has destroyed files three times.

Correction to the issue's premise

The issue comment framed server-management's ersatztv copy as "stale and dangerous" and ersatztv's as "correct". That is half right, and acting on it literally would have lost content:

  • server-management's copy is wrong only in its description: frontmatter (the "CRUD requires SQLite DB scripting" claim). Its body already said "Full REST CRUD is available under /api/v1; prefer it over direct DB writes".
  • Its body also carries ~150 lines the ersatztv copy lacks: backup/deploy scripts (#482), test/prod topology (#481), the streaming-mode + Dispatcharr teardown-race analysis (#500), VAAPI-vs-QSV on jazz (#498), table schema notes, and the channel-creation checklist.

So the reconciliation takes server-management's file as the base and ports ersatztv's unique content onto it — not the other way round.

Changes

jellyfin — directory deleted, replaced with a relative symlink to ~/server-management/.claude/skills/jellyfin, matching the pattern server-management already uses for beets/radarr/sonarr/yt-dlp.

ersatztv — reconciled as the canonical copy (ersatztv owns it per CLAUDE.md → Project Boundaries):

  • fixed the dangerous description: — it now says the REST API has write paths and SQLite is a fallback
  • ported ersatztv-only content: the silent root-owned api.key 401 trap, the lying OCI labels (#350), the local-vs-UTC log timestamp trap, the docs/endpoint-index.md pointer, settings/ffmpeg ordering, the scoped test-image refresh
  • added the #616 paging facts requested in the issue comment: 0-based pageNum, per-endpoint pageSize cap, and that playout/reset takes a channel id

Stale claims corrected in both copies (found while reconciling, verified against the decisions catalog):

  • prod auto-update: auto_update: false, manual DeployStack jazz-media only, no 03:00 fallback — the old text contradicted the fork's own CLAUDE.md
  • ErsatzTV library ids: Music Videos is 16 and Standup 14, not 8/9 — two of four entries were wrong; verified live via GET /api/v1/media-sources. Also noted these are ErsatzTV ids, not Jellyfin's
  • POST /api/v1/libraries/{id}/scan is the scan endpoint (there is no PUT …/sync)
  • external-URL logos do drive the on-screen bug now — the WatermarkSelector File.Exists() gate was removed in #502 (ffmpeg.external-logo-graphics-engine), logos cached at save time (graphics.channel-logo-caching)
  • M3U/XMLTV absolute URLs are no longer stuck on the request host — #340 added iptv.base_url (iptv.base-url)
  • unpinned the stale v26.8.0 image reference (current release is v26.12.0)

CLAUDE.md — both boundary lines now describe the actual arrangement, including the worktree caveat below.

Known caveat

A relative cross-repo symlink resolves only in a checkout at ~/ersatztv, so the jellyfin skill is unavailable inside a git worktree. This is inherent to the pattern server-management already uses; documented in CLAUDE.md rather than diverging to absolute paths (deviation policy).

Follow-up

The server-management side (~/server-management/.claude/skills/ersatztv → symlink into this repo) lands as a separate commit in that repo after this merges, so the symlink target already holds the reconciled content.

Verification

  • Symlink resolves from ~/ersatztv to the canonical jellyfin SKILL.md
  • Every stale claim above re-checked against docs/decisions/README.md records or live prod API, not memory
  • Docs-only / skills-only: no C# changed, so the independent-review rubric's code triggers (locks, auth handlers, migrations, >150 C# lines) don't apply. Touching .claude/ means the review-verdict auto-pass does not apply, so a real verdict is required.
fixes #617 ## What was wrong `.claude/skills/jellyfin/` was a real directory holding a stale fork of the canonical server-management copy — not the symlink `CLAUDE.md` describes. It presented `X-Emby-Token` as *the* auth pattern (v12 only accepts it via `EnableLegacyAuthorization`, slated for removal in v13) and omitted the `DELETE /Items/{id}` data-loss warning that has destroyed files three times. ## Correction to the issue's premise The issue comment framed server-management's `ersatztv` copy as "stale and dangerous" and ersatztv's as "correct". That is **half right**, and acting on it literally would have lost content: - server-management's copy is wrong **only in its `description:` frontmatter** (the "CRUD requires SQLite DB scripting" claim). Its **body** already said *"Full REST CRUD is available under `/api/v1`; prefer it over direct DB writes"*. - Its body also carries **~150 lines the ersatztv copy lacks**: backup/deploy scripts (#482), test/prod topology (#481), the streaming-mode + Dispatcharr teardown-race analysis (#500), VAAPI-vs-QSV on jazz (#498), table schema notes, and the channel-creation checklist. So the reconciliation takes **server-management's file as the base** and ports ersatztv's unique content onto it — not the other way round. ## Changes **`jellyfin`** — directory deleted, replaced with a relative symlink to `~/server-management/.claude/skills/jellyfin`, matching the pattern server-management already uses for `beets`/`radarr`/`sonarr`/`yt-dlp`. **`ersatztv`** — reconciled as the canonical copy (ersatztv owns it per `CLAUDE.md` → Project Boundaries): - fixed the dangerous `description:` — it now says the REST API has write paths and SQLite is a fallback - ported ersatztv-only content: the **silent root-owned `api.key` 401 trap**, the **lying OCI labels** (#350), the **local-vs-UTC log timestamp** trap, the `docs/endpoint-index.md` pointer, `settings/ffmpeg` ordering, the scoped test-image refresh - added the **#616 paging facts** requested in the issue comment: 0-based `pageNum`, per-endpoint `pageSize` cap, and that `playout/reset` takes a **channel** id **Stale claims corrected in both copies** (found while reconciling, verified against the decisions catalog): - prod auto-update: `auto_update: false`, manual `DeployStack jazz-media` only, **no 03:00 fallback** — the old text contradicted the fork's own `CLAUDE.md` - **ErsatzTV library ids: Music Videos is 16 and Standup 14**, not 8/9 — two of four entries were wrong; verified live via `GET /api/v1/media-sources`. Also noted these are ErsatzTV ids, not Jellyfin's - `POST /api/v1/libraries/{id}/scan` is the scan endpoint (there is no `PUT …/sync`) - external-URL logos **do** drive the on-screen bug now — the `WatermarkSelector` `File.Exists()` gate was removed in #502 (`ffmpeg.external-logo-graphics-engine`), logos cached at save time (`graphics.channel-logo-caching`) - M3U/XMLTV absolute URLs are no longer stuck on the request host — #340 added `iptv.base_url` (`iptv.base-url`) - unpinned the stale `v26.8.0` image reference (current release is v26.12.0) **`CLAUDE.md`** — both boundary lines now describe the actual arrangement, including the worktree caveat below. ## Known caveat A **relative** cross-repo symlink resolves only in a checkout at `~/ersatztv`, so the `jellyfin` skill is unavailable inside a git worktree. This is inherent to the pattern server-management already uses; documented in `CLAUDE.md` rather than diverging to absolute paths (deviation policy). ## Follow-up The server-management side (`~/server-management/.claude/skills/ersatztv` → symlink into this repo) lands as a separate commit in that repo **after** this merges, so the symlink target already holds the reconciled content. ## Verification - Symlink resolves from `~/ersatztv` to the canonical jellyfin `SKILL.md` - Every stale claim above re-checked against `docs/decisions/README.md` records or live prod API, not memory - Docs-only / skills-only: **no C# changed**, so the independent-review rubric's code triggers (locks, auth handlers, migrations, >150 C# lines) don't apply. Touching `.claude/` means the review-verdict auto-pass does **not** apply, so a real verdict is required.
timothy added 2 commits 2026-07-26 00:43:13 +02:00
`.claude/skills/jellyfin/` was a real directory holding a stale fork of the
canonical server-management copy, not the symlink CLAUDE.md described. It
documented `X-Emby-Token` as *the* auth pattern (v12 only accepts it via
`EnableLegacyAuthorization`, slated for removal in v13) and omitted the
`DELETE /Items/{id}` data-loss warning that has destroyed files three times.
Replaced with a relative symlink, matching the pattern server-management
already uses for beets/radarr/sonarr.

`.claude/skills/ersatztv/` had diverged in BOTH directions, so a replace in
either direction would have lost correct content. Contrary to the issue's
framing, server-management's copy was stale only in its `description:` — its
body already said "Full REST CRUD is available under /api/v1" and carried
~150 lines the ersatztv copy lacked (backup/deploy, test/prod topology,
streaming-mode analysis, VAAPI-vs-QSV, table schema notes, checklists). So
that file is the base, reconciled here as the canonical copy:

- fixed the dangerous `description:` claiming CRUD requires SQLite scripting
- ported ersatztv-only content: the silent root-owned api.key 401 trap, the
  lying OCI labels, the local-vs-UTC log timestamp trap, the endpoint-index
  pointer, `settings/ffmpeg` ordering, scoped test-image refresh
- added the #616 paging facts (0-based pageNum, per-endpoint pageSize cap,
  playout/reset takes a CHANNEL id) per the request on #617
- corrected prod auto-update: `auto_update: false`, manual DeployStack only,
  no 03:00 fallback (contradicted the fork's CLAUDE.md)
- corrected the ErsatzTV library ids: Music Videos is 16 and Standup 14, not
  8/9 — verified live via GET /api/v1/media-sources
- unpinned the stale v26.8.0 image reference (current release is v26.12.0)

fixes #617
fix(617): correct two stale logo claims carried in from both copies
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
review-verdict/h10 Awaiting review verdict for e7ae919
PR Gates / Docs update reminder (pull_request) Successful in 12s
Review verdict / Set review-verdict status (pull_request) Successful in 3s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m54s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m24s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
e7ae919959
Both copies still described pre-fix behavior:
- "External URL logos work for M3U but NOT for watermark burn-in (code
  checks File.Exists())" — the WatermarkSelector File.Exists gate was
  removed in #502 (ffmpeg.external-logo-graphics-engine); logos are cached
  at save time (graphics.channel-logo-caching).
- "M3U tvg-logo URLs hardcode http://localhost:8409" — #340 added the
  optional iptv.base_url advertised base URL (iptv.base-url).
timothy added 1 commit 2026-07-26 00:58:47 +02:00
fix(617): quote the description scalar so the embedded colon stays valid YAML
Review verdict / Set review-verdict status (pull_request) Successful in 2s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 11s
PR Gates / decisions lifecycle (pull_request) Successful in 21s
review-verdict/h10 Review-verdict: MERGEABLE @ 01fb07a
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m47s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 21s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
01fb07ac9d
The reconciled description ends with 'IMPORTANT: the fork has a full
versioned REST API...'. An unquoted YAML scalar containing ': ' parses as
a mapping, so the frontmatter failed to load ('mapping values are not
allowed here') — which would have silently broken skill discovery, the
one thing the description exists to drive. Quoting the value fixes it.

Caught by parsing the frontmatter with yaml.safe_load rather than eyeballing
it; the same latent defect exists in server-management's canonical jellyfin
SKILL.md and is fixed there separately.
Author
Owner

Review-verdict: MERGEABLE @ 01fb07a

Skills/docs only, 0 C# lines, so the independent-review rubric's code triggers (locks, auth, write handlers, migrations, >150 C# lines) do not apply; reviewed by self-review against sources. Adversarial pass found and fixed one real defect: the reconciled description embedded 'IMPORTANT: ' which made the YAML frontmatter unparseable (mapping values are not allowed here) and would have silently broken skill discovery - caught by yaml.safe_load, not eyeballing, and the same latent defect was fixed in server-management's canonical jellyfin SKILL.md that this PR symlinks to. Also verified: no content dropped from the old copy across 22 key terms, symlink committed as mode 120000 with the correct relative target, and every corrected claim re-checked against a decision record or the live prod API.

Review-verdict: MERGEABLE @ 01fb07a Skills/docs only, 0 C# lines, so the independent-review rubric's code triggers (locks, auth, write handlers, migrations, >150 C# lines) do not apply; reviewed by self-review against sources. Adversarial pass found and fixed one real defect: the reconciled description embedded 'IMPORTANT: ' which made the YAML frontmatter unparseable (mapping values are not allowed here) and would have silently broken skill discovery - caught by yaml.safe_load, not eyeballing, and the same latent defect was fixed in server-management's canonical jellyfin SKILL.md that this PR symlinks to. Also verified: no content dropped from the old copy across 22 key terms, symlink committed as mode 120000 with the correct relative target, and every corrected claim re-checked against a decision record or the live prod API.
timothy scheduled this pull request to auto merge when all checks succeed 2026-07-26 01:00:00 +02:00
timothy merged commit 262262856f into main 2026-07-26 02:13:29 +02:00
timothy deleted branch fix/617-skill-symlinks 2026-07-26 02:13:30 +02:00
Sign in to join this conversation.