Same pattern as the previous _resolve_forge_jar() fix — when --forge-dir
is not passed via CLI or FORGE_DIR env var, auto-detect the Forge
installation directory by:
1. Deriving from the already-resolved forge_jar path (parent of target/)
2. Checking common sibling directories (forge/, forge-repo/)
3. Checking platform user data dirs (~/.forge, ~/Forge, %APPDATA%/Forge)
Also audited all other CFG fields — forge_jar, forge_decks_dir, lab_jar,
port, results_dir all already have proper defaults or auto-resolve
fallbacks. Added Forge Dir, Lab JAR, and Results Dir to startup log
output for better diagnostics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
During the refactor from the monolith into services/, the forge JAR path
had no auto-resolve fallback — unlike lab_jar and forge_decks_dir which
both had resolver functions. When --forge-jar CLI arg and FORGE_JAR env
var are both unset, CFG.forge_jar stayed "" causing POST /api/lab/start
to fail with "Forge JAR not found: ".
Add _resolve_forge_jar() that scans common sibling directories for
forge-gui-desktop-*-jar-with-dependencies.jar (matching the pattern
from start-lab.bat), and wire it as the fallback in main().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Moved all 15 /api/ml/* endpoints, 4 state dicts, and 3 background
pipeline functions from lab_api.py into routes/ml.py. Wired via
app.include_router(ml_router) in lab_api.py.
- Remove plaintext Ximilar and Perplexity API keys from start-lab.bat
- Remove hardcoded key defaults from lab_api.py, lab_api_backup.py, lab_api_new.py
- start-lab.bat now reads from environment variables or .env file
- Add .env.example with placeholder values
- .env already in .gitignore (confirmed)
- Git history purged of all key values via git-filter-repo
Closes#26
Task 1: Move BUILTIN_COMMANDERS + load_commander_meta() to shared.py
Fix import_routes.py to use _shared.COMMANDER_META (module ref)
Task 2: Rewrite precon.py to use _shared.PRECON_INDEX (module ref)
Task 3: Add @app.on_event('startup') safety net for uvicorn --reload
Two fixes:
1. _parse_text_decklist(): replace fragile startswith() section-header
detection with case-insensitive regex so [Commander], [Main], [Deck]
etc. are always recognised regardless of capitalisation.
2. add_deck_card endpoint: when a card is added with is_commander=1,
also update the deck-level commander_name + commander_scryfall_id
so the portrait and top-bar label stay in sync.
Two fixes:
1. _parse_text_decklist(): replace fragile startswith() section header
detection with proper case-insensitive regex so [Commander], [Main],
[Deck] etc. are always recognised regardless of capitalisation.
2. add_deck_card endpoint: when a card is added with is_commander=1,
also update the deck-level commander_name and commander_scryfall_id
fields so the portrait and top-bar label stay in sync without
requiring a full reload.
Two places lost the commander flag:
1. _load_deck_cards_by_name() .dck fallback — used a flat in_cards
boolean for [Main], [Deck], and [Commander] sections so it couldn't
tell which section a card came from. Now tracks current_section and
sets is_commander=1 for cards in [Commander].
2. parse_dck_file() — stored section='Commander' in the card dict but
the frontend filters on c.is_commander, not c.section. Now includes
is_commander: 1/0 alongside the existing section field.
Replace all print() calls with structured logging using the Python
logging module. This provides:
- Named loggers per subsystem: commander_ai_lab.api, .batch, .sim,
.coach, .deckgen, .collection, .scanner, .ml, .cache, .pplx
- Dual output: console (StreamHandler) + rotating file (RotatingFileHandler)
at logs/commander-ai-lab.log (25 MB x 5 backups = 125 MB max)
- Configurable via env vars: CAL_LOG_DIR, CAL_LOG_MAX_MB, CAL_LOG_BACKUP_COUNT
- --verbose / -v flag for DEBUG-level output
- overnight-run.py: separate rotating log at logs/overnight-run.log (10 MB x 5)
- Sub-module loggers in ml/, scanner/, deck_sources/, src/ all propagate
to the root commander_ai_lab logger
- Stripped redundant [TAG] prefixes from messages (logger name carries context)
- Format: '2026-03-13 14:00:00 INFO [commander_ai_lab.batch] message'
Files changed: lab_api.py, overnight-run.py, and 11 sub-module .py files
176 total print() calls converted (101 in lab_api.py, 76 in sub-modules)
When collection_entries has multiple rows with the same scryfall_id
(e.g. Counterspell imported from two different sets), the LEFT JOIN
on deck_cards produced duplicate rows — making it appear like 2 copies
were added or removed.
Fixed all 6 deck_cards-to-collection_entries JOINs to use a
GROUP BY scryfall_id subquery, guaranteeing 1:1 cardinality.
- Added _fill_basic_lands() post-processing for V1 and V3 generators
- Strips LLM-provided basics, then distributes evenly across commander colors
- Always hits exactly 100 cards regardless of LLM output
- Frontend group counts now sum card.count instead of entry count
- Cards with count > 1 show '5x Forest' style prefix
- Colorless commanders get Wastes
Previously the coach required running simulations first to generate a
deck-reports/*.json file. Now if no simulation report is found, the
endpoint builds a lightweight DeckReport on the fly from the deck_cards
table — pulling card names, types, CMC, and color identity directly from
the DB. The LLM can still analyze composition and give advice, just
without simulation-derived performance metrics (impactScore, etc.).
Also removes the '[no report]' tag from the dropdown since all decks
can now be coached.
The decks table has 'commander_name' not 'commander'. The SQL query
was throwing OperationalError which the frontend silently caught,
resulting in an empty dropdown.
- lab_api.py: Flip serving priority — ui/ folder (legacy) served first, React dist/ only as fallback
- Legacy UI has full features: card images, mana costs, type grouping, collection filters, EDHREC recs, ratio dashboard, commander portraits, 3-column deck builder
- React SPA was causing blank pages and missing functionality
- Installer no longer builds or bundles React dist/
- All 7 pages work: Batch Sim, Collection, Deck Builder, Auto Gen, Simulator, Coach, Training
1. /api/lab/status: StatusResponse now includes run_id, games_completed,
total_games, current_decks aliases so the React SPA gets the field
names it expects.
2. /api/lab/decks returns {decks: [...]}, /api/lab/history returns
{results: [...]}, /api/lab/precons returns {precons: [...]} — but
the frontend expected bare arrays. Updated lab.ts API client to
unwrap these wrapper objects automatically.
GET /api/lab/status and /api/lab/result required batchId as a
mandatory query param, but the React SPA calls them with no param
on page load. FastAPI returned 422 Unprocessable Content, which
caused the BatchSimPage to crash and the UI to flash then vanish
in a retry loop.
Fix: batchId is now Optional[str] = None. When omitted, returns
the most recent active batch (or idle defaults for status).
Backend:
- POST /api/coach/chat: Multi-turn coaching chat with SSE streaming
- POST /api/coach/apply: Apply suggestions to deck (cut/add cards)
- GET /api/coach/cards-like: Embedding similarity search with ownership/price
- GET /api/coach/decks: Enhanced to show all decks with report status
- Goals config (power level, strategy, budget, focus areas) wired to prompts
- Session history with full diff tracking (cuts/adds/hints)
Frontend (complete CoachPage rewrite):
- Live multi-turn chat with streaming LLM responses
- Apply suggestions UI: checkbox cuts/adds, one-click apply to deck
- Goals config panel (power level slider, strategy chips, budget, focus areas)
- Session results view with summary, tips, mana advice, cuts, adds
- Session history browser with session drill-down
- Cards Like This search (embedding-powered card discovery)
- Price-aware card suggestions with collection ownership badges
- Responsive dark theme, tab navigation
Issue #1 — Winner's life total now captured:
- Parse verbose Forge log for life-total-change events
- Track last-known life per seat via 3 regex patterns
- Winner's final life set from log data (not left at default 40)
- Losers who died to mill/poison retain their actual life total
Issue #2 — Win condition classification formalized:
- New WinCondition enum (schema/WinCondition.java) with 8 categories
- Centralized Forge loss-reason-string → enum mapping (regex-based)
- UNKNOWN fallback with logging for unrecognized loss reasons
- All classification now goes through WinCondition.classify()
Issue #3 — JVM startup overhead reduced:
- Added -XX:+UseSerialGC (lighter GC for batch processes)
- Added -XX:TieredStopAtLevel=1 (faster JIT, less compilation)
- ForgeWorker stub class for future persistent batch mode
- Estimated reduction: ~2-3s → ~1-1.5s per game startup
Issue #4 — Single-threaded throughput optimization:
- New CLI flags: --ai-simplified, --ai-think-time, --benchmark
- JVM flags passed through to Forge subprocess
- Benchmark mode: run 1 game before batch, report wall/Forge/overhead times
- API support: aiSimplified, aiThinkTimeMs in StartRequest
Issue #5 — Threading, progress, and backpressure:
- --threads 0 or -1 auto-detects optimal count from CPU topology
- detectOptimalThreads() estimates physical cores (excludes HT)
- ProgressCallback interface for real-time sims/sec reporting
- Per-game progress emitted as [PROGRESS] lines, parsed by lab_api.py
- --max-queue flag for graceful backpressure (limits concurrent JVMs)
- Final throughput summary printed at batch completion
- StatusResponse now includes simsPerSec field
Also: .gitignore restored, lab_api.py updated with all new API fields