Skip to content

feat(creator): add notebook-backed youtube control review - #884

Merged
POWERFULMOVES merged 12 commits into
PMOVES.AI-Edition-Hardenedfrom
feat/pmoves-yt-integration-review
Mar 12, 2026
Merged

POWERFULMOVES merged 12 commits into
PMOVES.AI-Edition-Hardenedfrom
feat/pmoves-yt-integration-review

Conversation

@POWERFULMOVES

@POWERFULMOVES POWERFULMOVES commented Mar 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • extend the creator-control lane with notebook-backed YouTube review artifacts
  • add richer approval summaries in channel-monitor and messaging-gateway
  • bump PMOVES.YT to the new control-plane commit with playlist remove/reorder support

What Changed

  • queue YouTube control requests with draft/template metadata, notebook publication metadata, and human-readable summaries
  • publish optional Open Notebook artifacts for queued creator actions when notebook credentials are configured
  • enrich Discord review prompts and post-click confirmations with action summaries and notebook entry ids
  • expand PMOVES.YT control endpoints to cover playlist add/remove/reorder plus comment creation

Testing

  • python -m pytest -q PMOVES.YT/pmoves_yt_service/tests/test_youtube_control.py
  • python -m pytest -q pmoves/services/channel-monitor/tests/test_monitor.py -k "youtube_control"
  • python -m pytest -q pmoves/services/messaging-gateway/test_main.py
  • ruff check PMOVES.YT/pmoves_yt_service/yt.py PMOVES.YT/pmoves_yt_service/youtube_control.py PMOVES.YT/pmoves_yt_service/tests/test_youtube_control.py pmoves/services/channel-monitor/channel_monitor/main.py pmoves/services/channel-monitor/channel_monitor/monitor.py pmoves/services/channel-monitor/tests/test_monitor.py pmoves/services/messaging-gateway/main.py pmoves/services/messaging-gateway/test_main.py
  • python -m py_compile PMOVES.YT/pmoves_yt_service/yt.py PMOVES.YT/pmoves_yt_service/youtube_control.py pmoves/services/channel-monitor/channel_monitor/main.py pmoves/services/channel-monitor/channel_monitor/monitor.py pmoves/services/messaging-gateway/main.py

Reviewer Notes

  • This builds on the existing creator-control worktree lane and keeps notebook context explicit in the approval path.
  • Open Notebook publishing is best-effort and only activates when OPEN_NOTEBOOK_API_URL, OPEN_NOTEBOOK_API_TOKEN, and CHANNEL_MONITOR_YT_NOTEBOOK_ID are configured.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added YouTube control workflow to queue, review, and approve channel actions with notifications integration
    • Introduced channel source classification system to organize and categorize channels by ownership status
  • Documentation

    • Added comprehensive operator guides and worktree review documentation for creator network operations
    • Expanded agent documentation with new traversal paths and governance references
  • Chores

    • Updated CI/CD to properly initialize submodules during testing

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2384adfa-9ddf-4b74-b88a-f3be0f9c67dc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces YouTube control action management to the PMOVES platform. It extends the channel-monitor service with new request queueing and review workflows, adds a database schema for action persistence with row-level security, integrates Discord interaction handling in the messaging-gateway, and expands documentation with creator-focused traversal guides and a new worktree review runbook for PMOVES.YT integration testing.

Changes

Cohort / File(s) Summary
YouTube Control Action API & Storage
pmoves/services/channel-monitor/channel_monitor/main.py, pmoves/services/channel-monitor/channel_monitor/monitor.py, pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql
Introduces YouTubeControlRequest and YouTubeControlReviewRequest models; adds endpoints for queue/list/review YouTube control actions; implements lifecycle management including validation, notification, notebook publishing, and action execution; creates new pmoves_core schema with youtube_control_actions table, indexes, and RLS policies.
Channel Configuration & Metadata
pmoves/config/channel_monitor.json, pmoves/services/channel-monitor/channel_monitor/config.py
Adds source_class field to channel and playlist entries with values "owned" and "watched"; extends AddChannelRequest and DiscordDropRequest models with source_class parameter.
Channel-Monitor Service Integration
pmoves/services/channel-monitor/tests/test_monitor.py, pmoves/services/channel-monitor/README.md
Extends test scaffolding with async context helpers and YouTube control flow tests covering request persistence, validation, notification, template rendering, and approval/rejection workflows; documents new source_class field, control endpoints, payload structures, and Discord integration patterns.
Messaging Gateway YouTube Control Handler
pmoves/services/messaging-gateway/main.py, pmoves/services/messaging-gateway/test_main.py
Adds _handle_ytcontrol_interaction to process Discord interactions with approve/reject actions; forwards payloads to channel-monitor via HTTP; new test module mocks AsyncClient to verify interaction routing and response formatting.
Creator Workflow Documentation
pmoves/docs/AGENTS/CODEX_OPERATOR_HOME.md, pmoves/docs/AGENTS/README.md, pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-Creator.md, pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-Open-Notebook.md
Reorganizes and expands creator-network fast path with new submodule home references; adds Traversal role and Operator prompts sections; integrates CATACLYSM_STUDIOS_INC governance context; broadens Read alongside and Service posture coverage.
PMOVES.YT Worktree Review Runbook
pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md
New documentation defining dedicated worktree workflow for isolated PMOVES.YT integration review with setup steps, multi-lane traversal (creator strategy, runtime, channel-monitor, transcripts, playback, outreach, routing), health checks, audit snapshots, and escalation paths.
PMOVES.YT Integration Guidance
pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES.YT.md, pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-transcribe-and-fetch.md, pmoves/docs/NEXT_STEPS.md, pmoves/docs/PMOVES.AI PLANS/ROADMAP.md
Adds references to PMOVES_YT_CONTROL_WORKTREE_REVIEW.md in traversal guidance; clarifies that creator fast path now explicitly routes through strategy, notebook, and governance contexts; updates roadmap with creator-control routing.
Infrastructure & Runtime Updates
.github/workflows/python-tests.yml, pmoves/services/pmoves-yt/yt.py, PMOVES.YT (submodule reference)
CI workflow now recursively initializes git submodules; PMOVES.YT shim module adds package metadata and yt_dlp fallback initialization; PMOVES.YT submodule commit updated.

Sequence Diagram

sequenceDiagram
    participant Discord as Discord User
    participant MG as Messaging Gateway
    participant CM as Channel Monitor
    participant DB as Database
    participant YTC as YouTube Control<br/>(External)
    
    Discord->>MG: Interaction (approve/reject ytcontrol)
    MG->>MG: _handle_ytcontrol_interaction<br/>Extract actor & action
    MG->>CM: POST /api/monitor/youtube-control/review<br/>(action_ids, approve, actor)
    CM->>CM: review_youtube_control_actions<br/>Validate request & action
    alt Approve
        CM->>DB: Update status → approved<br/>Set approved_by, approval_note
        CM->>YTC: _invoke_yt_control_action<br/>(action, details)
        YTC-->>CM: Result (success/error)
        CM->>DB: Update result & error fields
    else Reject
        CM->>DB: Update status → rejected<br/>Set approved_by, approval_note
    end
    CM-->>MG: Review results
    MG-->>Discord: Interaction response<br/>(approval/rejection confirmation)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 With whiskers twitching and a hop so grand,
We've woven control through the YouTube land,
From Discord approvals to databases deep,
Creator workflows now flow through—no sheep!
Workflows dance, reviews align,
A multifaceted platform design!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provided covers Summary and Testing sections required by template but is missing the Required Checks checklist, Review Coordination section, and optional Follow-up Tasks section. Complete the pull request description by adding the Required Checks checklist with proper status, Review Coordination section, and any Follow-up Tasks to meet template requirements.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding notebook-backed YouTube control review functionality for creator operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pmoves-yt-integration-review
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

@codex review\n@coderabbitai review\n@kilocode review\n\nFocused review asks:\n- notebook-backed creator-control request flow in channel-monitor\n- Discord approval/rejection UX and summary payloads\n- PMOVES.YT control-plane surface for playlist add/remove/reorder + comment actions

@POWERFULMOVES POWERFULMOVES added codex python Pull requests that update python code labels Mar 12, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PMOVES.YT youtube_control.py ==="
cat PMOVES.YT/pmoves_yt_service/youtube_control.py

echo ""
echo "=== PMOVES.YT yt.py ==="
cat PMOVES.YT/pmoves_yt_service/yt.py

echo ""
echo "=== channel-monitor monitor.py ==="
cat pmoves/services/channel-monitor/channel_monitor/monitor.py

echo ""
echo "=== channel-monitor main.py ==="
cat pmoves/services/channel-monitor/channel_monitor/main.py

echo ""
echo "=== messaging-gateway main.py ==="
cat pmoves/services/messaging-gateway/main.py

Length of output: 148912


@POWERFULMOVES — full focused review below, covering all three areas.


1. Notebook-backed creator-control request flow (channel_monitor/monitor.py)

🔴 Critical: Execution failure silently leaves action in pending_review

In review_youtube_control_actions, if _invoke_yt_control_action raises (network error, HTTP 4xx/5xx), the exception propagates out of the loop and the UPDATE statement is never reached. The row remains stuck at pending_review indefinitely with no error recorded.

# review_youtube_control_actions – vulnerable block
if approve:
    result_payload = await self._invoke_yt_control_action(  # ← raises here
        action=action,
        details=details,
        approved_by=actor or "channel-monitor",
        approval_note=reason,
    )
    new_status = "approved"
    error_text = None

async with self._pool.acquire() as conn:          # ← never reached on exception
    await conn.execute("UPDATE ...", ...)

Suggested fix — catch and record execution failures:

if approve:
    try:
        result_payload = await self._invoke_yt_control_action(
            action=action,
            details=details,
            approved_by=actor or "channel-monitor",
            approval_note=reason,
        )
        new_status = "approved"
        error_text = None
    except Exception as exc:
        new_status = "failed"
        error_text = str(exc)
        result_payload = {"status": "failed", "error": str(exc)}
        LOGGER.error(
            "YouTube control execution failed for %s (%s): %s",
            action_id, action, exc
        )

🔴 Critical: Race condition — no row-level locking on approval

review_youtube_control_actions fetches rows with a plain SELECT ... WHERE status = 'pending_review', then updates them in a separate connection acquisition. Two concurrent review calls (e.g., two Discord button clicks in quick succession) can both fetch the same row and both invoke _invoke_yt_control_action, executing the YouTube control action twice.

# Two concurrent callers both see status='pending_review'
async with self._pool.acquire() as conn:
    rows = await conn.fetch(
        "SELECT ... FROM pmoves_core.youtube_control_actions WHERE status = 'pending_review' AND id = ANY(...)"
    )

Fix — use SELECT ... FOR UPDATE SKIP LOCKED or do an atomic UPDATE ... WHERE status='pending_review' RETURNING *:

UPDATE pmoves_core.youtube_control_actions
SET status = 'processing'
WHERE status = 'pending_review' AND id = ANY($1::uuid[])
RETURNING id, action, details

Fetch only rows you successfully transitioned to processing, then run execution and set final status.


🟡 Major: Invalid action strings are persisted to DB, only fail at execution time

create_youtube_control_request does not validate action against YOUTUBE_CONTROL_ACTION_LABELS before the DB INSERT. _prepare_youtube_control_details only raises for bad text in comment_create; _build_youtube_control_execution_payload raises ValueError for unknown actions, but that code runs in _invoke_yt_control_action — only at review time, not queue time. The row sits in pending_review with an unknown action permanently.

# monitor.py – add early validation
VALID_ACTIONS = set(YOUTUBE_CONTROL_ACTION_LABELS)

async def create_youtube_control_request(self, *, action: str, ...):
    if action not in VALID_ACTIONS:
        raise ValueError(
            f"Unsupported YouTube control action: {action!r}. "
            f"Valid actions: {sorted(VALID_ACTIONS)}"
        )
    ...

🟡 Major: Fragile _yt_control_base_url suffix stripping

def _yt_control_base_url(self) -> str:
    base = self.queue_url.rstrip("/")
    if base.endswith("/yt/ingest"):
        return base[: -len("/yt/ingest")]
    if base.endswith("/yt"):
        return base
    return base  # ← falls through with full path intact

If queue_url is http://pmoves-yt:8077/yt/ingest/v2 or any non-standard path, the control URL will be wrong. Consider exposing a dedicated CHANNEL_MONITOR_YT_CONTROL_URL env var instead of deriving it:

def _yt_control_base_url(self) -> str:
    explicit = (os.getenv("CHANNEL_MONITOR_YT_CONTROL_URL") or "").rstrip("/")
    if explicit:
        return explicit
    # fallback derivation (kept for backward compat)
    base = self.queue_url.rstrip("/")
    if base.endswith("/yt/ingest"):
        return base[: -len("/yt/ingest")]
    return base

🟢 Good: Notebook error state is correctly surfaced

When _publish_youtube_control_notebook_artifact fails, it returns _compact({"error": ..., "title": ..., "notebook_id": ...}) (non-None), which gets stored in details["notebook"]. Downstream, _notify_youtube_control_request distinguishes entry_id vs error and renders the appropriate Discord field. This is clean, intentional behaviour — worth documenting explicitly in the method's docstring.


🟢 Good: Notebook credentials are strictly opt-in

The three-env-var guard (OPEN_NOTEBOOK_API_URL, OPEN_NOTEBOOK_API_TOKEN, CHANNEL_MONITOR_YT_NOTEBOOK_ID) early-returning None is correct — no partial execution risk.


2. Discord approval/rejection UX and summary payloads (messaging-gateway/main.py)

🟡 Major: _invoke_yt_control_action drops response body on HTTP error

response.raise_for_status()
return response.json()

On a 4xx/5xx from PMOVES.YT, raise_for_status() propagates httpx.HTTPStatusError but discards the response body. This makes failures nearly impossible to debug in production logs. Capture it:

try:
    response.raise_for_status()
except httpx.HTTPStatusError as exc:
    raise RuntimeError(
        f"PMOVES.YT returned {exc.response.status_code}: "
        f"{exc.response.text[:500]}"
    ) from exc

🟡 Observation: Hardcoded rejection reason in Discord interactions

review_payload = {
    ...
    "reason": "approved from Discord" if approve else "rejected from Discord",
}

These canned strings always appear in _format_ytcontrol_response's Note: line. Since Discord button interactions have no text input step, this is expected — but consider either omitting the reason field entirely for button-triggered flows, or only rendering the Note: line when reason is non-trivial.


🟡 Observation: Full details dict included in notification metadata

payload = {
    ...
    "metadata": {
        ...
        "details": details,   # ← could be large; includes full draft, text content
    },
}

The details object may contain rendered comment text, draft variables, and notebook metadata. This is forwarded verbatim to the messaging gateway and stored in Discord message metadata. Consider trimming to a safe summary subset (action_id, action, summary, notebook_entry_id) before including in the notification payload.


🟢 Good: Ephemeral Discord responses (flags: 64)

All button interaction responses use "flags": 64 (ephemeral), which is correct — only the clicking user sees the confirmation, avoiding channel noise.


🟢 Good: _format_ytcontrol_response surfaces notebook entry ID

Surfacing notebook_entry_id in the Discord confirmation is excellent UX — reviewers can navigate directly to the notebook context after actioning.


🟢 Good: Signature verification before payload parsing

discord_webhook calls discord_platform.verify_signature(...) before await request.json(), which is the correct order — avoids parsing untrusted input before authentication.


3. PMOVES.YT control-plane surface

The PMOVES.YT/pmoves_yt_service/youtube_control.py and yt.py files were not present in the repository clone at the time of this review (likely under a submodule or separate worktree). The review below is based on the call sites in monitor.py.

Endpoint mapping and payload shapes

Action Endpoint Execution payload keys
playlist_add /yt/control/playlist/add playlist_id✱, video_id✱, position, execute, approved_by
playlist_remove /yt/control/playlist/remove playlist_item_id✱, playlist_id, video_id, execute, approved_by
playlist_reorder /yt/control/playlist/reorder playlist_item_id✱, playlist_id, video_id, position✱, execute, approved_by
comment_create /yt/control/comment video_id✱, text✱, parent_comment_id, execute, approved_by

✱ = logically required

🟡 Major: No upfront validation of required fields before DB persist

_build_youtube_control_execution_payload uses _compact(...) which strips None values, and falls back to {} if all are None. This means a playlist_add request missing both playlist_id and video_id is accepted and stored, only to fail silently at execution time against PMOVES.YT.

Add field guards in _prepare_youtube_control_details:

if action == "playlist_add":
    if not details.get("playlist_id") or not details.get("video_id"):
        raise ValueError("playlist_add requires playlist_id and video_id")
elif action == "playlist_remove":
    if not details.get("playlist_item_id"):
        raise ValueError("playlist_remove requires playlist_item_id")
elif action == "playlist_reorder":
    if not details.get("playlist_item_id") or details.get("position") is None:
        raise ValueError("playlist_reorder requires playlist_item_id and position")

Minor / Nits

  • No Prometheus metrics for YT control actions. DISCORD_DROP_REVIEWS_TOTAL has parallels for Discord drops; a YOUTUBE_CONTROL_TOTAL counter with action and result labels would make this observable.
  • asyncio.wait([self._shutdown.wait()], ...) in _channel_loop passes a bare coroutine to asyncio.wait, which is deprecated since Python 3.8 and raises a DeprecationWarning. Wrap with asyncio.ensure_future(self._shutdown.wait()) or use asyncio.wait_for / asyncio.sleep with Event polling. (Pre-existing, not introduced in this PR.)
  • YouTubeControlRequest model has no action field validator. Adding @validator("action") to check membership in YOUTUBE_CONTROL_ACTION_LABELS at the API boundary would fail fast with a clear 422 rather than a 400 from deep in the call stack.

Summary

# Severity Area Issue
1 🔴 Critical monitor Execution failure leaves action stuck in pending_review (no error capture)
2 🔴 Critical monitor Race condition — no SELECT FOR UPDATE before concurrent approval
3 🟡 Major monitor Invalid actions accepted at queue time, fail only at execution
4 🟡 Major monitor _yt_control_base_url fragile suffix stripping
5 🟡 Major monitor No required-field validation for playlist/comment payloads before DB persist
6 🟡 Major messaging-gateway HTTP error response body discarded in _invoke_yt_control_action
7 🟡 Minor messaging-gateway Full details in notification metadata; consider trimming
8 🟢 Good messaging-gateway Ephemeral responses, notebook entry surfaced in confirmation
9 🟢 Good monitor Notebook opt-in guard and error-state propagation both correct
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

Addressed the review findings in the creator-control lane.

Changes pushed in 4e5a8ddb:

  • claim pending YouTube control rows before review execution to avoid double-processing
  • persist failed execution attempts as failed with recorded error/result instead of leaving rows stuck in pending_review
  • validate action names and required fields at queue time
  • trim Discord notification metadata and preserve richer upstream error detail
  • bumped PMOVES.YT to include the follow-on Windows-safe core CI diff probe fix

Validation run locally:

  • python -m pytest -q pmoves/services/channel-monitor/tests/test_monitor.py -k "youtube_control"
  • python -m pytest -q pmoves/services/messaging-gateway/test_main.py
  • ruff check pmoves/services/channel-monitor/channel_monitor/main.py pmoves/services/channel-monitor/channel_monitor/monitor.py pmoves/services/channel-monitor/tests/test_monitor.py pmoves/services/messaging-gateway/main.py pmoves/services/messaging-gateway/test_main.py
  • python -m py_compile pmoves/services/channel-monitor/channel_monitor/main.py pmoves/services/channel-monitor/channel_monitor/monitor.py pmoves/services/channel-monitor/tests/test_monitor.py pmoves/services/messaging-gateway/main.py pmoves/services/messaging-gateway/test_main.py

@coderabbitai review
@kilocode review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pmoves/services/channel-monitor/channel_monitor/config.py (1)

31-38: ⚠️ Potential issue | 🟠 Major

Derive source_class from source_type instead of hardcoding the channel default.

This default is merged into all entries via DEFAULT_CONFIG["channels"][0], so older configs with playlist rows that don't yet have source_class will now be silently classified as "watched" instead of "owned".

🐛 Proposed fix
@@
     for channel in merged.get("channels", []):
         merged_channel = DEFAULT_CONFIG["channels"][0].copy()
         merged_channel.update(channel)
+        if not merged_channel.get("source_class"):
+            merged_channel["source_class"] = (
+                "owned" if merged_channel.get("source_type") == "playlist" else "watched"
+            )
         if merged_channel.get("channel_metadata_fields") is None:
             merged_channel["channel_metadata_fields"] = None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/config.py` around lines 31 -
38, DEFAULT_CONFIG currently hardcodes "source_class" in
DEFAULT_CONFIG["channels"][0], causing entries missing source_class (e.g., old
playlist rows) to inherit "watched"; remove the hardcoded "source_class" from
the default channel entry and update the merge/normalize logic that applies
DEFAULT_CONFIG["channels"][0] to per-row data so that when source_class is
missing you set it based on source_type (e.g., if source_type == "playlist" ->
"owned", otherwise -> "watched"); reference DEFAULT_CONFIG and the keys
source_class and source_type when making these changes.
pmoves/docs/PMOVES.AI PLANS/JELLYFIN_YOUTUBE_INTEGRATION.md (1)

152-167: ⚠️ Potential issue | 🟡 Minor

Setup instructions reference deprecated MCP YouTube Adapter.

The status update at lines 7-10 states that the MCP YouTube Adapter is "historical unless you explicitly choose to run that extra service," but Step 3 (lines 152-167) still instructs users to start the MCP YouTube Adapter via uvicorn. This creates confusion about which path to follow.

Consider updating Step 3 to reference the PMOVES.YT runtime validation from lines 65-71, or adding a note clarifying when users should follow the legacy MCP adapter path vs. the authoritative PMOVES.YT runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/docs/PMOVES.AI` PLANS/JELLYFIN_YOUTUBE_INTEGRATION.md around lines 152
- 167, Update Step 3 to remove the default instruction to start the legacy MCP
YouTube Adapter via the uvicorn command (services.mcp_youtube_adapter:app) and
instead point users to the PMOVES.YT runtime validation path (referencing the
PMOVES.YT runtime validation described earlier) as the authoritative option;
alternatively add a short clarifying note in Step 3 that the
uvicorn/mcp_youtube_adapter start is only required if users explicitly opt into
the historical MCP YouTube Adapter, and show when to choose the legacy adapter
versus PMOVES.YT runtime validation so readers are not confused.
🧹 Nitpick comments (7)
pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql (1)

23-24: Index review-queue lookups by status first.

The new flow lists/reviews pending actions newest-first. (action, status) will not help a WHERE status = ... ORDER BY created_at DESC query, so this will get more expensive as the audit table grows.

Suggested index
 create index if not exists idx_youtube_control_actions_action_status
   on pmoves_core.youtube_control_actions (action, status);
+
+create index if not exists idx_youtube_control_actions_status_created_at
+  on pmoves_core.youtube_control_actions (status, created_at desc);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql` around
lines 23 - 24, The current index idx_youtube_control_actions_action_status on
pmoves_core.youtube_control_actions is ordered (action, status) which doesn't
support queries like WHERE status = ... ORDER BY created_at DESC efficiently;
add or replace with an index that begins with status and includes created_at
(descending) and action (e.g., (status, created_at DESC, action)) so
review-queue lookups (filter by status and order by created_at DESC) and any
tie-breakers on action are covered; update the migration to create the new index
name and drop or no-op the old idx_youtube_control_actions_action_status if
desired.
pmoves/services/messaging-gateway/main.py (1)

68-69: Consider using *_FILE secret loading pattern for production hardening.

Per the coding guidelines, focus services should resolve critical secrets through Docker/K8s-style file mounts via services/common/env.py. The CHANNEL_MONITOR_SECRET is loaded directly from os.environ.get, which works but doesn't follow the preferred secret hardening convention.

♻️ Example using *_FILE pattern
+from services.common.env import get_secret
+
 # Environment configuration
 NATS_URL = os.environ.get("NATS_URL", "nats://nats:pmoves@nats:4222")
 ...
 CHANNEL_MONITOR_URL = os.environ.get("CHANNEL_MONITOR_URL", "http://channel-monitor:8097")
-CHANNEL_MONITOR_SECRET = os.environ.get("CHANNEL_MONITOR_SECRET", "")
+CHANNEL_MONITOR_SECRET = get_secret("CHANNEL_MONITOR_SECRET", "")

As per coding guidelines, "Focus services now resolve critical secrets through Docker/K8s-style file mounts (services/common/env.py), and the audit gate now blocks regressions to direct os.getenv secret reads."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/messaging-gateway/main.py` around lines 68 - 69, Replace the
direct os.environ.get usage for CHANNEL_MONITOR_SECRET with the project *_FILE
secret-loading pattern used by services/common/env.py: use the helper (or the
same logic) to check CHANNEL_MONITOR_SECRET_FILE first and read the file
contents, falling back to CHANNEL_MONITOR_SECRET env var only if the file isn't
present; leave CHANNEL_MONITOR_URL behavior unchanged but ensure you reference
CHANNEL_MONITOR_SECRET by name in the updated code so the audit gate recognizes
the hardened secret loading.
pmoves/services/messaging-gateway/test_main.py (1)

112-151: Consider adding error path test coverage.

The approval and rejection happy paths are well tested. Consider adding tests for:

  • HTTP error responses from channel monitor
  • Invalid custom_id formats (missing parts, wrong prefix)
  • Network failures (httpx exceptions)
💡 Example error path test
def test_handle_ytcontrol_interaction_http_error(monkeypatch):
    module = _load_main_module()

    class DummyResponse:
        status_code = 500
        def raise_for_status(self):
            raise module.httpx.HTTPStatusError("Server Error", request=None, response=self)

    class DummyAsyncClient:
        def __init__(self, *args, **kwargs):
            pass
        async def __aenter__(self):
            return self
        async def __aexit__(self, exc_type, exc, tb):
            return False
        async def post(self, url, headers=None, json=None):
            return DummyResponse()

    monkeypatch.setattr(module.httpx, "AsyncClient", DummyAsyncClient)

    payload = {
        "type": 3,
        "data": {"custom_id": "ytcontrol:approve:test-id"},
        "user": {"id": "99"},
    }

    response = module.asyncio.run(module._handle_ytcontrol_interaction(payload))

    assert response["type"] == 4
    assert "failed" in response["data"]["content"].lower()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/messaging-gateway/test_main.py` around lines 112 - 151, Add
error-path tests for _handle_ytcontrol_interaction by creating new test
functions (e.g., test_handle_ytcontrol_interaction_http_error,
test_handle_ytcontrol_interaction_invalid_custom_id,
test_handle_ytcontrol_interaction_network_failure) that mirror the existing test
style: monkeypatch module.httpx.AsyncClient with a DummyAsyncClient and
DummyResponse, but simulate failure modes — have DummyResponse.raise_for_status
raise module.httpx.HTTPStatusError to cover HTTP errors, provide payloads with
malformed custom_id (missing parts or wrong prefix) to assert the function
returns a user-facing failure message, and have DummyAsyncClient.post raise
module.httpx.ConnectError (or similar) to test network failures; use
module.asyncio.run(module._handle_ytcontrol_interaction(payload)) and assert
response["type"] == 4 and that the content indicates the failure.
pmoves/services/channel-monitor/tests/test_monitor.py (1)

623-677: Test does not cover execution failure scenarios.

The PR comments identified a critical issue: exceptions during execution in review_youtube_control_actions can leave rows stuck in "pending_review" status. This test only covers the happy path where execution succeeds. Consider adding a test for execution failure to verify error handling and status updates.

Do you want me to generate a test case for execution failure scenarios?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/tests/test_monitor.py` around lines 623 -
677, Add a new test alongside
test_review_youtube_control_actions_executes_pmoves_yt_call that simulates an
HTTP execution failure for monitor.review_youtube_control_actions: create a
DummyAsyncClient whose post method raises an exception (or returns a response
whose raise_for_status raises), set CHANNEL_MONITOR_YT_API_KEY and monkeypatch
channel_monitor.monitor.httpx.AsyncClient, and use a fake DB connection (as in
the existing test) to assert the function marks the action as processed with an
error (result shows failed/errored), increments processed count, and calls
conn.execute to update the row status from "pending_review" to the appropriate
failed state; reference monitor.review_youtube_control_actions, the test
function name, DummyAsyncClient, and conn.execute for locating the changes.
pmoves/services/channel-monitor/channel_monitor/main.py (1)

644-691: Consider adding Prometheus metrics for YouTube control actions.

The existing Discord drop endpoints have corresponding metrics (DISCORD_DROPS_TOTAL, DISCORD_DROP_REVIEWS_TOTAL). For observability parity, consider adding metrics for YouTube control actions to track queue, approval, and rejection rates.

📊 Proposed metrics additions
YOUTUBE_CONTROL_TOTAL = Counter(
    "channel_monitor_youtube_control_total",
    "Total number of YouTube control requests queued",
    labelnames=("action", "result"),
)

YOUTUBE_CONTROL_REVIEWS_TOTAL = Counter(
    "channel_monitor_youtube_control_reviews_total",
    "Total number of YouTube control review actions",
    labelnames=("action", "decision"),  # decision: approved/rejected
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/main.py` around lines 644 -
691, Add Prometheus counters for YouTube control parity with Discord drops and
increment them in the existing endpoints: declare YOUTUBE_CONTROL_TOTAL (labels:
action, result) and YOUTUBE_CONTROL_REVIEWS_TOTAL (labels: action, decision)
alongside the other metrics; in queue_youtube_control_action, after successful
monitor.create_youtube_control_request increment YOUTUBE_CONTROL_TOTAL with
action=payload.action and result="queued" (or "draft" if payload.draft is true)
and on the ValueError path increment with result="error"; in
review_youtube_control_actions increment YOUTUBE_CONTROL_REVIEWS_TOTAL for each
reviewed action with action equal to the action type and decision "approved" or
"rejected" based on payload.approve; place metric declarations near existing
metric definitions and import/instrument them the same way as
DISCORD_DROPS_TOTAL and DISCORD_DROP_REVIEWS_TOTAL.
pmoves/services/channel-monitor/channel_monitor/monitor.py (2)

1976-1982: Consider trimming metadata payload sent to messaging gateway.

The full details dict (including drafts, rendered text, notebook metadata) is included in notification metadata. As noted in PR comments, this could be verbose. Consider trimming to a concise summary for the notification payload.

✂️ Proposed trimmed metadata
         payload = {
             "platforms": active_platforms,
             "content": content,
             "embeds": [...],
             "buttons": buttons,
             "metadata": {
                 "action_id": action_id,
                 "action": action,
                 "request_source": request_source,
-                "details": details,
                 "summary": summary,
+                "notebook_entry_id": notebook_meta.get("entry_id"),
             },
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py` around lines 1976
- 1982, The metadata currently embeds the full details dict (in the block
building "metadata" with keys action_id, action, request_source, details,
summary) which can be very verbose; replace the raw details value with a trimmed
summary by implementing a small sanitizer (e.g., trim_notification_details or
summarize_details) that extracts only essential fields
(title/user/id/timestamps/concise change text or a generated short summary) and
explicitly omits heavy fields like drafts, rendered_text, notebook metadata;
then update the metadata construction to use the trimmed result (e.g.,
details_summary) instead of the full details object so notifications sent from
monitor.py contain a compact payload.

2063-2068: API key loading doesn't support *_FILE secret paths.

Per coding guidelines, prefer central env helpers and *_FILE secret loading paths for hardened secret handling. The current implementation reads API keys directly from environment variables without supporting file-based secrets.

🔐 Proposed enhancement for secret file support
+def _load_secret(env_var: str, file_env_var: str | None = None) -> str:
+    """Load secret from env var or file path."""
+    if file_env_var:
+        file_path = os.getenv(file_env_var, "").strip()
+        if file_path and os.path.isfile(file_path):
+            with open(file_path) as f:
+                return f.read().strip()
+    return (os.getenv(env_var) or "").strip()

     async def _invoke_yt_control_action(
         # ...
     ) -> Dict[str, Any]:
         # ...
         headers: Dict[str, str] = {}
-        api_key = (
-            os.getenv("CHANNEL_MONITOR_YT_API_KEY")
-            or os.getenv("NEXT_PUBLIC_BACKEND_API_KEY")
-            or os.getenv("BACKEND_API_KEY")
-            or ""
-        ).strip()
+        api_key = (
+            _load_secret("CHANNEL_MONITOR_YT_API_KEY", "CHANNEL_MONITOR_YT_API_KEY_FILE")
+            or _load_secret("BACKEND_API_KEY", "BACKEND_API_KEY_FILE")
+        )

As per coding guidelines, "Prefer central env helpers and *_FILE secret loading paths."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py` around lines 2063
- 2068, Replace the direct os.getenv chain that assigns api_key in monitor.py
with the project’s central env helper that supports *_FILE secret paths;
specifically, ensure you resolve CHANNEL_MONITOR_YT_API_KEY,
NEXT_PUBLIC_BACKEND_API_KEY, and BACKEND_API_KEY via the helper (or implement a
small helper like get_secret_from_env_or_file(var)) which checks VAR_FILE first,
reads the file contents if present, falls back to VAR, and returns a trimmed
string, then assign that result to api_key (preserve the .strip() behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@PMOVES-transcribe-and-fetch`:
- Line 1: The PMOVES-transcribe-and-fetch submodule update references a
non-existent commit SHA (a4968929b22f5f2625836aa9a1dfdcf6ba3e5459); verify the
SHA in the submodule repo and either push the missing commit to that remote or
update the submodule reference to a valid commit: checkout the
PMOVES-transcribe-and-fetch repository, confirm the correct commit SHA (or
create and push the intended commit), then in the superproject update the
submodule to that SHA (or run git submodule update --init --remote), commit the
updated submodule reference in the superproject, and push the change so the PR
points to an existing commit.

In `@pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md`:
- Around line 184-187: The sentence claiming "comment/reply actions" for the
first owned-channel YouTube Data API slice overstates capabilities; update the
PMOVES.YT description so it matches the supported-action list (`playlist_add`,
`playlist_remove`, `playlist_reorder`, `comment_create`) by removing "reply" or
explicitly stating only `comment_create` is supported (e.g., "comment create
only"), and ensure the phrase "comment/reply actions" is replaced wherever
present with the accurate term to keep the runbook and snapshot consistent.

In `@pmoves/docs/PMOVES.AI` PLANS/JELLYFIN_YOUTUBE_STATUS.md:
- Around line 5-10: The document's "Current Status" contradicts the quickstart:
it claims PMOVES.YT is the authoritative runtime and that POST /yt/search and
pmoves/scripts/backfill_jellyfin_metadata.py already implement the backfill, yet
the quickstart still instructs implementing that work; update the page so the
quickstart and status align by either (A) rewriting the quickstart into
validation/runbook steps that confirm POST /yt/search and the backfill script
behavior (e.g., tests to call POST /yt/search, expected responses, and how to
enable --link-youtube in pmoves/scripts/backfill_jellyfin_metadata.py), or (B)
moving the historical implementation notes into a clearly labeled "Historical
Appendix" and leaving the operational status and runbook/smoke-test instructions
in the main body; also ensure the status claims reference existing evidence
(PMOVES.YT runtime, POST /yt/search, and backfill script) and update any
runbook/smoke-test links mentioned in the doc.

In `@pmoves/docs/PMOVES.AI` PLANS/PMOVES.yt/CHANNEL_MONITOR_IMPLEMENTATION.md:
- Around line 14-17: The "Current status note (March 12, 2026)" claims the
service is beyond prototype but downstream sections (the header, checklist, and
status tracker) still present pending work; reconcile them by either updating
those sections to reflect completed items or explicitly marking them as
historical/archival. Specifically, edit the header to show "Production-ready"
(or "Historical" if archived), update the checklist items referenced in the
document to completed/removed and add dates/PR links for evidence, and adjust
the status tracker entries to reflect current production concerns (preferences,
observability, alignment) or prepend a clear "Historical (as of <date>):" label;
also add a short note linking to the authoritative PMOVES.YT runtime/docs and
runbooks/smoke tests to justify the status.

In `@pmoves/docs/services/pmoves-yt/README.md`:
- Around line 7-8: Replace the absolute Windows path in the README with a
repository-relative link to the PMOVES.YT submodule; specifically change the
link target "C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT" to a
repo-relative path like "pmoves_yt_service/" (or "./pmoves_yt_service/" or the
correct relative path to the submodule) so the authoritative runtime reference
under pmoves_yt_service/ resolves for all users and the note about
pmoves/services/pmoves-yt remaining a compatibility shim stays accurate.

In `@pmoves/services/channel-monitor/channel_monitor/main.py`:
- Around line 330-348: Add a Pydantic validator on YouTubeControlRequest.action
to enforce allowed values ("playlist_add", "playlist_remove",
"playlist_reorder", "comment_create") and raise a ValueError for any other
string so invalid actions fail fast at validation time; implement this by adding
a `@validator`("action") method on the YouTubeControlRequest class that checks
membership in the allowed set and returns the value or raises an error with a
clear message.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py`:
- Around line 344-351: Update the _yt_control_base_url method to first check for
an explicit CHANNEL_MONITOR_YT_CONTROL_URL environment variable and return it if
present; if not present, robustly derive the control base from self.queue_url by
parsing the URL (use urllib.parse) and removing only the known path segments
(/yt/ingest or /yt) rather than brittle string slicing, preserving scheme/netloc
and any query/fragment removal, and ensure the method returns a normalized URL
string; reference the _yt_control_base_url method and self.queue_url and use
os.getenv("CHANNEL_MONITOR_YT_CONTROL_URL") as the fallback lookup.
- Around line 2081-2129: The review_youtube_control_actions flow currently
fetches pending rows then processes them, causing a race where multiple workers
can pick the same id and no exception handling around _invoke_yt_control_action
means failures leave rows stuck in pending_review; fix by atomically claiming
rows from pmoves_core.youtube_control_actions (use SELECT ... FOR UPDATE SKIP
LOCKED inside a transaction or an UPDATE ... RETURNING to set a temporary status
like "processing" for the wanted_ids) before calling _invoke_yt_control_action,
and wrap the call in try/except so that on exception you update the row status
to a terminal failure state (e.g., "failed" or "rejected") and record the error
in result_payload/notes; ensure processed_ids/action_summaries and the final DB
UPDATE/INSERT use the new_status/result_payload values so no row remains pending
after an exception.

In `@pmoves/services/channel-monitor/README.md`:
- Around line 191-197: Add the five new control-plane environment variables to
the README Environment table: include entries for CHANNEL_MONITOR_YT_API_KEY,
CHANNEL_MONITOR_MESSAGING_URL, CHANNEL_MONITOR_YT_NOTEBOOK_ID,
OPEN_NOTEBOOK_API_URL, and OPEN_NOTEBOOK_API_TOKEN; for each entry state purpose
(e.g., API key for PMOVES.YT control endpoints, messaging gateway /v1/send for
approval notifications, notebook ID and OpenNotebook API URL/token for
publishing review artifacts), whether it is required, and an example format or
default; ensure the descriptions mention that CHANNEL_MONITOR_MESSAGING_URL is
used to route ytcontrol button interactions back to POST
/api/monitor/youtube-control/review and that CHANNEL_MONITOR_YT_API_KEY is
needed when PMOVES.YT requires X-API-Key.
- Around line 155-180: The README's owned-channel PMOVES.YT control action
example is using the wrong draft.source_class value; locate the sample payload
in the "owned-channel PMOVES.YT control actions" section and change the
draft.variables/draft.channel_name/draft.source_class block so that
"source_class" is "owned" (not "watched"), and update any nearby wording or
examples that imply third-party sources to reflect this is an owned-channel
flow.

In `@pmoves/services/channel-monitor/tests/test_monitor.py`:
- Around line 673-676: The test is brittle because _yt_control_base_url() only
handles a few hardcoded suffixes and there's no env override; update
_yt_control_base_url to accept a clear environment override (e.g.,
YT_CONTROL_BASE_URL) and robustly normalize queue URLs by trimming trailing
slashes and removing either "/yt/ingest" or "/yt" only if they appear as path
segments (not as substrings), then append "/control/comment"; update tests to
use the new env override and add cases for trailing slashes and non-standard
queue URLs to assert constructed URLs and preserve existing behavior when no
override is provided.

In `@pmoves/services/messaging-gateway/main.py`:
- Around line 184-198: The exception handler is losing server diagnostic info
because response.raise_for_status() is called before capturing the body; modify
the post call flow in the async block where httpx.AsyncClient is used so you
read response.text() or response.json() into a local (e.g., response_text /
body) immediately after receiving the response and before calling
response.raise_for_status(), then include that captured response in the
logger.warning and in the returned error "content" string (referencing response,
response.raise_for_status, logger, action_id, and review_payload to locate the
code). Ensure you still call raise_for_status() after capturing the body so HTTP
errors propagate to the except block with the response content available for
logs and the returned payload.

In `@pmoves/services/pmoves-yt/docs_catalog.py`:
- Around line 8-12: The code currently unconditionally inserts _SUBMODULE_ROOT
into sys.path and blindly imports pmoves_yt_service; instead verify the
authoritative submodule exists before mutating sys.path: check that
(_SUBMODULE_ROOT / "pmoves_yt_service") is a directory and contains a package
indicator (e.g., __init__.py) or that importlib.util.find_spec for that package
at that location would succeed, and if the check fails raise an explicit
ImportError/RuntimeError; only then insert str(_SUBMODULE_ROOT) into sys.path
and perform from pmoves_yt_service.docs_catalog import * so that
pmoves_yt_service is reliably loaded from the intended submodule rather than a
different installed package.

In `@pmoves/services/pmoves-yt/docs_sync.py`:
- Around line 8-12: The shim currently unconditionally inserts _SUBMODULE_ROOT
into sys.path which can mask a missing PMOVES.YT checkout; update the logic
around _SUBMODULE_ROOT (the pathlib Path defined at top) so you check
_SUBMODULE_ROOT.is_dir() before calling sys.path.insert(0,
str(_SUBMODULE_ROOT)), and if the directory does not exist raise a clear
exception (or call sys.exit) so the import from pmoves_yt_service.docs_sync
fails fast instead of silently pointing to an installed package.

In `@pmoves/services/pmoves-yt/README.md`:
- Around line 40-44: In the "Production defaults" section of README.md remove
the absolute Windows path reference to docker-compose.yml and replace it with a
relative or repository-root path (e.g., ./docker-compose.yml or a repo link) so
the docs are cross-platform; update the sentence that currently mentions
"docker-compose.yml" to reference the relative path and ensure the string
"YT_PLAYER_CLIENT=web_safari" and the note about keeping the compose override
aligned remain unchanged.
- Around line 5-15: The README currently contains absolute Windows paths (e.g.
"C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/...") — update those to
relative repository paths so they work across environments; replace each
absolute path with a relative path from the repo root such as
"PMOVES.YT/pmoves_yt_service/yt.py", "PMOVES.YT/pmoves_yt_service/docs_sync.py",
"PMOVES.YT/pmoves_yt_service/docs_catalog.py" and "PMOVES.YT/docs/RUNTIME.md"
and ensure the PMOVES.YT submodule reference remains consistent (keep the
`pmoves_yt_service/` suffix used in imports and docs).

In `@pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql`:
- Line 5: Add a DB-level constraint to reject unsupported action values by
altering the youtube_control_actions table: define the allowed set (e.g.,
"play","pause","stop","seek", etc.) and either create a Postgres ENUM type for
action and set the column to that type, or add a CHECK constraint on the action
column (e.g., ADD CONSTRAINT check_youtube_action CHECK (action IN (...))).
Update the migration script that currently defines "action text not null" to
instead create the enum type or add the CHECK constraint so inserts with invalid
actions fail at the DB level.
- Around line 36-39: The RLS policy "Public read youtube control actions" on
pmoves_core.youtube_control_actions uses using (true) which exposes sensitive
columns (approval_note, details, result, error); either remove this blanket
policy and associated grants and instead create a sanitized view/select-only
policy that explicitly allows only safe columns for anon/authenticated, or if
full access is intentional add
pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql to the SQL
policy lint allowlist per LOCAL_CI_CHECKS.md Section 4 so the
chit-contract-check passes.

---

Outside diff comments:
In `@pmoves/docs/PMOVES.AI` PLANS/JELLYFIN_YOUTUBE_INTEGRATION.md:
- Around line 152-167: Update Step 3 to remove the default instruction to start
the legacy MCP YouTube Adapter via the uvicorn command
(services.mcp_youtube_adapter:app) and instead point users to the PMOVES.YT
runtime validation path (referencing the PMOVES.YT runtime validation described
earlier) as the authoritative option; alternatively add a short clarifying note
in Step 3 that the uvicorn/mcp_youtube_adapter start is only required if users
explicitly opt into the historical MCP YouTube Adapter, and show when to choose
the legacy adapter versus PMOVES.YT runtime validation so readers are not
confused.

In `@pmoves/services/channel-monitor/channel_monitor/config.py`:
- Around line 31-38: DEFAULT_CONFIG currently hardcodes "source_class" in
DEFAULT_CONFIG["channels"][0], causing entries missing source_class (e.g., old
playlist rows) to inherit "watched"; remove the hardcoded "source_class" from
the default channel entry and update the merge/normalize logic that applies
DEFAULT_CONFIG["channels"][0] to per-row data so that when source_class is
missing you set it based on source_type (e.g., if source_type == "playlist" ->
"owned", otherwise -> "watched"); reference DEFAULT_CONFIG and the keys
source_class and source_type when making these changes.

---

Nitpick comments:
In `@pmoves/services/channel-monitor/channel_monitor/main.py`:
- Around line 644-691: Add Prometheus counters for YouTube control parity with
Discord drops and increment them in the existing endpoints: declare
YOUTUBE_CONTROL_TOTAL (labels: action, result) and YOUTUBE_CONTROL_REVIEWS_TOTAL
(labels: action, decision) alongside the other metrics; in
queue_youtube_control_action, after successful
monitor.create_youtube_control_request increment YOUTUBE_CONTROL_TOTAL with
action=payload.action and result="queued" (or "draft" if payload.draft is true)
and on the ValueError path increment with result="error"; in
review_youtube_control_actions increment YOUTUBE_CONTROL_REVIEWS_TOTAL for each
reviewed action with action equal to the action type and decision "approved" or
"rejected" based on payload.approve; place metric declarations near existing
metric definitions and import/instrument them the same way as
DISCORD_DROPS_TOTAL and DISCORD_DROP_REVIEWS_TOTAL.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py`:
- Around line 1976-1982: The metadata currently embeds the full details dict (in
the block building "metadata" with keys action_id, action, request_source,
details, summary) which can be very verbose; replace the raw details value with
a trimmed summary by implementing a small sanitizer (e.g.,
trim_notification_details or summarize_details) that extracts only essential
fields (title/user/id/timestamps/concise change text or a generated short
summary) and explicitly omits heavy fields like drafts, rendered_text, notebook
metadata; then update the metadata construction to use the trimmed result (e.g.,
details_summary) instead of the full details object so notifications sent from
monitor.py contain a compact payload.
- Around line 2063-2068: Replace the direct os.getenv chain that assigns api_key
in monitor.py with the project’s central env helper that supports *_FILE secret
paths; specifically, ensure you resolve CHANNEL_MONITOR_YT_API_KEY,
NEXT_PUBLIC_BACKEND_API_KEY, and BACKEND_API_KEY via the helper (or implement a
small helper like get_secret_from_env_or_file(var)) which checks VAR_FILE first,
reads the file contents if present, falls back to VAR, and returns a trimmed
string, then assign that result to api_key (preserve the .strip() behavior).

In `@pmoves/services/channel-monitor/tests/test_monitor.py`:
- Around line 623-677: Add a new test alongside
test_review_youtube_control_actions_executes_pmoves_yt_call that simulates an
HTTP execution failure for monitor.review_youtube_control_actions: create a
DummyAsyncClient whose post method raises an exception (or returns a response
whose raise_for_status raises), set CHANNEL_MONITOR_YT_API_KEY and monkeypatch
channel_monitor.monitor.httpx.AsyncClient, and use a fake DB connection (as in
the existing test) to assert the function marks the action as processed with an
error (result shows failed/errored), increments processed count, and calls
conn.execute to update the row status from "pending_review" to the appropriate
failed state; reference monitor.review_youtube_control_actions, the test
function name, DummyAsyncClient, and conn.execute for locating the changes.

In `@pmoves/services/messaging-gateway/main.py`:
- Around line 68-69: Replace the direct os.environ.get usage for
CHANNEL_MONITOR_SECRET with the project *_FILE secret-loading pattern used by
services/common/env.py: use the helper (or the same logic) to check
CHANNEL_MONITOR_SECRET_FILE first and read the file contents, falling back to
CHANNEL_MONITOR_SECRET env var only if the file isn't present; leave
CHANNEL_MONITOR_URL behavior unchanged but ensure you reference
CHANNEL_MONITOR_SECRET by name in the updated code so the audit gate recognizes
the hardened secret loading.

In `@pmoves/services/messaging-gateway/test_main.py`:
- Around line 112-151: Add error-path tests for _handle_ytcontrol_interaction by
creating new test functions (e.g., test_handle_ytcontrol_interaction_http_error,
test_handle_ytcontrol_interaction_invalid_custom_id,
test_handle_ytcontrol_interaction_network_failure) that mirror the existing test
style: monkeypatch module.httpx.AsyncClient with a DummyAsyncClient and
DummyResponse, but simulate failure modes — have DummyResponse.raise_for_status
raise module.httpx.HTTPStatusError to cover HTTP errors, provide payloads with
malformed custom_id (missing parts or wrong prefix) to assert the function
returns a user-facing failure message, and have DummyAsyncClient.post raise
module.httpx.ConnectError (or similar) to test network failures; use
module.asyncio.run(module._handle_ytcontrol_interaction(payload)) and assert
response["type"] == 4 and that the content indicates the failure.

In `@pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql`:
- Around line 23-24: The current index idx_youtube_control_actions_action_status
on pmoves_core.youtube_control_actions is ordered (action, status) which doesn't
support queries like WHERE status = ... ORDER BY created_at DESC efficiently;
add or replace with an index that begins with status and includes created_at
(descending) and action (e.g., (status, created_at DESC, action)) so
review-queue lookups (filter by status and order by created_at DESC) and any
tie-breakers on action are covered; update the migration to create the new index
name and drop or no-op the old idx_youtube_control_actions_action_status if
desired.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8465f1f8-1c94-4cc6-8076-422e07a1ff65

📥 Commits

Reviewing files that changed from the base of the PR and between 6fd5d74 and 378b4a3.

📒 Files selected for processing (35)
  • PMOVES-Wealth
  • PMOVES-supabase
  • PMOVES-transcribe-and-fetch
  • PMOVES.YT
  • Pmoves-Health-wger
  • pmoves/config/channel_monitor.json
  • pmoves/docker-compose.yml
  • pmoves/docs/AGENTS/CODEX_OPERATOR_HOME.md
  • pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md
  • pmoves/docs/AGENTS/README.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-Creator.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-Open-Notebook.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-transcribe-and-fetch.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES.YT.md
  • pmoves/docs/NEXT_STEPS.md
  • pmoves/docs/PMOVES.AI PLANS/CREATOR_NETWORK_CONTROL_PLANE.md
  • pmoves/docs/PMOVES.AI PLANS/JELLYFIN_YOUTUBE_INTEGRATION.md
  • pmoves/docs/PMOVES.AI PLANS/JELLYFIN_YOUTUBE_STATUS.md
  • pmoves/docs/PMOVES.AI PLANS/PMOVES.yt/CHANNEL_MONITOR_IMPLEMENTATION.md
  • pmoves/docs/PMOVES.AI PLANS/README_DOCS_INDEX.md
  • pmoves/docs/PMOVES.AI PLANS/ROADMAP.md
  • pmoves/docs/services/pmoves-yt/README.md
  • pmoves/env.shared.example
  • pmoves/services/channel-monitor/README.md
  • pmoves/services/channel-monitor/channel_monitor/config.py
  • pmoves/services/channel-monitor/channel_monitor/main.py
  • pmoves/services/channel-monitor/channel_monitor/monitor.py
  • pmoves/services/channel-monitor/tests/test_monitor.py
  • pmoves/services/messaging-gateway/main.py
  • pmoves/services/messaging-gateway/test_main.py
  • pmoves/services/pmoves-yt/README.md
  • pmoves/services/pmoves-yt/docs_catalog.py
  • pmoves/services/pmoves-yt/docs_sync.py
  • pmoves/services/pmoves-yt/yt.py
  • pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql

Comment thread PMOVES-transcribe-and-fetch Outdated
@@ -1 +1 @@
Subproject commit 5a57ca00185c6e04527cd2962feb2ac90491a0ad
Subproject commit a4968929b22f5f2625836aa9a1dfdcf6ba3e5459

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the submodule commit exists and fetch its summary.

# Check if the submodule directory exists
if [ ! -d "PMOVES-transcribe-and-fetch" ]; then
  echo "Error: PMOVES-transcribe-and-fetch submodule directory not found"
  exit 1
fi

cd PMOVES-transcribe-and-fetch

# Fetch the latest refs to ensure we have the commit
git fetch --quiet 2>/dev/null || true

# Check if the commit exists
if git cat-file -e a4968929b22f5f2625836aa9a1dfdcf6ba3e5459 2>/dev/null; then
  echo "✓ Commit exists"
  echo ""
  echo "Commit details:"
  git log -1 --oneline a4968929b22f5f2625836aa9a1dfdcf6ba3e5459
  echo ""
  git log -1 --format="Author: %an <%ae>%nDate: %ad%nSubject: %s%n%nBody:%n%b" a4968929b22f5f2625836aa9a1dfdcf6ba3e5459
else
  echo "✗ Commit a4968929b22f5f2625836aa9a1dfdcf6ba3e5459 not found in submodule"
  exit 1
fi

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 139


Submodule commit does not exist in repository.

The commit a4968929b22f5f2625836aa9a1dfdcf6ba3e5459 referenced in the PMOVES-transcribe-and-fetch submodule update does not exist in the submodule repository. Verify that:

  • The commit SHA is correct (check for typos).
  • The commit has been pushed to the submodule repository.
  • The correct branch or remote is being referenced.

Please provide the correct commit SHA or push the intended commit to the submodule repository before this change can be merged.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PMOVES-transcribe-and-fetch` at line 1, The PMOVES-transcribe-and-fetch
submodule update references a non-existent commit SHA
(a4968929b22f5f2625836aa9a1dfdcf6ba3e5459); verify the SHA in the submodule repo
and either push the missing commit to that remote or update the submodule
reference to a valid commit: checkout the PMOVES-transcribe-and-fetch
repository, confirm the correct commit SHA (or create and push the intended
commit), then in the superproject update the submodule to that SHA (or run git
submodule update --init --remote), commit the updated submodule reference in the
superproject, and push the change so the PR points to an existing commit.

Comment on lines +184 to +187
- PMOVES.YT is still stronger on ingest/read/download than owned-channel mutation:
- yt-dlp + companion + Invidious cover extraction and fallback download paths
- PMOVES.YT now has a first owned-channel YouTube Data API control slice for playlist-add and
comment/reply actions, but it is intentionally narrow and approval-gated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't claim reply support unless it's actually wired.

This snapshot says the first owned-channel slice includes playlist-add plus "comment/reply actions", but the supported action list later in the runbook narrows that to playlist_add, playlist_remove, playlist_reorder, and comment_create. If replies are not live yet, this sentence overstates the current control surface.

As per coding guidelines, "pmoves/docs/**: Check docs for operational accuracy: Keep status claims aligned with evidence in runbooks and smokes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md` around lines 184 -
187, The sentence claiming "comment/reply actions" for the first owned-channel
YouTube Data API slice overstates capabilities; update the PMOVES.YT description
so it matches the supported-action list (`playlist_add`, `playlist_remove`,
`playlist_reorder`, `comment_create`) by removing "reply" or explicitly stating
only `comment_create` is supported (e.g., "comment create only"), and ensure the
phrase "comment/reply actions" is replaced wherever present with the accurate
term to keep the runbook and snapshot consistent.

Comment thread pmoves/docs/PMOVES.AI PLANS/JELLYFIN_YOUTUBE_STATUS.md
Comment on lines +14 to +17
Current status note (March 12, 2026):
- the service is no longer just a prototype backlog item
- Google OAuth token storage and API-first fetch paths are already present
- the active gap is production cleanup: preferences, observability, and alignment with the authoritative `PMOVES.YT` runtime/docs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reconcile this status note with the rest of the implementation plan.

These bullets say the service is already past the prototype stage, but the header, checklist, and status tracker below still read like most of the work is pending. Please update the downstream sections or explicitly mark them as historical so operators do not get two different statuses from one page.

As per coding guidelines, pmoves/docs/**: "Check docs for operational accuracy: - Keep status claims aligned with evidence in runbooks and smokes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/docs/PMOVES.AI` PLANS/PMOVES.yt/CHANNEL_MONITOR_IMPLEMENTATION.md
around lines 14 - 17, The "Current status note (March 12, 2026)" claims the
service is beyond prototype but downstream sections (the header, checklist, and
status tracker) still present pending work; reconcile them by either updating
those sections to reflect completed items or explicitly marking them as
historical/archival. Specifically, edit the header to show "Production-ready"
(or "Historical" if archived), update the checklist items referenced in the
document to completed/removed and add dates/PR links for evidence, and adjust
the status tracker entries to reflect current production concerns (preferences,
observability, alignment) or prepend a clear "Historical (as of <date>):" label;
also add a short note linking to the authoritative PMOVES.YT runtime/docs and
runbooks/smoke tests to justify the status.

Comment on lines +7 to +8
- Authoritative runtime now lives in the [PMOVES.YT submodule](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT) under `pmoves_yt_service/`.
- Root `pmoves/services/pmoves-yt` is now a compatibility mirror/shim, not the source of truth.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace the local Windows path with a repo-relative link.

C:/Users/russe/... only works on one machine, so the rendered docs will carry a broken link for everyone else. Please point this at the submodule using a repository-relative path instead.

📝 Proposed fix
-- Authoritative runtime now lives in the [PMOVES.YT submodule](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT) under `pmoves_yt_service/`.
+- Authoritative runtime now lives in the [PMOVES.YT submodule](../../../../PMOVES.YT/) under `pmoves_yt_service/`.

As per coding guidelines, pmoves/docs/**: "Check docs for operational accuracy: - Flag command drift across Windows/WSL/Linux instructions."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Authoritative runtime now lives in the [PMOVES.YT submodule](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT) under `pmoves_yt_service/`.
- Root `pmoves/services/pmoves-yt` is now a compatibility mirror/shim, not the source of truth.
- Authoritative runtime now lives in the [PMOVES.YT submodule](../../../../PMOVES.YT/) under `pmoves_yt_service/`.
- Root `pmoves/services/pmoves-yt` is now a compatibility mirror/shim, not the source of truth.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/docs/services/pmoves-yt/README.md` around lines 7 - 8, Replace the
absolute Windows path in the README with a repository-relative link to the
PMOVES.YT submodule; specifically change the link target
"C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT" to a repo-relative path
like "pmoves_yt_service/" (or "./pmoves_yt_service/" or the correct relative
path to the submodule) so the authoritative runtime reference under
pmoves_yt_service/ resolves for all users and the note about
pmoves/services/pmoves-yt remaining a compatibility shim stays accurate.

Comment on lines +8 to +12
_SUBMODULE_ROOT = pathlib.Path(__file__).resolve().parents[3] / "PMOVES.YT"
if str(_SUBMODULE_ROOT) not in sys.path:
sys.path.insert(0, str(_SUBMODULE_ROOT))

def _capture_cmd(args: list[str]) -> str:
try:
proc = subprocess.run(args, capture_output=True, text=True, timeout=20)
if proc.returncode != 0:
return proc.stderr.strip() or proc.stdout.strip()
return proc.stdout
except Exception as exc: # best-effort
return f"<error: {exc}>"

def collect_yt_dlp_docs() -> Dict[str, Any]:
import yt_dlp # type: ignore
version = getattr(yt_dlp, "version", None)
if isinstance(version, str):
ver = version
else:
ver = getattr(yt_dlp, "__version__", "unknown")
docs: Dict[str, Any] = {
"version": ver,
"help_cli": _capture_cmd(["yt-dlp", "--help"]),
"extractors": _capture_cmd(["yt-dlp", "--list-extractors"]),
"user_agent": _capture_cmd(["yt-dlp", "--dump-user-agent"]),
"ts": datetime.now(timezone.utc).isoformat(),
}
return docs

def sync_to_supabase(docs: Dict[str, Any]) -> Dict[str, Any]:
keys = _candidate_keys()
if not keys:
raise RuntimeError("SUPABASE_SERVICE_ROLE_KEY (or equivalent) is required")
tool = "yt-dlp"
ver = docs.get("version") or "unknown"
rows = []
for k in ("help_cli", "extractors", "user_agent"):
content = docs.get(k)
# Store as JSON with `text` field for consistency
rows.append({
"tool": tool,
"version": str(ver),
"doc_type": k,
"content": {"text": content},
})
import requests
targets = [
# Preferred: proper PostgREST profile headers for pmoves_core schema.
{"url": f"{SUPA}/tool_docs?on_conflict=tool,version,doc_type", "schema": "pmoves_core"},
# Legacy fallback: existing callers that encode schema in table path.
{"url": f"{SUPA}/pmoves_core.tool_docs?on_conflict=tool,version,doc_type", "schema": None},
# Last fallback if schema support is not configured.
{"url": f"{SUPA}/tool_docs?on_conflict=tool,version,doc_type", "schema": None},
]

last_error: str | None = None
for target in targets:
missing_relation = False
transport_error = False
for key in keys:
headers = {
"apikey": key,
"Authorization": f"Bearer {key}",
"content-type": "application/json",
"Prefer": "resolution=merge-duplicates",
}
if target["schema"]:
headers["Accept-Profile"] = target["schema"]
headers["Content-Profile"] = target["schema"]
try:
r = requests.post(target["url"], headers=headers, data=json.dumps(rows), timeout=20)
except requests.RequestException as exc:
last_error = f"transport error: {exc}"
transport_error = True
break
try:
body = r.json()
except ValueError:
body = {"text": r.text}
if r.ok:
return {"status": "ok", "count": len(rows), "version": ver}
last_error = f"{r.status_code} {body}"
# JWT/key mismatch can happen when layered env files contain stale aliases.
# Continue trying available keys before failing hard.
if r.status_code in (401, 403):
continue
# Missing schema/table: move to next target strategy.
if r.status_code in (404, 406):
missing_relation = True
break
if missing_relation or transport_error:
continue

raise RuntimeError(f"Supabase upsert failed: {last_error}")

if __name__ == "__main__":
data = collect_yt_dlp_docs()
out = sync_to_supabase(data)
print(json.dumps(out))
from pmoves_yt_service.docs_sync import * # type: ignore # noqa: F401,F403

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "docs_sync.py" -o -name "docs_catalog.py" | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 192


🏁 Script executed:

fd -t f "docs_sync.py|docs_catalog.py" pmoves/services/

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 239


🏁 Script executed:

cat -n pmoves/services/pmoves-yt/docs_sync.py | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 530


🏁 Script executed:

cat -n pmoves/services/pmoves-yt/docs_catalog.py | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 536


🏁 Script executed:

cat -n pmoves/services/pmoves_yt/docs_sync.py | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 699


🏁 Script executed:

ls -la pmoves/services/pmoves-yt/ | head -20

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 688


🏁 Script executed:

rg "is_dir\(\)" pmoves/services/ -A 2 -B 2

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2106


Add a directory existence check before modifying sys.path.

This compatibility shim lacks protection against a missing PMOVES.YT checkout. If the expected sibling directory does not exist, sys.path.insert() will silently add a non-existent path, allowing import resolution to drift to a different installed pmoves_yt_service package. Add an _SUBMODULE_ROOT.is_dir() guard before inserting into sys.path to fail immediately when the submodule is missing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/pmoves-yt/docs_sync.py` around lines 8 - 12, The shim
currently unconditionally inserts _SUBMODULE_ROOT into sys.path which can mask a
missing PMOVES.YT checkout; update the logic around _SUBMODULE_ROOT (the pathlib
Path defined at top) so you check _SUBMODULE_ROOT.is_dir() before calling
sys.path.insert(0, str(_SUBMODULE_ROOT)), and if the directory does not exist
raise a clear exception (or call sys.exit) so the import from
pmoves_yt_service.docs_sync fails fast instead of silently pointing to an
installed package.

Comment on lines +5 to +15
Canonical source now lives in the [PMOVES.YT submodule](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT) under `pmoves_yt_service/`.

PMOVES.AI keeps this path only as a compatibility shim for:
- existing imports under `pmoves.services.pmoves_yt`
- older docs/test references that still point at `pmoves/services/pmoves-yt`

For real runtime/docs changes, edit:
- [PMOVES.YT/pmoves_yt_service/yt.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/yt.py)
- [PMOVES.YT/pmoves_yt_service/docs_sync.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/docs_sync.py)
- [PMOVES.YT/pmoves_yt_service/docs_catalog.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/docs_catalog.py)
- [PMOVES.YT/docs/RUNTIME.md](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/docs/RUNTIME.md)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace absolute Windows paths with relative repository paths.

The paths reference a specific Windows user directory (C:/Users/russe/Documents/GitHub/PMOVES.AI/...) which won't work for other developers or in CI environments. Use relative paths from the repository root instead.

🔧 Proposed fix
-Canonical source now lives in the [PMOVES.YT submodule](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT) under `pmoves_yt_service/`.
+Canonical source now lives in the [PMOVES.YT submodule](../../../PMOVES.YT) under `pmoves_yt_service/`.

 PMOVES.AI keeps this path only as a compatibility shim for:
 - existing imports under `pmoves.services.pmoves_yt`
 - older docs/test references that still point at `pmoves/services/pmoves-yt`

 For real runtime/docs changes, edit:
-- [PMOVES.YT/pmoves_yt_service/yt.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/yt.py)
-- [PMOVES.YT/pmoves_yt_service/docs_sync.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/docs_sync.py)
-- [PMOVES.YT/pmoves_yt_service/docs_catalog.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/docs_catalog.py)
-- [PMOVES.YT/docs/RUNTIME.md](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/docs/RUNTIME.md)
+- `PMOVES.YT/pmoves_yt_service/yt.py`
+- `PMOVES.YT/pmoves_yt_service/docs_sync.py`
+- `PMOVES.YT/pmoves_yt_service/docs_catalog.py`
+- `PMOVES.YT/docs/RUNTIME.md`

As per coding guidelines, "Flag command drift across Windows/WSL/Linux instructions."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Canonical source now lives in the [PMOVES.YT submodule](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT) under `pmoves_yt_service/`.
PMOVES.AI keeps this path only as a compatibility shim for:
- existing imports under `pmoves.services.pmoves_yt`
- older docs/test references that still point at `pmoves/services/pmoves-yt`
For real runtime/docs changes, edit:
- [PMOVES.YT/pmoves_yt_service/yt.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/yt.py)
- [PMOVES.YT/pmoves_yt_service/docs_sync.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/docs_sync.py)
- [PMOVES.YT/pmoves_yt_service/docs_catalog.py](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/pmoves_yt_service/docs_catalog.py)
- [PMOVES.YT/docs/RUNTIME.md](C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/docs/RUNTIME.md)
Canonical source now lives in the [PMOVES.YT submodule](../../../PMOVES.YT) under `pmoves_yt_service/`.
PMOVES.AI keeps this path only as a compatibility shim for:
- existing imports under `pmoves.services.pmoves_yt`
- older docs/test references that still point at `pmoves/services/pmoves-yt`
For real runtime/docs changes, edit:
- `PMOVES.YT/pmoves_yt_service/yt.py`
- `PMOVES.YT/pmoves_yt_service/docs_sync.py`
- `PMOVES.YT/pmoves_yt_service/docs_catalog.py`
- `PMOVES.YT/docs/RUNTIME.md`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/pmoves-yt/README.md` around lines 5 - 15, The README
currently contains absolute Windows paths (e.g.
"C:/Users/russe/Documents/GitHub/PMOVES.AI/PMOVES.YT/...") — update those to
relative repository paths so they work across environments; replace each
absolute path with a relative path from the repo root such as
"PMOVES.YT/pmoves_yt_service/yt.py", "PMOVES.YT/pmoves_yt_service/docs_sync.py",
"PMOVES.YT/pmoves_yt_service/docs_catalog.py" and "PMOVES.YT/docs/RUNTIME.md"
and ensure the PMOVES.YT submodule reference remains consistent (keep the
`pmoves_yt_service/` suffix used in imports and docs).

Comment on lines +40 to +44
## Production defaults

- Root compose intentionally overrides the submodule runtime with `YT_PLAYER_CLIENT=web_safari` plus a Safari user agent.
- bgutil POT provider is the preferred production path; companion/Invidious stay available as fallback lanes.
- For downloader/client changes, update the submodule first and keep the compose override in [docker-compose.yml](C:/Users/russe/Documents/GitHub/PMOVES.AI/pmoves/docker-compose.yml) aligned.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same issue: absolute Windows path in production defaults section.

Line 44 contains another absolute Windows path reference to docker-compose.yml.

🔧 Proposed fix
-- For downloader/client changes, update the submodule first and keep the compose override in [docker-compose.yml](C:/Users/russe/Documents/GitHub/PMOVES.AI/pmoves/docker-compose.yml) aligned.
+- For downloader/client changes, update the submodule first and keep the compose override in `pmoves/docker-compose.yml` aligned.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/pmoves-yt/README.md` around lines 40 - 44, In the "Production
defaults" section of README.md remove the absolute Windows path reference to
docker-compose.yml and replace it with a relative or repository-root path (e.g.,
./docker-compose.yml or a repo link) so the docs are cross-platform; update the
sentence that currently mentions "docker-compose.yml" to reference the relative
path and ensure the string "YT_PLAYER_CLIENT=web_safari" and the note about
keeping the compose override aligned remain unchanged.

Comment thread pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql Outdated
Comment thread pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql Outdated
@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== channel-monitor monitor.py ==="
cat pmoves/services/channel-monitor/channel_monitor/monitor.py

echo ""
echo "=== messaging-gateway main.py ==="
cat pmoves/services/messaging-gateway/main.py

echo ""
echo "=== channel-monitor tests ==="
cat pmoves/services/channel-monitor/tests/test_monitor.py

Length of output: 142813


@POWERFULMOVES — verified all seven items against the updated code. Everything looks solid.


✅ All previous findings addressed

# Severity Finding Status
1 🔴 Execution failure leaves row in pending_review Fixedtry/except around _invoke_yt_control_action; sets new_status = "failed", records error_text, UPDATE always runs. Covered by test_review_youtube_control_actions_marks_failed_when_execution_errors.
2 🔴 Race condition — no row-level lock before concurrent approval Fixed — CTE with FOR UPDATE SKIP LOCKED atomically transitions qualifying rows to processing within a transaction before any execution begins.
3 🟡 Invalid actions accepted at queue time Fixed_prepare_youtube_control_details raises ValueError for unknown action before the DB INSERT. conn.execute is not awaited on the invalid-action path (test asserts this).
4 🟡 _yt_control_base_url fragile suffix stripping Fixed — explicit CHANNEL_MONITOR_YT_CONTROL_URL env var checked first; derivation is retained as fallback only.
5 🟡 No required-field validation before DB persist FixedYOUTUBE_CONTROL_REQUIRED_FIELDS dict + missing_fields check in _prepare_youtube_control_details. playlist_add requires both playlist_id and video_id; test confirms ValueError with the correct message.
6 🟡 HTTP error response body discarded Fixed — both monitor._invoke_yt_control_action and messaging-gateway._handle_ytcontrol_interaction catch httpx.HTTPStatusError and re-raise with status_code + first 500 chars of the response body.
7 🟡 Full details dict in notification metadata Fixedmetadata["details"] now sends only the safe summary keys (playlist_id, playlist_item_id, video_id, position, text_preview, source_class, notebook_entry_id). Test assertion reflects the trimmed shape.

Minor residual items (no action required now)

Orphaned processing rows — rows claimed via FOR UPDATE SKIP LOCKED and transitioned to processing will remain in that state if the process crashes between the claim commit and the per-row final UPDATE. This is the standard trade-off when splitting the claim and execution phases; a periodic reaper (or a configurable processing timeout reset to pending_review) would harden this further. Worth a follow-up issue when the control plane stabilises.

asyncio.wait([self._shutdown.wait()], ...) in _channel_loop — pre-existing: passing a bare coroutine to asyncio.wait is deprecated since Python 3.8. Not introduced in this PR; wrapping with asyncio.ensure_future(self._shutdown.wait()) or replacing with asyncio.wait_for(asyncio.shield(self._shutdown.wait()), timeout=...) would clear the DeprecationWarning.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@POWERFULMOVES
POWERFULMOVES force-pushed the feat/pmoves-yt-integration-review branch from 4e5a8dd to 24cb93d Compare March 12, 2026 18:41
@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

Restacked this PR onto the current PMOVES.AI-Edition-Hardened base and force-pushed the branch.

Current scope is now the intended 9-commit creator-control lane only:

  • source classification + worktree review runbook
  • YouTube control audit table
  • channel-monitor queue/list/review flow
  • messaging-gateway Discord approval routing
  • notebook-backed review flow
  • review-flow hardening fixes
  • PMOVES.YT gitlink pinned to the latest control-plane + CI-fix commit

Local validation rerun on the restacked branch:

  • python -m pytest -q pmoves/services/channel-monitor/tests/test_monitor.py -k "youtube_control"
  • python -m pytest -q pmoves/services/messaging-gateway/test_main.py
  • ruff check pmoves/services/channel-monitor/channel_monitor/config.py pmoves/services/channel-monitor/channel_monitor/main.py pmoves/services/channel-monitor/channel_monitor/monitor.py pmoves/services/channel-monitor/tests/test_monitor.py pmoves/services/messaging-gateway/main.py pmoves/services/messaging-gateway/test_main.py
  • python -m py_compile pmoves/services/channel-monitor/channel_monitor/config.py pmoves/services/channel-monitor/channel_monitor/main.py pmoves/services/channel-monitor/channel_monitor/monitor.py pmoves/services/channel-monitor/tests/test_monitor.py pmoves/services/messaging-gateway/main.py pmoves/services/messaging-gateway/test_main.py

GitHub now reports the PR branch as mergeable; waiting on the fresh CI/review cycle.

@coderabbitai review
@kilocode review

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

@POWERFULMOVES — acknowledged. The restack onto PMOVES.AI-Edition-Hardened looks clean: the prior round closed all seven findings (both critical race/failure-capture issues, all three major validation/URL/metadata issues, and the HTTP error body issue), and local validation passed across pytest, ruff, and py_compile for the full file set.

Triggering a fresh incremental review on the restacked branch now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.


ytcontrol_response = await _handle_ytcontrol_interaction(payload)
if ytcontrol_response is not None:
return ytcontrol_response

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 6 months ago

In general, the fix is to avoid embedding raw exception information (exc) into the HTTP response that is returned to the Discord interaction caller. Instead, log the detailed error server-side and return a generic, user-friendly message that doesn’t include stack traces, status codes, or internal details.

Concretely, in pmoves/services/messaging-gateway/main.py, inside the except Exception as exc: block around the httpx.AsyncClient call (lines 199–204), we should keep the logging line as-is to preserve diagnostics, but change the returned dictionary so that the "content" field does not interpolate {exc}. We can keep the action id to give some context to the user, but the problem-specific detail should be removed or replaced with a generic phrase like “internal error” or “upstream service error”. No new imports are required.

So, update the block:

except Exception as exc:
    logger.warning("YouTube control Discord interaction failed for %s: %s", action_id, exc)
    return {
        "type": 4,
        "data": {"content": f"Review action failed for {action_id}: {exc}", "flags": 64},
    }

to something like:

except Exception as exc:
    logger.warning("YouTube control Discord interaction failed for %s: %s", action_id, exc)
    return {
        "type": 4,
        "data": {
            "content": f"Review action failed for {action_id} due to an internal error.",
            "flags": 64,
        },
    }

This preserves existing behavior (user still sees that the review failed for a given action_id, the message type and flags are unchanged) while eliminating exposure of exception details. The tainted data (exc) now flows only into the log, not into the response.

Suggested changeset 1
pmoves/services/messaging-gateway/main.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pmoves/services/messaging-gateway/main.py b/pmoves/services/messaging-gateway/main.py
--- a/pmoves/services/messaging-gateway/main.py
+++ b/pmoves/services/messaging-gateway/main.py
@@ -200,7 +200,10 @@
         logger.warning("YouTube control Discord interaction failed for %s: %s", action_id, exc)
         return {
             "type": 4,
-            "data": {"content": f"Review action failed for {action_id}: {exc}", "flags": 64},
+            "data": {
+                "content": f"Review action failed for {action_id} due to an internal error.",
+                "flags": 64,
+            },
         }
 
     content = _format_ytcontrol_response(body, action_id, approve)
EOF
@@ -200,7 +200,10 @@
logger.warning("YouTube control Discord interaction failed for %s: %s", action_id, exc)
return {
"type": 4,
"data": {"content": f"Review action failed for {action_id}: {exc}", "flags": 64},
"data": {
"content": f"Review action failed for {action_id} due to an internal error.",
"flags": 64,
},
}

content = _format_ytcontrol_response(body, action_id, approve)
Copilot is powered by AI and may make mistakes. Always verify output.
@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

Follow-up CI fixes pushed in e4eb794f.

This addresses the two failing root PR checks:

  • python-tests: checkout now fetches submodules recursively so the root pmoves-yt shim tests can see PMOVES.YT
  • sql-policy-lint: youtube_control_actions migration no longer exposes blanket USING (true) / anon grants; authenticated reads are now scoped by request_source, and action values are constrained at the DB layer
  • root pmoves/services/pmoves-yt/yt.py shim now preserves package context so the authoritative PMOVES.YT module can resolve its relative imports during test collection

Local validation:

  • python -m pytest -q pmoves/services/pmoves-yt/tests/test_docs_catalog.py
  • python -m pytest -q pmoves/services/channel-monitor/tests/test_monitor.py -k "youtube_control"
  • python -m pytest -q pmoves/services/messaging-gateway/test_main.py
  • policy grep against pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql for USING true / to anon

@coderabbitai review
@kilocode review

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== CI workflow(s) ==="
fd -e yml -e yaml . .github/workflows/ --exec cat {}

echo ""
echo "=== Migration SQL ==="
cat pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql

echo ""
echo "=== pmoves-yt yt.py shim ==="
cat pmoves/services/pmoves-yt/yt.py

echo ""
echo "=== pmoves-yt tests ==="
fd -e py . pmoves/services/pmoves-yt/tests/ --exec cat {}

Length of output: 138812


@POWERFULMOVES — verified all three CI fixes from e4eb794f. Everything looks correct.


✅ CI fixes verified

1. python-tests — submodule recursive checkout
The python-tests.yml checkout step now carries submodules: recursive, so the pmoves-yt shim tests can resolve PMOVES.YT/pmoves_yt_service/ at import time. ✅


2. sql-policy-lint — tightened migration policies
The migration is clean against the lint grep checks and notably more secure than the original:

Before After
Read policy USING (true) (open, any user) USING (pmoves_core.current_user_matches_request_source(request_source)) (scoped to service_role or matching user:<uid> prefix)
Grants anon + authenticated + service_role authenticated + service_role only (anon fully removed)
Action constraint None DB-layer CHECK (action IN ('playlist_add', ...)) — defense-in-depth on top of app-layer validation

The new composite index idx_youtube_control_actions_status_created_at on (status, created_at desc, action) is a useful bonus — it directly supports the FOR UPDATE SKIP LOCKED claim CTE pattern from the previous round. ✅


3. pmoves-yt shim — package context preserved
Setting __package__ = "pmoves_yt_service" and __path__ = [...] in the shim's globals before exec(compile(code, str(_IMPL), "exec")) is the correct approach — since exec without an explicit namespace dict uses the calling module's globals, relative imports within the authoritative yt.py resolve to the right package context. ✅


All previous findings remain closed. The lane is clean — triggering a fresh review pass now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-transcribe-and-fetch.md (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Clarify indirect routing through PMOVES.YT.

The operating rule states "align transcript/fetch outputs with Discord, Jellyfin, retrieval, and Tokenism consumers", but the implementation shows channel-monitor POSTs to a single queue_url endpoint (typically PMOVES.YT), not directly to these consumers. This is aspirational and depends on downstream routing from PMOVES.YT. Consider clarifying that alignment happens indirectly through PMOVES.YT's queue endpoint.

As per coding guidelines, "pmoves/docs/**: Check docs for operational accuracy: Keep status claims aligned with evidence in runbooks and smokes."

📝 Suggested clarification
-- align transcript/fetch outputs with Discord, Jellyfin, retrieval, and Tokenism consumers
+- align transcript/fetch outputs with downstream consumers (Discord, Jellyfin, retrieval, Tokenism) via PMOVES.YT queue routing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-transcribe-and-fetch.md` at
line 33, Update the docs to clarify that transcript/fetch outputs are routed
indirectly via PMOVES.YT by the channel-monitor POSTs to the single queue_url
endpoint rather than being sent directly to downstream consumers (Discord,
Jellyfin, retrieval, Tokenism); mention that alignment with those consumers
depends on PMOVES.YT's downstream routing and include a short note near the
"align transcript/fetch outputs..." line indicating this indirection and
dependency on PMOVES.YT handling of the queue_url requests.
♻️ Duplicate comments (2)
pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md (1)

172-175: ⚠️ Potential issue | 🟡 Minor

Don't claim reply support unless it's actually wired.

Lines 174-175 state "PMOVES.YT now has a first owned-channel YouTube Data API control slice for playlist-add and comment/reply actions", but line 197 and the code evidence (YouTubeControlRequest model) show that only comment_create is supported, not reply actions. Update "comment/reply actions" to "comment create only" or similar to match the actual supported actions.

As per coding guidelines, "pmoves/docs/**: Check docs for operational accuracy: Keep status claims aligned with evidence in runbooks and smokes."

📝 Suggested fix
-- PMOVES.YT now has a first owned-channel YouTube Data API control slice for playlist-add and
-  comment/reply actions, but it is intentionally narrow and approval-gated
+- PMOVES.YT now has a first owned-channel YouTube Data API control slice for playlist-add and
+  comment create actions (not reply), but it is intentionally narrow and approval-gated
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md` around lines 172 -
175, The doc incorrectly claims reply support for PMOVES.YT; update the phrasing
around PMOVES.YT to reflect the actual supported actions by changing
"comment/reply actions" to "comment create only" (or similar), and note that the
YouTubeControlRequest currently supports only comment_create (no reply actions)
and that the owned-channel Data API slice is approval-gated; adjust the sentence
referencing playlist-add and comment/reply to read something like "playlist-add
and comment_create (comment create only)" so it aligns with the
YouTubeControlRequest model.
pmoves/services/channel-monitor/README.md (1)

155-179: ⚠️ Potential issue | 🟡 Minor

Use owned in the owned-channel example.

This section is explicitly about owned-channel mutations, but the sample payload still sets "source_class": "watched", which points operators at the wrong lane.

✏️ Suggested edit
-        "source_class": "watched"
+        "source_class": "owned"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/README.md` around lines 155 - 179, The sample
payload for the owned-channel control action incorrectly sets the draft source
class to "watched"; update the example under "owned-channel PMOVES.YT control
actions" (the JSON block used for the comment_create action and the draft
object) to set "source_class": "owned" so the draft, notify lanes and operators
correctly reflect an owned-channel mutation.
🧹 Nitpick comments (1)
pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql (1)

17-34: Consider adding a CHECK constraint on the status column.

The action column is properly constrained, but status accepts any text value. Based on the review workflow in monitor.py, valid status values appear to be 'pending_review', 'processing', 'approved', 'rejected', and 'failed'. Adding a constraint would prevent invalid states at the database level.

♻️ Suggested constraint
   status text not null,
+  check (
+    status in ('pending_review', 'processing', 'approved', 'rejected', 'failed')
+  ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql` around
lines 17 - 34, The status column in pmoves_core.youtube_control_actions lacks a
constraint allowing invalid states; add a CHECK constraint (e.g.,
youtube_control_actions_status_check) on the status column limiting values to
'pending_review','processing','approved','rejected','failed' by either updating
the CREATE TABLE statement to include the CHECK on status or issuing an ALTER
TABLE ... ADD CONSTRAINT for the existing table so the database enforces valid
workflow states.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pmoves/services/channel-monitor/channel_monitor/main.py`:
- Around line 154-157: The source_class Field currently accepts any string and
relies on monitor._normalize_source_class() to fallback silently; change the API
models to validate and reject unknown values up front by restricting
source_class to the allowed set {"owned","partner","watched","candidate"}
(either via a Pydantic validator that checks the value against that set and
raises ValueError or by using an Enum/Literal type), update the Field
declaration for source_class and add the same validator/type to
DiscordDropRequest so typos like "owend" fail at the API boundary rather than
being normalized later.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py`:
- Around line 2165-2170: The audit row update currently leaves approved_by NULL
when actor is omitted; change the code that writes the DB/audit row to use the
same resolved reviewer identity passed to _invoke_yt_control_action (i.e., use
resolved_approver = actor or "channel-monitor") so both the payload and the
stored row record the same approver; update the places around the
_invoke_yt_control_action call (and the similar block at 2190-2205) to reference
resolved_approver for approved_by instead of actor directly.
- Around line 2091-2096: The code is falling back to a public API key when
building api_key (the os.getenv chain referencing NEXT_PUBLIC_BACKEND_API_KEY
and plain ""), which allows control-plane writes with a potentially exposed
credential; update the api_key loading in monitor.py to use the project's
central env helper that supports *_FILE secret loading and only read the
service-private secret (e.g., CHANNEL_MONITOR_YT_API_KEY via the helper), remove
the NEXT_PUBLIC_BACKEND_API_KEY and empty-string fallback, and ensure the
resulting logic (the variable api_key) fails loudly or logs an error if the
private secret is missing.
- Around line 173-184: In the comment_create branch (where action ==
"comment_create") add a guard after the text/template handling to validate that
a target is present: check normalized and draft_dict for a non-empty "video_id"
or "parent_comment_id" (e.g., normalized.get("video_id") or
draft_dict.get("video_id") or normalized.get("parent_comment_id") or
draft_dict.get("parent_comment_id")), and if neither exists raise a ValueError
(similar to the existing message style) so rows without a target cannot be
persisted or queued for PMOVES.YT.

In `@pmoves/services/messaging-gateway/main.py`:
- Around line 191-203: The code currently raises a RuntimeError including
exc.response.text and then returns str(exc) to Discord, leaking upstream
response bodies; change the error handling so you do not include upstream
response details in either the raised exception message or the ephemeral reply:
in the httpx.HTTPStatusError except block (around response.raise_for_status /
httpx.HTTPStatusError) remove or redact exc.response.text from the RuntimeError
(keep only status_code or a generic message) and log the full response or
exception internally (e.g., logger.exception or logger.error) for debugging;
then update the outer except handler that builds the Discord reply (the dict
returned with "type": 4 and "data": {"content": ...}) to return a generic
user-facing message like "Review action failed for {action_id}" without
including exc or its text, while still logging the full exception via
logger.warning/logger.exception for operators.

In `@pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql`:
- Around line 84-85: Update the table comment for
pmoves_core.youtube_control_actions to reflect that rows are mutable (they are
updated by monitor.py: fields like status, approved_by, approval_note, result,
error are modified) instead of claiming "Append-only audit trail"; change the
description to indicate it is an audit/history of YouTube API control actions
that may be updated with approval and result metadata, referencing the table
name pmoves_core.youtube_control_actions and the monitored fields (status,
approved_by, approval_note, result, error).

---

Outside diff comments:
In `@pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-transcribe-and-fetch.md`:
- Line 33: Update the docs to clarify that transcript/fetch outputs are routed
indirectly via PMOVES.YT by the channel-monitor POSTs to the single queue_url
endpoint rather than being sent directly to downstream consumers (Discord,
Jellyfin, retrieval, Tokenism); mention that alignment with those consumers
depends on PMOVES.YT's downstream routing and include a short note near the
"align transcript/fetch outputs..." line indicating this indirection and
dependency on PMOVES.YT handling of the queue_url requests.

---

Duplicate comments:
In `@pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md`:
- Around line 172-175: The doc incorrectly claims reply support for PMOVES.YT;
update the phrasing around PMOVES.YT to reflect the actual supported actions by
changing "comment/reply actions" to "comment create only" (or similar), and note
that the YouTubeControlRequest currently supports only comment_create (no reply
actions) and that the owned-channel Data API slice is approval-gated; adjust the
sentence referencing playlist-add and comment/reply to read something like
"playlist-add and comment_create (comment create only)" so it aligns with the
YouTubeControlRequest model.

In `@pmoves/services/channel-monitor/README.md`:
- Around line 155-179: The sample payload for the owned-channel control action
incorrectly sets the draft source class to "watched"; update the example under
"owned-channel PMOVES.YT control actions" (the JSON block used for the
comment_create action and the draft object) to set "source_class": "owned" so
the draft, notify lanes and operators correctly reflect an owned-channel
mutation.

---

Nitpick comments:
In `@pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql`:
- Around line 17-34: The status column in pmoves_core.youtube_control_actions
lacks a constraint allowing invalid states; add a CHECK constraint (e.g.,
youtube_control_actions_status_check) on the status column limiting values to
'pending_review','processing','approved','rejected','failed' by either updating
the CREATE TABLE statement to include the CHECK on status or issuing an ALTER
TABLE ... ADD CONSTRAINT for the existing table so the database enforces valid
workflow states.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dce71686-e948-4344-82a9-71395ddcdbdb

📥 Commits

Reviewing files that changed from the base of the PR and between 378b4a3 and e4eb794.

📒 Files selected for processing (21)
  • .github/workflows/python-tests.yml
  • PMOVES.YT
  • pmoves/config/channel_monitor.json
  • pmoves/docs/AGENTS/CODEX_OPERATOR_HOME.md
  • pmoves/docs/AGENTS/PMOVES_YT_CONTROL_WORKTREE_REVIEW.md
  • pmoves/docs/AGENTS/README.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-Creator.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-Open-Notebook.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-transcribe-and-fetch.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES.YT.md
  • pmoves/docs/NEXT_STEPS.md
  • pmoves/docs/PMOVES.AI PLANS/ROADMAP.md
  • pmoves/services/channel-monitor/README.md
  • pmoves/services/channel-monitor/channel_monitor/config.py
  • pmoves/services/channel-monitor/channel_monitor/main.py
  • pmoves/services/channel-monitor/channel_monitor/monitor.py
  • pmoves/services/channel-monitor/tests/test_monitor.py
  • pmoves/services/messaging-gateway/main.py
  • pmoves/services/messaging-gateway/test_main.py
  • pmoves/services/pmoves-yt/yt.py
  • pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql
✅ Files skipped from review due to trivial changes (1)
  • PMOVES.YT
🚧 Files skipped from review as they are similar to previous changes (6)
  • pmoves/docs/NEXT_STEPS.md
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES-Open-Notebook.md
  • pmoves/docs/PMOVES.AI PLANS/ROADMAP.md
  • pmoves/docs/AGENTS/README.md
  • pmoves/services/channel-monitor/channel_monitor/config.py
  • pmoves/docs/AGENTS/SUBMODULE_CODEX_HOMES/PMOVES.YT.md

Comment on lines +154 to +157
source_class: str | None = Field(
None,
description="Operator intent class (owned, partner, watched, candidate)",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject unknown source_class values at the API boundary.

These fields currently accept any string, and monitor._normalize_source_class() later falls back silently to watched or candidate. A typo like "owend" will succeed with the wrong routing semantics instead of failing fast.

🛡️ Suggested validator
 class AddChannelRequest(BaseModel):
@@
     source_class: str | None = Field(
         None,
         description="Operator intent class (owned, partner, watched, candidate)",
     )
+
+    `@validator`("source_class")
+    def _validate_source_class(cls, value: str | None) -> str | None:
+        if value is None:
+            return None
+        normalized = value.strip().lower()
+        allowed = {"owned", "partner", "watched", "candidate"}
+        if normalized not in allowed:
+            raise ValueError(f"source_class must be one of {sorted(allowed)}")
+        return normalized

Apply the same validator to DiscordDropRequest.

Also applies to: 301-304

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/main.py` around lines 154 -
157, The source_class Field currently accepts any string and relies on
monitor._normalize_source_class() to fallback silently; change the API models to
validate and reject unknown values up front by restricting source_class to the
allowed set {"owned","partner","watched","candidate"} (either via a Pydantic
validator that checks the value against that set and raises ValueError or by
using an Enum/Literal type), update the Field declaration for source_class and
add the same validator/type to DiscordDropRequest so typos like "owend" fail at
the API boundary rather than being normalized later.

Comment on lines +173 to +184
if action == "comment_create":
text_value = normalized.get("text")
if not isinstance(text_value, str) or not text_value.strip():
template = normalized.get("text_template") or draft_dict.get("text_template") or draft_dict.get("template")
template_vars = normalized.get("template_vars") or draft_dict.get("template_vars") or draft_dict.get("variables") or {}
if isinstance(template, str) and template.strip():
rendered = _render_template_text(template, template_vars if isinstance(template_vars, dict) else {})
normalized["text"] = rendered.strip()
normalized["template_rendered"] = True
else:
raise ValueError("comment_create requires text or text_template")
normalized["text_preview"] = _truncate_text(normalized.get("text"), limit=160)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate the target for comment_create.

comment_create currently validates the text/template, but it still accepts requests with neither video_id nor parent_comment_id. Those rows can be persisted and later sent to PMOVES.YT with no target object.

🔧 Suggested guard
     if action == "comment_create":
+        if normalized.get("video_id") in (None, "") and normalized.get("parent_comment_id") in (None, ""):
+            raise ValueError("comment_create requires video_id or parent_comment_id")
         text_value = normalized.get("text")
         if not isinstance(text_value, str) or not text_value.strip():
             template = normalized.get("text_template") or draft_dict.get("text_template") or draft_dict.get("template")
🧰 Tools
🪛 Ruff (0.15.5)

[warning] 183-183: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py` around lines 173
- 184, In the comment_create branch (where action == "comment_create") add a
guard after the text/template handling to validate that a target is present:
check normalized and draft_dict for a non-empty "video_id" or
"parent_comment_id" (e.g., normalized.get("video_id") or
draft_dict.get("video_id") or normalized.get("parent_comment_id") or
draft_dict.get("parent_comment_id")), and if neither exists raise a ValueError
(similar to the existing message style) so rows without a target cannot be
persisted or queued for PMOVES.YT.

Comment on lines +2091 to +2096
api_key = (
os.getenv("CHANNEL_MONITOR_YT_API_KEY")
or os.getenv("NEXT_PUBLIC_BACKEND_API_KEY")
or os.getenv("BACKEND_API_KEY")
or ""
).strip()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't fall back to a public API key for control-plane writes.

Using NEXT_PUBLIC_BACKEND_API_KEY here makes it easy to authorize PMOVES.YT mutations with a credential that may already be exposed to clients. This path should read only a service-private secret via the shared env helper / _FILE mechanism.

As per coding guidelines, "Prefer central env helpers and *_FILE secret loading paths. Flag direct critical-secret reads and plaintext fallbacks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py` around lines 2091
- 2096, The code is falling back to a public API key when building api_key (the
os.getenv chain referencing NEXT_PUBLIC_BACKEND_API_KEY and plain ""), which
allows control-plane writes with a potentially exposed credential; update the
api_key loading in monitor.py to use the project's central env helper that
supports *_FILE secret loading and only read the service-private secret (e.g.,
CHANNEL_MONITOR_YT_API_KEY via the helper), remove the
NEXT_PUBLIC_BACKEND_API_KEY and empty-string fallback, and ensure the resulting
logic (the variable api_key) fails loudly or logs an error if the private secret
is missing.

Comment on lines +2128 to +2147
async with self._pool.acquire() as conn:
async with conn.transaction():
rows = await conn.fetch(
"""
WITH claimed AS (
SELECT id
FROM pmoves_core.youtube_control_actions
WHERE status = 'pending_review'
AND id = ANY($1::uuid[])
ORDER BY created_at ASC
FOR UPDATE SKIP LOCKED
)
UPDATE pmoves_core.youtube_control_actions AS actions
SET status = 'processing'
FROM claimed
WHERE actions.id = claimed.id
RETURNING actions.id, actions.action, actions.details
""",
wanted_ids,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Claimed actions can get stranded in processing.

This claim step commits status='processing' before the PMOVES.YT call runs. If the worker dies after the claim and before the final update, the row stops appearing in list_pending_youtube_control_actions() and never becomes reviewable again. Please add a lease/retry path for stale processing rows.

Comment on lines +2165 to +2170
try:
result_payload = await self._invoke_yt_control_action(
action=action,
details=details,
approved_by=actor or "channel-monitor",
approval_note=reason,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Persist the same reviewer identity you send downstream.

When actor is omitted, _invoke_yt_control_action() uses "channel-monitor" but the row update still writes approved_by = NULL. That leaves the audit row inconsistent with the executed payload.

🧭 Suggested normalization
         for row in rows:
             action_id = str(row.get("id"))
             action = row.get("action")
             details = row.get("details") if isinstance(row.get("details"), dict) else {}
+            resolved_actor = actor or "channel-monitor"
             summary = details.get("request_summary") or _build_youtube_control_summary(action, details, details.get("draft"))
             notebook_meta = details.get("notebook") if isinstance(details.get("notebook"), dict) else {}
@@
                 try:
                     result_payload = await self._invoke_yt_control_action(
                         action=action,
                         details=details,
-                        approved_by=actor or "channel-monitor",
+                        approved_by=resolved_actor,
                         approval_note=reason,
                     )
@@
                     action_id,
                     new_status,
-                    actor,
+                    resolved_actor,
                     reason,

Also applies to: 2190-2205

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/channel-monitor/channel_monitor/monitor.py` around lines 2165
- 2170, The audit row update currently leaves approved_by NULL when actor is
omitted; change the code that writes the DB/audit row to use the same resolved
reviewer identity passed to _invoke_yt_control_action (i.e., use
resolved_approver = actor or "channel-monitor") so both the payload and the
stored row record the same approver; update the places around the
_invoke_yt_control_action call (and the similar block at 2190-2205) to reference
resolved_approver for approved_by instead of actor directly.

Comment on lines +191 to +203
try:
response.raise_for_status()
except httpx.HTTPStatusError as exc:
raise RuntimeError(
f"channel-monitor returned {exc.response.status_code}: "
f"{exc.response.text[:500]}"
) from exc
body = response.json()
except Exception as exc:
logger.warning("YouTube control Discord interaction failed for %s: %s", action_id, exc)
return {
"type": 4,
"data": {"content": f"Review action failed for {action_id}: {exc}", "flags": 64},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't echo upstream error details back to Discord users.

The inner RuntimeError includes the channel-monitor response body, and the outer handler returns str(exc) verbatim in the ephemeral message. That leaks internal diagnostics to anyone who clicks the button.

🔒 Suggested response handling
     except Exception as exc:
         logger.warning("YouTube control Discord interaction failed for %s: %s", action_id, exc)
         return {
             "type": 4,
-            "data": {"content": f"Review action failed for {action_id}: {exc}", "flags": 64},
+            "data": {
+                "content": f"Review action failed for {action_id}. Please retry or check service logs.",
+                "flags": 64,
+            },
         }
🧰 Tools
🪛 Ruff (0.15.5)

[warning] 194-197: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 199-199: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/services/messaging-gateway/main.py` around lines 191 - 203, The code
currently raises a RuntimeError including exc.response.text and then returns
str(exc) to Discord, leaking upstream response bodies; change the error handling
so you do not include upstream response details in either the raised exception
message or the ephemeral reply: in the httpx.HTTPStatusError except block
(around response.raise_for_status / httpx.HTTPStatusError) remove or redact
exc.response.text from the RuntimeError (keep only status_code or a generic
message) and log the full response or exception internally (e.g.,
logger.exception or logger.error) for debugging; then update the outer except
handler that builds the Discord reply (the dict returned with "type": 4 and
"data": {"content": ...}) to return a generic user-facing message like "Review
action failed for {action_id}" without including exc or its text, while still
logging the full exception via logger.warning/logger.exception for operators.

Comment on lines +84 to +85
comment on table pmoves_core.youtube_control_actions is
'Append-only audit trail for PMOVES.YT YouTube Data API playlist/comment control actions.';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Table comment is misleading - rows are updated, not append-only.

The comment states "Append-only audit trail," but the review workflow in monitor.py updates rows (modifying status, approved_by, approval_note, result, error). Consider revising to accurately describe the table's mutability.

📝 Suggested comment revision
 comment on table pmoves_core.youtube_control_actions is
-  'Append-only audit trail for PMOVES.YT YouTube Data API playlist/comment control actions.';
+  'Audit trail for PMOVES.YT YouTube Data API playlist/comment control actions with status-tracking updates.';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
comment on table pmoves_core.youtube_control_actions is
'Append-only audit trail for PMOVES.YT YouTube Data API playlist/comment control actions.';
comment on table pmoves_core.youtube_control_actions is
'Audit trail for PMOVES.YT YouTube Data API playlist/comment control actions with status-tracking updates.';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/supabase/migrations/20260312095500_youtube_control_actions.sql` around
lines 84 - 85, Update the table comment for pmoves_core.youtube_control_actions
to reflect that rows are mutable (they are updated by monitor.py: fields like
status, approved_by, approval_note, result, error are modified) instead of
claiming "Append-only audit trail"; change the description to indicate it is an
audit/history of YouTube API control actions that may be updated with approval
and result metadata, referencing the table name
pmoves_core.youtube_control_actions and the monitored fields (status,
approved_by, approval_note, result, error).

@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

@kilocode review\n\nRoot creator-control lane is mergeable. Please review the latest head 9e7c245 with focus on channel-monitor approval flow, messaging-gateway Discord actions, Supabase audit migration, and planning-doc accuracy.

@POWERFULMOVES
POWERFULMOVES merged commit 010e8c8 into PMOVES.AI-Edition-Hardened Mar 12, 2026
11 of 25 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the feat/pmoves-yt-integration-review branch March 13, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants