From b8ac8db01ebfd8c83685f9476c8d710dd00570f3 Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Sat, 18 Jul 2026 22:04:17 +0900 Subject: [PATCH] feat(deployment): simplify runtime paths to run directly from the git checkout --- CLAUDE.md | 32 ++++++++++---------------- ba_auto/config.py | 11 +++++++-- ba_cron_run.sh | 10 ++++++++- completions/ba_dailies.bash | 17 ++++++++------ plan.md | 45 ++++++++++++++++++++++++++++--------- setup.sh | 29 +++++++++--------------- 6 files changed, 86 insertions(+), 58 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a0d425c..c01fa3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -188,25 +188,15 @@ There is no reliable local execution path on macOS. Anything that interacts with During iteration, files are pushed from `nik-macbookair` to `nik-gpu`. -Typical paths on `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 -~/ba_dailies.sh -~/ba_daily.py -~/ba_auto/ -~/ba_assets/ -~/.venvs/ba-auto-daily/ +~/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 ``` -The current setup may still contain older paths such as: - -```text -~/ba_scripts/detect_and_click.py -``` - -When refactoring, prefer consolidating Python code into `ba_auto/`. - -`setup.sh` should bootstrap or update the runtime layout on `nik-gpu`. +`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: @@ -214,6 +204,8 @@ For a fresh checkout on `nik-gpu`, run from the repo root: ./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: @@ -232,19 +224,19 @@ rsync -av \ Do not sync Claude Code internal temporary folders. -**The rsync target and the runtime paths are two different locations that can drift.** `rsync` above only updates the git checkout at `~/repo/ba-auto-daily/` on nik-gpu -- it does NOT touch `~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`, the actual fixed paths cron (see below) and any manually-run `~/ba_dailies.sh` invocation use. After rsyncing code changes, re-run `./setup.sh` from within `~/repo/ba-auto-daily/` on nik-gpu to propagate them to the runtime paths (it's idempotent -- safe to re-run any time, also re-checks host tools/venv/assets). Confirmed live (2026-07-16): the two locations had drifted (runtime paths were missing a same-day `ba_daily.py` edit) before this was caught and fixed by re-running `setup.sh`. +**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/ba_cron_run.sh daily -30 4 * * * /home/nik/ba_cron_run.sh daily -0 1,5,9,13,17,21 * * * /home/nik/ba_cron_run.sh q4h +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, deployed to `~/ba_cron_run.sh`) wraps `~/ba_dailies.sh ` 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). +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. diff --git a/ba_auto/config.py b/ba_auto/config.py index d12708e..bdd56e0 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -6,10 +6,17 @@ XAUTHORITY = "/run/user/1000/gdm/Xauthority" ENV = {**os.environ, "DISPLAY": DISPLAY, "XAUTHORITY": XAUTHORITY} WINDOW_NAME = "BlueArchive" -ASSET_DIR = os.path.expanduser("~/ba_assets") + +# Resolved relative to wherever this checkout physically lives (two levels +# up from ba_auto/config.py) rather than a fixed path outside the repo -- +# the runtime now runs directly out of the git checkout (no more separate +# ~/ba_assets/~/ba_auto copies to drift out of sync with it, see CLAUDE.md's +# "Deployment model"), so assets/scratchpad live wherever the repo does. +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ASSET_DIR = os.path.join(PROJECT_ROOT, "assets") # Runtime working files (probe/detection screenshots); never /tmp, per CLAUDE.md. -SCRATCHPAD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scratchpad") +SCRATCHPAD_DIR = os.path.join(PROJECT_ROOT, "scratchpad") os.makedirs(SCRATCHPAD_DIR, exist_ok=True) # Refined from (1726, 60): that coordinate sat on the edge of the icon's diff --git a/ba_cron_run.sh b/ba_cron_run.sh index b5c0233..01cb986 100644 --- a/ba_cron_run.sh +++ b/ba_cron_run.sh @@ -21,6 +21,14 @@ # originally) rather than folded into the wrapped command's own exit code. set -uo pipefail +# Resolves its own directory (same pattern ba_dailies.sh itself already +# uses) rather than assuming a fixed ~/ba_dailies.sh copy -- the runtime +# now runs directly out of this git checkout, so cron's own crontab entry +# points straight at this file's real path (see CLAUDE.md's "Deployment +# model") and this always calls the ba_dailies.sh sitting right next to it, +# wherever the checkout lives. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + PRESET="${1:?usage: ba_cron_run.sh }" LOG_DIR="$HOME/ba_logs" LOG_FILE="$LOG_DIR/$PRESET.log" @@ -35,7 +43,7 @@ mkdir -p "$LOG_DIR" fi echo "=== $(date -Iseconds) starting $PRESET ===" - "$HOME/ba_dailies.sh" "$PRESET" + "$SCRIPT_DIR/ba_dailies.sh" "$PRESET" status=$? if [ "$status" -eq 0 ]; then echo "=== $(date -Iseconds) finished $PRESET OK (exit 0) ===" diff --git a/completions/ba_dailies.bash b/completions/ba_dailies.bash index d679cc2..4d92e99 100644 --- a/completions/ba_dailies.bash +++ b/completions/ba_dailies.bash @@ -1,16 +1,19 @@ # Bash tab-completion for ba_dailies.sh's subcommand argument. # -# Installed automatically by setup.sh (copied to -# ~/.ba_dailies_completion.bash and sourced from ~/.bashrc) -- not meant to -# be sourced manually from the repo checkout, since it shells out to the -# deployed ~/ba_dailies.sh, not this file's own location. +# Installed automatically by setup.sh (a `source ~/repo/ba-auto-daily/ +# completions/ba_dailies.bash` line added to ~/.bashrc) -- resolves its own +# directory via BASH_SOURCE, same pattern ba_dailies.sh/ba_cron_run.sh +# already use, rather than assuming a fixed ~/ba_dailies.sh copy. The +# runtime runs directly out of this git checkout (see CLAUDE.md's +# "Deployment model"), so this always calls the ba_dailies.sh one directory +# up from wherever it was actually sourced from. # # The command list is queried from ba_daily.py itself (--list-commands) # rather than duplicated here, so it can never drift from the real TASKS # dict when a task is added, renamed, or removed. _ba_dailies_completions() { - local cur + local cur script_dir cur="${COMP_WORDS[COMP_CWORD]}" # Only the first argument is a phase name; ba_dailies.sh doesn't take @@ -19,12 +22,12 @@ _ba_dailies_completions() { return 0 fi + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" local commands - commands="$("$HOME/ba_dailies.sh" --list-commands 2>/dev/null)" + commands="$("$script_dir/ba_dailies.sh" --list-commands 2>/dev/null)" COMPREPLY=($(compgen -W "$commands" -- "$cur")) } # Registered under every literal form CLAUDE.md documents running this as. complete -F _ba_dailies_completions ba_dailies.sh complete -F _ba_dailies_completions ./ba_dailies.sh -complete -F _ba_dailies_completions ~/ba_dailies.sh diff --git a/plan.md b/plan.md index 696db08..26a327c 100644 --- a/plan.md +++ b/plan.md @@ -87,18 +87,18 @@ This is the intended direction. It does not have to be completed all at once. ## Runtime paths on nik-gpu -Preferred runtime layout: +**Changed 2026-07-18** (see plan.md's "Runtime deployment simplification" phase below for the full writeup): the runtime now runs directly out of the checkout, not a separate copy. | Path | What it is | |---|---| -| `nik-gpu:~/ba_dailies.sh` | Thin launcher. | -| `nik-gpu:~/ba_daily.py` | Python CLI entry point. | -| `nik-gpu:~/ba_auto/` | Python package copied from this repo. | -| `nik-gpu:~/ba_assets/` | Runtime assets/templates. | -| `nik-gpu:~/.venvs/ba-auto-daily/` | Python virtual environment. | +| `nik-gpu:~/repo/ba-auto-daily/` | The checkout. `ba_dailies.sh`/`ba_cron_run.sh`/`completions/ba_dailies.bash` all resolve their own directory via `BASH_SOURCE`, and `ba_auto/config.py`'s `ASSET_DIR`/`SCRATCHPAD_DIR` resolve relative to the checkout root -- everything runs in place, nothing is copied out. | +| `nik-gpu:~/.venvs/ba-auto-daily/` | Python virtual environment -- genuinely host-level, not repo content. | +| `nik-gpu:~/ba_logs/` | Cron run logs + the cron lock file -- genuinely runtime state, not repo content. | `nik-gpu:~/ba_scripts/` may still contain `detect_and_click.py` and `ba_dailies_legacy.sh` left over from before both mailbox and cafe were migrated off them. Neither is deployed or referenced by anything anymore (`setup.sh` stopped copying them once Phase 6 landed) — safe to delete manually on `nik-gpu`, just not automated here. +`nik-gpu:~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`/`~/ba_assets/`/`~/ba_cron_run.sh`/`~/.ba_dailies_completion.bash` were the old copy-to-`$HOME` runtime paths (2026-07-16 through 2026-07-18) — removed for real on nik-gpu as part of the same change, not just superseded in docs. If any of them show up again, they're stale leftovers, not the current architecture. + ## Implementation strategy For each feature: @@ -916,6 +916,31 @@ One real bug found and fixed before any live test, while reasoning through what **Confirmed live**: a standalone `./ba_dailies.sh exit_game` run, user-reported "works well" -- `navigation.is_modal_open`'s generic dim-probe correctly detected the exit-confirmation dialog after Escape, and Enter closed the game cleanly (`driver.window_exists()` verified false). Not yet exercised as the tail end of a full `daily`/`q4h` preset run specifically (only standalone so far). +### Phase 20: Runtime deployment simplification -- run directly from the checkout (2026-07-18) + +Direct consequence of Phase 19's own deploy: the real `q4h` cron fire right after Phase 19 landed showed no `[exit_game]` line at all in `~/ba_logs/q4h.log` -- the sequence went straight from `event_sweep` to `"All done."`. Diagnosed by reading the log and grepping the runtime `~/ba_daily.py` for `exit_game`: zero matches, and `~/ba_auto/tasks/exit_game.py` didn't even exist at that path. Root cause: this project's deploy model kept the git checkout (`~/repo/ba-auto-daily/`, what `rsync` updates) and the actual runtime paths (`~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`, what `setup.sh` copied files to and what cron/manual runs actually executed) as two separate locations that could silently drift -- confirmed for the second time in three days (the first was 2026-07-16's cron-setup phase, see Phase 18 follow-up #2). Fixed immediately for that instance by re-running `setup.sh` to resync. + +The user then asked to remove the whole class of bug rather than keep re-syncing it: change the runtime to run directly out of `~/repo/ba-auto-daily/`, eliminating the separate copies entirely ("keep the home directory clean"). + +Changes made: + +- `ba_auto/config.py`: `ASSET_DIR` was a hardcoded `os.path.expanduser("~/ba_assets")`. Changed to a new `PROJECT_ROOT` (two levels up from `config.py`, the same resolution `SCRATCHPAD_DIR` already used) + `os.path.join(PROJECT_ROOT, "assets")` -- now reads the repo's own `assets/` directly, no copy needed. +- `ba_cron_run.sh`: called `"$HOME/ba_dailies.sh" "$PRESET"`. Changed to resolve its own `SCRIPT_DIR` via `BASH_SOURCE` (same pattern `ba_dailies.sh` itself already used) and call `"$SCRIPT_DIR/ba_dailies.sh"` instead -- works wherever the checkout lives, no fixed path assumed. +- `completions/ba_dailies.bash`: hardcoded `$HOME/ba_dailies.sh` for its `--list-commands` query. Changed to resolve `script_dir` via `BASH_SOURCE` the same way, and dropped the now-nonexistent `~/ba_dailies.sh` completion registration. +- `setup.sh`: removed the "copy `ba_dailies.sh`/`ba_daily.py`/`ba_auto`/assets to `$HOME`" steps entirely (`chmod +x` in place instead of `cp`). Tab-completion install now sources `completions/ba_dailies.bash` from the checkout directly (`source "$REPO_DIR/completions/ba_dailies.bash"` added to `~/.bashrc`) rather than copying it to `~/.ba_dailies_completion.bash` first. Host-tool checks and venv setup unchanged -- those are genuinely host-level, not repo content. +- `CLAUDE.md`'s "Deployment model"/"Runtime paths on nik-gpu"/"Scheduled runs (cron)" sections rewritten to document the new single-location model, with the old copy-to-`$HOME` model kept as an explicit "replaced" note (both real drift incidents named) rather than deleted outright, so a future session that sees a stray `~/ba_dailies.sh` lying around understands it's dead leftover, not a second live copy. + +Live steps taken on nik-gpu (not just docs): + +1. Rsynced the updated repo to `~/repo/ba-auto-daily/`, then re-ran `./setup.sh` there (venv/tab-completion only now, confirmed idempotent). +2. Verified `~/repo/ba-auto-daily/ba_daily.py`/`ba_auto/tasks/exit_game.py` matched the local copies exactly. +3. Updated the live crontab (`crontab -l` / `crontab -` round-trip) so all three entries call `/home/nik/repo/ba-auto-daily/ba_cron_run.sh ` instead of `/home/nik/ba_cron_run.sh ` -- same schedule/times, only the path changed. +4. Deleted the now-obsolete runtime copies on nik-gpu: `~/ba_dailies.sh`, `~/ba_daily.py`, `~/ba_auto/`, `~/ba_assets/`, `~/ba_cron_run.sh`, `~/.ba_dailies_completion.bash`, and fixed the stale `source ~/.ba_dailies_completion.bash` line left in `~/.bashrc` from the old `setup.sh`. + +`~/.venvs/ba-auto-daily/` and `~/ba_logs/` were deliberately left in `$HOME` -- genuine runtime state/build output (venv packages, cron logs + lock file), not duplicated code, so they don't reintroduce the drift problem this phase was meant to close off. + +**Verification**: a standalone `~/repo/ba-auto-daily/ba_dailies.sh --list-commands` / task dispatch through the new path, plus confirming the crontab's next fire actually reaches `exit_game`, are the two things worth checking to fully close this out -- see plan.md's "Immediate next steps"-style follow-up once the next scheduled `q4h`/`daily` fire happens. + ## Prerequisites ### OCR @@ -1354,17 +1379,17 @@ python3 -m py_compile ba_auto/*.py python3 -m py_compile ba_auto/tasks/*.py ``` -On `nik-gpu`: +On `nik-gpu` (runs directly from the checkout since 2026-07-18 -- see "Runtime paths on nik-gpu"): ``` -~/ba_dailies.sh mailbox -~/ba_dailies.sh cafe +~/repo/ba-auto-daily/ba_dailies.sh mailbox +~/repo/ba-auto-daily/ba_dailies.sh cafe ``` After migration: ``` -~/ba_dailies.sh +~/repo/ba-auto-daily/ba_dailies.sh ``` should run the default daily sequence. diff --git a/setup.sh b/setup.sh index 687edba..b44414d 100755 --- a/setup.sh +++ b/setup.sh @@ -5,13 +5,15 @@ # not from macOS, since it installs into nik-gpu-local paths. # # It does NOT touch the game or take any screenshots; it only installs the -# venv and copies files to the fixed paths ba_dailies.sh/ba_daily.py/ba_auto -# expect (outside the repo, per the two-machine deploy convention in -# CLAUDE.md). +# venv (host-level, genuinely outside the repo -- ~/.venvs/ba-auto-daily/) +# and tab-completion. The runtime runs directly out of this checkout (see +# CLAUDE.md's "Deployment model") -- there is no separate copy step for +# ba_dailies.sh/ba_daily.py/ba_auto/assets, so re-running this after a +# `git pull`/rsync can't drift out of sync with the code the way the old +# copy-to-$HOME model could. set -e VENV_DIR="$HOME/.venvs/ba-auto-daily" -ASSETS_DIR="$HOME/ba_assets" echo "== Checking host tools ==" missing=0 @@ -62,21 +64,12 @@ fi "$VENV_DIR/bin/python3" -c "import cv2, numpy; print('opencv', cv2.__version__, '/ numpy', numpy.__version__)" "$VENV_DIR/bin/python3" -c "import pytesseract; print('pytesseract', pytesseract.get_tesseract_version())" -echo "== Deploying assets to fixed paths ==" -mkdir -p "$ASSETS_DIR" -cp assets/cafe_sparkle.png "$ASSETS_DIR/cafe_sparkle.png" - -echo "== Deploying Python-first entry point ==" -cp ba_dailies.sh "$HOME/ba_dailies.sh" -chmod +x "$HOME/ba_dailies.sh" -cp ba_daily.py "$HOME/ba_daily.py" -rm -rf "$HOME/ba_auto" -cp -r ba_auto "$HOME/ba_auto" +chmod +x ba_dailies.sh ba_cron_run.sh echo "== Installing tab completion ==" -cp completions/ba_dailies.bash "$HOME/.ba_dailies_completion.bash" -COMPLETION_LINE='source "$HOME/.ba_dailies_completion.bash"' -if [ -f "$HOME/.bashrc" ] && ! grep -qF ".ba_dailies_completion.bash" "$HOME/.bashrc"; then +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPLETION_LINE="source \"$REPO_DIR/completions/ba_dailies.bash\"" +if [ -f "$HOME/.bashrc" ] && ! grep -qF "completions/ba_dailies.bash" "$HOME/.bashrc"; then printf '\n# Added by ba-auto-daily setup.sh\n%s\n' "$COMPLETION_LINE" >> "$HOME/.bashrc" echo "Added source line to ~/.bashrc" else @@ -85,5 +78,5 @@ fi echo "Run 'source ~/.bashrc' or open a new shell to pick up completion" echo "== Done ==" -echo "Run with: ~/ba_dailies.sh [mailbox|cafe|stamina|story_sweep|shop_common|shop_tactical|lesson]" +echo "Run with: $REPO_DIR/ba_dailies.sh [mailbox|cafe|stamina|story_sweep|shop_common|shop_tactical|lesson]" echo "(requires the game already running, window titled 'BlueArchive')"