Session Closeout: Autonomous Trading Agent (2026-04-17)

## Session Closeout: Autonomous Trading Agent (2026-04-17)

### What Was Built
A fully autonomous swing trading agent at `~/repos/trading-agent/` that manages an Alpaca paper trading account ($100k). The system operates on a cron schedule during market hours, consumes differentiated data sources, and makes trading decisions through Claude CLI.

### Architecture
“`
SEC EDGAR → ┐
FRED API → ┤
Alpaca News →├→ collector (PM2) → SQLite → run.sh (cron) → Claude CLI
EIA Petroleum→┤ ↓
Market Data → ┘ TRADE_SIGNAL blocks

executor.py → risk.py → Alpaca API

Discord #trading
“`

### Trading Strategies
| Strategy | Allocation | Edge |
|———-|———–|——|
| News Reactive | 40% | Speed — act on breaking news before market prices it |
| Alt Data Deep Dive | 35% | Information — EIA petroleum, vessel tracking, trends |
| Insider Following | 15% | Form 4 insider buys >$100k, cluster buys |
| Macro Rotation | 10% | FRED yield curve, employment, CPI → sector ETFs |

### Risk Engine (All Deterministic Python)
– Max 25% per position, 5 position cap
– -3% daily circuit breaker halts all trading
– 10% cash reserve, 8% trailing stops
– No shorts, margin, options, or leveraged ETFs
– Claude proposes trades; code enforces limits

### First Trading Run Results
Claude analyzed SEC filings (14 insider trades), FRED data (unemployment 4.3%, yield curve +0.55%, WTI $100.72, VIX 17.94), and 8 news articles. Three trades queued for Monday:

1. **LCII** (50 shares) — Merger arb: Patrick Industries confirmed acquisition discussions
2. **XLE** (100 shares) — Macro rotation: steepening yield curve + $100 oil supports energy
3. **META** (5 shares) — Layoff margin play: 10% workforce cut announced for May 20

Total deployment: ~$12,940 (13% of portfolio)

### Technical Details
– **5 commits**, 3,473 lines across 24 files
– PM2 `trading-collector` service: polls 6 data sources on schedule
– Cron: `*/30 13-19 * * 1-5` (every 30min during market hours)
– Per-strategy virtual sub-accounts for independent P&L tracking
– Research agent can spawn Claude deep-dives on high-signal tickers
– Discord #trading channel with trade notifications + daily/weekly reports

### Data Flow Verified
– SEC EDGAR: 14 Form 4 insider trades (AAPL, META, AMZN, MSFT, JPM)
– FRED: 10 indicators including VIX, oil, yield curve, HY spreads
– Alpaca News: 15 articles, 5 triggered (including Strait of Hormuz headline)
– Market Data: Live prices for all 27 watchlist tickers

Browser Agent: FB Timeout Root Cause + React Input Fix (2026-04-17)

Context

Browser-agent commands on Facebook timed out after 20 seconds. Root cause: the TM script had a hardcoded 20s per-command timeout, while FB pages take 60-120s per command due to heavy React rendering.

Root Causes Found & Fixed

  • 20s TM script timeout: Increased to 60s default. Server now passes CLI timeout to TM script (was silently dropped before).
  • 60s server cap: Increased to 300s.
  • clickAny hitting validation errors: Two-pass matching — exact matches preferred over startsWith.
  • React ignoring input events: Reset _valueTracker, use InputEvent with inputType, execCommand(‘insertText’).
  • Condition dropdown selector: Use [aria-haspopup=”listbox”] + clickAny “Used – Good” (capital G).

Releases

  • browser-agent v1.14.0: timeout fix + clickAny precision
  • browser-agent v1.14.1-2: React InputEvent + _valueTracker
  • browser-agent v1.15.0: execCommand insertText
  • Extension v1.2.0: CDP trusted input via chrome.debugger
  • fb-marketplace-poster: curl extraction, timeout increase, selector fixes

What Works Now on FB

Title, price, description, photo upload, condition dropdown, Next/Publish all work. Category autocomplete remains manual — FB uses anti-automation checks beyond isTrusted.

Listing Published

Lumos Kickstart Bike Helmet – Black, $40, Bicycle Accessories, Used – Good, 3 photos.

Full closeout: privateContext/deliverables/closeouts/2026-04-17-browser-agent-fb-timeout-fix.md

Session Closeout: Live Dashboard at pezant.ca/dashboard (2026-04-17)

# Session Closeout: Live Dashboard at pezant.ca/dashboard (2026-04-17)

## Summary
Built and deployed a live infrastructure dashboard at `pezant.ca/dashboard`, wiring up the existing frontend skeleton to real data sources and fixing the OAuth return-to-origin flow.

## What Was Done

### 1. Apache Proxy Configuration
– Added `RewriteRule` for `/dashboard` → port 3003 in Apache config on VM
– Added OIDC exemption for `POST /tools/dashboard/api/metrics` (placed **after** the `/tools` OIDC block so it overrides correctly)
– Reloaded Apache — dashboard accessible at `pezant.ca/dashboard`

### 2. Metrics Push Script (`scripts/push-metrics.sh`)
– Created push script that runs locally in WSL every 5 minutes
– Collects **Claude usage** (5h/7d windows with percentages and reset times) from `check-usage.sh` cache
– Collects **autonomous dev** stats (run number, last cost, exit code) from `state.json`
– POSTs assembled JSON to `https://pezant.ca/tools/dashboard/api/metrics` with Bearer auth
– Registered as PM2 cron job (`dashboard-push`, `*/5 * * * *`)

### 3. pezantTools Deployment
– Merged ~40 commits from `main` into VM’s `production` branch
– Brought dashboard view, routes, lib/dashboard.js, and auth module to production
– Resolved untracked file conflicts on VM (stash + clean + merge)

### 4. OAuth Return-to-Origin Fix
– `requireOAuth` middleware now saves `req.originalUrl` to `req.session.returnTo`
– OAuth callback reads `returnTo` from session instead of hardcoding `/tools/upload`
– Visiting `/dashboard` while logged out now correctly returns to `/dashboard` after Google login

## Technical Details

### Data Flow
“`
WSL (every 5 min) VM (pezant-tools)
┌──────────────────┐ HTTPS POST ┌─────────────────────┐
│ push-metrics.sh │ ──────────────→ │ /tools/dashboard/ │
│ • check-usage │ Bearer auth │ api/metrics │
│ • autonomousDev │ │ → dashboard- │
│ /state.json │ │ metrics.json │
└──────────────────┘ └─────────────────────┘

┌─────────────────────┐
│ Dashboard EJS view │
│ • PM2 (live query) │
│ • System (live) │
│ • Claude usage │
│ • Autodev stats │
│ 30s auto-refresh │
└─────────────────────┘
“`

### Apache OIDC Ordering Lesson
`` with `AuthType None` must appear **after** `` with `AuthType openid-connect`. Apache processes Location blocks in order — later blocks override earlier ones for the same request. Placing the exemption before `/tools` has no effect.

### Files Changed
| File | Change |
|——|——–|
| `pezantTools/scripts/push-metrics.sh` | **New** — metrics collection + push |
| `pezantTools/lib/auth.js` | Added `returnTo` session save |
| `pezantTools/server.js` | OAuth callback reads `returnTo` |
| `pezantTools/CLAUDE.md` | Documented dashboard feature |
| `pezantTools/lib/dashboard.js` | Already existed, deployed to VM |
| `pezantTools/views/dashboard.ejs` | Already existed, deployed to VM |
| VM Apache config | Added `/dashboard` proxy + OIDC exemption |

### Commits
– `6f40984` — Add dashboard metrics push script
– `e89c7af` — Add live dashboard with PM2, system health, and metrics widgets
– `3be857c` — Fix datetime deprecation warning in push-metrics.sh
– `b99a213` — Document dashboard in CLAUDE.md
– `ab00b6d` — Redirect back to original URL after OAuth login

## What’s Not Wired Yet (v2)
– **Agent Journal widget** — needs Discord API read or local journal log file
– **Free Games widget** — needs aggregation from free-games-pipeline data
– **Uptime monitor** — `/tmp/uptime-monitor-state.json` on VM could feed a new widget
– **Git activity** — cross-repo commit/PR stats via `gh` CLI
– **Data staleness indicator** — `pushed_at` timestamp exists but not shown in UI

## Verification
– `pezant.ca/dashboard` → redirects to Google login → returns to dashboard after auth
– PM2 Services widget shows 21 processes with status/memory/uptime
– System Health shows disk/memory/load
– Claude Usage shows 36% (5h) and 36% (7d) with reset timers
– Autonomous Dev shows run #2, $3.01 cost
– Metrics push runs every 5 min via PM2 cron `dashboard-push`

Job Landscape Survey & Application Materials (2026-04-16)

Session Summary

3-hour session reactivating job search. Created a landscape survey of 35+ elite-caliber roles, established a quality bar, prepped OpenAI application materials, and identified Thinking Machines Lab as a new target.

Key Outcomes

  • Caliber bar established: 5-criteria filter (company prestige, seniority, comp trajectory, AI centrality, direct posting link) codified across pipeline config, rules, and wiki
  • OpenAI: 3 roles selected (Personalization, API Agents, Safety Measurement). Applied to 2 during session. Discovered no cover letter field exists – only Additional Information text box
  • Thinking Machines Lab: Research PM at Mira Murati’s $2B frontier AI lab. Materials prepped. Warm intro path via OpenAI connections identified
  • Resume updated: GDrive resume refreshed with current numbers (30+ repos, 260+ sessions, 10+ apps, browser agent, learning agent, knowledge wiki)
  • Process gaps fixed: Voice source of truth (my-voice repo), liveness verification (company careers page not aggregators), resume sourcing (GDrive not local variants)

Open Items

  • Apply to OpenAI API Agents
  • Apply to Thinking Machines Lab + draft warm intro outreach
  • Decide on non-AI roles (Roblox, Reddit, Snap, Affirm)
  • Liveness audit for other pipeline companies

Job Pipeline Audit: Filter Fixes, Integrations, Reporting (2026-04-16)

Context

Full audit of the job pipeline system. The filter was too aggressive, excluding real AI roles at Sierra, Harvey, Anyscale, and Bay Area locations like Redwood City and Foster City. 254 roles were stuck at outreach_drafted with no nudge mechanism. No Discord or Sheets integration.

What Changed

Filter Fixes (highest impact)

  • Location: Expanded allowlist with 12 Bay Area cities + united states, usa, anywhere
  • AI-native companies: 30 companies (Sierra, Harvey, Anyscale, ElevenLabs, etc.) now auto-pass domain filter without inflating fit scores
  • Director PM: Director of Product Management correctly detected as PM role (was being rejected as Not a PM role)

New Features

  • Stale outreach nudge: Daily digest now surfaces roles stuck at outreach_drafted for 7+ days. New pipeline stale CLI command.
  • Data cleanup: Auto-archives REJECTED/ARCHIVED jobs older than 30 days to recoverable JSONL
  • Discord #top-jobs: New channel with webhook, posts new strong-fit roles daily
  • Google Sheets sync: 305 active jobs synced to Pipeline Auto tab in Job Search spreadsheet
  • CLAUDE.md: Full system documentation for the pipeline

Test Coverage

219 tests passing (was 169). +50 new tests covering filter fixes, stale reports, resume variant selection, cover letter generation, and outreach drafting.

Key Decisions

  • AI_NATIVE_COMPANIES separate from Tier 1/2: bypasses domain filter but does not boost scoring
  • Removed us from location list (matched Austin). usa and united states cover it
  • Created Pipeline Auto sheet tab to avoid overwriting hand-curated Pipeline tab
  • Discord self-service: created channel + webhook via bot API instead of asking user

Open Items

  1. Re-filter 130 excluded roles with new filter logic
  2. Triage 254 stale outreach drafts
  3. Configure google-api-python-client for automated daily Sheets sync

Commit: cd231e4 on assortedLLMTasks/main, 12 files, 781 insertions

Anthropic Recruiter Reply + Browser Agent Public Release

# Anthropic Recruiter Reply + Browser Agent Public Release

**Date:** 2026-04-10
**Duration:** ~45 minutes
**Repos touched:** assortedLLMTasks, claude-browser-agent (new)

## Context & Motivation

Laura Small (Anthropic recruiter) reached out about the PM Consumer role referral. The HM had already reviewed Nick’s profile and leaned toward candidates with more founder experience, so the reply needed to hedge toward other roles while demonstrating deep product usage.

Session evolved from drafting the email reply into creating a public-ready version of the browser-agent repo to strengthen the project portfolio shared with the recruiter.

## Decisions Made

### 1. Email Positioning Strategy
– **Decision:** Lead with PM Consumer excitement but prominently hedge toward “exploring where I’d be the best fit”
– **Alternatives:** (a) Go all-in on PM Consumer, (b) Ask about specific other roles
– **Rationale:** HM already soft-passed for founder experience. The real play is keeping Laura engaged as a router to other roles. Being explicitly open makes her job easy without signaling desperation.
– **Trade-offs:** Slight risk of seeming unfocused, mitigated by the strong project list anchoring Claude expertise.

### 2. Project List Format
– **Decision:** Bullet points with plain descriptions + GitHub links, no inline repo name parentheticals
– **Rationale:** Recruiter audience — scannable > technical. GitHub link lets her dig deeper if she wants.

### 3. Browser Agent Public Release
– **Decision:** Create a fresh `claude-browser-agent` repo from a scrubbed copy rather than making the existing repo public or using GitHub fork
– **Alternatives:** (a) Make browser-agent public directly, (b) GitHub fork
– **Rationale:** Git history in the private repo contains hardcoded SSH credentials, Discord webhooks, VM paths. Fresh repo with clean history is the only safe option.

### 4. Userscript Configuration Approach
– **Decision:** Use `GM_getValue(“BROWSER_AGENT_API”)` with localhost default instead of hardcoded domain
– **Alternatives:** Build-time injection, separate config file
– **Rationale:** Tampermonkey scripts don’t have a build step. GM_getValue is the standard TM storage mechanism and can be set from browser console. Simplest approach.

## What Was Built / Changed

### 1. Recruiter Reply Draft (`assortedLLMTasks`)
– Created `applications/anthropic/pm_consumer/recruiter_reply_draft.md`
– Structured to hedge toward other roles while showcasing Claude Code depth
– Committed: [f046e39](https://github.com/npezarro/assortedLLMTasks/commit/f046e39)

### 2. Public Browser Agent Repo (`claude-browser-agent`)
– Created fresh repo: https://github.com/npezarro/claude-browser-agent
– Scrubbed 6 files from the private `browser-agent` repo:
– `deploy.sh` — SSH creds → env vars (`VM_USER`, `VM_HOST`, `VM_KEY`, `VM_PATH`)
– `agent-server.js` — Hardcoded VM paths → `process.cwd()` fallback; removed GitHub clone URL
– `browser-cli.sh` — Removed WSL paths, SCP credentials, cowork-sync/capture-daemon commands
– `browser-agent.user.js` — Hardcoded `pezant.ca` → configurable `GM_getValue` with localhost default; removed `@updateURL`/`@downloadURL`
– `install.html` — Cleaned to only show browser agent (removed freeGames scripts)
– Excluded: `context.md`, `progress.md`, `.env` (internal notes / secrets)
– Added: `.env.example`, `README.md` with full setup guide
– Security verification: `grep -rn` for all sensitive patterns returns zero matches
– Committed: [e1b5d5a](https://github.com/npezarro/claude-browser-agent/commit/e1b5d5a)

## Learnings Captured

– `.env.*` in gitignore catches `.env.example` — use `!.env.example` negation pattern when standardizing gitignore
– For public forks of private repos: always fresh-init rather than filter-branch. Cleaner and less error-prone.

## Open Items & Follow-ups

1. **Rotate credentials** — `BROWSER_AGENT_KEY` and Discord cowork webhook should be rotated (they were in `.env` which is gitignored, but the values existed in the private repo’s working copy)
2. **Send the email** — Draft is ready in `applications/anthropic/pm_consumer/recruiter_reply_draft.md`, needs final review and send
3. **Add browser-agent line to email** — User has the suggested line; needs to add it before sending
4. **claude-tray-notifier** — Identified as another potential repo to highlight but not added to the email draft

## Key Files

– [Recruiter reply draft](https://github.com/npezarro/assortedLLMTasks/blob/main/applications/anthropic/pm_consumer/recruiter_reply_draft.md)
– [claude-browser-agent repo](https://github.com/npezarro/claude-browser-agent)
– [claude-browser-agent README](https://github.com/npezarro/claude-browser-agent/blob/main/README.md)

Browser Agent v1.8.0 — Edge Hang Prevention

Context

Browser-agent TM userscript was causing Edge to hang during extended sessions. Root cause: aggressive DOM polling (two uncoordinated loops at 2s and 3s), accumulated timeout timers, and server-side memory leaks from orphaned state.

Fixes (v1.8.0)

  • Merged polling loops: Two separate timers to single 3s tick(). Cuts DOM polling ~40%.
  • Cached getPageState(): 2s TTL cache prevents redundant DOM traversal on heartbeats.
  • Command timeout cleanup: Promise.race timers now cleared on completion via finally block.
  • Circular console buffer: O(1) ring buffer replaces O(n) .shift() on every log entry.
  • Server cleanup routines: 30s interval prunes dead tabs, expired resultWaiters (10min TTL), and orphaned command queues.

Key Decisions

  • Single 3s polling interval is sufficient — SPA navigation detection latency goes from 2s to 3s, negligible for the use case.
  • Heartbeat page state can be up to 2s stale (cached). Explicit getState commands still return fresh data.
  • Server cleanup runs every 30s automatically, no longer depends on /health endpoint being hit.

Deployment

Deployed via SCP to VM (browser-agent directory is not a git repo). PM2 restarted. TM script hosted at pezant.ca/browser-agent.user.js for auto-update.

Open Items

  • Monitor Edge stability over next few sessions
  • If still hanging: consider requestIdleCallback wrapper, MutationObserver for SPA detection, or increasing POLL_MS to 5s

Commit 8645c0a | Repo

Deep Closeout: Auto Shorts Analytics, Learning Agent & Experimentation Framework

# Auto Shorts: Analytics, Learning Agent & Experimentation Framework
**Date:** 2026-04-09
**Duration:** ~4 hours
**Repos touched:** auto-shorts, shorts-pipeline

## Context & Motivation
The Auto Shorts system generates YouTube Shorts from long-form cooking videos for the Chef Agathe channels (EN + FR) but had zero visibility into performance. The user wanted to:
1. Measure how different titles, descriptions, and cutting approaches perform
2. Build a system that learns per-channel what resonates and applies those learnings automatically
3. Create an experimentation framework to test new approaches, measure results, and graduate winners

The goal is to maximize a mix of views, watch completions, and shares.

## Decisions Made

### Analytics via YouTube APIs (not scraping)
– **Decision:** Use YouTube Data API v3 for basic stats + YouTube Analytics API for watch completion
– **Alternatives:** YouTube Studio scraping, manual data entry
– **Rationale:** API is reliable, automated, and gives per-video granularity
– **Trade-offs:** Analytics API requires separate OAuth scope (yt-analytics.readonly) — needed GCP Console changes and channel re-auth

### Server-side token exchange (not worker-dependent)
– **Decision:** OAuth callback exchanges tokens directly via Google’s token endpoint in Node.js
– **Alternatives:** Original design delegated to Python worker for token exchange
– **Rationale:** Worker wasn’t running on VM; server-side exchange is self-contained
– **Trade-offs:** Token files stored on VM in data/tokens/ rather than in shorts-pipeline repo

### Learnings injected via instructions field (zero pipeline changes)
– **Decision:** Channel learnings and experiment instructions are prepended to the job’s `instructions` field at poll time
– **Alternatives:** Modify pipeline.py to read learnings directly, add a new field to the job schema
– **Rationale:** The worker’s `auto_select_preset()` and `analyze_transcript()` already read instructions — prepending context is invisible to the pipeline
– **Trade-offs:** Instructions can get long with many learnings; capped at top 10

### Experiments seed jobs immediately by default
– **Decision:** Creating an experiment pulls unprocessed videos from the library and creates jobs right away
– **Alternatives:** Wait for auto-poll to find new uploads (could take days)
– **Rationale:** User wanted experiments to start producing results quickly; existing library has 140+ EN and 66+ FR unprocessed videos
– **Trade-offs:** Uses existing library videos rather than only new uploads. “Wait for next auto-generate” is available as opt-out

## What Was Built / Changed

### Phase 1: YouTube Analytics Dashboard
– **New tables:** `clip_analytics_snapshots` (per-clip stats over time), `analytics_fetch_log`
– **Migrations:** `shorts_clips.channel_id`, `shorts_channels.analytics_scope`, `shorts_channels.last_analytics_fetch`
– **8 analytics query functions** in shorts-queue.js including preset comparison, channel summaries, snapshot history
– **Python fetcher** (`shorts-pipeline/fetch_analytics.py`): pulls stats from YouTube Data API, batches 50 videos per request, posts to server. Auto-syncs tokens from server for scope changes
– **3 dashboard views:** main overview (preset comparison bar chart, top performers), channel detail (sortable table), clip detail (growth chart with deltas)
– **Nav update:** Analytics link added to all 7 existing EJS views
– 12 new tests

### Phase 2: Deep Analytics (YouTube Analytics API)
– GCP Console: YouTube Analytics API enabled, yt-analytics.readonly scope added to OAuth consent screen (done manually via Cowork)
– OAuth auth-url route supports `?analytics=1` to request extended scope
– Settings UI shows “Enable Deep Analytics” / “Re-authenticate Analytics” button per channel
– `auth_channel.py –analytics` flag for CLI re-auth
– `fetch_analytics.py –mode full` pulls averageViewDuration, averageViewPercentage, shares
– Both Chef Agathe channels re-authed with analytics scope, initial data seeded (20 clips)

### Server-Side Token Exchange
– Replaced worker-dependent exchange with direct Node.js exchange via Google’s token endpoint
– Tokens saved to `data/tokens/` on VM
– New `GET /worker/token` route lets local fetcher pull tokens remotely
– `fetch_analytics.py` auto-syncs tokens from server when running in full mode

### Learning Agent
– **New file:** `lib/shorts-learning-agent.js` — periodic script that reviews per-channel analytics, sends data to Claude, extracts structured insights
– **New table:** `channel_learnings` (category, insight, confidence, sample_size, source, active)
– Claude prompt includes all clip data, preset comparison, and current learnings for refinement
– Insights categorized: duration, content_type, style, timing, general
– Confidence tied to sample size: low (<15), medium (15-30), high (>30)
– Can be triggered manually via POST /learnings/:channelId/run or run as standalone script
– Initial run produced 10 EN insights and 8 FR insights

### Experimentation Framework
– **New table:** `channel_experiments` (name, hypothesis, instructions, target_count, produced_count, status)
– **New table:** `experiment_suggestions` (persisted AI-generated suggestions)
– **Migration:** `shorts_clips.experiment_id` tags clips to experiments
– **Experiment lifecycle:** active → completed → graduated/rejected. Queued experiments auto-activate when current completes
– **Worker integration:** `/worker/poll` injects learnings + experiment instructions into job’s instructions field. `/worker/update` tags clips with experiment_id and increments produced_count
– **Auto-poll awareness:** New jobs check for active experiments and attach experimentId in metadata
– **Job seeding:** `seedExperimentJobs()` creates jobs from existing library videos immediately
– **Results comparison:** `getExperimentComparison()` compares experiment cohort vs baseline metrics
– **Graduation:** Converts experiment instructions into permanent channel learnings
– **AI suggestions:** POST `/experiments/:channelId/suggestions` generates 3 experiment ideas via Claude, persisted for later use. “Start This” / “Start All” buttons
– **UI:** Experiments page with active experiment progress bar, create form (Start Now vs Wait), queued experiments section, past experiments with status badges, suggestion cards with prior suggestions collapsible

### Weekly Learnings Summary
– **New file:** `lib/shorts-weekly-summary.js` — builds per-channel digest
– Sends email via SMTP (pezant.projects@gmail.com) to channel owner emails
– Posts to Discord #shorts-learnings channel (webhook created)
– Includes learnings, experiment status, and how-to guide
– Cron: every Monday at 9:03am on VM

### Operational Improvements
– **Stale job sweeper:** Runs every poll cycle, marks jobs stuck >2 hours as failed
– **Channel switcher:** Dropdown on all per-channel pages (analytics, learnings, experiments)
– **Auto-generate toggle:** Replaced checkbox with clear ON/OFF button + confirmation dialog
– **Static index.html fix:** Removed `public/index.html` that was overriding the EJS dashboard route
– **Channel lookup fix:** Routes use `getChannelById()` instead of filtering by logged-in email

## Architecture & Design

“`
┌─────────────────┐
│ YouTube APIs │
│ Data + Analytics│
└────────┬────────┘

┌──────────────┐ poll/update ┌────────┴────────┐ fetch_analytics.py
│ Python Worker │◄──────────────────►│ auto-shorts │◄───────────────────
│ (local WSL) │ learnings + │ server (VM) │ posts snapshots
│ pipeline.py │ experiment │ :3007 │
│ │ injected into ├─────────────────┤
│ │ instructions │ SQLite DB │
└──────────────┘ │ ├ shorts_jobs │
│ ├ shorts_clips │ ┌─────────────┐
┌──────────────┐ │ ├ clip_analytics│───►│ Dashboard │
│ Learning │ Claude analysis │ ├ channel_ │ │ /analytics │
│ Agent │◄──────────────────►│ │ learnings │ │ /learnings │
│ (6h cron) │ extract insights│ ├ channel_ │ │ /experiments│
└──────────────┘ │ │ experiments │ └─────────────┘
│ └ experiment_ │
┌──────────────┐ │ suggestions │ ┌─────────────┐
│ Weekly │ └─────────────────┘───►│ Email + │
│ Summary │ │ Discord │
│ (Mon 9am) │ └─────────────┘
└──────────────┘

Feedback loop:
Analytics fetched → Learning agent analyzes → Insights stored →
Worker poll injects learnings into prompt → Better clips produced →
Analytics measured → Cycle repeats
“`

## Learnings Captured

| Learning | Location |
|———-|———-|
| Test before asking user (channel lookup email mismatch, static index.html) | memory/feedback_test_before_asking.md (updated) |
| OAuth scope mismatch causes RefreshError when tokens have different scope | shorts-pipeline fix in fetch_analytics.py |
| duration_seconds is NULL for synced videos (playlistItems API doesn’t return it) | auto-shorts getUnprocessedVideos fix |

## Open Items & Follow-ups

1. **Worker rate limiting:** Currently processes back-to-back. Could add configurable delay between jobs to prevent YouTube 429 errors on large batches
2. **Learning agent cron:** Not yet set up as PM2 cron on VM — currently manual or via API trigger. Should be every 6 hours
3. **Queued experiments:** 6 experiments queued across both channels — will auto-activate as current ones complete
4. **Analytics fetch cron:** Should set up periodic `fetch_analytics.py –all –mode full` to keep stats current
5. **yt-dlp deprecation:** Worker logs warn about missing JS runtime — may need `deno` installed
6. **Experiment results review:** Once the 2 active experiments complete their 5 shorts each, results should be reviewed and graduated/rejected

## Key Files

### auto-shorts (Node.js server)
– `lib/shorts-queue.js` — All DB tables, migrations, query functions
– `lib/shorts-routes.js` — All routes (analytics, learnings, experiments, worker)
– `lib/shorts-auto-poll.js` — Auto-generation, experiment seeding, stale job sweeper
– `lib/shorts-learning-agent.js` — Periodic Claude-based analysis
– `lib/shorts-weekly-summary.js` — Weekly email + Discord digest
– `views/auto-shorts-analytics.ejs` — Main analytics dashboard
– `views/auto-shorts-analytics-channel.ejs` — Channel detail with sort/filter
– `views/auto-shorts-analytics-clip.ejs` — Clip detail with growth chart
– `views/auto-shorts-learnings.ejs` — Per-channel learnings management
– `views/auto-shorts-experiments.ejs` — Experiment creation, suggestions, results
– `ANALYTICS_SETUP.md` — Setup guide and cowork handoff instructions

### shorts-pipeline (Python worker tools)
– `fetch_analytics.py` — YouTube stats fetcher with token sync
– `auth_channel.py` — OAuth management with –analytics flag

Deep Closeout: CLI Mirror Shadow Testing

Context & Motivation

Before fully transitioning Discord request handling from the existing debate/direct-execution path to the CLI mirror streaming approach, we needed to run both in parallel to evaluate CLI mirror’s real-world performance and catch any bugs.

The CLI mirror approach provides 1.5s streaming updates with visible tool calls in Discord threads — a significantly better UX than the current batch-response model.

What Was Built

Every fresh request in #requests now also fires a shadow run through #cli-mirror (fire-and-forget). Both the existing debate/direct path and the CLI mirror streaming path execute simultaneously.

Key Details

  • Shadow mirrors don’t count against activeJobs concurrency limits
  • Thread names suffixed with (mirror) for easy identification
  • Metrics source tagged as cli-mirror-shadow
  • Only fresh requests mirrored (not session resumes/replies)
  • Fire-and-forget: if the mirror fails, the original request still completes

Architecture

User posts in #requests
    |
index.js request handler
    |-- [fire-and-forget] mirrorRequestToCliMirror() --> #cli-mirror thread (streaming)
    |                                                     --> 1.5s polling, tool call display
    |
    +-- [awaited] existing path (debate/handleRequest) --> #requests reply (batch)

Decisions

  • Fire-and-forget shadow rather than replacing the existing path — zero risk, easy to remove
  • No concurrency tracking for shadows — prevents false blocking of real requests
  • Only fresh requests — session continuity (–resume) is path-specific

Files Changed

  • src/bot/cliMirror.js — Added mirrorRequestToCliMirror() (+128 lines)
  • src/bot/index.js — Shadow trigger in request handler (+8 lines)

Open Items

  1. Monitor resource usage (two Claude processes per request during testing)
  2. Compare quality and latency between both paths
  3. Once satisfied, remove shadow and switch #requests to CLI mirror as primary

Repo: centralDiscord | Commit: faff71f