# CLAUDE.md This file provides guidance to Claude Code when working with code in this repository. ## What this project is This repository is a personal Blue Archive JP daily-automation project. It controls the real Blue Archive PC/Steam/Proton client running on a Linux machine named `nik-gpu`. The development machine is a MacBook named `nik-macbookair`. **The original goal is a backend port, not a rewrite.** The reference project (`~/repo/baas-reference/`) only works against an Android emulator via ADB/uiautomator2, typically run on a Windows PC. This project's job is to make that same automation logic work against the real JP client running under Proton on Linux instead — porting the reference's decision logic, navigation, and state-detection approach, including its use of OCR, as faithfully as practical, and replacing only the parts that are genuinely Android-specific with local desktop equivalents. Android-specific parts include: - ADB input - uiautomator2 taps - Android UI-object-tree queries - emulator-specific assumptions Local desktop equivalents include: - `xdotool` - `scrot` - OpenCV - OCR against screenshots - local image/color/template detection Inventing a different, simpler approach to avoid porting a piece of reference logic is a deviation from the project goal, not a shortcut toward it. The automation backend is local desktop control: - `xdotool` for mouse/keyboard/window control - `scrot` for screenshots - Python/OpenCV for image matching and color/template detection - OCR, using Tesseract or PaddleOCR, wherever the reference implementation uses OCR for a feature - no Android emulator control - no ADB - no uiautomator2 ### OCR policy OCR is **not** an Android-specific concern. The reference's OCR-driven region, stage-name, and currency matching runs against a screenshot and can be ported to this backend using `scrot` captures. Only the reference's input mechanism is Android-specific. Two features, Stamina/AP mission claim and Normal/Hard story AP sweep, were originally built without OCR by substituting ad hoc pixel probes, fixed coordinates, or randomized selection for the reference's OCR-driven navigation. For story sweep, that substitution produced more real bugs during live testing than porting the reference's actual approach would have: - a wrong modal-open probe - an unverified button click that silently under-spent AP - a modal that does not close on Escape - a latent hazard where a mistimed keypress could have started a real battle See `plan.md` Phase 9's retrospective for the full writeup. That issue has since been fixed: `story_sweep.py` now ports the reference's actual OCR-based region/stage targeting. See `plan.md` Phase 10. The retrospective stays here as the concrete, lived reason for the OCR policy, not as a description of the current state of that task. Going forward: when a reference feature's control flow depends on OCR, set up OCR and port that logic. Do not invent a non-OCR workaround just to avoid the setup cost. Only skip OCR for a specific step if the reference itself does not use OCR there. The reference project is located at: ```text ~/repo/baas-reference/ ``` That repository contains the full Blue Archive Auto Script implementation. It should be treated as the behavioral reference for this project. ## Core architecture decision This project must be Python-first. **Do not implement new Blue Archive automation logic in `ba_dailies.sh`.** `ba_dailies.sh` is only a thin runtime/deployment launcher for convenience on `nik-gpu`. All feature logic must live in Python. The intended structure is: ```text ba-auto-daily/ ├── ba_dailies.sh ├── ba_daily.py ├── ba_auto/ │ ├── __init__.py │ ├── driver.py │ ├── detector.py │ ├── navigation.py │ ├── config.py │ ├── tasks/ │ │ ├── __init__.py │ │ ├── mailbox.py │ │ ├── cafe.py │ │ ├── stamina.py │ │ ├── group.py │ │ └── ... │ └── reference_notes/ │ └── mapping.md ├── assets/ ├── screenshots/ ├── scratchpad/ ├── setup.sh ├── plan.md └── CLAUDE.md ``` `scripts/` previously held one-off Bash/Python helpers. It was deleted once mailbox and cafe migrated off it. Do not recreate `scripts/` as a place to stash feature logic. The exact layout can evolve, but the architectural rule should not change: - Bash launches Python. - Python owns automation logic. - The reference repository guides feature behavior. - Local driver primitives adapt that behavior to the PC/Steam/Proton setup. ## Reference-first rule Before implementing any new feature, inspect the matching reference implementation in: ```text ~/repo/baas-reference/module/ ``` Do not start by inventing a Bash click sequence. For every feature, first identify: 1. Which reference file implements it. 2. Which class/function contains the main control flow. 3. What the reference uses for state detection. 4. What the reference uses for retries/failure handling. 5. Which parts depend on Android/uiautomator2 and must be replaced. 6. Which parts can be ported directly as Python control flow. 7. Which local driver primitives are missing. Then implement the feature in Python under: ```text ba_auto/tasks/ ``` ## Reference repository usage The reference repository is **read-only**. ### Allowed - read reference modules - inspect control flow - reuse architecture ideas - reuse retry/state-machine structure - reuse task decomposition ideas - reuse constants/config concepts when appropriate - write local notes describing how a reference module maps to this project ### Not allowed - edit files in `~/repo/baas-reference/` - reimplement a reference feature from scratch in Bash - create a local solution that ignores the reference flow when a reference implementation already exists - substitute a pixel probe, fixed coordinate, or randomized shortcut for reference logic that uses OCR, just to avoid setting up OCR ## Two-machine architecture Development happens on: ```text nik-macbookair ``` Runtime happens on: ```text nik-gpu ``` The Blue Archive client and X display live on `nik-gpu`. Assume: ```text DISPLAY=:0 GDM XAUTHORITY under /run/user/1000 ``` The game runs under Steam/Proton on the Linux desktop. There is no reliable local execution path on macOS. Anything that interacts with the game must be deployed to `nik-gpu`. ## Deployment model During iteration, files are pushed from `nik-macbookair` to `nik-gpu`. **The runtime runs directly out of the git checkout at `~/repo/ba-auto-daily/` on nik-gpu.** (Changed 2026-07-18 -- see below for the old model this replaced.) There is no separate copy of the code anywhere else. Typical paths on `nik-gpu`: ```text ~/repo/ba-auto-daily/ # the checkout -- ba_dailies.sh, ba_daily.py, ba_auto/, assets/ all live here and run in place ~/.venvs/ba-auto-daily/ # Python venv -- genuinely host-level, not repo content ~/ba_logs/ # cron run logs + the cron lock file -- genuinely runtime state, not repo content ``` `ba_dailies.sh`, `ba_cron_run.sh`, and `completions/ba_dailies.bash` all resolve their own directory via `BASH_SOURCE`/`dirname` rather than assuming a fixed install path -- they work correctly wherever the checkout lives, so nothing needs to be copied out of it. `ba_auto/config.py`'s `ASSET_DIR`/`SCRATCHPAD_DIR` are likewise resolved relative to the checkout (`PROJECT_ROOT`, two levels up from `config.py`), not a fixed path outside it. For a fresh checkout on `nik-gpu`, run from the repo root: ```bash ./setup.sh ``` `setup.sh` only installs the venv and host-level tab-completion now -- it does NOT copy any project files anywhere. Re-running it after a `rsync`/`git pull` is still fine (idempotent, also re-checks host tools/venv), but no longer strictly necessary for code changes to take effect, since there's nothing left to propagate -- the checkout IS the runtime. After initial setup, individual changes may be pushed with `scp` or `rsync`. Prefer syncing the project from the repository root: ```bash rsync -av \ --exclude='.git/' \ --exclude='__pycache__/' \ --exclude='*.pyc' \ --exclude='.claude/settings.local.json' \ --exclude='graphify-out/' \ --exclude='screenshots/' \ --exclude='scratchpad/' \ ./ nik-gpu:~/repo/ba-auto-daily/ ``` Do not sync Claude Code internal temporary folders. **Older model, replaced 2026-07-18:** this project used to copy code out to fixed paths (`~/ba_dailies.sh`, `~/ba_daily.py`, `~/ba_auto/`, `~/ba_assets/`, `~/ba_cron_run.sh`, `~/.ba_dailies_completion.bash`) via `setup.sh`, separate from the `~/repo/ba-auto-daily/` checkout `rsync` updates -- meaning a code change could be rsynced to the checkout but never reach the copies cron/manual runs actually used, unless `setup.sh` was re-run afterward. This bit for real, twice: once on 2026-07-16 (a same-day `ba_daily.py` edit missing from the runtime paths, caught and fixed by re-running `setup.sh`), and again on 2026-07-18 (the newly-added `exit_game` task/preset entries were completely absent from a real `q4h` cron run -- confirmed via the run's own log, which dispatched `login`/`cafe`/`mailbox`/`stamina`/`event_sweep` then stopped, no `exit_game` line at all -- root-caused to the exact same drift). Per explicit user request ("keep the home directory clean"), the copy step was removed entirely rather than just re-synced again, closing off this whole class of bug instead of continuing to hit it. If you ever see `~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`/`~/ba_assets/` existing again on nik-gpu outside the checkout, that's stale leftover from this old model, not the current architecture -- don't assume anything reads from them. ### Scheduled runs (cron) As of 2026-07-16, `ba_dailies.sh` presets run unattended on a schedule via cron on nik-gpu, not just via manual/CLI invocation. `crontab -l` on nik-gpu is the source of truth; as installed: ```text 30 3 * * * /home/nik/repo/ba-auto-daily/ba_cron_run.sh daily 30 4 * * * /home/nik/repo/ba-auto-daily/ba_cron_run.sh daily 0 1,5,9,13,17,21 * * * /home/nik/repo/ba-auto-daily/ba_cron_run.sh q4h ``` Times are nik-gpu's local system time (`Asia/Tokyo`/JST). `ba_cron_run.sh` (repo root, run directly from the checkout -- see "Deployment model" above) wraps `ba_dailies.sh ` (resolved as the sibling file next to its own `BASH_SOURCE`) in a shared, non-blocking `flock` (`~/ba_logs/ba_dailies.lock`) so overlapping fire times can never launch concurrent `xdotool`/`scrot` sessions against the same game window, and logs each run to its own `~/ba_logs/.log` (`daily.log`/`q4h.log`, never interleaved). It contains no game-automation logic -- pure locking/logging plumbing, same category as `ba_dailies.sh` itself. To check whether a scheduled run actually worked: `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` -- every run logs one of `OK`/`FAILED (exit N)`/`SKIPPED (previous run still in progress)`, deliberately distinct outcomes (a lock-skip and a genuinely crashed task both exit 1, so the wrapper checks lock acquisition as its own explicit step rather than folding it into the wrapped command's exit code). This means: resources (AP, tickets, currency) may already be spent by the time a session starts, independent of anything done in that session -- check `~/ba_logs/` before assuming a given game state is from manual testing. The `daily`/`q4h` preset definitions themselves live in `ba_daily.py`'s `PRESETS` dict (see `plan.md`'s Phase 18 follow-ups for the full writeup), not here -- keep this section in sync if the schedule or preset names change. ## Runtime dependencies on nik-gpu These are host-level dependencies. Confirm they exist before assuming a bug is in the project code. Required now: - `xdotool` - `scrot` - `python3` Required Python packages: - `opencv-python` or `opencv-python-headless` - `numpy` - `pytesseract` Expected venv: ```text ~/.venvs/ba-auto-daily/bin/python3 ``` Quick check: ```bash ssh nik-gpu "which xdotool scrot tesseract && ~/.venvs/ba-auto-daily/bin/python3 -c 'import cv2, numpy, pytesseract; print(cv2.__version__)'" ``` OCR engine dependency is set up as of Phase 10: - Tesseract via the `tesseract-ocr` apt package - `pytesseract` in the venv Installing `tesseract-ocr` needs interactive `sudo`, so `setup.sh` should check for it but should not assume it can install it automatically. As of Phase 14's live-debugging (see `plan.md`'s Phase 14 follow-up #5), the `tesseract-ocr-jpn` language pack is also needed, but only for `event_sweep.py`'s `_is_finished_event_page` check (Japanese free-text OCR to detect a finished event's own "イベント期間が終了しました" page) -- every other task's OCR only ever needed the base `eng` data (digit/whitelisted-character reads), so this is a soft dependency specific to that one task, not a project-wide requirement. `setup.sh` checks for it via `tesseract --list-langs` and prints the install command (`sudo apt-get install -y tesseract-ocr-jpn`) if missing, but does not hard-fail setup over it, same interactive-sudo caveat as the base package. `ba_auto/detector.py`'s `read_text()` and `read_int()` wrap OCR for occasional single-crop reads. See `story_sweep.py` for the first real usage. `read_text()` takes a `lang` parameter (default `"eng"`) for this. ## Bash policy `ba_dailies.sh` should be a thin launcher only. Preferred shape: ```bash #!/usr/bin/env bash set -euo pipefail VENV_PYTHON="${VENV_PYTHON:-$HOME/.venvs/ba-auto-daily/bin/python3}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" exec "$VENV_PYTHON" "$SCRIPT_DIR/ba_daily.py" "$@" ``` Acceptable Bash responsibilities: - choose Python binary - set environment variables - call Python entry point - provide compatibility with old command names - fail early if the Python entry point is missing Not acceptable in Bash: - feature state machines - long click sequences - OpenCV logic - OCR logic - retry loops for game states - feature-specific navigation - shop/sweep/battle logic - new `do_` game automation functions If an existing Bash function exists, migrate it to Python rather than extending it. ## Python entry point The intended Python CLI entry point is: ```text ba_daily.py ``` It should support commands such as: ```bash ./ba_dailies.sh ./ba_dailies.sh mailbox ./ba_dailies.sh cafe ./ba_dailies.sh stamina ./ba_dailies.sh story_sweep ./ba_dailies.sh group ``` No argument should run the default daily flow. Current default flow in `ba_daily.py`'s `DEFAULT_ORDER`: 1. focus game 2. mailbox 3. cafe 4. stamina 5. future daily tasks `story_sweep` spends AP rather than reclaiming something free, so it is deliberately excluded from the default flow. It must be invoked explicitly. The CLI should dispatch into task modules under: ```text ba_auto/tasks/ ``` ## Driver layer Create and maintain a driver layer in: ```text ba_auto/driver.py ``` The driver layer should wrap the PC/Steam/Proton backend. It should provide reusable primitives such as: ```python focus_game() screenshot() click(x, y) double_click(x, y) drag(start, end, duration) swipe(start, end, duration) keypress(key) wait(seconds) wait_until(...) color_at(...) region_average_color(...) template_match(...) find_and_click_template(...) ``` Feature modules should not directly shell out to `xdotool` or `scrot` unless a driver primitive is missing and is being added. Prefer: ```python driver.click(x, y) ``` over: ```python subprocess.run(["xdotool", "click", "1"]) ``` This keeps the project close to the reference architecture while replacing only the backend-control layer. ## Detector layer Image matching, color matching, and OCR wrappers should live in: ```text ba_auto/detector.py ``` or in clearly named helper classes/functions. The cafe sparkle-matching logic formerly in `scripts/detect_and_click.py` has been migrated into `ba_auto/detector.py` as `find_cafe_sparkle()`. It should be called in-process rather than as a per-click subprocess. The detector should support: - screenshot input - template matching - threshold tuning - masked matching - click-offset handling - OCR crop reads - debug image output to `scratchpad/` Avoid one Python cold start per click attempt where possible. Prefer long-running Python task logic that can take repeated screenshots and click repeatedly from one process. ## Navigation layer Common navigation should live in: ```text ba_auto/navigation.py ``` Use this for shared flows such as: - returning home - opening main menu - opening mailbox - opening cafe - opening shop - opening lesson/schedule - closing popups - generic back/escape handling - waiting for known UI states Do not duplicate navigation click sequences inside every task if they can be shared. ## Task modules Each feature should have a task module: ```text ba_auto/tasks/.py ``` Examples: ```text ba_auto/tasks/mailbox.py ba_auto/tasks/cafe.py ba_auto/tasks/stamina.py ba_auto/tasks/story_sweep.py ba_auto/tasks/group.py ``` Each task module should expose a clear function such as: ```python run(driver, config) ``` or: ```python run_mailbox(driver, config) ``` Keep task files feature-focused. ## Reference mapping notes Maintain a mapping file at: ```text ba_auto/reference_notes/mapping.md ``` Before or during implementation of a feature, update the mapping. Use this format: ```markdown | Local feature | Reference file | Reference functions/classes | Local file | Backend replacements | Status | |---|---|---|---|---|---| | Cafe | `module/cafe.py` or relevant file | `...` | `ba_auto/tasks/cafe.py` | uiautomator2 tap -> xdotool click, screenshot -> scrot/OpenCV | In progress | ``` ## Working conventions Always work from the repository root: ```bash cd ~/repo/ba-auto-daily ``` Use `scratchpad/` in the project root for all temporary, generated, diagnostic, calibration, and investigation files. Examples: - cropped calibration images - debug screenshots - annotated match results - temporary Python probes - temporary investigation notes - one-off image-analysis scripts - temporary OCR experiments - screenshots copied back from `nik-gpu` for analysis Create it if missing: ```bash mkdir -p scratchpad ``` Do **not** use Claude Code's internal temporary directories such as: ```text /private/tmp/claude-* /tmp/claude-* ``` Do **not** run probes from `/private/tmp/claude-*` or write temporary scripts there. These paths are outside the repository and often trigger extra permission prompts, interrupting the development flow. When a temporary Python probe is needed, write it into `scratchpad/` first, then run it from the repository root. Preferred pattern: ```bash cat > scratchpad/probe_image.py <<'PY' from PIL import Image img = Image.open("scratchpad/shop_probe_04_checked.png") print(img.size) points = [ ("checkbox_checked_center", (972, 297)), ("checkbox_checked_bg", (975, 300)), ("card_border_left", (940, 380)), ("card_border_unselected", (1165, 380)), ("buy_button", (1751, 1112)), ("cancel_button", (1525, 1112)), ] for name, xy in points: print(name, xy, img.getpixel(xy)) PY python3 scratchpad/probe_image.py ``` Avoid this pattern: ```bash cd /private/tmp/claude-*/scratchpad && python3 -c "..." ``` Also avoid large multiline `python3 -c "..."` commands when they contain comments or complex quoting. Prefer a temporary script under `scratchpad/` because it is easier to inspect, rerun, and keep within workspace permissions. `screenshots/` contains human reference captures. They are useful for calibration and documentation, but they are not necessarily automated test fixtures. `assets/` contains local template images. These should be captured from the local game setup where possible. ## Temporary probe policy Temporary probes are allowed, but they must be workspace-local. For non-trivial investigation, use: ```text scratchpad/.py ``` Examples: ```text scratchpad/probe_shop_checkbox.py scratchpad/probe_cafe_income.py scratchpad/probe_story_modal.py scratchpad/probe_ocr_region.py ``` A temporary probe should: - run from the repository root - read inputs from `scratchpad/`, `screenshots/`, or `assets/` - write outputs to `scratchpad/` - avoid `/tmp`, `/private/tmp`, and Claude Code internal paths - avoid relying on absolute `/private/tmp/claude-*` paths - be deletable once the investigation is complete Prefer: ```bash python3 scratchpad/probe_shop_checkbox.py ``` Do not prefer: ```bash python3 -c "large multiline script..." ``` For very small one-liners, `python3 -c` is acceptable only when it does not require changing directories into `/private/tmp/claude-*`. ## Bash command safety patterns Claude Code has a hard-coded static safety layer on Bash commands, separate from and not configurable via `.claude/settings.json` permission rules — confirmed live: an exact matching `allow` rule was already present for one of these triggers and the command was still blocked. It flags specific shell-parse-tree shapes for manual approval regardless of whether the command is actually destructive. Five triggers have been confirmed live in this project. Avoid all five by construction; don't spend time trying to get any of them allow-listed, that has not been shown to work for any of them. 1. **A shell glob passed directly to `rm`/`mv`/`cp`** (e.g. `rm -f scratchpad/*.png`) — "Glob patterns are not allowed in write operations." A `find`-piped `while IFS= read` loop and `find -exec {} +` are *also* flagged, not just the raw glob. The only reliable fix for scratchpad cleanup: `clean_scratchpad.sh` (repo root) wraps the glob-matching inside a script file, which the guard doesn't parse into — `bash clean_scratchpad.sh '*.png' '*.log'` runs prompt-free, both locally and over `ssh` to nik-gpu (see the `clean-scratchpad` skill). For any other destructive command, spell out literal filenames directly instead. 2. **`cd some/dir && ` in one compound command** — "Compound command contains cd with write operation." Applies to `rm`/`mv`/`cp` and to `>` redirection alike, even with fully literal filenames, no glob involved. Fix: address the target with `~/repo/ba-auto-daily/...` or an absolute path instead of `cd`-ing first, or issue the `cd` as its own separate command before the write. 3. **A `for`/`while` loop with `$var` expansion** — "Contains simple_expansion." Applies even to fully read-only loops (`grep`, `echo`, zero destructive risk). Fix: pass every target as a literal argument to a single command that accepts multiple files directly (e.g. `grep -c "pattern" file1.py file2.py file3.py`, which prints `file:count` per file on its own), or write out one literal command per item instead of looping. 4. **Process substitution `<(...)`/`>(...)`** — "Contains process_substitution." E.g. `diff <(ssh nik-gpu "cat remote_file") local_file`. Fix: split into steps — `ssh nik-gpu "cat remote/path" > scratchpad/check_file.py`, then a plain `diff scratchpad/check_file.py local/path`, then `rm -f -- scratchpad/check_file.py`. 5. **A `#` comment on its own line inside a multi-line quoted `python3 -c "..."`** — "Newline followed by # inside a quoted argument can hide arguments from path validation." This is the same case the "Temporary probe policy" section above already warns about — write the code to `scratchpad/probe_.py` and run it with a bare `python3 scratchpad/probe_.py` instead. If a new, sixth static-safety trigger shows up, assume it's the same class of hard guard rather than a permission gap: find the literal/non-dynamic equivalent command and add it to this list. ## Testing and checks Because the game only runs on `nik-gpu`, local macOS testing is limited. Before deploying, run static/syntax checks locally: ```bash bash -n ba_dailies.sh python3 -m py_compile ba_daily.py python3 -m py_compile ba_auto/*.py python3 -m py_compile ba_auto/tasks/*.py ``` For temporary investigation scripts, use: ```bash python3 scratchpad/.py ``` Do not run temporary investigation scripts from Claude Code's internal `/private/tmp/claude-*` workspace. On `nik-gpu`, run real integration tests against the live game. Example: ```bash ssh nik-gpu "~/repo/ba-auto-daily/ba_dailies.sh cafe" ``` When debugging image matching, write debug images to: ```text scratchpad/ ``` When debugging on `nik-gpu`, copy relevant screenshots or debug images back into the local project `scratchpad/` if they need local analysis. ## Existing features Current project state: mailbox, cafe, stamina, story_sweep, story_sweep_hard, event_sweep, shop_common, shop_tactical, lesson, arena, and bounty are all migrated to real Python. No Bash feature logic remains. (This list has drifted behind a few other migrated tasks -- login, gem_shop, circle, exit_game -- not tracked here yet; see `ba_daily.py`'s own `TASKS` dict for the true current set.) - `ba_dailies.sh` is a thin launcher that execs `ba_daily.py` - `ba_daily.py` dispatches `mailbox`, `cafe`, `stamina`, `story_sweep`/`story_sweep_force`, `story_sweep_hard`/`story_sweep_hard_force`, `event_sweep`, `shop_common`, `shop_tactical`, `lesson`, `arena`, `bounty`, and default flow to `ba_auto/tasks/` - default flow is `mailbox`, `cafe`, `stamina` - `story_sweep` (and its `story_sweep_force` override), `story_sweep_hard` (and its `story_sweep_hard_force` override), `event_sweep`, `shop_common`, `shop_tactical`, `lesson`, `arena`, and `bounty` are opt-in only since they spend AP/credits/tactical coin/lesson tickets/an arena ticket/a bounty ticket rather than reclaiming something free. Both `story_sweep` and `story_sweep_hard` additionally refuse to run at all unless their tab's reward campaign is currently active, unless overridden via `story_sweep_force`/`story_sweep_hard_force` -- see `ba_auto/tasks/story_sweep.py`/`story_sweep_hard.py` and `plan.md` Phases 21-22 - every task, whether run individually or as part of the default flow, self-heals back to the home screen both before it starts and after it ends — `ba_daily.py`'s `_run_task()` calls a retrying `_ensure_home()` before dispatch and wraps the dispatch itself in a `try/finally` calling `navigation.return_to_home()`, so cleanup runs regardless of success, an early-return failure, or an uncaught exception. The pre-task check is self-healing, not a hard gate, per explicit user direction: if `_ensure_home()` still can't confirm home after its own bounded retries, the task is attempted anyway rather than aborted, trusting each task's own click-then-verify steps to fail safely if the starting state really was bad. This is centralized rather than duplicated per-task; see plan.md's "Return-to-home audit" (and its self-heal-not-abort follow-up) for why (a real audit found most tasks had little to no reliable cleanup on several paths) and for a real bug this surfaced and fixed in `navigation.is_on_subscreen`/`return_to_home` itself (a modal open on top of a subscreen was indistinguishable from the true home screen using the header-brightness probe alone — fixed by also checking `is_modal_open`) - `ba_auto/tasks/mailbox.py` and `ba_auto/tasks/cafe.py` click with `ba_auto/driver.py` primitives and verify state with `driver.color_at` and `ba_auto/navigation.py` - mailbox and cafe were ported from the reference patterns around `module/mail.py` and `module/cafe_reward.py` - no legacy bridge remains - `ba_auto/tasks/stamina.py` claims the Mission panel's bulk `一括受取` button - see `plan.md` Phase 8 for stamina details - `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets from `config.STORY_SWEEP_TARGETS`, plus one daily-rotating target (`config.STORY_SWEEP_ROTATION_*`) that cycles through a fixed region's stages one per day - story sweep navigates via OCR, using region-number readout and stage-label matching, rather than random selection - see `plan.md` Phase 10 for the OCR-based story sweep port, and its "Phase 10 follow-up" for the rotation-target fix - `story_sweep.py` also has its own campaign-active guard (`story_sweep_force` to override), an explicit Normal-tab click-and-verify (needed now that `story_sweep_hard` can run earlier in the same preset and leave Hard tab selected), and the same `_confirm_dialog_is_sweep`/self-heal-navigation/no-false-negative-MAX-verify fixes `story_sweep_hard.py` needed — see `plan.md` Phase 22 for the full rewrite, live-tested successfully on the first real attempt (a real MAX sweep of the day's rotation target, correctly detected as swept) - `ba_auto/tasks/story_sweep_hard.py` sweeps a fixed, user-supplied priority-ordered list of Hard-mode `(region, stage)` targets (`config.HARD_STORY_SWEEP_TARGETS`) via the in-game MAX button (capped at 3x by the game itself), reusing story_sweep.py's region-nav and stage-info-modal machinery directly (confirmed pixel-identical) but with its own fixed 3-row stage list (no scrolling/OCR-label search — Hard always has exactly missions 1-3) and its own campaign-active guard: it refuses to spend any AP unless the pink "キャンペーン中" reward-campaign banner is showing on the region-info card, unless run via the separate `story_sweep_hard_force` command - a real gold-button money hazard exists in this same modal for Hard specifically: a stage that already used all 3 of today's auto-sweep clears still shows a clickable 入場 button, and attempting to sweep it raises a real "spend 40 Pyroxene/blue gems to refill today's clear count?" dialog, not just an AP-insufficient prompt — guarded by two independent layers (an OCR pre-check on the modal's own count field before ever clicking MAX, plus an OCR fallback on the dialog's own text, both declining via Escape rather than any positional click) - see `plan.md` Phase 21 for the full live-testing writeup, including the navigation-cascade bug, the false-negative MAX-click abort bug, and the gem-refill hazard, all found and fixed against the real game - `ba_auto/tasks/shop_common.py` and `ba_auto/tasks/shop_tactical.py` share a checkbox-grid-then-bulk-buy flow (`ba_auto/tasks/shop_utils.py`) against config-driven `(row, col, name, expected_price)` targets (`config.COMMON_SHOP_TARGETS` / `config.TACTICAL_SHOP_TARGETS`) - item identification is by fixed grid position, not per-item OCR — the reference's own `get_item_position` indexes an external static price table this repo doesn't have, so a locally pixel-scanned position table is the faithful port, not an OCR-avoidance shortcut; price-digit OCR is layered on top as an extra catalog-drift safety check the reference doesn't even do per-item - both shops were live-tested with real purchases (see `plan.md` Phase 11), which also surfaced a real, previously-unknown per-refresh-cycle purchase cap on these items (not shown as a visible counter) — the task correctly detected the now-unselectable items and safely declined rather than misfiring - `ba_auto/tasks/lesson.py` sweeps every unlocked region's schedule grid (a scrollable list of 12 named regions, each opening a grid modal of up to 9 location cards), picking the highest-affection available lesson each time until lesson tickets or lessons run out - per-cell affection is read via `detector.read_int_on_heart_badge`, a dedicated OCR path for the pink/magenta heart-shaped badge — the project's normal grayscale-threshold OCR (`read_int`) misreads it, because the badge's own outline stroke survives the same threshold as the digit glyph; a "done today" portrait keeps its number and gets a green checkmark added alongside it rather than losing the number, so done-ness is checked via that checkmark's color, not inferred from a failed OCR read - lesson was live-tested with real tickets spent (see `plan.md` Phase 12), which surfaced two real bugs from that assumption gap plus an OCR contamination issue — both fixed; see Phase 12 for the full writeup - a later regression broke `LESSON_TICKET_OCR_RECT` entirely (a stray katakana fragment at the crop's left edge made tesseract drop the leading digit, `"7/7"` reading as `"/7"`, aborting every run) — fixed by tightening the rect; re-validated live with 7 real tickets spent and correct re-reads after every schedule. See `plan.md`'s "Phase 12 follow-up" - a second regression then surfaced: clicking the schedule icon doesn't always land on the Location Select list — the game can resume directly on whichever region's per-region isometric map was last open (a previous run's Ctrl-C interruption left it stuck there), which broke navigation for every region identically since the list's scroll/row-click logic doesn't apply to that screen. Fixed via `lesson._ensure_location_select_list`, which detects the per-region map's own "all schedules" button already showing and returns via the back button before sweeping. Validated by deliberately reproducing the stuck state and confirming recovery with a real ticket spend. See `plan.md`'s "Phase 12 follow-up #2" - `ba_auto/tasks/arena.py` fights ranked Tactical Challenge (Arena) battles in a loop until the OCR'd ticket count reaches 0 (per explicit user direction, 2026-07-12 — this reverses the original one-battle-per-invocation design, which matched the reference's own per-call pacing via its background scheduler; see `plan.md`'s Phase 13 follow-up #2), waiting `config.ARENA_POST_BATTLE_COOLDOWN` (30s) between fights for the real in-game lockout, then collects both reward slots once at the end - Tactical Challenge is reached via a card inside the お仕事 (Work) hub, not a bottom-nav icon; the reference's separate opponent-info and formation-edit screens are merged into one modal here with a live ticket-count preview confirming the real fight-commit click - arena was live-tested for real across all 5 of the account's daily tickets (2 WIN, 1 LOSE, 2 spent debugging), which surfaced three real bugs: a level-OCR crop too small for tesseract despite looking legible to the eye, level text being bright-on-dark unlike every other OCR read in this project (fixed via a new `detector.read_int_white_on_dark`), and — most importantly — the post-fight WIN/LOSE result modal proving undetectable by precisely locating its own confirm button (WIN and LOSE are different heights; widening the search region to cover both then caught stray cyan-ish pixels in the opponent list's own portrait art, false-positive-clicking into an unrelated opponent's info modal). Fixed by abandoning per-button color detection for a bounded blind-Enter-press loop (matching `lesson.py`'s own `_run_one_schedule` pattern), gated by a hard safety check against the one modal where Enter is genuinely dangerous — the opponent-info modal's own attack-formation button is also Enter-bound and spends a real ticket. See `plan.md` Phase 13 for the full writeup - `ba_auto/detector.py` has `find_cafe_sparkle()`, the sparkle template-match ported in-process from the now-deleted `scripts/detect_and_click.py`, and `find_template()`/`template_visible()`, a generalized named-template matcher built for arena but ultimately unused there — state detection stayed OCR/color-probe-driven throughout, like every other task - `scripts/ba_dailies_legacy.sh` and `scripts/detect_and_click.py` have been deleted - the `scripts/` directory itself no longer exists - `ba_auto/driver.py` primitives are wired into all migrated task modules - existing driver primitives include `run_command`, `focus_game`, `click`, `move_mouse`, `scroll`, `drag`, `keypress`, `screenshot`, `wait`, and `color_at` - `drag(start_x, start_y, end_x, end_y, duration, steps)` (added 2026-07-14 for `cafe.py`'s camera panning) is a real mousedown/incremental-mousemove/mouseup gesture, distinct from `scroll()`'s wheel-based one — `scroll()`'s own comment already documents that a plain drag does NOT register as a list-scroll gesture in this Proton client, but that finding was specific to scrollable list widgets; a room-view camera is a different UI surface and needs an actual drag - `focus_game()` calls both `xdotool windowactivate` and `xdotool windowraise` on the Blue Archive window, not just the former — confirmed live (twice, first during `event_sweep.py` debugging, then again during a `bounty` run, see `plan.md`'s "Return-to-home audit follow-up #2") that a stray anti-cheat `XIGNCODE` window can render on top of the game and silently intercept clicks at fixed positions (notably the shared `navigation.BACK_BUTTON` coordinate), even though `xdotool getactivewindow` still reports `BlueArchive` as active throughout — `windowactivate` changes input focus, not window stacking order, so only `windowraise` actually clears it. `navigation.return_to_home` also calls `driver.focus_game()` once, partway through its retry budget, as a second escalation for the case the overlay appears mid-run rather than only at a task's own start - `ba_auto/navigation.py` has shared state probes used across tasks: - `is_on_subscreen` - `is_modal_open` - `wait_for_state()` `wait_for_state()` is a scoped port of the reference's `core/picture.py::co_detect`: watch for any of several named states, react to known non-terminal ones, and stop on a recognized terminal one. `story_sweep.py` additionally has its own local modal probes and close logic because the stage-info modal is wide enough to break `is_modal_open`'s default probe point, and it does not close on Escape. See `plan.md` Phase 9. The story sweep modal renders at least two different internal layouts: - a plain one for the bonus `-A` stage - a taller tabbed one for regular numbered stages Their button coordinates differ. This was discovered live in Phase 10. Live testing found both the mailbox-icon and cafe-icon fixed coordinates were flaky. They missed the first click and worked on retry. Neither task originally verified state before proceeding, so a missed click cascaded into blind actions and could reach an unverified Escape press on the home screen, which triggers Blue Archive's own "exit the game?" confirmation. See `plan.md` Phases 5–6 for the full writeup. This is the concrete reason every task now verifies state before acting rather than trusting fixed coordinates or a single click blindly. `is_on_subscreen`'s header-brightness probe alone cannot tell "the true home screen" apart from "a subscreen with a modal open on top" — a modal's own screen-wide dimming overlay darkens the header probe point the same way home's own background art can, confirmed live via direct pixel comparison (2026-07-12). `navigation.return_to_home` now checks a combined `_not_home` helper (`is_on_subscreen(driver) or is_modal_open(driver)`) instead of `is_on_subscreen` alone, so it can no longer mistake a stuck modal for having reached home. See `plan.md`'s "Return-to-home audit" for the live incident this was found from (a task's fixed home-relative click coordinates landed on the wrong screen entirely because of this exact false reading) and the fix. Rank-up popups mid-pat-loop are now handled. A pat that crosses an affection-rank threshold shows a full-screen `絆ランクアップ!` cutscene with no cafe header. `find_cafe_sparkle()` can never recognize this because it is nothing like the sparkle template. The loop used to spin uselessly against it for the rest of the room's click budget. `cafe.py`'s `_dismiss_rank_up_if_shown()` reuses the existing `navigation.is_on_subscreen` header-brightness probe. This was confirmed against `screenshots/cafe/student/01-02`: - rank-up cutscene: header probe reads `r < 200` - normal cafe screen: header probe reads `r > 200` It presses Enter until the cutscene clears, porting the reference's own `to_cafe()` navigation handling of `relationship_rank_up` from `module/cafe_reward.py`. This is not yet live-confirmed against a real cafe-pat rank-up trigger specifically. It is grounded in real captured screenshots, but treat it as implemented-but-unverified until one happens naturally during a real cafe run. The same full-screen `絆ランクアップ!` cutscene *was* confirmed live during Lesson/Schedule work (see `plan.md` Phase 12), and that work found `is_on_subscreen`'s fixed header probe reads inconsistently across different characters' cutscene art — bright for one, which would make cafe's "keep pressing Enter while NOT on subscreen" loop think the cutscene had already cleared when it hadn't. Not necessarily hazardous on its own (it would just stop dismissing early and let the pat loop retry against a still-showing cutscene), but worth a more robust probe if cafe's rank-up path ever misbehaves live. Not yet verified for cafe: - whether camera zoom/pan can drift over a long unattended run - the reference project zooms out before detecting - this project currently does not - testing did not reproduce a failure from skipping it - see `plan.md` Phase 6 "Not verified" list ## Known cafe gaps to verify during migration When migrating or modifying cafe logic, verify these manually against the actual current behavior: - whether student rotation popups are handled - whether rank-up popups are handled - whether the view is zoomed/centered before sparkle detection - whether detection still works when text overlaps the student - whether one Python process can handle repeated sparkle detection faster than repeated cold starts - whether both cafe rooms are handled consistently - whether cafe income claim is robust against popup timing Do not assume the old Bash implementation handles these correctly. ## Migration goal The migration goal is: - preserve existing behavior - move feature logic from Bash into Python - keep `ba_dailies.sh` as launcher only - make detection logic reusable - prepare for future features by copying the reference project's structure where appropriate ## Anti-patterns Do not: - add new `do_` Bash functions - build a giant Bash automation script - recreate reference logic as fixed-coordinate Bash click chains - skip reading the reference module before implementing a feature - put OCR in Bash - put OpenCV state machines in Bash - make every feature shell out independently to `xdotool` - duplicate common navigation in every task - copy reference image assets blindly - edit `~/repo/baas-reference/` - implement event-specific features before the generic reusable machinery exists - invent a pixel probe, fixed-coordinate, or randomized substitute for reference logic that uses OCR, just to avoid setting up OCR - treat OCR as an Android-specific concern that can be deferred indefinitely - run temporary probes from `/private/tmp/claude-*` instead of the repository `scratchpad/` - create investigation scripts outside the repository when they can live in `scratchpad/` - use multiline `python3 -c "..."` commands for non-trivial probes - rely on Claude Code internal temporary paths in committed code, notes, commands, or debugging workflow - write generated debug files into the repository root when they belong in `scratchpad/` ## Feature implementation workflow For every new feature: 1. Read the relevant `~/repo/baas-reference/module/...` file. 2. Summarize the upstream feature flow in notes or comments. 3. Add or update the reference mapping table. 4. Identify missing local driver primitives. 5. Implement or improve those primitives in `ba_auto/driver.py` or `ba_auto/detector.py`. 6. Implement the feature in `ba_auto/tasks/.py`. 7. Add CLI dispatch in `ba_daily.py`. 8. Keep `ba_dailies.sh` unchanged unless launcher behavior changes. 9. Run syntax checks. 10. Deploy to `nik-gpu`. 11. Test against the live game. 12. Update `plan.md` status. 13. Move temporary investigation files into `scratchpad/`, or delete them if they are no longer useful. ## Priority when uncertain When uncertain, prefer this order: 1. Preserve existing working behavior. 2. Follow the reference project's control flow, including its use of OCR. 3. Do not deprioritize OCR-based navigation/matching in favor of a simpler non-OCR substitute. 4. Keep logic in Python. 5. Add reusable driver primitives instead of feature-specific hacks. 6. Use local screenshots/assets only when backend differences require it. 7. Use `scratchpad/` for temporary work and diagnostics. 8. Avoid large rewrites that do not move the project closer to reference-driven Python architecture. ## User preference The user wants this project to be as close to the original Blue Archive Auto Script architecture as practical, without recreating logic that already exists. The user specifically does not want Claude Code to keep converting feature work into Bash. The user also wants Claude Code to avoid using `/private/tmp/claude-*` for development probes when a repository-local `scratchpad/` directory can be used instead, because those paths trigger permission prompts and interrupt the development flow. Respect those preferences.