name: new-api-version
description: Guided workflow for standing up a NEW Jellyfin apiVersion tier (V3, V4, …) in JellyRock when an upstream release restructures the API enough to need a new if m.getApiVersion() >= N dispatch level. Wraps the "Adding Support for New Server Versions" recipe in docs/dev/jellyfin-server-versioning.md and walks every surface that must move together — the boundary map (YAML), its BrightScript twin resolveApiVersion(), the sdkVN.bs endpoints, the dispatch branches, the device profile, the manifest tier-clamp, and the validators — stopping at each step so the tier split can't land half-built. Use when proactive RC/master triage (via /server-upgrade <rc-or-unstable>) shows a breaking API shift that a per-method >= N branch must cover. NOT for routine endpoint additions inside an existing tier (those use the existing >= 2 shape directly).
model: sonnet
user-invocable: true
allowed-tools: Read, Edit, Write, Grep, Glob, Bash(npm run docs:api-manifest:), Bash(npm run lint:apiversion-consistency:), Bash(npm run lint:endpoint-availability:), Bash(npm run lint:docs:), Bash(npm run test:scripts:), Bash(grep:), Bash(rg:), Bash(date:)
/new-api-version — stand up a new apiVersion tier (V3, V4, …)
JellyRock dispatches Jellyfin endpoints by integer apiVersion tier with if m.getApiVersion() >= N (V1 = server 10.7–10.8, V2 = 10.9+). When an upstream release restructures the API enough that a per-method branch needs a new level, you stand up the next tier. This skill wraps the canonical recipe — docs/dev/jellyfin-server-versioning.md → "Adding Support for New Server Versions" — and walks each surface in order so a contributor can't land a half-built tier. Read that section first; the steps below sequence it and add the verify gates.
The load-bearing invariant: three twins must agree. docs/dev/jellyfin-version-boundaries.yml (what the server-upgrade tooling reads), source/utils/misc.bs resolveApiVersion() (what the app runs to pick the tier), and the sdkVN.bs filename convention the manifest generator keys off. If they disagree, the dispatch is dead code or the tooling's tier-relevance analysis lies. Every step keeps them in lockstep.
Step 0 — Confirm a new tier is actually warranted
A new tier is justified only when an endpoint JellyRock uses changes shape in a way a single new >= N branch must guard (moved path, restructured request/response, a param that moved) — the V1→V2 kind of shift, not a one-off field tweak. Additive fields are handled with isValid()/?? in place; a routine new endpoint uses the existing top tier's >= 2 shape directly (see source/api/CLAUDE.md) — stop and use that if that's all this is. Ask the user to name the concrete breaking change(s) and the server version X that introduces them (usually surfaced by /server-upgrade <rc-or-unstable>'s proactive triage). Do not proceed without X.
Step 1 — Boundary map + its BrightScript twin (do these together)
These are the two halves of the same fact; they ship in one change set.
Boundary map — in
docs/dev/jellyfin-version-boundaries.yml: the next tierN= (highest existing key) + 1. Flip the currently-activetier tofrozenwith a concretemaxServer(last release beforeX); add tierNasactive,minServer: X,maxServer: null. Propose the diff and wait for confirmation (two systems read this file). The loader enforces exactly oneactivetier and it must be the unbounded one, so flip-and-add is atomic. Example V3 at10.12.0:tiers: 2: { minServer: '10.9.0', maxServer: '10.11.10', status: frozen } # ← was active/null 3: { minServer: '10.12.0', maxServer: null, status: active } # ← newresolveApiVersion()twin — insource/utils/misc.bs, add the new tier's check above the existing ones (highest version wins), mirroring the YAMLminServer, and update the header comment:if versionChecker(serverVersion, "10.12.0") ' ← V3 min, matches boundaries.yml tier 3 return 3 end if if versionChecker(serverVersion, "10.9.0") return 2 end if return 1
Step 2 — Add the sdkVN.bs user-endpoint shim + dispatch branches
The user-scoped endpoints that moved are implemented in version-named shim files (source/api/sdkV1.bs, source/api/sdkV2.bs) — the manifest generator's tier-clamp keys off these filenames (Step 4), so the convention is load-bearing, not cosmetic.
- Create
source/api/sdkV<N>.bswith the new endpoint shapes (model it onsdkV2.bs). - Add the
if m.getApiVersion() >= Ndispatch branches insource/api/ApiClient.bs— keep the existing>= 2shape, put the new>= Nbranch ahead of it.grep -rn "getApiVersion" source/surfaces every dispatch site (ApiClient.bs,sdk.bs,items.bs,deviceCapabilities.bs) so none is missed. - The endpoint-specific request/response logic is yours to fill from the diff — this skill scaffolds the file + branches and points at the sites; it does not invent the new API shape. Leave a clear
TODO(V<N>)marker on any branch you stub before the spec finalizes (RCs can still change — that's why/server-upgradere-diffs).
Step 3 — Device profile branch
If the new tier changes the device-profile shape, add a V<N> branch in source/utils/deviceCapabilities.bs (the V1/V2 selectors are already internal; V<N> follows the same getApiVersionFromGlobal()-keyed pattern). Skip if the profile is unchanged.
Step 4 — Manifest tier-clamp + regenerate
The API-usage manifest (docs/architecture/api-usage-manifest.json) is AST-generated and drift-gated. Its cross-function clamp (scripts/generate/api-usage-manifest.js, ~line 467) pins endpoints by shim filename. Add the new tier and clamp the now-frozen middle tier:
sdkV<N>.bs→minApiVersion = Math.max(min, N)- the previously-active shim (e.g.
sdkV2.bs) → gains amaxApiVersion = Math.min(max ?? N-1, N-1)clamp (it's now the frozen middle tier)
Then regenerate + commit:
npm run docs:api-manifest # re-extracts; the >= N branches from Step 2 are picked up automatically
Step 5 — Endpoint-availability ledger (if relevant)
If the new tier introduces endpoints reached on older servers via a guard/sibling, register them in docs/dev/jellyfin-endpoint-availability.yml (see its header for the schema) so the floor-coverage check doesn't re-flag them every release, then validate:
npm run lint:endpoint-availability # checks each entry's guard/sibling claim against current code
Step 6 — Verify statically (no Roku hardware needed)
Standing up a tier is verified entirely by static analysis of the .bs sources — the same kind of AST scan CI already runs over the API files — so you never have to sideload to a device to confirm the wiring:
- Twin-consistency gate (the load-bearing check) —
npm run lint:apiversion-consistencyparsesresolveApiVersion()frommisc.bswith the BrighterScript AST and asserts itsversionChecker(serverVersion, "X.Y.Z") → Nguards exactly match the boundary map (rightminServerper tier, right fallback, highest-tier-first order). If Step 1's two halves drifted, this fails with the specific mismatch. This is the check that replaces the old hardware unit test. - Dispatch wiring —
npm run docs:api-manifestre-extracts the manifest from the AST; the>= Nbranches from Step 2 + thesdkV<N>.bsfilename now show the new tier range on the affected endpoints. A missing branch shows up as drift in the committed manifest (CI-gated). - Boundary validity —
npm run test:scriptsrunsversion-boundaries.test.js(loads + validates the real committed map; a malformed Step 1 edit fails here),apiversion-consistency.test.js, and the manifest drift gate.npm run lint:docschecks the doc references. - Optional, not required — extend
tests/source/unit/utils/resolveApiVersion.spec.bswith a>= X→Ncase if you want belt-and-suspenders runtime coverage, but the static gate above already proves the twin is correct offline. Don't block the tier on a device being reachable.
Step 6b — Docs + fingerprint
- Prose twins — update
docs/dev/jellyfin-server-versioning.md(the tier tables + "Version Detection" steps) andsource/api/CLAUDE.md's "10.7–10.8 (V1) / 10.9+ (V2)" line to include the new tier. Alsodocs/user/jellyfin-server-feature-matrix.mdif user-facing support changed. - Fingerprint — no new fingerprint for the RC/master that prompted this; the next acknowledged stable release commits its fingerprint via the normal
/server-upgradeflow. The floor (10.7.0) never moves, so the backward/symmetry checks need no change.
Step 7 — Capture the decision + close out
Standing up a tier is decision-shaped (it closes off cramming the new shape into the existing tier). Offer to invoke /log decision with a slug like apiversion-v<N>-split capturing why X warranted a new tier. Summarize the surfaces touched and flag any TODO(V<N>) stubs left for when the spec finalizes. If this came from a /server-upgrade <rc> triage, remind the user to re-run that against the FINAL stable when it ships (the RC can still change).
When NOT to use
- A routine new endpoint or additive field on a current-tier server → use the existing
if m.getApiVersion() >= 2shape directly; no new tier. - A registry-schema change for a user setting → that's
/new-migration. - You just want to know what an RC/master build changes →
/server-upgrade <rc-or-unstable>(this skill is the follow-up when that triage says a new tier is warranted).
Sub-agent invocation
To invoke from a parent sub-agent: parent passes Read .claude/skills/new-api-version/SKILL.md and follow Steps 0-7 to stand up apiVersion tier N for server version X=<version>, wrapping the jellyfin-server-versioning.md recipe; keep jellyfin-version-boundaries.yml, resolveApiVersion(), and the sdkVN.bs/manifest-clamp convention in lockstep; scaffold the shim + dispatch branches and stop at each validator in the Task prompt.