Three of the old brief's own instructions did not survive contact:
- the config.sh fingerprint recipe CANNOT WORK. Four policy fields are
frozenset, so json.dumps(default=str) serialises them in per-process
iteration order; three runs gave three digests. Replaced with the repo's
own deterministic exporter (baseline 83e05e82) AND a diff of the sourced
environment, because the hash alone passed clean through both bugs hit
this session.
- extract_shell_functions.py does NOT reliably place its source line. It
got the position wrong three ways: before SCRIPT_DIR existed, before
STATE_DIR (every path then expanded against an empty prefix), and on a
nested re-source instead of the top-level one.
- "100% line coverage, always" now has a measured exception, and stating
it as an absolute is actively dangerous. daemon_apps.sh has five
uncovered `done <file` lines whose loops call pm; a reader who pipes
them -- as this session correctly did in curfew_net.sh, where the body
is pure -- reintroduces a measured 1-of-54 bug in the code that decides
which apps get disabled at home. It is now a PROHIBITION at the top of
the file, with the line numbers and mapfile as the only alternative.
Also new: the kcov artifact taxonomy with an explicit safe/unsafe column,
the mutation tooling and the rule that an equivalent mutation means dead
code to delete rather than a test to write, and the two hazards
phone_focus_mode now carries (config.sh at exactly 250 with fifteen
loader-pinned variables enforced only by a comment; deploy.sh's two
hardcoded lists now living in two files).
Two numbers were checked rather than recalled, and both were wrong in the
draft: 120 mutations across six specs, not 127; and TWO copies of the
Haversine, not three -- daemon_location.sh's calc_distance IS focus_daemon's
original, moved, which I had double-counted in an earlier commit message.
Running two mutation specs concurrently leaves a mutation applied and reads
as a survivor; that cost a false 16/17 here and is now written down.
295 -> 250 lines. The old brief counted itself among the over-cap files and
was 45 lines past the gate that never checks .md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
13 KiB
Next session: §3 restructures, then §4's deployed-copy trap
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.
Over-cap: 15 (12 shell, 2 kotlin, 1 dart). phone_focus_mode/ is done —
no file there is over the cap. Working tree clean, main in sync.
Two standing decisions from the user, already made — do not re-litigate:
- 100% line coverage on what you extract — with the one measured exception in §0.1, which is a prohibition, not a target to chase.
- 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.
0. Read this first — three rules that cost real time to learn
0.1 NEVER pipe a while read loop whose body calls pm/adb/iptables
These lines are deliberately uncovered and must stay that way:
| file | lines | why |
|---|---|---|
phone_focus_mode/daemon_apps.sh |
35, 60, 110, 130, 158 | trailing done < file |
Each is the redirect of a loop whose body calls pm. Piping the file in
instead lets pm inherit and drain the loop's stdin, so it processes one
package instead of all of them — measured in this repo as 1 of 54. kcov
cannot attribute a trailing done < file, so those five lines read as
uncovered forever; that is the price and it is paid knowingly. They are
pinned by three mutations in meta/scripts/fixtures/mutations/daemon_libs.json
that redirect them from /dev/null and fail the suite.
If you ever must remove such a redirect line, use mapfile -t arr < file
then for x in "${arr[@]}" — never a pipe — and only after confirming
nothing in the body reads stdin. Piping is correct where the body is pure
(curfew_net.sh did it safely); the body is what decides.
0.2 kcov artifact taxonomy
Four constructs are instrumented but never reported, so they hold a file below 100% no matter how well tested:
| construct | safe to restructure? |
|---|---|
| multi-line array/list literal | yes — put it on one line |
{ ... } >file group |
yes — redirect per statement, or one printf |
multi-line mapfile < <(...) |
yes — put the substitution on one line |
embedded multi-line awk '...' |
no — collapsing it destroys readability |
trailing done < file |
only if the body is stdin-pure (§0.1) |
Before restructuring, prove the lines actually execute with a mutation. Two
files are knowingly below 100% for this reason and both are documented:
daemon_location.sh 65.00% (the awk Haversine) and daemon_apps.sh 93.42%.
0.3 An equivalent mutation means dead code, not a missing test
meta/scripts/mutate_shell.py <spec> runs one of the six specs in
meta/scripts/fixtures/mutations/ — 120 mutations, all killed.
A survivor is either a missing assertion or equivalent code. Twice this
session it was the latter, and both times the right fix was deleting the dead
branch: an unreachable -f guard in hosts_mount.sh, and a redundant
comment filter in ctl_usage.sh. Never resolve a survivor by weakening the
mutation or adding a test that cannot fail.
A no-op (the find string matched nothing) counts as a failure — a stale
spec entry silently stops testing. Run specs one at a time: two runs at
once edit the same subjects and leave a mutation applied, which reads as a
survivor and dirties the tree.
1. §3 — the three restructures (start here)
Verbatim moves cannot get these under the cap, so verify_shell_split.sh
does not apply. Each needs a real test at 100%.
install_plagiarism_tools.sh(534, only 3 tiny functions) — ~500 lines of top-level code. Wrap coherent stages in functions first, then move them. Most mechanical of the three; do it first.verify_shell_split.shonly hashes function bodies, so it is blind to top-level code — readgit diffon the entry script before committing.libre_translate.sh(488, 18 funcs) — the user chose the approach: move the whole config-globals cluster into one lib that owns both the writes and the reads. Do not try relocatingparse_argsalone; four attempts failed.diagnose_pacman_hook_stall.sh(493, 15 funcs) —run_onewritesLAST_ELAPSED,mainreads it. Emits SC2153 (PACMAN_BINvsPACMAN_PID) once split: a real finding to resolve, never to suppress.
2. §4 — last, and only after §3
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 breaks. Teach the installer to deploy the
directory first, in its own commit, re-baseline, then split.
You cannot run sudo pacman -S from the Bash tool — it deadlocks on
db.lck. Hand the user a ! sudo pacman -S <pkg> line plus the expected
output.
3. Out of scope until the user says otherwise
setup_midnight_shutdown.sh (1734), check_and_enable_services.sh (1301),
generate_study_materials.sh (1017), pacman_wrapper.sh (929),
setup_night_lockdown.sh (918), hosts/install.sh (912),
setup_hosts_guard.sh (576), EnforcementRunner.kt (564),
DevicePolicyBridge.kt (415), status_page_state.dart (307).
pacman_wrapper.sh carries the same live-deployment trap as §2. The Kotlin
and Dart files need a different verification stack — 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.
Tooling
meta/scripts/check_file_length.sh --all — the 250-line gate. Run
before every commit: a split that fixes one file while pushing its new
library over the cap is a net zero, and that happened three times.
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.
meta/scripts/mutate_shell.py <spec> — see §0.3.
meta/scripts/extract_shell_functions.py — moves functions brace-by-
brace. Never slice by line range: it also cut through a multi-line
quoted string in config.sh and left an unterminated quote, which the policy
loader's regex tolerated while the shell could no longer source the file.
Do not trust where it puts the source line. It got the position wrong
three times: before SCRIPT_DIR existed, before STATE_DIR (so every path
expanded against an empty prefix), and on a nested re-source rather than
the top-level one. Always grep -c the anchor and check the placement.
meta/scripts/verify_shell_split.sh <pre-split-rev> <old> <new>... — for
a partial split list the old path among the new paths too, or it reports
a false DIFFERENCE.
Rules that will bite you
- No suppressions, ever. Every time this came up, the seam or the code was wrong. A comment line starting with the word "shellcheck" is parsed as a directive (SC1073) — reword it.
- A file must not assign a global it never reads (SC2034). The hook runs
shellcheckwith no-x, so each file stands alone. Constants travel with their readers; a global genuinely written on both sides of a seam stays in the entry script (NET_BUILT,CURRENT_MODE,NEEDS_GPS_FETCH). - New sourced libs need a shebang AND the executable bit. The hook reads
the git index: stage with
git add --chmod=+x, and note that a plaingit addafterwards resets it. This cost three failed commits. pre-commit run --files <changed>before committing.prettierandci-mirrorrun on pre-push.npx prettier --writeany.mdyou touch.- Every commit touching code needs evidence in
docs/superpowers/evidence/<slug>-<date>.json. Staging ≥4 code files also needs a freshdocs/superpowers/contracts/*.json. Put the measured coverage number in it, not a rounded-up one. - Work directly on
main.git stashand branch creation are blocked; usegit worktree add --detachfor a clean baseline. - Do not wire the file-length hook into pre-commit. It lands last, once
check_file_length.sh --allexits 0. 15 files are still over. - Cap pytest memory:
systemd-run --user --scope -p MemoryMax=2G -p MemorySwapMax=0. - Watch
jscpd(fails above 2%). Measure in a clean HEAD worktree — the working tree reads ~2.5% because of vendored.venv, but HEAD is 1.57%. Per-file test harnesses are near-identical by nature; drive repeated shapes through a table rather than repeating the block.
phone_focus_mode — what changed, and its two live hazards
Nine commits took it from 8 over-cap files to zero. config.sh 571→250,
focus_ctl.sh 1091→171+8, deploy.sh 875→241+6, plus monitor,
phone_backup.sh, curfew_enforcer.sh, hosts_enforcer.sh and
focus_daemon.sh. 515 assertions across ten suites, from ~60 before.
Hazard 1 — config.sh is at EXACTLY 250 with no headroom. 184 of those
lines are the fifteen variables python_pkg/focus_policy/loader.py reads by
regex over config.sh's own text. Move one into a config_*.sh sibling
and the loader does not error — it silently yields an empty set, which for
WHITELIST reads as "hide everything". The list is in config_paths.sh's
header and enforced only by that comment.
Verify both of these before and after touching it:
python3 -m python_pkg.focus_policy --config phone_focus_mode/config.sh | sha256sum
# 83e05e82dd1683e1dff1c79a96fec0a4a56aa73295604c681ae355b40a590ba3
plus a diff of the sourced environment (79 variables) against the
pre-change commit. The hash alone is not enough: it passed clean through
both bugs hit this session — a mangled quote and a STATE_DIR ordering
error. The previous brief's recipe (asdict + json.dumps(default=str),
baseline 1624750c…) cannot work: four policy fields are frozenset, so
default=str serialises them in per-process iteration order.
Hazard 2 — deploy.sh's two hardcoded file lists are now in two files.
The push list is in deploy_phases.sh, the cp list in deploy_install.sh. A
new phone-side sibling must go in both, or it is staged and never lands,
and whatever sources it fails to start with no obvious cause. Check with
comm in both directions; config_secrets.sh is deliberately asymmetric.
Known pre-existing state — not yours, do not fix silently
bash phone_focus_mode/lib/tests/test_magisk_service.shhangs. Confirmed identical at HEAD before this session's work; unrelated to it.bash linux_configuration/tests/test_security_hardening.shexits 1 with❌ FAIL: Compulsive block wrappers installed. Belongs to §2.bucket_catch/packages/frontendhas 4 eslint errors, documented indocs/superpowers/evidence/bucket-catch-eslint-2026-08-17.json.- Eight splits are UNVERIFIED ON DEVICE:
tether_enforcer,dns_enforcer(both pre-existing), pluscurfew_enforcer,hosts_enforcer,focus_daemon,config.sh,focus_ctlanddeploy.sh. One real deploy validates all of them at once — butdeploy.shis itself the untested one, so watch it closely and keep an adb push of the directory as the fallback. - Two copies of the Haversine exist:
daemon_location.sh:calc_distance(the original, moved) andctl_daemon.sh:cmd_status's inline awk. Both are tested against known city distances, but not against each other.ctl_is_curfew_nowlikewise duplicatesdaemon_location.sh'sis_curfew_now; those two are pinned by an agreement test. - The eight
ctl_*.shlibraries are at 41.79–92.98%, and unlike the two files in §0.2 most of that is genuinely untested status-reporting code, not a measurement artifact. Closing it means a device fixture per enforcer.
Testing notes specific to this repo
-
phone_focus_mode's shell tests are not in CI —shell-tests.ymluses an explicit list coveringlinux_configuration/tests/only. Run by hand:for t in test_dns_iptables test_monitor test_tether_enforcer test_curfew_net \ test_hosts_libs test_daemon_libs test_backup_capture test_ctl_libs \ test_adb_common test_adb_trusted; do printf '%-22s %s\n' "$t" "$(bash phone_focus_mode/lib/tests/$t.sh | tail -1)" done # expect: 24 / 82 / 17 / 41 / 69 / 108 / 22 / 136 / 10 / 6 -
A test that passes proves nothing on its own.
test_dns_enforcer.shwas deleted last session because its six assertions targeted three functions that never existed. Check the count, the coverage, and the mutations. -
Test files are subject to the 250-line cap too. Split them into a harness plus sourced case files, keeping one entry point so a single coverage command still measures every subject.
-
set -ekills a suite silently. Guard any subject call whose nonzero exit is not part of its contract, and never seed$$as a pid a subcommand willkill— the suite signals itself and dies mid-run with no error.