From b9608bbdd86f6a86ce6d05e05032b25bf47714f7 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 21 Aug 2026 15:17:11 +0200 Subject: [PATCH] NEXT_SESSION_PROMPT.md: rewrite for a fresh session, 5 files left The three safe-tier files are done; all five remaining are deployment-trap tier. Records what the last session actually learned rather than restating the plan: - the shfmt bug that killed the install_leechblock attempt (it silently rewrites `[google-chrome-stable]` as `[google - chrome - stable]` in an associative array, which would have broken browser detection outright), plus the grep that catches it; - the phase line-ranges and lib layout for install_leechblock that were already verified byte-identical, so the next session can redo it quickly rather than rediscovering them; - the six-step method all three completed splits used, including the rule that you never run an installer just to capture a baseline; - ten sharp edges that each cost real time, from subshells stranding state to kcov mis-attributing brace-group closers; - the three latent defects fixed along the way, and the note that one of them leaves two phantom entries in this machine's live unblock state file which the next install will self-heal. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV --- NEXT_SESSION_PROMPT.md | 420 ++++++++++++++++++++++++----------------- 1 file changed, 245 insertions(+), 175 deletions(-) diff --git a/NEXT_SESSION_PROMPT.md b/NEXT_SESSION_PROMPT.md index 0672d12e..77d87d13 100644 --- a/NEXT_SESSION_PROMPT.md +++ b/NEXT_SESSION_PROMPT.md @@ -1,174 +1,222 @@ -# Next session: Phase-driven campaign — CI green → 250-cap zero → gates → shell coverage +# Next session: 250-line cap campaign — 5 files left, all deployment-trap tier > **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. -This file drives a **multi-session campaign** with four phases, run strictly -in order. Do not skip ahead to a later phase while an earlier one is -incomplete — re-check the phase's exit condition at the start of every -session, even if this doc claims it's done, since state can drift between -sessions. +**The user's original request** (verbatim intent): make it true FOR SURE that +all files are under the 250-line cap, then add a 100% shell test-coverage gate, +then add a pre-commit rule that blocks commits when any file is over 250 lines +AND checks that GitHub Actions is green on this repo. -**The user's request that created this campaign** (verbatim intent): make it -true FOR SURE that all files are under the 250-line cap, then add a 100% -shell test-coverage gate, then add a pre-commit rule that blocks commits when -any file is over 250 lines AND checks that GitHub Actions is green on this -repo. +Plus a standing instruction from the last session: **fix every real defect you +find along the way**, don't just pin it in a test. Three latent bugs were fixed +that way already (see "Defects found and fixed" below) — keep doing that. -## How to run this session (read before anything else) +## How to run this session -**Do not ask "should I continue?" between files.** The queue below is already -approved. Work it to completion, or until you hit a genuine design fork or an -irreversible action. Ending a turn to narrate progress mid-queue is a bug — -summarise once, at the end. This was violated twice in the previous session -despite being written in `~/.claude/memories/workflow-rules.md`; it is -repeated here because the standing rule alone did not hold. +**Do not ask "should I continue?" between files.** The queue is approved. Work +it to completion, or until a genuine design fork or an irreversible action. +Ending a turn to narrate progress mid-queue is a bug — summarise once, at the +end. **Standing decisions — do NOT re-litigate:** -1. **100% line coverage on what you extract/write tests for** — same bar - every prior split has used. -2. **Split blind where there is no device.** Do not deploy to the phone, do - not ask to. Prove splits with tests, hashes and real runs instead. -3. **CI must be fixed before any new gate is added.** (Phase 0 is done; this - just means don't add a gate on top of a red baseline if CI regresses.) -4. **The shell coverage gate is a NEW, separate requirement** covering every - `.sh` file repo-wide. That is Phase 3 — do not conflate it with Phase 1. +1. **Split first, then reach 100% coverage in a SEPARATE follow-up commit.** + The user chose this explicitly (option b) over doing both at once, because + it keeps each split provably behaviour-preserving. Record the measured + coverage number in the evidence JSON either way — never waive it. +2. **One commit per split file**, each with its own evidence JSON _and_ a + contract JSON (a split always stages ≥4 code files, so the contract gate + always fires — bake it into the checklist). +3. **Split blind where there is no safe way to run the thing.** Prove splits + with byte-identical output, textual verification and hashes instead. +4. **Archiving is acceptable if you can prove a file is dead** — no + references, no installed copy, no unit/timer/hook invoking it. Surface the + evidence and ask before deleting. Default is split, not delete. -The old "one file split per session" pacing decision is **superseded**: the -user explicitly asked for multiple files per session ("this is a very slow -way of doing it"). Batch through the queue. +## Phase 0: CI green — verify only ---- +`gh run list --limit 5` should show `Pre-commit checks` and `Shell tests` green +on `main`. Both were green at `47478791`. -## Phase 0: CI green — **DONE 2026-08-18, re-verify only** +Two traps worth keeping: -`gh run list --limit 5` should show `Pre-commit checks`, `Shell tests` and -`Python tests` green on `main`. Fixed by adding `librsvg2-bin` to both -workflows (`4326ea4`) and making `python-tests.yml` self-triggering + -`workflow_dispatch` (`4a35be7`). +- `python-tests.yml` is path-filtered. A commit touching only shell paths will + NOT trigger it — expected, not a failure. +- `gh run rerun` replays the historical commit's workflow YAML, not `main`'s. + It cannot verify a workflow-file fix; push a real commit instead. -**Two traps worth keeping:** - -- `python-tests.yml` is path-filtered. A commit touching only workflow files - or only non-Python paths will NOT trigger it — that is expected, not a - failure. Check the workflow's own last run rather than assuming. -- `gh run rerun` replays the **historical commit's** workflow YAML, not - `main`'s current one. It cannot verify a workflow-file fix. Push a real - commit under a watched path instead. - -## Phase 1: Drive the 250-line cap to zero — **IN PROGRESS, 8 files left** +## Phase 1: Drive the cap to zero — 5 files left Re-run `bash meta/scripts/check_file_length.sh --all` first; the list drifts. ``` 1734 linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh -1225 linux_configuration/scripts/periodic_background/check_and_enable_services.sh -1017 linux_configuration/scripts/single_use/utils/generate_study_materials.sh 929 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh 918 linux_configuration/scripts/periodic_background/digital_wellbeing/setup_night_lockdown.sh - 913 linux_configuration/scripts/periodic_background/hosts/install.sh 705 linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh 485 linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh ``` -**Everything remaining is a shell script.** All three `focus_owner` files are -done (`47e05d1`, `6efb4ba`, `7d7b3c7`) — that repo is confirmed live and -enforcing, not a dead-code candidate. +**All five are the deployment-trap tier.** The three safe-tier files are done. -### The queue, in this order +### Suggested order: smallest first -**Safe tier — do these first, no deployment trap:** +`install_leechblock.sh` (485) → `block_compulsive_opening.sh` (705) → +`setup_night_lockdown.sh` (918) → `pacman_wrapper.sh` (929) → +`setup_midnight_shutdown.sh` (1734). -1. `check_and_enable_services.sh` (1225) — a `lib/` already exists beside it - at `linux_configuration/scripts/periodic_background/lib/` (currently holds - only `periodic_browser.sh`), but **no `lib/tests/` yet** — you will create - it. Referenced by `install_makepkg_wrapper.sh` and - `pacman/lib/integrity.sh`. -2. `generate_study_materials.sh` (1017) — referenced by `repo_to_study.sh` - and `lib/repo_study_steps.sh`; a `lib/` with `repo_study_steps.sh` already - exists beside it. -3. `hosts/install.sh` (913) — **widest reference surface of the three: 13 - referencing files**, including `install_core_system.sh`, - `setup_periodic_system.sh`, `fresh-install/main.sh` and - `phone_focus_mode/deploy_phases.sh`. Handle with more care than its line - count suggests; `grep -rl` before and after. +### `install_leechblock.sh` — START HERE, and read this first -All three were confirmed live/referenced — **none is an archive candidate**, -so don't spend a round asking "do you still use this?" for these three. +This was **attempted and deliberately reverted** last session. The split itself +worked; it was abandoned because of a tooling bug you must not repeat: -**Deployment-trap tier — 4 files, handle last and very carefully:** +> **`shfmt -w` CORRUPTS associative-array keys containing hyphens.** +> `[google-chrome-stable]="Google Chrome"` became +> `[google - chrome - stable]="Google Chrome"` — silently, and it would have +> made browser detection fail entirely. shfmt parses the key as arithmetic. +> +> **Before every `shfmt -w` on a file with a `declare -A`, and again after:** +> `grep -nE '\[[a-z0-9]+ - ' ` must come back empty. None of the three +> committed splits are affected (checked); only this file has such keys. -`setup_midnight_shutdown.sh`, `pacman_wrapper.sh`, `install_leechblock.sh`, -`block_compulsive_opening.sh`. These are copied to `/usr/local/…` and -`pacman_wrapper.sh` prefers the deployed copy on **every pacman invocation** -— a naive split breaks `pacman -S` on this machine. Re-verify the mechanism -before assuming (`grep -n "prefer\|deployed" pacman_wrapper.sh` near line -831). You cannot run `sudo pacman -S` from the Bash tool (deadlocks on -`db.lck`); hand the user a `! sudo pacman -S ` line plus the expected -output if live verification is ever needed. +What already worked, so you can redo it quickly: -**`setup_midnight_shutdown.sh` additionally needs a non-standard plan.** Its -39 top-level-looking functions are NOT all real: several (`log`, -`now_epoch`/`current_epoch`, `cmd_add`/`cmd_list`/`cmd_remove`, -`require_root`) are duplicated because the outer script _generates_ inner -standalone scripts via heredocs (`create_shutdown_check_script`, -`create_override_manager_script`, `install_monitor_service`) that get written -to `/usr/local/bin/`. `extract_shell_functions.py` and -`verify_shell_split.sh` both assume real top-level functions and will -mishandle a heredoc body. The plan that will work: extract only the genuine -outer orchestration functions, treat each `create_*_script` heredoc as one -atomic unit that moves whole (never split its interior), and verify by -hashing the _emitted_ `/usr/local/bin/*` content before and after -(`sha256sum` on a rendered-heredoc temp file), not just by running -`verify_shell_split.sh` on the outer file. +- `--help` exits before any write — the only safe execution path. Capture it + as a baseline and diff after. +- Six phases wrap cleanly, verified byte-identical, at these **pre-split** + line ranges: `resolve_version` 119–138, `download_extension` 139–200, + `inject_default_config` 201–231, `detect_browsers` 232–257, + `wire_up_browsers` 348–392, `install_firefox_policy` 393–486. +- Four libs: `leechblock_fetch.sh` (get_latest_tag, resolve_version, + download_extension), `leechblock_config.sh` (inject_default_config), + `leechblock_browsers.sh` (**detect_browsers + replace_browser_in_place + + wire_up_browsers — keep these three together**, see below), + `leechblock_firefox.sh` (install_firefox_policy). +- **Put `detect_browsers` in the same lib as `wire_up_browsers`.** It writes + `BROWSERS`/`FIREFOXES`, which `wire_up_browsers` reads; splitting them + creates a write-only cross-file global and an unfixable SC2034. +- `set -Eeuo pipefail` (note the `-E`) does **not** match + `extract_shell_functions.py`'s anchor regex, so it will not insert the + source lines. Add them by hand. +- `get_latest_tag` has three curl fallbacks (releases API → tags API → + redirect header parse) and is fully testable with a curl shim. Worth 100%: + a silent failure there installs the wrong version. -### Reference split to copy +### The deployment trap, per file -`linux_configuration/scripts/single_use/fixes/lib/` + `lib/tests/` is the -worked example — look at the `pacman_hook_stall_*` family: +- **`pacman_wrapper.sh`** — copied to `/usr/local/bin` and the deployed copy is + preferred on **every pacman invocation**. A naive split breaks `pacman -S` on + this machine. Re-verify the mechanism (`grep -n "prefer\|deployed"`) before + assuming. You cannot run `sudo pacman -S` from the Bash tool (deadlocks on + `db.lck`); hand the user a `! sudo pacman -S ` line plus the expected + output if a live check is ever needed. Byte-identical hash equivalence of the + deployed artifact is accepted as sufficient proof (user's call, Q5). +- **`setup_midnight_shutdown.sh`** — needs a non-standard plan. Its ~39 + top-level-looking functions are NOT all real: several (`log`, `now_epoch`, + `cmd_add`/`cmd_list`/`cmd_remove`, `require_root`) are duplicated because the + outer script _generates_ inner standalone scripts via heredocs + (`create_shutdown_check_script`, `create_override_manager_script`, + `install_monitor_service`) written to `/usr/local/bin/`. + `extract_shell_functions.py` and `verify_shell_split.sh` both assume real + top-level functions and will mishandle a heredoc body. **The plan that + works:** extract only the genuine outer orchestration functions, treat each + `create_*_script` heredoc as one atomic unit that moves whole, and verify by + hashing the _emitted_ `/usr/local/bin/*` content before and after + (`sha256sum` on a rendered-heredoc temp file), not just by running + `verify_shell_split.sh` on the outer file. -- `lib/tests/pacman_hook_stall_harness.sh` — sourced, not executed; - `_t_pass`/`_t_fail`/`_t_eq`, `mktemp -d` + `trap cleanup EXIT`, PATH-shim - fakes for external tools. -- `lib/tests/pacman_hook_stall_entry_harness.sh` — separate fixture setup for - the entry-script test, split out early so neither file passes 250 lines. -- One `test_.sh` per lib, asserting against real function calls. -- `test_diagnose_pacman_hook_stall_*.sh` — runs the **actual entry script as - a subprocess** against the same fakes. +## The method that worked three times — copy it -### Tooling +Every one of the three completed splits followed this, and it caught real bugs +each time: -- `meta/scripts/check_file_length.sh --all` — the gate. Run before every - commit. Applies to test files too. +1. **Capture a baseline BEFORE any edit.** Either a golden output fixture (if + the script writes to a directory you control — `generate_study_materials` + took `$RESULTS_DIR` as `$1`) or, if it writes to hardcoded system paths, a + `--help`/`--status` run plus `sha256sum` of everything it could touch. + **Never run an installer to get a baseline** — an aborted run can leave the + system half-configured. `hosts/install.sh` had no safe mode at all and was + proven textually only; that is acceptable and was recorded as such. +2. **Wrap top-level blocks into phase functions WITHOUT re-indenting them.** + `extract_shell_functions.py` only moves functions, and most of these files + are mostly top-level code. Quoted heredoc terminators must stay at column 0. + Let `shfmt` indent afterwards. +3. **Prove each wrapped body is byte-identical** to its original line range + with a Python diff, and check a line-multiset for lost content. This is what + covers the top-level code that `verify_shell_split.sh` cannot see. +4. **Run `verify_shell_split.sh ...`** — additions + are your new wrappers; any `-`/`+` pair on the same name is a body you + changed and must justify. +5. **Re-verify the baseline** after every step, including after `shfmt`. +6. **Then** write tests, fix what they expose, commit. + +## Tooling + +- `meta/scripts/check_file_length.sh --all` — the gate. Applies to test files + too; two test files needed splitting twice to stay under it. - `meta/scripts/extract_shell_functions.py` — moves functions brace-by-brace. - **Never slice by line range.** Verify placement with `grep -c` on the - anchor. -- `meta/scripts/verify_shell_split.sh ...` — - proves every function moved verbatim. For a **partial** split, list the old - path among the new paths too. -- `meta/scripts/shell_coverage.sh [min]` — kcov wrapper, - 100% minimum. Hand it the test script **directly**; `bash script.sh` - instruments the bash binary and silently reports 0/0. + **Never slice by line range.** Anchors the source line on `set -e`-ish lines; + `set -Eeuo pipefail` does not match. +- `meta/scripts/verify_shell_split.sh ...` — for a partial + split, list the old path among the new paths too. +- `meta/scripts/shell_coverage.sh [min]` — kcov wrapper. Hand + it the test script **directly**; `bash script.sh` instruments bash and + reports 0/0. Point it at a `run_all.sh` when several test files jointly cover + one lib. -### Sharp edges that have bitten before +## Sharp edges that have bitten (each one cost real time) -- `local a="$1" b="${a}"` **fails under `set -u`** — separate `local` - statements. -- A function that calls `exit` (not `return`) kills the whole test script if - called directly in an `if` condition. Use a subshell: `if (fn args); then`. -- A backgrounded process needs `disown` before `kill`/`wait` behaves. -- **SIGINT to a backgrounded job was unreliable here; SIGTERM was not.** -- If a script computes a real resource allocation from live system state, - make the constants env-overridable and pin them tiny in every test. -- **A file must not assign a global it never reads** (SC2034). The hook runs - `shellcheck` with no `-x`, but `.shellcheckrc`'s `external-sources=true` + - `source-path=SCRIPTDIR` means a `source=` annotation on the _sourcing_ file - still resolves. +- **`shfmt -w` corrupts `[hyphenated-keys]` in associative arrays.** Grep after + every run. This is what killed the leechblock attempt. +- **A subshell strands state.** `out=$(check_foo)` or `(cd x && check_foo)` + throws away every global the function set, so assertions silently test + nothing. Redirect to a file and `cat` it instead. +- **`grep -v` exits 1 when it filters every line**, which kills the caller + under `set -e`. Use `{ grep -v ... || true; }`. +- **Order matters: `grep -v '^#' file | head -n N`, not `head | grep`** — the + other way, comment lines count against N. +- **`printf '---...'` parses leading dashes as options.** Use + `printf '%s' '---'`. +- **A function whose pipeline starts with a failing `grep` aborts under + `set -o pipefail`** if called bare. Production often escapes this by calling + it inside `if`, which suppresses `set -e`. Call it via `if` in tests too. +- **`local a="$1" b="${a}"` fails under `set -u`** — separate `local` lines. +- **A function that calls `exit` kills the test script** if called directly in + an `if` condition. Use a subshell: `if (fn args); then`. - **New sourced libs need a shebang AND the executable bit**, staged with - `git add --chmod=+x` — a plain `git add` afterwards resets it. + `git add --chmod=+x` — a plain `git add` afterwards resets it. This bit + every single commit; check `git ls-files -s` for `100755` before committing. +- **SC2034 on a cross-file global is real, not noise** — the hook runs + shellcheck with no `-x` and genuinely cannot see the reader. Fix it by + moving the writer next to the reader, or by routing writes through a + setter/getter in the reader's file. Never suppress. +- **kcov mis-attributes `}` lines closing a `{ ... } >>file` group**, and + reports them uncovered even when a passing assertion proves the group ran. + Diagnose it (run the suite _under kcov_ and confirm it still passes) rather + than chasing it; record it in the evidence. -**Exit condition for Phase 1:** `check_file_length.sh --all` exits 0. +## Rules that will bite you + +- **No suppressions, ever.** No `# noqa`, no per-file-ignore without asking + every time, no new shellcheck disable. +- **Every commit touching code needs evidence** in + `docs/superpowers/evidence/-.json`, **plus a contract** in + `docs/superpowers/contracts/` (≥4 code files staged — always true here). Put + the **measured** number in it, not a rounded one. +- **A test file broken by your commit is a same-session bug, not a TODO.** Run + `gh run list` after every push. +- `pre-commit run --files ` **after** `git add`. `prettier` and + `ci-mirror` run on **pre-push**. `npx prettier --write` any `.md`/`.json` you + touch, evidence and contract files included. +- `codespell` runs in pre-commit and rejects some spellings you would not + expect, including in code comments and test descriptions. It caught one in + the last session. Fix the wording rather than adding an ignore. +- Work directly on `main`. `git stash` and branch creation are blocked; use + `git worktree add --detach` for a clean baseline (but NEVER `git filter-repo` + inside a worktree — it shares the object store). +- Watch `jscpd` (fails above 2%). The test harnesses are the risk: source one + shared harness per `lib/tests/` dir rather than copying setup blocks. ## Phase 2: Wire the file-length gate into pre-commit @@ -179,69 +227,91 @@ repo-wide, including unrelated ones. `bash meta/scripts/check_file_length.sh --all`, following the existing `language: system` + `pass_filenames: false` shape (see `ai-evidence-contract` / `no-polling-antipatterns`). Repo-wide, not - per-file: a file that grows past 250 lines without being staged this - commit should still be caught. -2. **GitHub-Actions-green check — GENUINE DESIGN FORK, ask the user first.** - A pre-commit hook cannot block on "is CI green" for a commit that does not + per-file: a file that grows past 250 lines without being staged this commit + should still be caught. +2. **GitHub-Actions-green check — GENUINE DESIGN FORK, ask the user first.** A + pre-commit hook cannot block on "is CI green" for a commit that does not exist yet. Two real options: - **(a)** check the latest run on `main` is green before allowing a new - commit — catches "building on a known-broken baseline", not "did MY - change break it"; + commit — catches "building on a known-broken baseline", not "did MY change + break it"; - **(b)** a **pre-push** hook running the CI workflows' local equivalent before allowing the push — closer to the real guarantee, and `ci-mirror` - already does this for the Python/pre-commit side. Extending it to also - run `Shell tests`' local equivalent is likely the more honest answer. + already does this for the Python/pre-commit side. Extending it to also run + `Shell tests`' local equivalent is likely the more honest answer. This is the one place in Phases 1–2 where stopping to ask is correct. -3. Verify by making a throwaway file exceed 250 lines, confirming the commit - is blocked with a clear message, then removing it. +3. Verify by making a throwaway file exceed 250 lines, confirming the commit is + blocked with a clear message, then removing it. ## Phase 3: Repo-wide 100% shell coverage gate — largest phase, many sessions -**Do NOT start until Phases 1 AND 2 are done.** - -487 `.sh` files exist repo-wide vs ~15 `lib/tests/` dirs following the -established pattern — on the order of 450+ untested scripts, roughly an order -of magnitude more work than Phase 1. Re-count before starting. +**Do NOT start until Phases 1 AND 2 are done.** Re-count first; it was ~487 +`.sh` files repo-wide against a handful of `lib/tests/` dirs. **Ask the user to confirm scope before starting Phase 3 at all**, and ask whether the gate should be repo-wide from day one or **ratchet** (only new or modified files must be covered, with a shrinking allowlist of pre-existing -untested files). A hard repo-wide gate would block unrelated commits to any -of 450+ files until every one has tests. Recommend the ratchet, but it is the +untested files). A hard repo-wide gate would block unrelated commits to any of +450+ files until every one has tests. Recommend the ratchet, but it is the user's call. --- -## Rules that will bite you +## What the last session finished (do not redo) -- **No suppressions, ever.** No `# noqa`, no per-file-ignore without asking - every time, no new shellcheck disable. -- **Every commit touching code needs evidence** in - `docs/superpowers/evidence/-.json`. Staging **≥4 code files - also needs** a fresh `docs/superpowers/contracts/*.json`. Put the - **measured** number in it, not a rounded one. -- **An archive does not need new tests.** The 100%-coverage rule binds code - that is _split_ or newly _written_, not code that is _deleted_. -- **A test file broken by your commit is a same-session bug, not a TODO.** - Run `gh run list` after every push — CI-only suites (like - `linux_configuration/tests/*.sh` run directly by `shell-tests.yml`) are not - caught by pre-commit's file-scoped hooks. -- `pre-commit run --files ` **after** `git add`. `prettier` and - `ci-mirror` run on **pre-push**. `npx prettier --write` any `.md`/`.json` - you touch, evidence and contract files included. -- Work directly on `main`. `git stash` and branch creation are blocked; use - `git worktree add --detach` for a clean baseline (but NEVER - `git filter-repo` inside a worktree — it shares the object store and will - corrupt the main repo; always a throwaway `git clone` for that). -- Watch `jscpd` (fails above 2%). Measure in a clean HEAD worktree. -- Cap pytest memory: - `systemd-run --user --scope -p MemorySwapMax=0 -p MemoryMax=2G`. -- **Known tooling bug, fix opportunistically:** - `~/.claude/scripts/dart_format_changed.sh ` reports "No changed Dart - files" even when files changed, if invoked with a repo path from outside - that repo — it `cd`s into `$repo` then runs `git status --porcelain`, whose - paths are relative to the **git worktree root**, so its `[[ -f $path ]]` - check fails for every file. Workaround used: `dart format ` directly. - Real fix: `cd` to the git toplevel, or strip the prefix. +Five commits, all pushed, CI green: + +| Commit | What | +| ---------- | ----------------------------------------------------- | +| `9085a4e5` | `check_and_enable_services.sh` 1225 → 202, eight libs | +| `0a69d183` | Same file: `$SYSROOT` prefix → 100% on 7 of 8 libs | +| `08f3c764` | Same file: fixed the status-downgrade bug (below) | +| `241652ca` | `generate_study_materials.sh` 1017 → 148, six libs | +| `47478791` | `hosts/install.sh` 913 → 150, six libs + a data file | + +### Defects found and fixed (all pre-existing, all verified) + +1. **A warning masked an error in 8 places** (`services_units.sh`, + `services_hosts.sh`). `status="warning"` was assigned unconditionally, + overwriting an `error` set by an earlier check. In the three unit checks + this meant `report_and_fix` — which repairs only on `error` — **never ran**, + so a timer that was both disabled and inactive was reported and then left + broken. Proven with an A/B: without the guards `status=warning, repair ran +0`; with them `status=error, repair ran 1`. +2. **`generate_study_materials`**: `--top` was silently ignored for imports + (hardcoded `head -20` in 3 loops); comment lines became flashcards (only 1 + of 9 loops filtered them); `python_doc_url` required a `$2` it never reads, + which aborted any caller using `set -u`. +3. **`hosts/install.sh`**: `extract_unblock_entries_from_script` matched its + own `PROTECTED_UNBLOCK_LIST_START`/`_END` markers as if they were domains. + **They are in this machine's live `/etc/hosts.unblock-entries.state` right + now.** After the fix the whitelist reads as having shrunk by 2, which the + guard allows, so the next install self-heals the state — no manual step, but + the next run will report 2 removed entries. + +### Known-good patterns to copy + +- `linux_configuration/scripts/periodic_background/lib/tests/` — the fullest + example: a shared harness, a `services_assert.sh` split out for the cap, a + `services_path.sh` that owns PATH so `command -v` is controllable, and a + `run_all.sh` used as the coverage subject. +- `linux_configuration/scripts/periodic_background/hosts/lib/tests/` — how to + test a script you must never execute: shim `chattr`/`curl`, point the + already-variable state-file paths at a tmpdir, and add + `${HOSTS_INSTALL_SCRIPT_PATH:-$0}`-style overrides where a path is otherwise + underivable. +- `linux_configuration/scripts/periodic_background/hosts/custom_entries.hosts` + — a ~320-line heredoc turned into a data file, with the guard that reads it + updated in the same commit and the extracted set diffed (254 domains, + identical) to prove the guard still sees what it saw before. + +### Known tooling bug, fix opportunistically + +`~/.claude/scripts/dart_format_changed.sh ` reports "No changed Dart +files" even when files changed, if invoked with a repo path from outside that +repo — it `cd`s into `$repo` then runs `git status --porcelain`, whose paths are +relative to the **git worktree root**, so its `[[ -f $path ]]` check fails for +every file. Workaround: `dart format ` directly. Real fix: `cd` to the +git toplevel, or strip the prefix.