NEXT_SESSION_PROMPT.md: rewrite for a fresh session, 5 files left
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 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
This commit is contained in:
Krzysztof Rudnicki 2026-08-21 15:17:11 +02:00
parent 47478791d8
commit b9608bbdd8

View File

@ -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]+ - ' <file>` 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 <pkg>` 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 <pkg>` 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_<lib>.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 <pre-rev> <old> <old> <new>...`** — 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 <pre-split-rev> <old> <new>...` —
proves every function moved verbatim. For a **partial** split, list the old
path among the new paths too.
- `meta/scripts/shell_coverage.sh <test> <subject> [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 <rev> <old> <new>...` — for a partial
split, list the old path among the new paths too.
- `meta/scripts/shell_coverage.sh <test> <subject> [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/<slug>-<date>.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 <changed>` **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/<slug>-<date>.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 <changed>` **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 <repo>` 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 <files>` 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 <repo>` 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 <files>` directly. Real fix: `cd` to the
git toplevel, or strip the prefix.