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
17 KiB
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.
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.
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
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:
- 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.
- 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).
- Split blind where there is no safe way to run the thing. Prove splits with byte-identical output, textual verification and hashes instead.
- 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.
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.
Two traps worth keeping:
python-tests.ymlis path-filtered. A commit touching only shell paths will NOT trigger it — expected, not a failure.gh run rerunreplays the historical commit's workflow YAML, notmain's. It cannot verify a workflow-file fix; push a real commit instead.
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
929 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh
918 linux_configuration/scripts/periodic_background/digital_wellbeing/setup_night_lockdown.sh
705 linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh
485 linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh
All five are the deployment-trap tier. The three safe-tier files are done.
Suggested order: smallest first
install_leechblock.sh (485) → block_compulsive_opening.sh (705) →
setup_night_lockdown.sh (918) → pacman_wrapper.sh (929) →
setup_midnight_shutdown.sh (1734).
install_leechblock.sh — START HERE, and read this first
This was attempted and deliberately reverted last session. The split itself worked; it was abandoned because of a tooling bug you must not repeat:
shfmt -wCORRUPTS 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 -won a file with adeclare -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.
What already worked, so you can redo it quickly:
--helpexits 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_version119–138,download_extension139–200,inject_default_config201–231,detect_browsers232–257,wire_up_browsers348–392,install_firefox_policy393–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_browsersin the same lib aswire_up_browsers. It writesBROWSERS/FIREFOXES, whichwire_up_browsersreads; splitting them creates a write-only cross-file global and an unfixable SC2034. set -Eeuo pipefail(note the-E) does not matchextract_shell_functions.py's anchor regex, so it will not insert the source lines. Add them by hand.get_latest_taghas 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.
The deployment trap, per file
pacman_wrapper.sh— copied to/usr/local/binand the deployed copy is preferred on every pacman invocation. A naive split breakspacman -Son this machine. Re-verify the mechanism (grep -n "prefer\|deployed") before assuming. You cannot runsudo pacman -Sfrom the Bash tool (deadlocks ondb.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.pyandverify_shell_split.shboth assume real top-level functions and will mishandle a heredoc body. The plan that works: extract only the genuine outer orchestration functions, treat eachcreate_*_scriptheredoc as one atomic unit that moves whole, and verify by hashing the emitted/usr/local/bin/*content before and after (sha256sumon a rendered-heredoc temp file), not just by runningverify_shell_split.shon the outer file.
The method that worked three times — copy it
Every one of the three completed splits followed this, and it caught real bugs each time:
- Capture a baseline BEFORE any edit. Either a golden output fixture (if
the script writes to a directory you control —
generate_study_materialstook$RESULTS_DIRas$1) or, if it writes to hardcoded system paths, a--help/--statusrun plussha256sumof everything it could touch. Never run an installer to get a baseline — an aborted run can leave the system half-configured.hosts/install.shhad no safe mode at all and was proven textually only; that is acceptable and was recorded as such. - Wrap top-level blocks into phase functions WITHOUT re-indenting them.
extract_shell_functions.pyonly moves functions, and most of these files are mostly top-level code. Quoted heredoc terminators must stay at column 0. Letshfmtindent afterwards. - 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.shcannot see. - 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. - Re-verify the baseline after every step, including after
shfmt. - 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. Anchors the source line onset -e-ish lines;set -Eeuo pipefaildoes 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.shinstruments bash and reports 0/0. Point it at arun_all.shwhen several test files jointly cover one lib.
Sharp edges that have bitten (each one cost real time)
shfmt -wcorrupts[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 andcatit instead. grep -vexits 1 when it filters every line, which kills the caller underset -e. Use{ grep -v ... || true; }.- Order matters:
grep -v '^#' file | head -n N, nothead | grep— the other way, comment lines count against N. printf '---...'parses leading dashes as options. Useprintf '%s' '---'.- A function whose pipeline starts with a failing
grepaborts underset -o pipefailif called bare. Production often escapes this by calling it insideif, which suppressesset -e. Call it viaifin tests too. local a="$1" b="${a}"fails underset -u— separatelocallines.- A function that calls
exitkills the test script if called directly in anifcondition. Use a subshell:if (fn args); then. - New sourced libs need a shebang AND the executable bit, staged with
git add --chmod=+x— a plaingit addafterwards resets it. This bit every single commit; checkgit ls-files -sfor100755before committing. - SC2034 on a cross-file global is real, not noise — the hook runs
shellcheck with no
-xand 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{ ... } >>filegroup, 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.
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 indocs/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 listafter every push. pre-commit run --files <changed>aftergit add.prettierandci-mirrorrun on pre-push.npx prettier --writeany.md/.jsonyou touch, evidence and contract files included.codespellruns 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 stashand branch creation are blocked; usegit worktree add --detachfor a clean baseline (but NEVERgit filter-repoinside a worktree — it shares the object store). - Watch
jscpd(fails above 2%). The test harnesses are the risk: source one shared harness perlib/tests/dir rather than copying setup blocks.
Phase 2: Wire the file-length gate into pre-commit
Do NOT start until Phase 1 exits 0 — wiring it early breaks every commit repo-wide, including unrelated ones.
-
Add a
localhook to.pre-commit-config.yamlrunningbash meta/scripts/check_file_length.sh --all, following the existinglanguage: system+pass_filenames: falseshape (seeai-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. -
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
mainis green before allowing a new 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-mirroralready does this for the Python/pre-commit side. Extending it to also runShell tests' local equivalent is likely the more honest answer.
This is the one place in Phases 1–2 where stopping to ask is correct.
- (a) check the latest run on
-
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. 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 user's call.
What the last session finished (do not redo)
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)
- A warning masked an error in 8 places (
services_units.sh,services_hosts.sh).status="warning"was assigned unconditionally, overwriting anerrorset by an earlier check. In the three unit checks this meantreport_and_fix— which repairs only onerror— never ran, so a timer that was both disabled and inactive was reported and then left broken. Proven with an A/B: without the guardsstatus=warning, repair ran 0; with themstatus=error, repair ran 1. generate_study_materials:--topwas silently ignored for imports (hardcodedhead -20in 3 loops); comment lines became flashcards (only 1 of 9 loops filtered them);python_doc_urlrequired a$2it never reads, which aborted any caller usingset -u.hosts/install.sh:extract_unblock_entries_from_scriptmatched its ownPROTECTED_UNBLOCK_LIST_START/_ENDmarkers as if they were domains. They are in this machine's live/etc/hosts.unblock-entries.stateright 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, aservices_assert.shsplit out for the cap, aservices_path.shthat owns PATH socommand -vis controllable, and arun_all.shused as the coverage subject.linux_configuration/scripts/periodic_background/hosts/lib/tests/— how to test a script you must never execute: shimchattr/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 cds 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.