diff --git a/docs/superpowers/plans/2026-07-29-repository-skills-implementation-sync.md b/docs/superpowers/plans/2026-07-29-repository-skills-implementation-sync.md
new file mode 100644
index 000000000..972d1ad45
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-29-repository-skills-implementation-sync.md
@@ -0,0 +1,2013 @@
+# Repository Skills and Implementation Synchronization Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Synchronize IPTVnator's eight repository skills with current implementation, filter internal notes from public releases, and fix Stalker series flag and playback-position identity defects without losing legacy progress.
+
+**Architecture:** Keep the three workstreams independently testable: release output and guidance, provider-local Stalker compatibility, and repository skill/document maintenance. Release filtering is an additive public-extraction mode; Stalker compatibility uses series-scoped IDs plus an explicit legacy alias; mechanical skill constraints become a small dependency-free validator.
+
+**Tech Stack:** Node.js `node:test`, GitHub Actions YAML, Angular 21 signals and standalone services, TypeScript, Jest, Nx, Markdown skills and architecture documentation.
+
+---
+
+## Execution Order and Commit Boundaries
+
+1. Bootstrap the locked workspace.
+2. Add public release extraction.
+3. Wire the release workflow and guidance.
+4. Normalize Stalker series flags.
+5. Add series-scoped Stalker episode IDs.
+6. Reconcile and migrate legacy Stalker positions.
+7. Document the Stalker behavior and add its release note.
+8. Add mechanical repository-skill validation.
+9. Refresh Nx and SQLite skills/docs.
+10. Refresh theme, UI, Xtream, and Stalker skills/docs.
+11. Run skill application scenarios and the complete validation ladder.
+
+Execute Tasks 1-11 strictly in numbered order in the shared worktree. Tasks 5
+and 6 must use the same worker because Task 6 consumes Task 5's deliberately
+uncommitted interface changes. Do not parallelize edits, tests, staging, or
+commits: the workers share one filesystem, Git index, and `HEAD`, and Task 5's
+temporary signature change can also invalidate project-wide checks. Read-only
+audits and reviews may run in parallel, but each task's implementation and
+commit must finish before the next begins.
+
+### Task 1: Bootstrap and establish the baseline
+
+**Files:**
+- Verify: `package.json`
+- Verify: `pnpm-lock.yaml`
+- Verify: `docs/superpowers/specs/2026-07-29-repository-skills-implementation-sync-design.md`
+
+- [ ] **Step 1: Confirm the intended branch and clean starting point**
+
+Run:
+
+```bash
+git branch --show-current
+git status --short
+```
+
+Expected: branch is `agent/skill-implementation-sync`; status is clean.
+
+- [ ] **Step 2: Install the locked dependencies**
+
+Run:
+
+```bash
+pnpm install --frozen-lockfile
+```
+
+Expected: exit 0 and no change to `pnpm-lock.yaml`.
+
+- [ ] **Step 3: Verify Nx workspace discovery**
+
+Run:
+
+```bash
+pnpm nx show projects
+```
+
+Expected: output includes `release-tools`, `portal-stalker-data-access`,
+`portal-stalker-feature`, `shared-interfaces`, `web`, and `web-e2e`.
+
+- [ ] **Step 4: Record fresh behavior baselines**
+
+Run:
+
+```bash
+node --test --test-reporter=tap tools/release/release-notes.test.mjs
+pnpm exec jest \
+ --config libs/portal/stalker/data-access/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts
+pnpm exec jest \
+ --config libs/portal/stalker/feature/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts
+```
+
+Expected: the existing tests pass before new red tests are introduced.
+
+### Task 2: Filter internal notes from the public release body
+
+**Files:**
+- Modify: `tools/release/release-notes.test.mjs:21`
+- Modify: `tools/release/release-notes.test.mjs:446`
+- Modify: `tools/release/extract-changelog-section.mjs:27`
+
+- [ ] **Step 1: Hoist the shared fixture and add seven failing contracts**
+
+Move the existing `const changelog = [...]` fixture from inside
+`describe('extractSection')` to module scope immediately before that suite, so
+the existing and new suites share it. Add a module-level
+`internalOnlyChangelog` fixture with the current generated internal block.
+Import `extractPublicSection`, `parseExtractArguments`, and `runExtractorCli`
+beside `extractSection`, then add four public-extraction tests:
+
+```js
+describe('extractPublicSection', () => {
+ it('removes the generated internal block from a mixed release', () => {
+ assert.equal(
+ extractPublicSection(changelog, '0.24.0'),
+ ['### Features', '', '- **playback** — Up Next rail.'].join('\n')
+ );
+ });
+
+ it('leaves a public-only release unchanged', () => {
+ const publicOnly =
+ '# 0.24.0 (2026-08-01)\n\n### Fixes\n\n- public fix';
+
+ assert.equal(
+ extractPublicSection(publicOnly, '0.24.0'),
+ '### Fixes\n\n- public fix'
+ );
+ });
+
+ it('returns an empty body for an internal-only release', () => {
+ assert.equal(
+ extractPublicSection(internalOnlyChangelog, '0.24.0'),
+ ''
+ );
+ });
+
+ it('preserves unrelated details blocks', () => {
+ const source = [
+ '# 0.24.0 (2026-08-01)',
+ '',
+ '',
+ 'Migration guide
',
+ '',
+ 'Keep this text.',
+ '',
+ ' ',
+ '',
+ '',
+ 'Internal changes
',
+ '',
+ '- **deps** — parser bump.',
+ '',
+ ' ',
+ ].join('\n');
+ const result = extractPublicSection(source, '0.24.0');
+
+ assert.match(result, /Migration guide<\/summary>/);
+ assert.match(result, /Keep this text\./);
+ assert.doesNotMatch(result, /Internal changes|parser bump/);
+ });
+});
+```
+
+Add direct CLI-contract coverage without spawning a process or reading the
+repository's real changelog:
+
+```js
+describe('extract changelog CLI contract', () => {
+ it('parses --public before the version', () => {
+ assert.deepEqual(
+ parseExtractArguments(['--public', '0.24.0']),
+ { version: '0.24.0', publicOnly: true }
+ );
+ });
+
+ it('rejects malformed, unknown, duplicate, or extra arguments', () => {
+ for (const args of [
+ [],
+ ['0.24'],
+ ['--unknown', '0.24.0'],
+ ['--public', '--public', '0.24.0'],
+ ['0.24.0', 'extra'],
+ ]) {
+ assert.equal(parseExtractArguments(args), null);
+ const result = runExtractorCli(changelog, args);
+ assert.equal(result.exitCode, 2);
+ assert.equal(result.stdout, '');
+ assert.match(result.stderr, /Usage:/);
+ }
+ });
+
+ it('exits successfully with no output for an internal-only public body', () => {
+ assert.deepEqual(
+ runExtractorCli(
+ internalOnlyChangelog,
+ ['--public', '0.24.0']
+ ),
+ { exitCode: 0, stdout: '', stderr: '' }
+ );
+ });
+});
+```
+
+- [ ] **Step 2: Run the test and verify RED**
+
+Run:
+
+```bash
+node --test tools/release/release-notes.test.mjs
+```
+
+Expected: FAIL because the module does not export the three new functions.
+
+- [ ] **Step 3: Implement exact-block public extraction**
+
+Add to `extract-changelog-section.mjs`:
+
+```js
+const INTERNAL_DETAILS_BLOCK =
+ /(?:^|\n\n)\nInternal changes<\/summary>\n\n[\s\S]*?\n\n<\/details>(?=\n\n|$)/g;
+
+/**
+ * Extracts the authored public body while preserving the complete changelog.
+ *
+ * @param {string} changelog full CHANGELOG.md content
+ * @param {string} version bare semver
+ * @returns {string | null}
+ */
+export function extractPublicSection(changelog, version) {
+ const section = extractSection(changelog, version);
+
+ return section === null
+ ? null
+ : section.replace(INTERNAL_DETAILS_BLOCK, '').trim();
+}
+```
+
+Add pure argument and CLI-result boundaries:
+
+```js
+const CLI_USAGE =
+ 'Usage: extract-changelog-section.mjs [--public] ';
+
+export function parseExtractArguments(args) {
+ const publicFlagCount = args.filter(
+ (argument) => argument === '--public'
+ ).length;
+ const positional = args.filter(
+ (argument) => argument !== '--public'
+ );
+
+ if (
+ publicFlagCount > 1 ||
+ positional.length !== 1 ||
+ !/^\d+\.\d+\.\d+$/.test(positional[0])
+ ) {
+ return null;
+ }
+
+ return {
+ version: positional[0],
+ publicOnly: publicFlagCount === 1,
+ };
+}
+```
+
+Export `runExtractorCli(changelog, args)` returning
+`{ exitCode, stdout, stderr }`. It must:
+
+- return exit 2 plus `CLI_USAGE` for invalid arguments;
+- select `extractPublicSection` only in public mode;
+- preserve the existing detailed missing-section diagnostic and exit 1;
+- preserve the raw-mode empty-section diagnostic and exit 1; and
+- return exit 0 with empty stdout/stderr for an internal-only public section.
+
+Refactor `main()` into a thin adapter: read `CHANGELOG.md`, call
+`runExtractorCli(changelog, process.argv.slice(2))`, write the returned stdout
+and stderr to their matching streams, and assign `process.exitCode`. Keep the
+existing import guard.
+
+- [ ] **Step 4: Run the test and verify GREEN**
+
+Run:
+
+```bash
+node --test --test-reporter=tap tools/release/release-notes.test.mjs
+```
+
+Expected: 44 tests pass, 0 fail.
+
+- [ ] **Step 5: Commit the public extractor**
+
+```bash
+git add \
+ tools/release/extract-changelog-section.mjs \
+ tools/release/release-notes.test.mjs
+git commit -m "fix(release): filter internal notes from public body"
+```
+
+### Task 3: Wire public release extraction and synchronize release guidance
+
+**Files:**
+- Modify: `tools/release/release-notes.test.mjs`
+- Modify: `tools/release/project.json`
+- Modify: `.github/workflows/build-and-make.yaml`
+- Modify: `.changes/README.md`
+- Modify: `.codex/skills/release-cut/SKILL.md`
+- Modify: `.claude/skills/release-cut/SKILL.md`
+- Modify: `.codex/skills/release-notes/SKILL.md`
+- Modify: `.claude/skills/release-notes/SKILL.md`
+- Modify: `AGENTS.md`
+- Modify: `CLAUDE.md`
+
+- [ ] **Step 1: Add a failing workflow contract test**
+
+Add `readFileSync` to the existing `node:fs` import and add:
+
+```js
+it('uses public extraction for authored tag-release text', () => {
+ const workflow = readFileSync(
+ new URL(
+ '../../.github/workflows/build-and-make.yaml',
+ import.meta.url
+ ),
+ 'utf8'
+ );
+
+ assert.match(
+ workflow,
+ /extract-changelog-section\.mjs --public "\$\{VERSION\}"/
+ );
+});
+```
+
+- [ ] **Step 2: Run the workflow contract test and verify RED**
+
+Run:
+
+```bash
+node --test tools/release/release-notes.test.mjs
+```
+
+Expected: FAIL because `build-and-make.yaml` does not pass `--public`.
+
+- [ ] **Step 3: Make the workflow request public output**
+
+Change the tag command to:
+
+```bash
+BODY="$(node tools/release/extract-changelog-section.mjs --public "${VERSION}")"
+```
+
+Update the nearby workflow comment to state that the complete changelog keeps
+internal notes and an internal-only release intentionally has no authored
+public text. Add this input to `release-tools:test`:
+
+```json
+"{workspaceRoot}/.github/workflows/build-and-make.yaml"
+```
+
+- [ ] **Step 4: Run the workflow contract test and verify GREEN**
+
+Run:
+
+```bash
+node --test --test-reporter=tap tools/release/release-notes.test.mjs
+```
+
+Expected: 45 tests pass, 0 fail.
+
+- [ ] **Step 5: Replace the release-notes skill and its mirror**
+
+Apply this exact content to both release-notes skill paths:
+
+````markdown
+---
+name: release-notes
+description: "Use when a change may need a .changes note, the Release note gate fails, or deciding whether type: internal or no-release-note applies."
+---
+
+# Release Notes
+
+Every user-visible change gets one direct `.changes/-.md` file. The
+area matches the conventional-commit scope; the body is present tense, user
+language, one to three sentences, and at most 400 characters.
+
+## Format
+
+```markdown
+---
+type: fix
+area: stalker
+issues: [1234]
+screenshot: optional-manifest-slug
+---
+
+Stalker series now resume the correct episode.
+```
+
+`type` is `breaking`, `feature`, `fix`, `perf`, or `internal`. Omit optional
+fields instead of inventing values. Never add a version or PR number.
+
+`internal` records invisible maintenance. It stays collapsed in `CHANGELOG.md`
+but is omitted from the blog and the authored public GitHub body. GitHub's
+generated commit list may still mention the underlying commits.
+
+## Skip or Label
+
+The gate auto-exempts website, E2E and mock-server apps, `*.spec.{js,ts}`,
+`*.e2e.{js,ts}`, snapshots, any `/testing/` path, and Markdown. Other test-only,
+docs, CI, workflow, or pure-refactor PRs use `no-release-note` when the gate
+would otherwise require a note. At least one newly added direct
+`.changes/*.md` file satisfies the gate.
+
+## Verify
+
+```bash
+pnpm run release:notes:validate
+```
+
+Full format: `.changes/README.md`. Gate policy:
+`tools/release/check-release-note-gate.mjs`.
+
+The `.codex` and `.claude` copies of this skill must remain byte-identical.
+````
+
+- [ ] **Step 6: Replace the release-cut skill and its mirror**
+
+Apply this exact content to both release-cut skill paths:
+
+````markdown
+---
+name: release-cut
+description: Use when preparing, cutting, tagging, publishing, or verifying an IPTVnator release or its release assets.
+---
+
+# Release Cut
+
+The tag build takes authored public text from the new CHANGELOG section. Keep
+the full changelog committed before tagging.
+
+## Preflight
+
+Work from clean, current `master` with the intended remote named explicitly.
+Confirm `package.json` contains bare semver, the exact `v` tag does not
+exist locally or remotely, CI is green, and all notes validate.
+
+```bash
+pnpm run release:notes:validate
+pnpm run i18n:check
+```
+
+## Generate
+
+1. Set `package.json.version`.
+2. Run `pnpm run release:notes:changelog`.
+3. Minor release: run `pnpm run release:notes:blog` and finish every editorial
+ field. Patch release: edit the existing `vX-Y` post; do not scaffold or
+ force-overwrite it.
+4. Capture required manifest screenshots only against mock servers:
+ `pnpm nx run electron-backend:build-e2e`, then
+ `pnpm run release:screenshots`.
+5. Consume notes only after reviewing all generated output:
+ `node tools/release/build-release-notes.mjs --consume`.
+
+The consume command is the destructive boundary: it deletes the direct note
+files. Stage only release-owned files, including exact website post/assets and
+`git add -A -- .changes`, then commit and create the exact tag.
+
+```bash
+git commit -m "chore(release): v0.24.0"
+git tag v0.24.0
+```
+
+## Push and External Effects
+
+Push only the intended branch and tag; never use broad `git push --tags`.
+
+```bash
+git push --atomic origin \
+ HEAD:refs/heads/master \
+ refs/tags/v0.24.0
+```
+
+Master and `v*` pushes can publish Docker images. The tag build creates a draft
+GitHub release. Verify authored text plus generated commits and all required
+macOS, Windows, DEB, RPM, Pacman (`.pacman`/`.pkg.tar.*`), AppImage, Snap,
+Flatpak, updater metadata, blockmaps, and
+`linux-frame-copy-runtime-sources.tar.xz`.
+
+After verification, manually publish the GitHub release. That publication
+automatically verifies its Snap assets and uploads them to `edge`.
+Installed-Snap smoke and candidate/stable promotion remain manual. Keep the
+blog draft during artifact verification; publish it in a follow-up commit and
+verify the website deployment.
+
+## Failure Safety
+
+Missing CHANGELOG section: regenerate, commit, delete the bad tag locally and
+remotely only after resolving its exact target, then retag. Never publish a
+draft until the source archive and Snap contract pass.
+
+The `.codex` and `.claude` copies of this skill must remain byte-identical.
+````
+
+- [ ] **Step 7: Synchronize canonical release documentation**
+
+Replace the `internal` paragraph in `.changes/README.md` with:
+
+```markdown
+`type: internal` records invisible maintenance. Internal notes stay collapsed in
+`CHANGELOG.md`, are omitted from the blog scaffold, and are removed from the
+authored public GitHub body by
+`extract-changelog-section.mjs --public`. GitHub's generated commit list remains
+separate. An internal-only release can therefore have an empty authored body.
+```
+
+Replace its skip guidance with the exact gate behavior:
+
+```markdown
+The gate auto-exempts website, E2E and mock-server apps, `*.spec.{js,ts}`,
+`*.e2e.{js,ts}`, snapshots, any `/testing/` path, and Markdown. For other
+test-only, documentation, CI/workflow, or pure-refactor changes under
+`apps/`/`libs/`, apply `no-release-note` when no user-visible note is warranted.
+```
+
+Add the same public/internal distinction and automatic Snap `edge`/Docker side
+effects to the mirrored release-policy sections in `AGENTS.md` and `CLAUDE.md`.
+Keep their process bullets textually synchronized.
+
+- [ ] **Step 8: Verify release guidance and mirrors**
+
+Run:
+
+```bash
+cmp -s \
+ .codex/skills/release-cut/SKILL.md \
+ .claude/skills/release-cut/SKILL.md
+cmp -s \
+ .codex/skills/release-notes/SKILL.md \
+ .claude/skills/release-notes/SKILL.md
+! rg -n '^[[:space:]]*git push .*--tags' \
+ .codex/skills/release-cut/SKILL.md \
+ .claude/skills/release-cut/SKILL.md
+! rg -n 'separate manual flow' \
+ .codex/skills/release-cut/SKILL.md \
+ .claude/skills/release-cut/SKILL.md
+node --test \
+ tools/release/release-notes.test.mjs \
+ tools/release/release-note-gate.test.mjs \
+ tools/release/build-release-notes.test.mjs \
+ tools/release/screenshot-guards.test.mjs
+pnpm nx test release-tools
+```
+
+Expected: both `cmp` commands and both negated `rg` checks exit 0; no
+executable broad tag push or stale manual-flow wording exists; all release
+tests pass.
+
+- [ ] **Step 9: Commit workflow and release guidance**
+
+```bash
+git add \
+ .github/workflows/build-and-make.yaml \
+ tools/release/project.json \
+ tools/release/release-notes.test.mjs \
+ .changes/README.md \
+ .codex/skills/release-cut/SKILL.md \
+ .codex/skills/release-notes/SKILL.md \
+ .claude/skills/release-cut/SKILL.md \
+ .claude/skills/release-notes/SKILL.md \
+ AGENTS.md \
+ CLAUDE.md
+git commit -m "docs(release): synchronize release workflow guidance"
+```
+
+### Task 4: Normalize every Stalker series flag decision
+
+**Files:**
+- Modify: `libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts`
+- Modify: `libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts`
+- Modify: `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts`
+- Modify: `libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts`
+- Modify: `libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts`
+- Verify: `libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts`
+
+- [ ] **Step 1: Add selection characterization and a failing progress regression**
+
+In `stalker-catalog-facade.service.spec.ts`, retain the store mock in a local
+variable and add parameterized coverage:
+
+```ts
+it.each([true, 1, '1'] as const)(
+ 'selects a VOD series when is_series is %p',
+ (isSeries) => {
+ const service = TestBed.inject(StalkerCatalogFacadeService);
+ const store = TestBed.inject(StalkerStore) as unknown as {
+ setSelectedItem: jest.Mock;
+ };
+
+ service.selectItem({
+ id: '42',
+ name: 'Boolean Series',
+ is_series: isSeries,
+ });
+
+ expect(store.setSelectedItem).toHaveBeenCalledWith(
+ expect.objectContaining({ id: '42', is_series: true })
+ );
+ }
+);
+
+it.each([true, 1, '1'] as const)(
+ 'reports series progress semantics when is_series is %p',
+ (isSeries) => {
+ const service = TestBed.inject(StalkerCatalogFacadeService);
+
+ expect(
+ service.getItemProgress({
+ id: '42',
+ name: 'Series',
+ is_series: isSeries,
+ })
+ ).toEqual({ hasSeriesProgress: false });
+ }
+);
+```
+
+Add `false` and `0` non-series cases that expect an undefined normalized
+`is_series` selection field and ordinary VOD progress
+`{ progress: 0, isWatched: false }`.
+
+Use the smallest valid `StalkerVodSource` fixture accepted by the compiler; do
+not cast away missing required fields if the existing factory already provides
+one.
+
+- [ ] **Step 2: Run the facade spec and verify RED**
+
+Run:
+
+```bash
+pnpm exec jest \
+ --config libs/portal/stalker/feature/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts
+```
+
+Expected: the boolean progress case fails because the facade currently checks
+only `1` and `'1'`. Selection cases already pass through normalization inside
+`buildStalkerSelectedVodItem`; they are characterization coverage for removing
+the redundant local comparison.
+
+- [ ] **Step 3: Route facade and detail decisions through the normalizer**
+
+Import `isStalkerSeriesFlag` from
+`@iptvnator/portal/stalker/data-access`. Replace both direct comparisons in
+`StalkerCatalogFacadeService`:
+
+```ts
+const needsSeriesFetch =
+ this.contentType() === 'vod' && isStalkerSeriesFlag(item.is_series);
+```
+
+```ts
+const isSeries =
+ this.contentType() === 'series' ||
+ isStalkerSeriesFlag(item.is_series);
+```
+
+Use the same helper in `StalkerCatalogDetailComponent.isSeriesDetail`:
+
+```ts
+return Boolean(
+ item &&
+ (this.contentType() === 'series' ||
+ isStalkerSeriesFlag(item.is_series))
+);
+```
+
+- [ ] **Step 4: Lock the store resource to the same three-value contract**
+
+Change the `vodSeriesSeasonsResource` guard in
+`with-stalker-series.feature.ts` from truthiness to:
+
+```ts
+!isStalkerSeriesFlag(selectedItem.is_series)
+```
+
+Import the helper from `../../stalker-vod.utils`. In
+`with-stalker-series.feature.spec.ts`, parameterize the existing
+`is_series: '1'` resource test over `true`, `1`, and `'1'`, and add one
+unsupported truthy value such as `'true'` that must not issue the season
+request.
+
+- [ ] **Step 5: Run targeted Stalker tests and verify GREEN**
+
+Run:
+
+```bash
+pnpm exec jest \
+ --config libs/portal/stalker/feature/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts
+pnpm exec jest \
+ --config libs/portal/stalker/data-access/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts
+! rg -n \
+ 'is_series\s*===|String\(.*is_series|!selectedItem\.is_series' \
+ libs/portal/stalker
+```
+
+Expected: both Jest commands and the negated `rg` check pass; no raw
+series-flag decisions remain. Logging and value-preserving serialization may
+still mention `is_series`.
+
+- [ ] **Step 6: Commit the normalized flag contract**
+
+```bash
+git add \
+ libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.spec.ts \
+ libs/portal/stalker/feature/src/lib/stalker-catalog-facade.service.ts \
+ libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts \
+ libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts \
+ libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts
+git commit -m "fix(stalker): normalize catalog series flags"
+```
+
+### Task 5: Give lazy VOD-series episodes parent-scoped identities
+
+**Files:**
+- Modify: `libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts`
+- Modify: `libs/portal/stalker/data-access/src/lib/stalker-series.adapters.ts`
+- Modify: `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
+
+- [ ] **Step 1: Replace the old identity test with collision regressions**
+
+Extend the local episode metadata type:
+
+```ts
+type EpisodeWithMetadata = {
+ custom_sid?: string;
+ id?: string;
+ legacyTrackingId?: number;
+ originalId?: string;
+ originalCmd?: string;
+};
+```
+
+Update every `mapVodSeriesEpisodes` call to pass an options object. Add tests
+which prove:
+
+1. the same parent, provider episode, season, and episode number are
+ deterministic;
+2. the same season/episode in parent series `100` and `200` produces different
+ `id` values;
+3. those two episodes retain the same `legacyTrackingId`;
+4. two provider episode IDs within one parent also produce different IDs; and
+5. `mapRegularSeriesEpisodes` keeps its existing identity behavior.
+
+Representative calls:
+
+```ts
+const firstSeries = mapVodSeriesEpisodes(seasons, {
+ parentSeriesId: 100,
+ fallbackPoster: 'poster.jpg',
+});
+const secondSeries = mapVodSeriesEpisodes(seasons, {
+ parentSeriesId: 200,
+ fallbackPoster: 'poster.jpg',
+});
+```
+
+- [ ] **Step 2: Run the adapter spec and verify RED**
+
+Run:
+
+```bash
+pnpm exec jest \
+ --config libs/portal/stalker/data-access/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts
+```
+
+Expected: FAIL because the current adapter has no options object or
+`legacyTrackingId`, and identical season/episode pairs collide across parents.
+
+- [ ] **Step 3: Add an explicit mapping contract**
+
+In `stalker-series.adapters.ts`, add:
+
+```ts
+export interface MapVodSeriesEpisodesOptions {
+ parentSeriesId: string | number;
+ fallbackPoster?: string;
+}
+
+export interface StalkerMappedEpisode extends XtreamSerieEpisode {
+ legacyTrackingId?: number;
+ originalId?: string;
+ originalCmd?: string;
+}
+```
+
+Replace the VOD branch of `generateEpisodeId` with two named helpers:
+
+```ts
+function generateLegacyVodEpisodeId(
+ episodeNum: number,
+ seasonKey: string
+): number {
+ return hashString(`vod_${seasonKey}_${episodeNum}`);
+}
+
+function generateVodEpisodeId(options: {
+ parentSeriesId: string | number;
+ providerEpisodeId: string;
+ seasonKey: string;
+ episodeNum: number;
+}): number {
+ return hashString(
+ JSON.stringify([
+ 'vod',
+ String(options.parentSeriesId),
+ options.providerEpisodeId,
+ options.seasonKey,
+ options.episodeNum,
+ ])
+ );
+}
+```
+
+Keep the regular-series `generateEpisodeId` path unchanged. Change the public
+signature to:
+
+```ts
+export function mapVodSeriesEpisodes(
+ seasons: ReadonlyArray,
+ options: MapVodSeriesEpisodesOptions
+): Record
+```
+
+For every VOD episode, set:
+
+```ts
+const providerEpisodeId = String(episode.id ?? '');
+const legacyTrackingId = generateLegacyVodEpisodeId(
+ episodeNum,
+ seasonKey
+);
+const trackingId = generateVodEpisodeId({
+ parentSeriesId: options.parentSeriesId,
+ providerEpisodeId,
+ seasonKey,
+ episodeNum,
+});
+```
+
+Use `options.fallbackPoster` for the fallback artwork and expose both
+`originalId: providerEpisodeId` and `legacyTrackingId` on the mapped episode.
+
+- [ ] **Step 4: Pass the selected parent identity at the only production call**
+
+In `StalkerSeriesViewComponent.mappedSeasons`, capture `displayItem` once and
+call:
+
+```ts
+mapVodSeriesEpisodes(this.vodSeriesSeasons(), {
+ parentSeriesId: this.toSeriesId(displayItem?.id ?? 0),
+ fallbackPoster: displayItem?.info?.movie_image,
+})
+```
+
+Do not derive the parent from a season or episode field; the selected catalog
+item, normalized through the same `toSeriesId` path used by persistence, is the
+scope represented by `seriesXtreamId`.
+
+- [ ] **Step 5: Run the adapter spec and verify GREEN**
+
+Run:
+
+```bash
+pnpm exec jest \
+ --config libs/portal/stalker/data-access/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts
+```
+
+Expected: all scoped-identity and legacy-ID cases pass.
+
+Do not commit yet. Task 6 wires legacy reconciliation in the same atomic
+behavior commit so a scoped-ID build can never ship without resume
+compatibility.
+
+### Task 6: Reconcile and migrate legacy Stalker playback positions
+
+**Files:**
+- Create: `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.ts`
+- Create: `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts`
+- Create: `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.position-compatibility.spec.ts`
+- Verify: `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts`
+- Modify: `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
+- Include uncommitted Task 5 changes in:
+ `libs/portal/stalker/data-access/src/lib/stalker-series.adapters.spec.ts`
+- Include uncommitted Task 5 changes in:
+ `libs/portal/stalker/data-access/src/lib/stalker-series.adapters.ts`
+
+- [ ] **Step 1: Add failing pure reconciliation tests**
+
+Create the compatibility spec and cover these cases:
+
+1. an exact new ID wins when both new and legacy rows exist, while the
+ compatible legacy row remains available only as cleanup metadata;
+2. a legacy row from the current `seriesXtreamId` becomes an alias under the
+ new ID;
+3. a row with a different parent series is ignored;
+4. present `seasonNumber` or `episodeNumber` values must match the mapped
+ episode, while absent legacy metadata remains compatible;
+5. episodes without `legacyTrackingId` never receive an alias;
+6. saving a migrated position awaits the new save before clearing the old ID;
+7. a rejected save never clears the old row;
+8. save and clear helpers reject legacy cleanup for a different/missing
+ parent, equal old/new IDs, non-episode content, or conflicting playlist; and
+9. clearing an exact scoped row clears its confirmed legacy ID before the
+ scoped ID, after which a fresh reconciliation cannot resurrect progress;
+10. a rejected legacy clear leaves the exact scoped row untouched, while a
+ scoped-clear failure after legacy cleanup also leaves exact progress
+ available.
+
+Use fixtures with two parent series that share a legacy tracking ID. Assert
+call order with an array pushed by the repository mocks rather than relying
+only on invocation counts.
+
+- [ ] **Step 2: Run the new spec and verify RED**
+
+Run:
+
+```bash
+pnpm exec jest \
+ --config libs/portal/stalker/feature/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts
+```
+
+Expected: FAIL because the module does not exist.
+
+- [ ] **Step 3: Implement the pure compatibility boundary**
+
+Export:
+
+```ts
+export interface ReconciledStalkerSeriesPositions {
+ positionsByTrackingId: Map;
+ legacyPositionByTrackingId: Map;
+}
+
+export function reconcileStalkerSeriesPositions(options: {
+ seriesXtreamId: number;
+ episodesBySeason: Readonly<
+ Record
+ >;
+ seriesPositions: readonly PlaybackPositionData[];
+}): ReconciledStalkerSeriesPositions;
+
+export async function saveStalkerSeriesPosition(options: {
+ repository: Pick<
+ PortalPlaybackPositions,
+ 'savePlaybackPosition' | 'clearPlaybackPosition'
+ >;
+ playlistId: string;
+ position: PlaybackPositionData;
+ legacyPosition?: PlaybackPositionData;
+}): Promise;
+
+export async function clearStalkerSeriesPosition(options: {
+ repository: Pick;
+ playlistId: string;
+ position: PlaybackPositionData;
+ legacyPosition?: PlaybackPositionData;
+}): Promise;
+```
+
+`reconcileStalkerSeriesPositions` must:
+
+- index only `contentType: 'episode'` rows whose `seriesXtreamId` equals the
+ requested parent;
+- receive those rows only from
+ `getSeriesPlaybackPositions(playlistId, seriesXtreamId)`; never broaden the
+ migration with `getAllPlaybackPositions`;
+- preserve exact current-ID rows first;
+- inspect `legacyTrackingId` only on `StalkerMappedEpisode`;
+- require a present legacy `seasonNumber` and/or `episodeNumber` to equal the
+ mapped episode (`null`/`undefined` means absent; compare other runtime values
+ numerically);
+- inspect a compatible legacy row even when an exact scoped row already won;
+- record that original legacy row in `legacyPositionByTrackingId`, keyed by
+ the new tracking ID, so exact-row save/clear operations can remove it; and
+- only when no exact row exists, clone the compatible legacy row under the new
+ ID, setting the current parent, season, and episode metadata without mutating
+ the source row.
+
+Factor one private ownership predicate shared by both persistence helpers.
+`saveStalkerSeriesPosition` must always await the new save first. It clears the
+legacy row and returns `true` only when all of these hold:
+
+```ts
+position.contentType === 'episode'
+legacyPosition?.contentType === 'episode'
+position.contentXtreamId !== legacyPosition.contentXtreamId
+position.seriesXtreamId != null
+legacyPosition.seriesXtreamId != null
+position.seriesXtreamId === legacyPosition.seriesXtreamId
+(!position.playlistId || position.playlistId === playlistId)
+(!legacyPosition.playlistId || legacyPosition.playlistId === playlistId)
+```
+
+Otherwise it returns `false` after saving the new row. Let save/clear failures
+reject so callers cannot mistake a partial migration for success.
+
+When the same ownership predicate confirms a distinct legacy row,
+`clearStalkerSeriesPosition` must clear the legacy ID first and the scoped
+`position.contentXtreamId` second, then return `true`. This ordering is
+load-bearing: if legacy cleanup fails, the exact row remains; if scoped cleanup
+then fails, the exact row still represents the uncleared state and the legacy
+row cannot resurrect. Without a confirmed legacy row, clear only the scoped ID
+and return `false`. Tests should back repository rows with a `Map`, remove rows
+in the mock, and rerun reconciliation after a dual clear.
+
+- [ ] **Step 4: Run the pure compatibility spec and verify GREEN**
+
+Run:
+
+```bash
+pnpm exec jest \
+ --config libs/portal/stalker/feature/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts
+```
+
+Expected: all reconciliation, guard, and save-before-clear tests pass.
+
+- [ ] **Step 5: Add failing component integration regressions in a focused spec**
+
+Create `stalker-series-view.position-compatibility.spec.ts` with the smallest
+TestBed harness needed for position loading, lazy episodes, time updates, and
+toggle requests. Do not append these cases to
+`stalker-series-view.component.spec.ts`: it is already close to the 1200-line
+test ceiling. Prove:
+
+- positions returned before lazy episodes load are reconciled after
+ `loadEpisodesForSeason` populates the season;
+- an exact scoped position beats a compatible legacy row but retains that
+ legacy row as cleanup metadata;
+- a slow response for series A cannot overwrite positions after selection
+ changes to series B;
+- inline time updates and watched toggles save the scoped ID and then clear a
+ confirmed legacy ID;
+- after an exact or migrated save, an effect/reconciliation rerun does not
+ restore the removed legacy alias from raw rows;
+- clearing a scoped watched state also clears its confirmed legacy row, and a
+ reload/reconciliation does not make the old progress reappear; and
+- no legacy clear occurs for regular series or an unconfirmed legacy alias.
+
+Use deferred promises for the stale-response case. Instantiate the real
+component with its injected repository/store mocks so the signals and effects,
+not only the pure helper, are exercised.
+
+- [ ] **Step 6: Run the component spec and verify RED**
+
+Run:
+
+```bash
+pnpm exec jest \
+ --config libs/portal/stalker/feature/jest.config.ts \
+ --runInBand \
+ libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.position-compatibility.spec.ts
+```
+
+Expected: the lazy reconciliation, stale-response, and migration assertions
+fail against the current single map/direct-save implementation.
+
+- [ ] **Step 7: Store raw rows separately and reconcile whenever episodes change**
+
+In `StalkerSeriesViewComponent`, add:
+
+```ts
+private readonly rawSeriesPositions =
+ signal([]);
+private readonly legacyPositionByTrackingId =
+ signal