Retarget the brief at the user's three explicit asks
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

The user has decided all three of the questions the previous brief left
open: fix the _VENDOR_KW Intel bug, split the phone_focus_mode scripts
despite the deployment coupling, and restructure the three parked files.
Records them as instructions rather than open questions, with the specific
facts each needs.

For the phone splits, accepting the risk does not mean skipping the copy
lists: deploy.sh has two hardcoded per-file lists and never deploys lib/, so
the brief now spells out that both must grow and that the seam is a sibling
file. dns_enforcer.sh and lib/monitor.sh are called out first because both
have runnable tests, so those splits can be proved the way tether_enforcer
was. config.sh is flagged separately - focus_policy/loader.py parses it.

For the restructuring, notes that verify_shell_split.sh no longer applies
once code stops being a verbatim move, so each of the three needs a test or
a real run instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-17 14:43:20 +02:00
parent 2cd62587d6
commit 52f3452ba6

View File

@ -1,23 +1,122 @@
# Next session: shell splits, tests first
# Next session: three explicit asks, then keep splitting
> **Paste this whole file into a fresh Claude session opened in `~/testsAndMisc`.**
> It is self-contained. Do not go looking for the previous session's context.
## Where things stand
Over-cap: **37 → 24** (20 shell, 2 kotlin, 1 dart, 1 markdown — this file).
17 commits pushed. **Python is done**: every `.py` is under the cap at 100%
line+branch coverage where the gate measures it.
Over-cap: **37 → 25** (21 shell, 2 kotlin, 1 dart, 1 markdown — the markdown is
this file). 15 commits, `0cf487b..HEAD`, all pushed.
The user has given three direct instructions. Do them in this order, each as
its own commit. They are already decided — do **not** re-litigate them.
**Python is done.** Every `.py` in the repo is under the cap, at 100% line and
branch coverage where the gate measures it. The remaining work is shell, plus
three files in other languages.
---
## Tooling built last session — use it, don't rebuild it
## 1. Fix the `_VENDOR_KW` Intel bug
Two scripts do the mechanical half. Both were validated before use; don't
re-derive them.
`python_pkg/vscode_optimizer/_hardware.py:22`:
**`meta/scripts/extract_shell_functions.py`** moves functions into a library:
```python
_VENDOR_KW = {"nvidia": "NVIDIA", "amd": "AMD", "ati": "AMD", "intel": "Intel"}
```
`_detect_gpu` does `for kw, vendor in _VENDOR_KW.items(): if kw in low:`.
Every real `lspci` display line contains **"VGA comp*ati*ble controller"**, so
`"ati"` matches unconditionally and `"intel"` is unreachable. **Intel iGPUs are
reported as AMD**, which switches on the GPU-accelerated terminal and AMD-only
Electron flags. This machine is unaffected only because `"nvidia"` is checked
first.
**Do:**
- Fix it so vendor detection keys on the device description rather than the
whole line — the text after the last `:` is already extracted into
`hw.gpu_model` one line above. Match against that, not `low`.
- Delete the `strict=True` xfail `test_detect_gpu_should_recognise_an_intel_igpu`
in `python_pkg/vscode_optimizer/tests/test_gpu_disk.py` and make it a normal
passing test. **The xfail is `strict=True`, so a fix without removing the
marker fails the suite** — that is deliberate.
- The parametrised `test_detect_gpu_maps_each_vendor_keyword` currently
_characterises_ the bug: two of its cases assert `"AMD"` for an Intel line and
for an unknown vendor. Update both to the correct expectations.
- Verify with a real run: `python3 -m python_pkg.vscode_optimizer --dry-run`
must still detect this machine's RTX 3090 as NVIDIA and propose the same
**18 changes**. Then confirm the repo gate: bare `pytest` (memory-capped)
at 100%.
## 2. Split the `phone_focus_mode` scripts anyway
The user has accepted the deployment risk. **This does not mean skip the copy
lists** — it means proceed with the splits, doing them correctly.
`deploy.sh` has **two** hardcoded per-file lists and never deploys `lib/`:
- an `adb_cmd push "$DEPLOY_DIR/<f>" "/data/local/tmp/focus_stage/<f>"` (~line 381)
- an `adb_root "cp /data/local/tmp/focus_stage/<f> $REMOTE_DIR/<f>"` (~line 497)
**Every new sibling must be added to BOTH, in the same commit.** Missing the
push leaves the copy pointing at a file that was never staged. Use a **sibling
file** beside the entry script, never a `lib/` member — these scripts source
`"$SCRIPT_DIR/<name>.sh"` and `lib/` does not exist on the phone.
`tether_enforcer.sh` was done last session and is the worked example — read that
commit before starting.
Targets, cheapest first, with what can verify each:
| file | lines | verifiable by |
| -------------------- | ----- | ------------------------------------------------------------------------------------ |
| `dns_enforcer.sh` | 325 | `lib/tests/test_dns_enforcer.sh` — **runnable** |
| `phone_backup.sh` | 333 | no test; not in deploy.sh's lists at all |
| `curfew_enforcer.sh` | 367 | no test |
| `hosts_enforcer.sh` | 421 | no test |
| `lib/monitor.sh` | 449 | `lib/tests/test_monitor.sh` — **runnable**; already a lib, not deployed by deploy.sh |
| `focus_daemon.sh` | 543 | no test |
| `config.sh` | 571 | **`python_pkg/focus_policy/loader.py` parses it** — see below |
| `deploy.sh` | 835 | not deployed itself; entry+`lib/` IS safe here |
| `focus_ctl.sh` | 1091 | no test |
Start with `dns_enforcer.sh` and `lib/monitor.sh`: both have real test scripts,
so you can prove the split the way `tether_enforcer` was proved (run the test
before and after; the count must match exactly).
**`config.sh` is special.** `python_pkg/focus_policy/loader.py` is a
mini-interpreter that parses it as the live source of truth. Splitting it breaks
that loader unless the parser follows. `focus_policy` has 100% tests that will
catch it — run them. Plan the two changes together, or do `config.sh` last.
## 3. Restructure the three parked files
Last session parked these because a verbatim function move cannot get them under
the cap. The user has authorised restructuring. **Restructuring means the
verbatim harness no longer applies, so each one needs a test or a real run
instead** — say plainly which you used.
- **`install_plagiarism_tools.sh` (534, only 3 tiny functions).** ~500 lines of
top-level code. Wrap coherent stages in functions, then move those. This is
the most mechanical of the three; do it first.
- **`libre_translate.sh` (488, 18 funcs).** After the clean seams are taken,
`parse_args` (111 lines), `write_env_file`, `detect_container_user` and `main`
all assign **and** read the same configuration globals. Four attempts were
spent relocating them; that does not work. Restructure `parse_args` — e.g.
split the case body into per-flag helpers, or move the whole config-globals
cluster into one lib that owns both writes and reads.
- **`diagnose_pacman_hook_stall.sh` (493, 15 funcs).** Same shape: `run_one`
writes `LAST_ELAPSED`, `main` reads it. Also emits **SC2153**
(`PACMAN_BIN` vs `PACMAN_PID`) once split — that is a real shellcheck finding
to resolve, not to suppress.
---
## Tooling — use it, don't rebuild it
**`meta/scripts/extract_shell_functions.py`** — moves functions into a library.
Walks brace-by-brace and lifts only function blocks. **Never slice by line
range**: these scripts interleave top-level commands between function
definitions, and a range slice sweeps those into the library where they run at
source time and out of order. It refuses to guess when there is no `set -e`
anchor (as in the `#!/system/bin/sh` phone scripts) — place the source line by
hand there, after the existing `. "$SCRIPT_DIR/config.sh"`.
```bash
python3 meta/scripts/extract_shell_functions.py <script> <lib> \
@ -25,162 +124,104 @@ python3 meta/scripts/extract_shell_functions.py <script> <lib> \
# What this library is for.'
```
It walks the file brace-by-brace and lifts only function blocks. **Do not slice
by line range** — these scripts interleave top-level commands between their
function definitions, and a range slice sweeps those into the library, where
they run at source time and in the wrong order. That failure cost a rebuild on
`fresh-install/main.sh`; sourcing the bad version blocked on a sudo password
prompt, which is how it surfaced.
**`meta/scripts/verify_shell_split.sh`** proves the move was verbatim:
**`meta/scripts/verify_shell_split.sh`** — proves a move was verbatim by
normalising each function through `shfmt -mn` and comparing hashes. Sees through
reformatting, catches a one-character logic change, names the function.
```bash
bash meta/scripts/verify_shell_split.sh HEAD <old-path> <new-path>...
# IDENTICAL: all N top-level functions moved verbatim
```
It normalises each function through `shfmt -mn` and compares hashes, so it sees
through reformatting but catches a one-character logic change, naming the
function that differs. **Run it again after pre-commit autofixes** — it caught
`export` edits that lint had accepted but that changed two function bodies.
**Re-run it after pre-commit autofixes** — it caught `export` edits that lint had
accepted but that changed two function bodies. It does **not** apply to task 3,
where the code is deliberately being restructured.
## The rule that decides where a shell seam can fall
**A file must not assign a global it never reads.** That is SC2034, the repo
forbids suppressions, and the pre-commit hook runs `shellcheck` with **no
`-x`**, so a `# shellcheck source=` directive will not make the checker follow
the link. Each file has to stand alone.
**A file must not assign a global it never reads.** That is SC2034; the repo
forbids suppressions; the pre-commit hook runs `shellcheck` with **no `-x`**, so
a `# shellcheck source=` directive will not make it follow the link. Each file
stands alone — check with `shellcheck <lib>` before committing.
The precedent at `install_pacman_wrapper.sh:29` is about _definitions-only_
libs. It does **not** mean "assignments stay in the entry script" — it means
writer and readers must sit in the same file. `steam_compatibility.sh` split
cleanly only after `detect_system` moved next to `score_game`, and
`load_cache_map` next to `main`.
The precedent at `install_pacman_wrapper.sh:29` is about _definitions-only_ libs.
It does **not** mean "assignments stay in the entry script" — it means writer and
readers must sit in the same file. `steam_compatibility.sh` split cleanly only
once `detect_system` moved next to `score_game` and `load_cache_map` next to
`main`. This is the shell analogue of the `monkeypatch.setattr` target trap.
This is the shell analogue of the `monkeypatch.setattr` trap: the question is
always which file's namespace owns the name. Check with `shellcheck <lib>`
standalone before committing.
## Still-live deployment trap (task 2 does not cancel this)
## Deployment triage — the trap the trace harness cannot see
`docs/shell-split-verification.md` has the full table. The load-bearing part:
- **`phone_focus_mode/*` (8 over-cap scripts)** — `deploy.sh` copies a
**hardcoded per-file list** into `/data/local/tmp/focus_mode` and **never
deploys `lib/`**. A new sibling must be added to that list in the same
commit, or focus mode dies on the phone at its first `source`. These scripts
source only `config.sh`, which is on the list, so a **sibling-file** seam
works and an **entry+`lib/`** seam does not.
- **`install_leechblock.sh` (485), `block_compulsive_opening.sh` (705)** —
copied to `/usr/local/…`, and `pacman_wrapper.sh:831` prefers the deployed
copy on **every pacman invocation**. Teach the installer to deploy the
directory first, in its own commit, then re-baseline, then split. Highest
blast radius — do these last.
- **The rest** — no deployed copy; entry+`lib/` is safe.
## What to do next, cheapest first
The gate counts **files**, so prefer many small wins over one 1734-line monster.
1. **`generate_study_materials.sh` (1017)**, **`check_and_enable_services.sh`
(1301)**, **`setup_night_lockdown.sh` (918)**, **`setup_midnight_shutdown.sh`
(1734)** — big but no deployed copy. Expect 4–6 libraries each; group by the
state each function touches, per the rule above.
2. **`phone_focus_mode`**: `tether_enforcer.sh` (267) is 17 lines over and the
cheapest file left anywhere. Then `dns_enforcer.sh` (325), `phone_backup.sh`
(333), `curfew_enforcer.sh` (367). **Add every new sibling to `deploy.sh`'s
copy list in the same commit.** `deploy.sh` itself (833) is over-cap too, and
is not deployed, so it can take an entry+`lib/` shape.
3. **The two Kotlin files** (`EnforcementRunner.kt` 564,
`DevicePolicyBridge.kt` 415) and **`status_page_state.dart` (307)**. Gradle
needs `JAVA_HOME=/usr/lib/jvm/java-21-openjdk` and `--rerun-tasks`; a plain
`gradlew test` reports `UP-TO-DATE` and proves nothing.
4. **The pacman pair**, last, per the triage above.
### Parked, with reasons
- **`libre_translate.sh` (488)** — after its clean seams are taken, `parse_args`
(111 lines), `write_env_file`, `detect_container_user` and `main` all assign
**and** read the same configuration globals. Getting the entry under the cap
needs `parse_args` restructured, not relocated — a refactor, not a verbatim
move. Four attempts were spent here; don't repeat them without a test first.
- **`diagnose_pacman_hook_stall.sh` (493)** — same shape. `run_one` writes
`LAST_ELAPSED`, which `main` reads; keeping them together leaves the entry at 292. Also emits SC2153 (`PACMAN_BIN` vs `PACMAN_PID`) once split.
- **`install_plagiarism_tools.sh` (534)** — only 3 tiny functions and ~500 lines
of top-level code. Nothing to move; needs restructuring.
**`install_leechblock.sh` (485)** and **`block_compulsive_opening.sh` (705)** are
copied to `/usr/local/…`, and `pacman_wrapper.sh:831` prefers the deployed copy
on **every pacman invocation**. Split them naively and every `pacman -S` on this
machine fails. Teach the installer to deploy the directory first, in its own
commit, then re-baseline, then split. **Do these last.** Note you cannot run
`sudo pacman -S` from the Bash tool — it deadlocks on `db.lck`; hand the user a
`! pacman -S <pkg>` line and state the expected output.
## Rules that will bite you
- **No suppressions, ever.** No `# noqa`, `# type: ignore`, `# shellcheck
disable`, no lowered coverage threshold. If a split seems to need one, the
seam is wrong — that was true every time it came up last session.
- **Run the actual script.** `steam_compatibility.sh --help` printing usage and
exiting 0 is what proved four source lines resolved; no static check shows
that. For scripts too dangerous to run (`main.sh` installs packages,
`setup_passwordless_system.sh` writes sudoers), say so and rely on `bash -n`
plus sourcing each library in a subshell to prove it has no side effects.
disable`, no lowered coverage threshold. Every time this came up last session
the seam was wrong, not the linter.
- **Run the actual thing.** `steam_compatibility.sh --help` exiting 0 is what
proved four source lines resolved. For scripts too dangerous to run
(`main.sh` installs packages; `setup_passwordless_system.sh` writes sudoers),
say so and rely on `bash -n` plus sourcing each library in a subshell to prove
it has no side effects.
- **Check `check_file_length.sh --all` _before_ committing** — a split that fixes
one file while pushing a new library over the cap is a net zero. It happened
twice last session.
- **Every commit touching code needs evidence** in
`docs/superpowers/evidence/<slug>-<date>.json` (copy `template.json`).
Staging **≥4 code files also needs** a fresh
`docs/superpowers/contracts/*.json`. Validate both with
`meta/scripts/validate_{evidence,contract}.py`.
- **Check `check_file_length.sh --all` _before_ committing.** A split that fixes
one file while pushing a new library over the cap is a net zero.
- New sourced libs: shebang **and** the executable bit. The
`check-shebang-scripts-are-executable` hook reads the **git index**, so stage
the mode with `git add --chmod=+x`.
`docs/superpowers/evidence/<slug>-<date>.json` (copy `template.json`). Staging
**≥4 code files also needs** a fresh `docs/superpowers/contracts/*.json`.
Validate with `meta/scripts/validate_{evidence,contract}.py`.
- New sourced libs need a shebang **and** the executable bit. The
`check-shebang-scripts-are-executable` hook reads the **git index** — stage the
mode with `git add --chmod=+x`.
- `pre-commit run --files <changed>` before committing. **`prettier` and
`ci-mirror` run on pre-push, not pre-commit.** Run `npx prettier --write` on
any `.md` you touch, including this one.
`ci-mirror` run on pre-push, not pre-commit.** `npx prettier --write` any `.md`
you touch, including this one.
- Work directly on `main`. `git stash` and branch creation are blocked by hooks.
Verify a push landed with `git status -sb` showing no `[ahead N]`.
- **Do not wire the file-length pre-commit hook.** It lands last, once
`bash ~/utils/scripts/check_file_length.sh --all` exits 0. 25 files are still
over.
- Cap pytest memory: `systemd-run --user --scope -p MemoryMax=2G -p MemorySwapMax=0`.
`check_file_length.sh --all` exits 0. 24 files are still over.
- Cap pytest memory:
`systemd-run --user --scope -p MemoryMax=2G -p MemorySwapMax=0`.
## Known pre-existing state — not yours, do not fix silently
- **`python_pkg/vscode_optimizer` has a real bug, pinned by a strict xfail.**
`_VENDOR_KW` maps `"ati" → "AMD"`, and `"ati"` is a substring of `"VGA
compATIble controller"`, which appears in **every** `lspci` display line. So
the `"intel"` key is unreachable and Intel iGPUs are reported as AMD, which
switches on the GPU-accelerated terminal and AMD-only Electron flags. This
machine is unaffected only because `"nvidia"` is checked first. The xfail is
`strict=True`, so fixing the bug without removing the marker fails the suite.
**The user has been told and has not yet decided.**
- `bash linux_configuration/tests/test_security_hardening.sh` exits 1 with one
failure, `❌ FAIL: Compulsive block wrappers installed`. It belongs to
- `bash linux_configuration/tests/test_security_hardening.sh` exits 1 with
`❌ FAIL: Compulsive block wrappers installed`. Belongs to
`block_compulsive_opening.sh`.
- **`bucket_catch/packages/frontend` has 4 eslint errors.** Documented with
- **`bucket_catch/packages/frontend` has 4 eslint errors**, documented with
measured reasoning in
`docs/superpowers/evidence/bucket-catch-eslint-2026-08-17.json`. Notably
`docs/superpowers/evidence/bucket-catch-eslint-2026-08-17.json`.
`usePuzzleGameLoop.ts:129`'s `Map.get(...)!` cannot be fixed with
`?? Infinity` — that was tried and measured at 99.54% coverage, because the
default side is unreachable. `npm run coverage` is green: 145 tests, 100%.
`?? Infinity` — tried, measured at 99.54%, because the default side is
unreachable. `npm run coverage` is green: 145 tests, 100%.
- Repo-wide `jscpd` reports ~2.5% from the working tree but 1.47% at HEAD in a
clean worktree — the excess is vendored `.venv` site-packages. Don't chase it.
- The `tether_enforcer.sh` split is **unverified on the phone**. Its test passes
(17/17), and both `deploy.sh` lists were updated, but no deploy has run. If
tether enforcement misbehaves after the next deploy, look there first.
## Testing notes specific to this repo
- `linux_configuration/tests` **is** in CI, but never by name: `pyproject.toml`
sets `testpaths`, so the bare `pytest` in `python-tests.yml` collects it.
Behaviour is gated; coverage is not (`--cov=python_pkg` only).
- **Non-`python_pkg` modules are tested via `linux_configuration/tests/`**,
whose `conftest.py` puts standalone script directories on `sys.path`. Add a
directory there rather than moving code into `python_pkg/` — that move drags
the file under a `fail_under = 100` gate and, for anything with a by-path
caller, breaks it.
- `name-tests-test` requires every `.py` under `tests/` to be named `test_*.py`.
Shared helpers go in `conftest.py`, exposed as **fixtures** — `conftest` is
not importable by module name.
- For a **test-file** split the discriminating check is the test **count**, not
a green run: a file outside the runner's glob is silently never collected.
That caught a real breakage last session when a move separated a test from
its doubles.
- `phone_focus_mode`'s shell tests are **not** in CI. `shell-tests.yml` uses an
explicit file list covering `linux_configuration/tests/` only — so splitting a
script that **is** on that list means deciding whether the list grows or the
test sources the library.
- `python_pkg/focus_policy/loader.py` parses `phone_focus_mode/config.sh`.
Splitting that config breaks the loader unless the parser follows; its 100%
tests will catch it, but plan the two together.
sets `testpaths`, so bare `pytest` collects it. Behaviour is gated; coverage is
not (`--cov=python_pkg` only).
- **Non-`python_pkg` modules are tested via `linux_configuration/tests/`**, whose
`conftest.py` puts standalone script dirs on `sys.path`. Add a directory there
rather than moving code into `python_pkg/` — that move drags the file under a
`fail_under = 100` gate and breaks any by-path caller.
- `name-tests-test` requires every `.py` under `tests/` to be `test_*.py`. Shared
helpers go in `conftest.py` as **fixtures** — `conftest` is not importable by
module name.
- For a **test-file** split the discriminating check is the test **count**, not a
green run: a file outside the runner's glob is silently never collected. That
caught a real breakage last session.
- `phone_focus_mode`'s shell tests are **not** in CI — `shell-tests.yml` uses an
explicit list covering `linux_configuration/tests/` only. Run them by hand.
- `focus_owner` gradle needs `JAVA_HOME=/usr/lib/jvm/java-21-openjdk` and
`--rerun-tasks`; a plain `gradlew test` reports `UP-TO-DATE` and proves nothing.