22-task plan across 6 phases (data layer, watcher service stages,
API gateway, dashboard, container/deps, K8s revival). Each task
includes TDD checkboxes, file paths, acceptance criteria, and
commit commands. References the design doc (ab382af) for schemas,
prompts, and the Terraform diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
40 KiB
Meet Kevin Revival Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Ship the v1 Meet Kevin pipeline (RSS poll → caption extraction → Claude Sonnet 4.6 structured analysis → Postgres → ticker-centric UI) inside the existing viktor/trading repo, and revive the K8s stack with the 3 expensive services disabled and the new watcher container added.
Architecture: Single long-running async service (services/meet_kevin_watcher/) walks each video through 3 sequential stages and writes directly to Postgres. No Redis streams in v1. The existing api_gateway gains 11 new routes; the dashboard gains 5 new pages under /meet-kevin/*. The K8s revival is a single infra/stacks/trading-bot/main.tf edit (uncomment block + remove 3 disabled containers + add 1 new container + extend ExternalSecret + bump common env).
Tech Stack: Python 3.12, FastAPI, SQLAlchemy 2.0 (async), Pydantic v2, Alembic, feedparser, yt-dlp>=2026.04 (PyPI not apt), anthropic>=0.40 SDK with tool-use forcing for structured output + prompt caching, React 19 + TanStack Query + recharts + Tailwind 4, Terraform/Terragrunt, Vault KV + ExternalSecret.
Reference: spec at 2026-05-21-meet-kevin-revival-design.md (commit ab382af). The spec is the source of truth for schemas, prompts, endpoint shapes, and the Terraform diff — this plan sequences the implementation and pins down acceptance criteria per step.
Workspace: all backend + dashboard work happens inside /home/wizard/code/trading-bot/ (the cloned trading repo). The K8s revival in Phase 6 happens inside /home/wizard/code/infra/. Commits use me@viktorbarzin.me as the author email (personal-repo convention).
Pattern crib-sheet (read once before starting):
- Service main loop pattern:
services/news_fetcher/main.py—asyncio.Eventshutdown, signal handlers,setup_telemetry, graceful close - Config pattern:
services/news_fetcher/config.py— extendsshared.config.BaseConfig,TRADING_env prefix - Model pattern:
shared/models/news.py— SQLAlchemy 2.0Mapped[]/mapped_column()style,TimestampMixinfrombase.py - Schema pattern:
shared/schemas/news.py— Pydantic v2BaseModel,Field(ge=…, le=…) - Migration pattern:
alembic/versions/b2c3d4e5f6a7_add_fundamentals_table.py—op.create_tablewith explicit columns, no autogen - API route pattern:
services/api_gateway/routes/news.py—APIRouter(prefix="/api/..."),_user=Depends(get_current_user),request.app.state.db_session_factory - Test pattern:
tests/services/test_news_fetcher.py— pytest-asyncio auto mode,MagicMock/AsyncMock, committed fixture files - Dashboard page pattern:
dashboard/src/pages/NewsFeed.tsx— TanStack Query, axios viaapi/client.ts, Tailwind onbg-slate-800cards - Sidebar nav pattern:
dashboard/src/components/Layout.tsx'snavItemsarray
Phase 1 — Data layer
Task 1: Pydantic schemas for the Meet Kevin pipeline
Files:
- Create:
shared/schemas/meet_kevin.py - Modify:
tests/test_schemas.py(append cases under a# --- Meet Kevin schemas ---section)
Required exports (match these names exactly — they're consumed by later tasks):
- Enums:
TickerAction(buy/sell/hold/watch/avoid),TimeHorizon(intraday/days/weeks/months/long_term/unspecified),MarketOutlook(bullish/neutral/bearish/mixed),VideoStatus(discovered/captioned/analyzed/failed/skipped),TranscriptSource(captions_manual/captions_auto/none) - LLM tool-input:
MeetKevinTickerMention(symbol uppercased via@field_validator, convictionField(ge=0.0, le=1.0)),MeetKevinAnalysis(market_outlook_direction, market_outlook_reasoning, macro_themes:list[str], key_risks:list[str], summary, tickers:list[MeetKevinTickerMention]) - API response shapes:
TranscriptSegment,VideoSummary,VideoDetail,StockSummary,StockMention,StockTimeline,TimelineBucket,PipelineHealth
Full field-level schemas in the spec under "Data model" and "LLM output schema".
-
Step 1: Write failing tests in
tests/test_schemas.pycovering: ticker mention validates, symbol auto-uppercases, conviction out of[0,1]raisespydantic.ValidationError, fullMeetKevinAnalysisvalidates with one ticker entry. -
Step 2: Run tests, verify they fail with
ModuleNotFoundError:
python -m pytest tests/test_schemas.py -v -k "meet_kevin"
-
Step 3: Implement
shared/schemas/meet_kevin.pyper the spec. Use Pydantic v2 (field_validator,model_config). -
Step 4: Re-run tests, verify pass.
-
Step 5: Commit
git add shared/schemas/meet_kevin.py tests/test_schemas.py
git -c user.email=me@viktorbarzin.me commit -m "feat: add Meet Kevin pydantic schemas (analysis + API shapes)"
Task 2: SQLAlchemy models
Files:
- Create:
shared/models/meet_kevin.py - Modify:
shared/models/__init__.py(add 5 imports +__all__entries) - Modify:
tests/test_models.py(append)
Tables (all use BigInteger PKs, TimestampMixin, JSONB for json columns, SAEnum with named types per the spec):
KevinChannel(tablekevin_channels) — youtube_channel_id unique, poll_enabled bool, poll_interval_seconds int default 10800, daily_cost_cap_usd Numeric(8,2) default 5.00, last_polled_atKevinVideo(tablekevin_videos) — channel_id FK, youtube_video_id unique+indexed, title, description, published_at indexed, duration_seconds, thumbnail_url, status enum + index, failure_reason, retry_count default 0, processed_atKevinTranscript(tablekevin_transcripts) — video_id unique FK, source enum, language, raw_text, segments_json JSONB, word_countKevinAnalysis(tablekevin_analyses) — video_id FK indexed, model, prompt_version, market_outlook_direction enum, market_outlook_reasoning, macro_themes_json + key_risks_json JSONB, summary, raw_response_json JSONB, prompt_tokens/completion_tokens int, cost_usd Numeric(10,4)KevinStockMention(tablekevin_stock_mentions) — video_id + analysis_id FKs, symbol indexed, action enum, conviction Numeric(4,3), time_horizon enum, rationale_quote, video_timestamp_seconds nullable
Each table establishes Mapped[]/mapped_column() relationships so the ORM nav matches the spec (videos → transcript, analyses, mentions).
-
Step 1: Add
test_meet_kevin_models_importabletotests/test_models.pyassertingKevinChannel.__tablename__ == "kevin_channels"(etc.) and thatKevinVideo.__table__.c.statusis an Enum containing "discovered". -
Step 2: Verify failure.
-
Step 3: Implement
shared/models/meet_kevin.py. -
Step 4: Update
shared/models/__init__.py— import the 5 classes and add them to__all__. -
Step 5: Re-run, verify pass.
-
Step 6: Commit
git add shared/models/meet_kevin.py shared/models/__init__.py tests/test_models.py
git -c user.email=me@viktorbarzin.me commit -m "feat: add Meet Kevin SQLAlchemy models (5 tables)"
Task 3: Alembic migration
Files:
- Create:
alembic/versions/c3d4e5f6a7b8_add_meet_kevin_tables.py
Requirements:
revision = "c3d4e5f6a7b8",down_revision = "b2c3d4e5f6a7"(current head frompython -m alembic heads)- Create 5 named ENUM types:
kevin_video_status,kevin_transcript_source,kevin_market_outlook,kevin_ticker_action,kevin_time_horizon op.create_tablefor all 5 tables matching the SQLAlchemy models exactly (column types, defaults, indexes)- Composite index
ix_kevin_stock_mentions_symbol_created_aton(symbol, created_at DESC)for ticker-timeline queries - Partial index
ix_kevin_videos_status_pendingonstatusWHERE status IN ('discovered', 'captioned')for the pipeline scan - Seed the one channel:
INSERT INTO kevin_channels (youtube_channel_id, title) VALUES ('UCUvvj5lwue7PspotMDjk5UA', 'Meet Kevin') downgrade()reverses everything including the 5 enum types
Pattern model: alembic/versions/b2c3d4e5f6a7_add_fundamentals_table.py.
- Step 1: Confirm current head
python -m alembic heads
# expected: b2c3d4e5f6a7 (head)
-
Step 2: Write the migration file.
-
Step 3: Apply against the docker-compose Postgres
docker compose up -d postgres
TRADING_DATABASE_URL=postgresql+asyncpg://trading:trading@localhost:5432/trading \
python -m alembic upgrade head
# expected: "Running upgrade b2c3d4e5f6a7 -> c3d4e5f6a7b8"
- Step 4: Verify 5 tables exist:
docker compose exec -T postgres psql -U trading -d trading -c "\dt kevin_*"
# expected: 5 rows
-
Step 5: Round-trip check (
downgrade -1shows 0 rows;upgrade headrestores). -
Step 6: Commit
git add alembic/versions/c3d4e5f6a7b8_add_meet_kevin_tables.py
git -c user.email=me@viktorbarzin.me commit -m "feat: add Alembic migration for Meet Kevin tables"
Phase 2 — Watcher service stages
Task 4: Service scaffold + config
Files:
- Create:
services/meet_kevin_watcher/__init__.py(empty) - Create:
services/meet_kevin_watcher/config.py
MeetKevinWatcherConfig extends BaseConfig, exposing (defaults shown):
-
meet_kevin_channel_id: str = "UCUvvj5lwue7PspotMDjk5UA" -
meet_kevin_poll_interval_seconds: int = 10800 -
meet_kevin_max_caption_retries: int = 5 -
meet_kevin_max_llm_retries: int = 3 -
meet_kevin_llm_model: str = "claude-sonnet-4-6" -
meet_kevin_prompt_version: str = "v1" -
meet_kevin_daily_cost_cap_usd: float = 5.0 -
anthropic_api_key: str = "" -
meet_kevin_workdir: str = "/tmp/meet_kevin_captions" -
otel_service_name: str = "meet-kevin-watcher" -
otel_metrics_port: int = 9097(next free port after market_data's 9096) -
Step 1: Create the dir, empty
__init__.py, and the config file. -
Step 2: Verify importable:
python -c "from services.meet_kevin_watcher.config import MeetKevinWatcherConfig; print(MeetKevinWatcherConfig().otel_metrics_port)"→9097. -
Step 3: Commit
git add services/meet_kevin_watcher/__init__.py services/meet_kevin_watcher/config.py
git -c user.email=me@viktorbarzin.me commit -m "feat: scaffold meet_kevin_watcher service + config"
Task 5: RSS poller
Files:
- Create:
services/meet_kevin_watcher/rss_poller.py - Create:
tests/services/meet_kevin_watcher/__init__.py(empty) - Create:
tests/services/meet_kevin_watcher/test_rss_poller.py - Create:
tests/fixtures/meet_kevin_rss.xml(real fixture)
Exports:
-
@dataclass(frozen=True) DiscoveredVideo(youtube_video_id, title, description, published_at, thumbnail_url) -
parse_feed(xml_bytes: bytes) -> list[DiscoveredVideo]— uses stdlibxml.etree.ElementTree, returns[]on parse error/empty -
async fetch_feed(channel_id: str, client: httpx.AsyncClient) -> bytes— GET with 15s timeout, returnsb""on HTTP error -
Atom namespaces:
{"a": "http://www.w3.org/2005/Atom", "y": "http://www.youtube.com/xml/schemas/2015", "m": "http://search.yahoo.com/mrss/"} -
Feed URL template:
https://www.youtube.com/feeds/videos.xml?channel_id={channel_id} -
Step 1: Capture real fixture
mkdir -p tests/fixtures
curl -sL "https://www.youtube.com/feeds/videos.xml?channel_id=UCUvvj5lwue7PspotMDjk5UA" \
-o tests/fixtures/meet_kevin_rss.xml
test -s tests/fixtures/meet_kevin_rss.xml || { echo "FAIL: empty fixture"; exit 1; }
-
Step 2: Write failing tests asserting:
parse_feed(fixture.read_bytes())returns ≥1DiscoveredVideowith 11-char video_id, title, published_at, thumbnail starting with httpsparse_feed(b"")andparse_feed(b"<?xml version='1.0'?><not-a-feed/>")both return[]
-
Step 3: Verify failure (ModuleNotFoundError).
-
Step 4: Implement
rss_poller.py. -
Step 5: Verify pass.
-
Step 6: Commit
git add services/meet_kevin_watcher/rss_poller.py \
tests/services/meet_kevin_watcher/__init__.py \
tests/services/meet_kevin_watcher/test_rss_poller.py \
tests/fixtures/meet_kevin_rss.xml
git -c user.email=me@viktorbarzin.me commit -m "feat(meet-kevin): RSS poller for YouTube uploads"
Task 6: Caption extractor
Files:
- Create:
services/meet_kevin_watcher/caption_extractor.py - Create:
tests/services/meet_kevin_watcher/test_caption_extractor.py - Create:
tests/fixtures/sample.srt(3-segment SRT mentioning NVDA)
Exports:
@dataclass(frozen=True) CaptionResult(source, language, raw_text, segments, word_count)parse_srt(text: str) -> list[dict]— segments with{start: float, end: float, text: str}. Handles SRT and yt-dlp's webvtt-converted variants;[]on empty/malformed.async extract_captions(video_id: str, workdir: str, sub_lang_glob: str = "en.*") -> CaptionResult | None— calls yt-dlp subprocess via the internal_run_yt_dlphelper, then parses any<video_id>*.srtleft in workdir. Prefers manual subs (no.auto.in filename) over auto-generated. Cleans up SRT files after parsing. ReturnsNoneif no SRT was produced.- Internal
async _run_yt_dlp(cmd: list[str], cwd: str) -> int— wrapsasyncio.create_subprocess_execfor testability (mocked in tests).
yt-dlp invocation:
yt-dlp --write-auto-sub --write-sub --sub-lang en.* --skip-download --convert-subs srt \
-o "<video_id>.%(ext)s" "https://www.youtube.com/watch?v=<video_id>"
-
Step 1: Create
tests/fixtures/sample.srtwith 3 segments (one mentioning NVDA). -
Step 2: Write tests:
parse_srtextracts 3 segments with correct start/end/textparse_srt("")returns[]extract_captionsreturns aCaptionResultwhen yt-dlp writes an SRT (mock_run_yt_dlpto return 0, pre-place SRT in tmp_path)extract_captionsreturnsNonewhen no SRT lands
-
Step 3: Verify failure.
-
Step 4: Implement the module.
-
Step 5: Verify pass.
-
Step 6: Commit
git add services/meet_kevin_watcher/caption_extractor.py \
tests/services/meet_kevin_watcher/test_caption_extractor.py \
tests/fixtures/sample.srt
git -c user.email=me@viktorbarzin.me commit -m "feat(meet-kevin): caption extractor via yt-dlp"
Task 7: LLM analyzer (Claude Sonnet 4.6 with tool-use forcing)
Files:
- Create:
services/meet_kevin_watcher/llm_analyzer.py - Create:
tests/services/meet_kevin_watcher/test_llm_analyzer.py
Exports:
@dataclass(frozen=True) LlmCallResult(analysis: MeetKevinAnalysis, raw_response: dict, prompt_tokens, completion_tokens, cost_usd: Decimal)SYSTEM_PROMPT(module constant) — analyst-style system instructions per the spec's "LLM output schema" section; MUST mentionsubmit_analysis(the tool name) and the structured contractcompute_cost_usd(model: str, input_tokens: int, output_tokens: int) -> Decimal— uses_PRICINGdict keyed by model; pinned prices in code:"claude-sonnet-4-6": (Decimal("3"), Decimal("15"))($/M input, $/M output); unknown model →Decimal("0")+ warning logclass LlmAnalyzer(client: AsyncAnthropic, model: str, prompt_version: str)— single methodasync analyze(title, description, published_at, transcript_text, transcript_segments) -> LlmCallResult
Anthropic call shape:
-
client.messages.create(model=..., max_tokens=4096, system=[{type: "text", text: SYSTEM_PROMPT, cache_control: {type: "ephemeral"}}], tools=[ANALYSIS_TOOL], tool_choice={type: "tool", name: "submit_analysis"}, messages=[{role: "user", content: <metadata + transcript>}]) -
ANALYSIS_TOOLdefines a JSON schema mirroringMeetKevinAnalysis(see spec) -
Response: pick the first
tool_usecontent block, validateblock.inputviaMeetKevinAnalysis.model_validate(...). RaiseValueErrorif notool_useblock (caller marks failed). -
Step 1: Write tests using
MagicMock/AsyncMockforanthropic.AsyncAnthropic:- Happy path: tool_use response with valid input → returns
LlmCallResultwith parsedMeetKevinAnalysis, correct token counts,cost_usd > 0 - No tool_use block in response →
ValueError("tool_use") compute_cost_usd("claude-sonnet-4-6", 1_000_000, 1_000_000) == Decimal("18.0000")compute_cost_usd("unknown-model", 1000, 1000) == Decimal("0")SYSTEM_PROMPTcontains"submit_analysis"and"ticker"
- Happy path: tool_use response with valid input → returns
-
Step 2: Verify failure.
-
Step 3: Implement the module.
-
Step 4: Verify pass.
-
Step 5: Commit
git add services/meet_kevin_watcher/llm_analyzer.py \
tests/services/meet_kevin_watcher/test_llm_analyzer.py
git -c user.email=me@viktorbarzin.me commit -m "feat(meet-kevin): Claude Sonnet 4.6 LLM analyzer (tool-use forcing + prompt cache)"
Task 8: Pipeline orchestrator + service main loop
Files:
- Create:
services/meet_kevin_watcher/pipeline.py - Create:
services/meet_kevin_watcher/main.py - Create:
tests/services/meet_kevin_watcher/test_pipeline.py
Exports from pipeline.py:
@dataclass(frozen=True) PipelineDeps(extract_captions, analyze, daily_cost_used, model, prompt_version, daily_cost_cap_usd: Decimal, workdir)— dependency injection container so the pipeline is unit-testableasync process_one_video(video: KevinVideo, session: AsyncSession, deps: PipelineDeps) -> str— runs the next pipeline stage and returns the new status. Status transitions:discovered→extract_captions(). None → status=failed, failure_reason="no_captions". Otherwise insertKevinTranscript, advance tocaptioned.captioned→ checkdaily_cost_used() < daily_cost_cap_usd. If over cap, leave ascaptioned(skip). Otherwise callanalyze(), on success insertKevinAnalysis+ NKevinStockMentionrows, advance toanalyzedand setprocessed_at. On exception, incrementretry_count; markfailedafter 3 retries.
async daily_cost_used(session) -> Decimal—SUM(cost_usd)fromKevinAnalysiswherecreated_at >= date_trunc('day', now())
main.py orchestrator (model: services/news_fetcher/main.py):
-
async run()entry point: load config, set up logging + OTEL + counters (meet_kevin.videos_discovered,meet_kevin.captions_extracted,meet_kevin.llm_calls,meet_kevin.llm_cost_usd), create DB engine + session factory, instantiateAsyncAnthropic+LlmAnalyzer, buildPipelineDeps, install SIGTERM/SIGINT signal handlers onshutdown_event. -
Main loop: poll RSS for each
poll_enabledchannel (useINSERT … ON CONFLICT (youtube_video_id) DO NOTHING RETURNING idto dedupe), then walk all videos with status in("discovered", "captioned")ordered bypublished_at ASC, callprocess_one_videofor each, commit per-video. -
After each iteration,
await asyncio.wait_for(shutdown_event.wait(), timeout=poll_interval_seconds)so shutdown is responsive. -
On exit:
await anthropic.close(),await engine.dispose(). -
if __name__ == "__main__": asyncio.run(run()) -
Step 1: Write 3 pipeline tests using
MagicMockforKevinVideo+AsyncMockfor session:- Happy path:
discovered→captioned→analyzed, both deps awaited once - No captions: returns
"failed", setsfailure_reason="no_captions",analyzeNOT awaited - Cost cap exceeded: status stays
"captioned",analyzeNOT awaited
- Happy path:
-
Step 2: Verify failure.
-
Step 3: Implement
pipeline.pythenmain.py. -
Step 4: Verify all watcher tests pass
python -m pytest tests/services/meet_kevin_watcher/ -v
# expected: ~14 passing (2 RSS + 4 caption + 5 LLM + 3 pipeline)
- Step 5: Commit
git add services/meet_kevin_watcher/pipeline.py \
services/meet_kevin_watcher/main.py \
tests/services/meet_kevin_watcher/test_pipeline.py
git -c user.email=me@viktorbarzin.me commit -m "feat(meet-kevin): pipeline orchestrator + service main loop"
Phase 3 — API gateway
Task 9: /api/meet-kevin/* routes
Files:
- Create:
services/api_gateway/routes/meet_kevin.py - Modify:
services/api_gateway/main.py— import +app.include_router(meet_kevin_router)next to the other route registrations - Create:
tests/api_gateway/routes/__init__.py(empty, if missing) - Create:
tests/api_gateway/routes/test_meet_kevin.py
Router pattern (model: services/api_gateway/routes/news.py):
router = APIRouter(prefix="/api/meet-kevin", tags=["meet-kevin"])- All endpoints behind
_user=Depends(get_current_user) - DB access via
request.app.state.db_session_factory
Endpoints to implement (full request/response shapes in the spec's "API surface" section):
GET /health— counts by status fromkevin_videos, last poll timestamp fromkevin_channels, daily cost fromSUM(kevin_analyses.cost_usd) WHERE created_at >= date_trunc('day', now()), cap from channel rowGET /channels— list allKevinChannelrowsPATCH /channels/{id}— accept body fieldspoll_enabled,poll_interval_seconds,daily_cost_cap_usdGET /videos?status=&q=&page=&per_page=— paginated, ordered bypublished_at DESC, withtop_tickerspopulated by joining latestkevin_stock_mentionsper video (ordered by conviction DESC), andoutlook/one_line_summaryfrom the latestkevin_analysesrow per videoGET /videos/{id}— single video + analysis (latest by created_at) + first 5 ticker mentions ordered by conviction DESC +transcript_available: boolGET /videos/{id}/transcript— return segments_json + source + language; 404 if missingPOST /videos/{id}/reprocess?stage=captions|analysis|auto— reset status: failed→discovered for captions/auto; captioned|failed|analyzed→captioned for analysis; resetretry_countandfailure_reasonGET /stocks— distinct symbols withmention_count,last_seen_at,latest_action(most recent mention),latest_conviction,avg_convictionGET /stocks/{symbol}— all mentions for the uppercased symbol joined with their videos, ordered bypublished_at DESC; 404 if noneGET /stocks/{symbol}/timeline?bucket=day|week—date_trunc(bucket, created_at)aggregation:avg_conviction,mention_count,net_action_score(sum of conviction for buy minus sum of conviction for sell, others = 0)GET /dashboard— latest analyzed video + its analysis + top 5 mentions, plus top conviction last 7 days (SELECT symbol, max(conviction), count() ... GROUP BY symbol LIMIT 10), plus 14-day outlook trend (date_trunc('day', kevin_analyses.created_at)×market_outlook_directiongrouping)
-
Step 1: Write 3 API tests using FastAPI
TestClient:GET /api/meet-kevin/health→ 200 withcounts_by_status,daily_cost_usd,daily_cost_cap_usdkeysGET /api/meet-kevin/videos(empty DB) →{"videos": [], "total": 0, "page": 1, "per_page": 20}GET /api/meet-kevin/stocks(empty DB) →{"stocks": []}
Use
monkeypatch.setenv("TRADING_DEV_MODE_AUTH_BYPASS", "true")if the repo has that flag, otherwise stubget_current_uservia dependency override. -
Step 2: Verify failure (404 — router not registered yet).
-
Step 3: Implement the route module.
-
Step 4: Register the router in
main.py. -
Step 5: Verify pass.
-
Step 6: Commit
git add services/api_gateway/routes/meet_kevin.py \
services/api_gateway/main.py \
tests/api_gateway/routes/test_meet_kevin.py \
tests/api_gateway/routes/__init__.py 2>/dev/null
git -c user.email=me@viktorbarzin.me commit -m "feat(api): /api/meet-kevin/* routes (11 endpoints)"
Phase 4 — Dashboard
The repo has no FE unit test suite. For each FE task, verification is
npm run build(zero TS/ESLint errors) followed by manual browser checks once the backend is up (Task 17).
Task 10: TypeScript types + API client
Files:
- Create:
dashboard/src/types/meetKevin.ts - Create:
dashboard/src/api/meetKevin.ts
Types (one-to-one with the spec's API response shapes): TickerAction, TimeHorizon, MarketOutlook, VideoStatus, VideoSummary, TickerMention, VideoAnalysis, VideoDetail, TranscriptSegment, Transcript, StockSummary, StockMention, PipelineHealth, DashboardData.
API client (meetKevinApi) — thin wrappers on the shared axios client from dashboard/src/api/client.ts. Methods: health, dashboard, listVideos(params), getVideo(id), getTranscript(id), reprocess(id, stage), listStocks, getStock(symbol), getStockTimeline(symbol, bucket). Each method calls client.get/post with the path under /meet-kevin/... (the shared client already has /api as its base).
-
Step 1: Create the two files.
-
Step 2: Verify TS compiles
cd dashboard && npm run build && cd ..
- Step 3: Commit
git add dashboard/src/types/meetKevin.ts dashboard/src/api/meetKevin.ts
git -c user.email=me@viktorbarzin.me commit -m "feat(dashboard): Meet Kevin TypeScript types + API client"
Task 11: Reusable Meet Kevin components
Files:
- Create:
dashboard/src/components/meetKevin/ActionChip.tsx - Create:
dashboard/src/components/meetKevin/ConvictionBar.tsx - Create:
dashboard/src/components/meetKevin/YouTubeEmbed.tsx - Create:
dashboard/src/components/meetKevin/index.ts(barrel export)
ActionChip — accepts action: TickerAction prop. Color-coded chip per action: buy=green, sell=red, hold=slate, watch=yellow, avoid=rose. Tailwind classes only, no new CSS.
ConvictionBar — accepts value: number (0-1). Renders a clipped div with a blue fill bar (bg-blue-400 over bg-slate-700), width = pct%.
YouTubeEmbed — forwardRef<YouTubeEmbedHandle, {videoId, startSeconds?}>. Renders an <iframe> pointed at https://www.youtube-nocookie.com/embed/<id>?start=<n>&enablejsapi=1. useImperativeHandle exposes seekTo(seconds) which postMessages {event: "command", func: "seekTo", args: [seconds, true]} to the iframe's contentWindow. The component uses the standard 16:9 aspect ratio via pb-[56.25%].
-
Step 1: Implement all 4 files.
-
Step 2: Verify build
cd dashboard && npm run build && cd ..
- Step 3: Commit
git add dashboard/src/components/meetKevin/
git -c user.email=me@viktorbarzin.me commit -m "feat(dashboard): reusable Meet Kevin components (ActionChip, ConvictionBar, YouTubeEmbed)"
Task 12: Home page (/meet-kevin)
File: Create dashboard/src/pages/meetKevin/Home.tsx
Behavior:
-
Uses
useQuerykeyed['meet-kevin', 'dashboard']callingmeetKevinApi.dashboard, refetch every 60s. -
Uses
useQuerykeyed['meet-kevin', 'health']callingmeetKevinApi.health, refetch every 60s. -
Empty state (no
latest_video): "Waiting for first poll" card withlast_poll_atformatted. -
Otherwise: latest video hero card (thumbnail + outlook badge + title + ticker chips + one-line summary, linking to
/meet-kevin/videos/<id>), top conviction list (last 7 days, withConvictionBarper row, each row linking to/meet-kevin/stocks/<symbol>), and an outlook trendlinerecharts.LineChartover the 14-day data (map outlookbullish/neutral/mixed/bearishto+1/0/0/-1for the numeric Y-axis). -
Tailwind aesthetic:
bg-slate-800 border border-slate-700 rounded-xl p-5cards (matches existing dashboard). -
Step 1: Implement the page.
-
Step 2: Build verification.
-
Step 3: Commit
git add dashboard/src/pages/meetKevin/Home.tsx
git -c user.email=me@viktorbarzin.me commit -m "feat(dashboard): Meet Kevin home page"
Task 13: Videos feed page (/meet-kevin/videos)
File: Create dashboard/src/pages/meetKevin/Videos.tsx
Behavior:
-
Filters:
<select>for status,<input>for title search; both bumppageto 1 on change. -
useQuerycallsmeetKevinApi.listVideos({ status, q, page, per_page: 20 }). -
Renders 2-column grid (
md:grid-cols-2) of video cards: thumbnail (left), title + date + status badge + failure_reason + ticker chips. -
Each card links to
/meet-kevin/videos/<id>. -
Pagination Prev/Next buttons (model:
NewsFeed.tsx). -
Step 1: Implement.
-
Step 2: Build verification.
-
Step 3: Commit
git add dashboard/src/pages/meetKevin/Videos.tsx
git -c user.email=me@viktorbarzin.me commit -m "feat(dashboard): Meet Kevin videos feed page"
Task 14: Video detail page (/meet-kevin/videos/:id)
File: Create dashboard/src/pages/meetKevin/VideoDetail.tsx
Behavior:
-
useParams<{id: string}>()→videoId: number. -
useQueryforgetVideo(videoId); seconduseQueryforgetTranscript(videoId)enabled only whendetail.transcript_available. -
useMutationforreprocess(videoId, stage)— visible button whenstatusisfailedordiscovered; invalidates the video query on success. -
Layout: title + published date, embedded
<YouTubeEmbed ref={playerRef} videoId={v.youtube_video_id} />, tab strip (Analysis / Transcript / Raw JSON).- Analysis tab: outlook card (banner + reasoning), macro themes chips, key risks bulleted list, summary card, per-ticker grid — each ticker row has
<ActionChip>,<ConvictionBar>, time horizon, italicizedrationale_quote, and a "▶ MM:SS" button callingplayerRef.current?.seekTo(video_timestamp_seconds). - Transcript tab: scrollable list of segments; clicking a segment seeks the iframe to
Math.floor(start). - Raw JSON tab:
<pre>{JSON.stringify(detail.analysis, null, 2)}</pre>.
- Analysis tab: outlook card (banner + reasoning), macro themes chips, key risks bulleted list, summary card, per-ticker grid — each ticker row has
-
Disable Transcript tab if
transcript_availableis false. -
Step 1: Implement.
-
Step 2: Build verification.
-
Step 3: Commit
git add dashboard/src/pages/meetKevin/VideoDetail.tsx
git -c user.email=me@viktorbarzin.me commit -m "feat(dashboard): Meet Kevin video detail page (tabs + iframe + deep-links)"
Task 15: Stocks list + drill-down
Files:
- Create:
dashboard/src/pages/meetKevin/Stocks.tsx - Create:
dashboard/src/pages/meetKevin/StockDetail.tsx
Stocks.tsx:
useQuerycallsmeetKevinApi.listStockswith 60s refetch.- Renders a table: Symbol (link to
/meet-kevin/stocks/<symbol>), Mentions, Last seen, Latest (ActionChip + percent), Avg conviction (ConvictionBar). - Empty state: "No analyses yet" card.
StockDetail.tsx:
-
useParams<{symbol: string}>()uppercased. -
Two
useQuerys:getStock(sym)andgetStockTimeline(sym, "day"). -
Layout: large
$SYMBOLheader + back link, line chart ofnet_action_scoreover time (recharts), then chronological mention list. Each mention card links to its source/meet-kevin/videos/<id>, shows date, ActionChip, time horizon, ConvictionBar, percent, video title, and italicizedrationale_quote. -
Step 1: Implement both pages.
-
Step 2: Build verification.
-
Step 3: Commit
git add dashboard/src/pages/meetKevin/Stocks.tsx dashboard/src/pages/meetKevin/StockDetail.tsx
git -c user.email=me@viktorbarzin.me commit -m "feat(dashboard): Meet Kevin stocks list + per-ticker drill-down"
Task 16: Wire routes + sidebar
Files:
- Modify:
dashboard/src/App.tsx— import the 5 page components and add 5 routes inside the protected<Route path="/" element={...}>block, BEFORE the catch-all redirect:
<Route path="meet-kevin" element={<MeetKevinHome />} />
<Route path="meet-kevin/videos" element={<MeetKevinVideos />} />
<Route path="meet-kevin/videos/:id" element={<MeetKevinVideoDetail />} />
<Route path="meet-kevin/stocks" element={<MeetKevinStocks />} />
<Route path="meet-kevin/stocks/:symbol" element={<MeetKevinStockDetail />} />
- Modify:
dashboard/src/components/Layout.tsx— append tonavItems:
{ to: '/meet-kevin', label: 'Meet Kevin', icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z' },
(Heroicon outline play-circle path — matches the video theme.)
-
Step 1: Apply both edits.
-
Step 2: Build verification + verify nav link renders.
-
Step 3: Commit
git add dashboard/src/App.tsx dashboard/src/components/Layout.tsx
git -c user.email=me@viktorbarzin.me commit -m "feat(dashboard): wire Meet Kevin routes + sidebar entry"
Task 17: Manual QA against docker-compose
Verification only — no commit (unless smoke notes are added).
-
Step 1: Boot infra:
docker compose up -d postgres redis, thenpython -m alembic upgrade head. -
Step 2: Start api-gateway with
TRADING_DEV_MODE_AUTH_BYPASS=true python -m services.api_gateway.main &. -
Step 3: Insert one fake row set via psql so the UI has data to render (one
kevin_videosanalyzed, onekevin_analyses, onekevin_stock_mentionsfor NVDA with conviction 0.82). Use theINSERT … RETURNING id \\gsetpattern with psql. -
Step 4: Start the dashboard:
cd dashboard && VITE_DEV_MODE=true VITE_API_BASE_URL=http://localhost:8000/api npm run dev. -
Step 5: Walkthrough at
http://localhost:5173/meet-kevin:- Home renders latest video card + NVDA in top conviction list
/meet-kevin/videosshows the fake row/meet-kevin/videos/<id>opens the iframe; the NVDA ticker card's "▶" deep-link calls seekTo on the player/meet-kevin/stocksshows NVDA/meet-kevin/stocks/NVDAshows the timeline + mention card
-
Step 6: Cleanup:
docker compose down; kill the api-gateway process.
Phase 5 — Container & dependencies
Task 18: Add meet_kevin extras + Dockerfile
Files:
- Modify:
pyproject.toml— under[project.optional-dependencies]add:
meet_kevin = ["yt-dlp>=2026.04", "feedparser>=6.0", "anthropic>=0.40", "httpx>=0.27"]
- Modify:
docker/Dockerfile.service— update theuv pip install --system --no-cache-dir ".[api,news,sentiment,trading,backtester]"line to also includemeet_kevin:
uv pip install --system --no-cache-dir ".[api,news,sentiment,trading,backtester,meet_kevin]" && \\
-
Step 1: Apply both edits.
-
Step 2: Sanity-check locally:
pip install -e ".[meet_kevin]"(should resolve cleanly). -
Step 3: Commit
git add pyproject.toml docker/Dockerfile.service
git -c user.email=me@viktorbarzin.me commit -m "feat: add meet_kevin extras (yt-dlp, feedparser, anthropic)"
Task 19: Push → CI build
-
Step 1:
git push origin master -
Step 2: Verify Woodpecker picked up the latest pipeline (response of
GET /api/repos/viktor/trading/pipelinesshowsrunningorpendingmatching the head commit). -
Step 3: Wait for pipeline transition to terminal state via an
untilloop polling Woodpecker. Do not pre-sleep — let the condition drive cadence (~30s checks). Expected:success. -
Step 4: Confirm DockerHub got a fresh tag matching the head SHA short-form (
hub.docker.com/v2/repositories/viktorbarzin/trading-bot-service/tags).
Phase 6 — K8s revival
Task 20: Vault secrets
-
Step 1:
vault login -method=oidc -
Step 2: Patch
secret/trading-bot:
read -rs ANTHROPIC_KEY; echo
vault kv patch secret/trading-bot \
anthropic_api_key="$ANTHROPIC_KEY" \
meet_kevin_channel_id="UCUvvj5lwue7PspotMDjk5UA"
unset ANTHROPIC_KEY
- Step 3: Verify with
vault kv get -field=meet_kevin_channel_id secret/trading-bot.
Task 21: Uncomment + edit infra/stacks/trading-bot/main.tf
Workspace: this happens in /home/wizard/code/infra/, NOT the trading repo. Required action: claim presence for the infra stack before applying (per /home/wizard/code/CLAUDE.md "Agent Presence" rule):
~/code/scripts/presence claim stack:trading-bot --purpose "Phase 6 revival — uncomment stack + add meet-kevin-watcher container"
-
Step 1: Remove the outer
/* … */block-comment (lines ~2 and ~631). -
Step 2: In the
external_secretresource'stemplate.dataanddata = […], add 2 keys:TRADING_ANTHROPIC_API_KEY←anthropic_api_key, andTRADING_MEET_KEVIN_CHANNEL_ID←meet_kevin_channel_id. Both referencekey = "trading-bot"(the existing Vault path). -
Step 3: In
locals.common_env, add:
TRADING_MEET_KEVIN_POLL_INTERVAL_SECONDS = "10800"
TRADING_MEET_KEVIN_DAILY_COST_CAP_USD = "5"
TRADING_MEET_KEVIN_LLM_MODEL = "claude-sonnet-4-6"
TRADING_MEET_KEVIN_PROMPT_VERSION = "v1"
-
Step 4: Delete the three worker containers from the
trading-bot-workersPod spec:news-fetcher,sentiment-analyzer,trade-executor. -
Step 5: Add the new
meet-kevin-watchercontainer to the same Pod spec (aftermarket-data):
container {
name = "meet-kevin-watcher"
image = "viktorbarzin/trading-bot-service:latest"
image_pull_policy = "Always"
command = ["python", "-m", "services.meet_kevin_watcher.main"]
dynamic "env" {
for_each = local.common_env
content {
name = env.key
value = env.value
}
}
env { name = "TRADING_OTEL_METRICS_PORT"; value = "9097" }
env_from { secret_ref { name = "trading-bot-secrets" } }
env_from { secret_ref { name = "trading-bot-db-creds" } }
resources {
requests = { cpu = "10m", memory = "128Mi" }
limits = { memory = "256Mi" }
}
}
-
Step 6: Final container order is signal-generator [0], learning-engine [1], market-data [2], meet-kevin-watcher [3]. Update
lifecycle.ignore_changesto only ignore image on container indices 0–3 plusdns_config(drop the index-4 and index-5 lines that were there for the 6-container layout). -
Step 7:
scripts/tg plan— verify the plan creates the namespace + secrets + Deployments + Service + Ingress, with the new container in the workers pod. -
Step 8:
scripts/tg apply --non-interactive(expectApply complete!). -
Step 9: Verify K8s:
kubectl get ns trading-bot
kubectl -n trading-bot get pods,deploy,svc,ingress
kubectl -n trading-bot get pod -l app=trading-bot-workers -o jsonpath='{.items[0].spec.containers[*].name}'
# expected: signal-generator learning-engine market-data meet-kevin-watcher
- Step 10: Commit + push the infra change
git -C /home/wizard/code/infra add stacks/trading-bot/main.tf
git -C /home/wizard/code/infra -c user.email=me@viktorbarzin.me \
commit -m "trading-bot: revive K8s stack + add meet-kevin-watcher; remove 3 disabled workers"
git -C /home/wizard/code/infra push
- Step 11:
~/code/scripts/presence release stack:trading-bot.
Task 22: Production smoke test
- Step 1: Tail watcher logs
kubectl -n trading-bot logs -l app=trading-bot-workers -c meet-kevin-watcher --tail=200 -f
Expected within ~60s: Starting meet-kevin-watcher, then RSS poll: N new videos (likely up to 15 from first-run backfill).
-
Step 2: Poll until at least one video reaches
status='analyzed'(use anuntil [ $(SELECT count(*)) -ge 1 ]loop with sleep 30s — no pre-sleep). -
Step 3: Hit the API:
curl -sk https://trading.viktorbarzin.me/api/meet-kevin/health(with appropriate auth cookie) — expectcounts_by_status.analyzed >= 1,daily_cost_usd > 0. -
Step 4: Open
https://trading.viktorbarzin.me/meet-kevinin a browser and verify the home + videos + stocks pages render with real Meet Kevin data.
Self-review (run inline before publishing)
-
Spec coverage: each design-doc section maps to one or more tasks:
- Channel + seed → Task 3
- Data model (5 tables) → Tasks 2, 3
- Pipeline 3 stages → Tasks 5, 6, 7, 8
- LLM output schema → Tasks 1, 7
- Daily cost cap → Task 8 (logic) + Task 9 (
/healthsurface) - 11 API endpoints → Task 9
- 5 dashboard pages → Tasks 12, 13, 14, 15
- Container changes → Task 18
- Vault secrets → Task 20
- K8s revival (Terraform edit) → Task 21
- Observability metrics → Task 8 main loop (
setup_telemetry+ counters) - Failure modes → Tasks 5-8 (per-stage retry / status logic) + Task 9 (
reprocessendpoint) - First-run backfill (~$1.50 burst) → Tasks 8 + 22 (covered by the daily cost cap)
-
Placeholder scan: no TBD / "implement later" / "similar to Task N". Where a section references "see spec", the spec section is named explicitly and the spec is committed at
ab382af(immutable for this plan). -
Type consistency: schema/class/enum/table names match across tasks (
MeetKevinAnalysis,KevinVideo,kevin_stock_mentions,TickerAction.SELL, env prefixTRADING_MEET_KEVIN_*, metrics port9097).
Execution
Two options:
- Subagent-Driven (recommended) — fresh subagent per task with two-stage review between tasks. Uses
superpowers:subagent-driven-development. - Inline execution — work through tasks in this session with periodic checkpoints. Uses
superpowers:executing-plans.