NEXT_SESSION_PROMPT.md: phased CI-green + 250-cap + gates + shell-coverage campaign
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Rewritten as a 4-phase recurring driver per user request: (0) fix current
CI red state — found Pre-commit checks AND Python tests both failing with
an identical, pre-existing coverage gap (python_pkg/artgate/routes/vector.py:156,
99.98% total, rsvg-convert missing on the GitHub Actions runner) that
predates this session by at least a day; Shell tests was also red from
this session's own archive commit, already fixed in e48f253 and confirmed
green — (1) drive the 11 remaining over-cap files to zero, same
dead-code-check-in-first pattern as every prior split — (2) only then wire
check_file_length.sh into pre-commit as a hard gate, plus a CI-green check
(mechanism deliberately left as an open question for the user: check the
latest main run, or extend the pre-push ci-mirror hook) — (3) a new,
separately-scoped repo-wide 100% shell coverage gate, measured at 487 .sh
files against only 14 existing lib/tests/ directories, explicitly flagged
as needing its own user confirmation and ratchet-vs-hard-gate decision
before starting given the scale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-18 23:03:35 +02:00
parent e48f253b23
commit eb46b365cf

View File

@ -1,136 +1,245 @@
# Next session: pick the next §3/§4 target — no split candidate is currently in scope
# Next session: Phase-driven campaign — CI green → 250-cap zero → gates → shell coverage
> **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: **11** (10 shell, 2 kotlin, 1 dart — `focus_owner`'s two Kotlin files
count once regardless of listing style). Working tree clean, `main` in sync
as of the `setup_hosts_guard.sh` archive commit (see §0).
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 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.
Two standing decisions from the user, already made — do **not** re-litigate:
1. **100% line coverage on what you extract** — same bar every session so far
has applied. Does not apply to archived/deleted code (no split happened).
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.
Three decisions made when this campaign was scoped (2026-08-18) — also do
**not** re-litigate without asking first:
3. **This is a recurring driver, not a single mega-session.** One phase step
per session (usually one file split, or one CI fix), same pace as every
split so far. Update this file at the end of each session for the next.
4. **CI must be fixed before any new gate is added.** A "must be green" gate
is meaningless while CI is already red for unrelated reasons.
5. **The new shell coverage gate is a NEW, separate requirement** covering
every `.sh` file repo-wide (not just newly-split ones). This is Phase 3,
explicitly the largest phase — do not conflate it with Phase 1's cap work.
---
## 0. What just happened (read this, don't repeat it)
## Phase 0: Fix current CI red state — **STATUS: in progress, check first**
`setup_hosts_guard.sh` (576 lines) was this session's assigned split target.
The dead-code check-in (per the §0 lesson from two sessions before this one)
turned up evidence the file might be fully superseded — confirmed by
inspecting live machine state: the legacy pacman hooks it installs were
absent from `/etc/pacman.d/hooks/`, its systemd units were all `disabled`,
and guard-lib's `/etc/guard-lib/targets/{hosts,nsswitch,resolved}.conf` were
all already populated. The user confirmed: archive, don't split.
Three workflows were found red on `main` as of 2026-08-18, discovered while
scoping this campaign:
Scope grew past the one named file once the directory was surveyed: the
**whole legacy hosts-guard subsystem** (17 files — `setup_hosts_guard.sh`,
`enforce-hosts.sh`, `enforce-nsswitch.sh`, `enforce-resolved.sh`, 6 systemd
units, `install_pacman_hooks.sh`, `pacman-hooks/*`, `psychological/*`, both
READMEs) only existed to support the retired system, so it went to
`testsAndMisc-archive` together via `git filter-repo` (same procedure as the
plagiarism-tools and libre_translate archives). `plugins/nsswitch-plugin.sh`
and `plugins/resolved-plugin.sh` were **kept** — they're guard-lib's live
plugins, registered by absolute path in `/etc/guard-lib/targets/*.conf`.
1. **`Pre-commit checks` AND `Python tests` workflows — same root cause,
both persistent and predating this campaign.** Both fail with
`FAIL Required test coverage of 100% not reached. Total coverage: 99.98%`
(6565 lines/1 miss, 1630 branches/1 miss). The gap is
`python_pkg/artgate/routes/vector.py:156` (95% coverage in CI). Root
cause: CI logs show `SKIPPED python_pkg/artgate/tests/test_routes.py:124:
rsvg-convert not installed` — the `ubuntu-latest` GitHub Actions runner
lacks `rsvg-convert`, so a test that would cover line 156 doesn't run
there, while it passes locally (and in the pre-push `ci-mirror` hook,
confirmed — that hook passed on both of this session's pushes) where
`rsvg-convert` is installed. **This predates this campaign by at least a
day** — `Python tests` was already failing with the identical signature
on 2026-08-17 commits (`vscode_optimizer: split...`,
`transcribe_helpers: split...`, etc.), long before the hosts-guard
archive started. **Not yet fixed** — pick this up first, it blocks BOTH
workflows simultaneously since they share the same coverage command.
Likely fix: add `rsvg-convert` (librsvg) to whichever GitHub Actions
runner setup step precedes pytest in `.github/workflows/pre-commit.yml`
and `.github/workflows/python-tests.yml`, OR make the skipped test's
coverage line conditionally excludable when the tool is genuinely absent
(ask the user which they'd rather have — installing the real dependency
in CI is almost certainly correct here, but confirm before choosing an
`# pragma: no cover`-style route given the no-suppressions rule).
2. **`Shell tests` workflow** — was also red (this campaign's own earlier
commit broke `test_hosts_guard_pacman_integration.sh` by archiving files
it referenced). **Already fixed** this session, commit `e48f253`, and
confirmed green on the very next push (`gh run view 32185366621` →
`success`). Re-verify it's still green before trusting this note
(`gh run list --limit 3`) — don't assume it stayed fixed if something
else touched shell tests since.
`check_and_enable_services.sh`'s `check_hosts()` function was rewritten in
the same commit (user confirmed this too) to check guard-lib state
(`guardctl file-guard status <name>`) instead of the legacy systemd
units/enforce-scripts it used to check and self-repair. Verified live:
`sudo bash check_and_enable_services.sh --status` now reports the hosts
service `ok` against the actual migrated machine, where the old code would
have reported `error` (three units it expected enabled were `disabled` by
design). `linux_configuration/.github/copilot-instructions.md` (always-loaded)
and one comment in `hosts/install.sh` were also updated to stop naming the
archived files.
**Exit condition for Phase 0:** `gh run list --limit 5` shows the most
recent run of `Pre-commit checks`, `Python tests`, AND `Shell tests` on
`main` all as `success`. Do not proceed to Phase 1 until this is true. If a
session starts and Phase 0 isn't done, finish it before touching anything
else — don't let it linger while new work piles on top of a broken
baseline.
**Lesson (now 3 for 4):** the check-in keeps changing scope, and this time it
changed scope _twice_ — first archive-vs-split, then "just this file" vs
"the whole dead subsystem." Keep surveying the directory (`grep -rl`, not
just the named file) before proposing an archive's file list, and keep
confirming scope growth with the user before executing it.
## Phase 1: Drive the 250-line cap to zero
## 0.1 Known defect found but NOT fixed this session — flag again if touching guard-lib
**Status as of 2026-08-18: 11 files over cap.** Re-run
`bash meta/scripts/check_file_length.sh --all` at the start of every session
in this phase — the exact list drifts as other work touches these files.
`/etc/guard-lib/targets/nsswitch.conf` and `.../resolved.conf` both have
`PLUGIN=` pointing at the **repo checkout**
(`/home/kuhy/testsAndMisc/linux_configuration/scripts/periodic_background/hosts/guard/plugins/*.sh`)
instead of `/usr/local/share/guard-lib-plugins/`, which
`migrate_hosts_guard_to_guard_lib.sh`'s own header warns against by name
("pointing it at a repo checkout... silently breaks enforcement the day that
directory moves"). `/usr/local/share/guard-lib-plugins/` does not exist on
this machine at all — the migration's plugin-install step appears to have
never run. Not fixed this session: it needs a sudo mutation of a live
security-enforcement system plus `guardctl` re-registration, which the user
was not asked to approve. If a future session touches guard-lib, nsswitch,
resolved, or moves/renames this checkout, raise this first.
```
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
564 focus_owner/android/app/src/main/kotlin/com/kuhy/focus_owner/EnforcementRunner.kt
485 linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh
415 focus_owner/android/app/src/main/kotlin/com/kuhy/focus_owner/DevicePolicyBridge.kt
307 focus_owner/lib/status_page_state.dart
```
## 1. This session's task: none pre-selected — ask the user
Every file currently over the 250-line cap is already on an explicit
out-of-scope list from a prior session (§3 below, or the equivalent of the
old §4). There is no unclaimed split candidate right now. Options, in
roughly ascending order of friction:
- Re-run `bash meta/scripts/check_file_length.sh --all` fresh (files may have
grown/shrunk since this doc was written) and see if a genuinely new
candidate appeared.
- Ask the user whether any §3/§4 file should be taken out of its hold (e.g.
"is `install_leechblock.sh`/`block_compulsive_opening.sh` still deployed at
`/usr/local/…`? still preferred by `pacman_wrapper.sh:831`?" — re-verify,
don't assume the old note is still true).
- Ask the user for a completely different task.
**Ask before picking** — do not silently start splitting a file that a prior
session explicitly parked, and do not assume "no candidate" means "nothing to
do here" without checking with the user first.
## 2. §3 — do not touch this session (unless the user lifts it)
`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**. Splitting them naively breaks every
`pacman -S` on this machine. Re-verify this is still true (grep
`pacman_wrapper.sh` for the deployed-copy preference) before assuming the old
note still holds — a lot has changed guard-lib-side this session, worth the
30-second re-check.
**Two of these carry a live-deployment trap, handle last or very carefully:**
`install_leechblock.sh` and `block_compulsive_opening.sh` are copied to
`/usr/local/…`, and `pacman_wrapper.sh:831` prefers the deployed copy on
**every pacman invocation**. Splitting them naively breaks every
`pacman -S` on this machine. `pacman_wrapper.sh` itself carries the same
trap (it IS the thing that prefers the deployed copies). Re-verify this is
still true before assuming the old note holds — a lot changed guard-lib-side
in the session that wrote this doc, worth the 30-second re-check
(`grep -n "prefer\|deployed" pacman_wrapper.sh` near line 831).
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 if `pacman_wrapper.sh` ever needs live verification.
## 3. Out of scope until the user says otherwise
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.
`setup_midnight_shutdown.sh` (1734), `check_and_enable_services.sh` (1225,
edited this session but not split — same out-of-scope reasoning as before),
`generate_study_materials.sh` (1017), `pacman_wrapper.sh` (929),
`setup_night_lockdown.sh` (918), `hosts/install.sh` (913, one comment edited
this session), `EnforcementRunner.kt` (564), `DevicePolicyBridge.kt` (415),
`status_page_state.dart` (307) — check `check_file_length.sh --all` for the
current exact list before assuming this is still accurate, since files
change size between sessions.
**Before splitting ANY file in this list, run the dead-code check-in first**
(ask the user: "do you still use this?"). This lesson has held 3 of the last
4 sessions — files on this list have turned out to be fully superseded by
something else (guard-lib replaced the entire hosts-guard subsystem this
way). Don't assume a file needs splitting just because it's long; it might
need archiving instead. When surveying a candidate for archival, survey its
**whole directory and cross-references** (`grep -rl <name>` repo-wide), not
just the named file — this has changed scope from "split one file" to
"archive 17 files" once already.
`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.
**Exit condition for Phase 1:**
`bash meta/scripts/check_file_length.sh --all` exits 0 with no files listed.
Do not proceed to Phase 2 until this is true.
## 4. Rules that will bite you (carried forward, still true)
## Phase 2: Wire the file-length gate into pre-commit
- **Ask before large scope changes.** Three of the last four sessions have
changed scope mid-session because the user volunteered new information, or
a check-in (dead-code, or in this session's case a directory survey)
surfaced something bigger than the named file. That is fine and expected —
just don't assume silence means "keep going as originally scoped."
**Do NOT do this until Phase 1's exit condition is met.** Wiring it early
breaks every commit repo-wide, including unrelated ones, for as long as
Phase 1 remains incomplete.
Once `check_file_length.sh --all` exits 0:
1. Add a `local` hook to `.pre-commit-config.yaml` that runs
`bash meta/scripts/check_file_length.sh --all` and fails the commit
(non-zero exit) if any file is found. Follow the existing local-hook
pattern already in the file (see e.g. the `ai-evidence-contract` or
`no-polling-antipatterns` hooks for the `language: system` +
`pass_filenames: false` shape — this check is repo-wide, not
per-file, since a file that grows past 250 lines without being staged
this commit should still be caught).
2. Also add the GitHub-Actions-green check here (see below) if it hasn't
been added yet — the user asked for both in the same pre-commit rule,
though they can land as two hooks in the same commit rather than one
combined script if that's cleaner.
3. **GitHub-Actions-green check, exact mechanism to decide with the user
first (ask, don't assume):** a pre-commit hook cannot literally block a
commit on "is CI green" for a commit that doesn't exist yet — CI runs
AFTER a push, not before a commit. Two real options:
- **(a) Check the LATEST run on `main` is green** before allowing a new
commit — catches "you're building on top of a known-broken baseline"
but not "did MY change break something," since that can only be known
after pushing.
- **(b) A pre-PUSH hook** (this repo already has a pre-push stage, see
`ci-mirror`) that runs the equivalent of the CI workflows locally
before allowing the push — closer to the actual guarantee wanted, and
`ci-mirror` already does exactly this for the Python/pre-commit side.
Extending it (or adding a sibling hook) to also run the `Shell tests`
workflow's local-equivalent commands before push may be the more
honest way to satisfy "checks GitHub workflow outputs green" than
querying `gh run list` for a run that hasn't happened yet.
Ask the user which they want (or both) before implementing — this is a
genuine design fork, not a mechanical step.
4. Verify by making a throwaway file exceed 250 lines and confirming the
commit is actually blocked with a clear error message, then removing the
throwaway file, before considering this phase done.
**Exit condition for Phase 2:** the new hook(s) exist in
`.pre-commit-config.yaml`, `pre-commit run --files <touched>` fails when a
file exceeds 250 lines (verified with a throwaway test), and the mechanism
chosen for the CI-green check is documented in this doc's next revision.
## Phase 3: Repo-wide 100% shell coverage gate — **the largest phase, expect many sessions**
**Do NOT start this until Phase 1 AND Phase 2 are both done.** This is
explicitly scoped as separate, much larger work — do not let it bleed into
or block the 250-cap campaign.
**Scale, measured 2026-08-18:** 487 `.sh` files exist repo-wide (excluding
`.venv/`, vendored, and build-output dirs) versus only 14 `lib/tests/`
directories following the established split-and-test pattern. A genuinely
repo-wide "100% line coverage, every shell script" gate would immediately
demand test suites for on the order of 450+ currently-untested scripts. This
is roughly an order of magnitude more work than Phase 1's 11 files. Re-count
before starting (`find . -name '*.sh' -not -path '*/.venv/*' -not -path
'*/node_modules/*' -not -path '*/testsAndMisc_builds/*' -not -path
'*/testsAndMisc_binaries/*' -not -path '*/.git/*' | wc -l`), since Phase 1
will have grown the tested-file count somewhat.
**Ask the user to confirm scope before starting Phase 3 at all** — 487
files is a genuinely large body of future work and deserves an explicit
"yes, still want this" check before a session starts writing hundreds of
test files, not just a paste of an old plan. Also ask whether the gate
should apply repo-wide from day one (blocking ANY commit touching an
untested `.sh` file) or ratchet in incrementally (e.g. only newly-added or
newly-modified files must be covered, with a frozen allowlist of
pre-existing untested files that shrinks over time) — a hard repo-wide gate
turned on immediately would block unrelated commits to any of the 450+
untested files until every one of them gets a test suite, which could stall
unrelated work for a long time. A ratcheting gate is the much more common
pattern for retrofitting coverage onto an existing codebase and is worth
raising as the recommended default, but it's the user's call.
Use `meta/scripts/shell_coverage.sh <test> <subject> [min]` (kcov wrapper)
and the established `lib/tests/` + harness + PATH-shim pattern from every
split so far (see Phase 1's testing-pattern reference below) — there is no
reason for shell test infrastructure to differ between "coverage added
because we split the file" and "coverage added because a new gate demands
it."
**Exit condition for Phase 3:** every `.sh` file in the repo (or every file
not on an explicitly agreed allowlist, if ratcheting was chosen) has a test
suite reaching the agreed minimum coverage, and a pre-commit/CI gate enforces
it going forward. Given the scale, expect this phase's "exit condition" to
itself be revised into something more incremental once the user weighs in.
---
## Rules that will bite you (carried forward, still true)
- **Ask before large scope changes.** Repeatedly, a check-in or a directory
survey has surfaced something bigger than the named task. That's expected —
don't assume silence means "keep going as originally scoped."
- **Survey the whole directory before scoping an archive, not just the named
file.** `grep -rl <filename-stem>` across the repo, then `ls` the file's
own directory — this session's file list grew from 1 to 17 files this way.
own directory.
- **No suppressions, ever.** No `# noqa`, no per-file-ignore added without
asking every time, no shellcheck disable beyond what's already justified
inline.
inline. This applies to Phase 0's coverage gap too — don't reach for a
coverage-exclude pragma without asking first; installing the missing
dependency in CI is the more likely correct fix.
- **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=`
@ -143,37 +252,39 @@ needs `JAVA_HOME=/usr/lib/jvm/java-21-openjdk` and `--rerun-tasks`; a plain
- **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-up one. This session's pair
(`archive-setup-hosts-guard-2026-08-18.json` in both `evidence/` and
`contracts/`) documents an archive-plus-live-script-fix, not a pure split —
read it if the next task is another archive-vs-split judgment call.
- **An archive does not need new tests.** The 100%-coverage standing decision
binds code that gets _split_, not code that gets _deleted_. Neither this
session's archive nor the two before it added tests for the archived code.
**measured** number in it, not a rounded-up one.
- **An archive does not need new tests.** The 100%-coverage standing
decision binds code that gets _split_ or newly _written_, not code that
gets _deleted_.
- **A test file broken by an archive commit is a same-session bug, not a
future TODO.** This campaign already hit this once
(`test_hosts_guard_pacman_integration.sh`, fixed in `e48f253`) — `gh run
list` after every push, not just `pre-commit run --files <staged>`
locally, since CI-only test suites (like `linux_configuration/tests/*.sh`
run directly by `shell-tests.yml`) aren't caught by pre-commit's
file-scoped hooks.
- 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 with the main repo and will
corrupt it; always a throwaway `git clone` for that).
- **Do not wire the file-length hook into pre-commit.** It lands last, once
`check_file_length.sh --all` exits 0.
`git worktree add --detach` for a clean baseline (but NEVER
`git filter-repo` inside a worktree — it shares the object store with the
main repo and will corrupt it; 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`.
- **Never let a test allocate unbounded real memory.**
## 5. Tooling reference
## Tooling reference
**`meta/scripts/check_file_length.sh --all`** — the 250-line gate. Run
**before every commit**. Applies to test files too, not just the file you're
splitting.
**before every commit**. Applies to test files too, not just the file
you're splitting.
**`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/extract_shell_functions.py`** — moves functions
brace-by-brace. **Never slice by line range**. Verify placement with `grep -c`
on the anchor.
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 a split moved every function verbatim. For a **partial** split list
@ -183,19 +294,19 @@ the old path among the new paths too.
required.
**Archiving a dead file/subsystem** (no dedicated script — hand-run each
time): clone to a scratch dir, `git filter-repo --force --path <each current
path> --path <each historical path from `git log --all --follow
time): clone to a scratch dir, `git filter-repo --force --path <each
current path> --path <each historical path from `git log --all --follow
--name-only`>`, clone `testsAndMisc-archive` separately, add the filtered
clone as a remote, `git merge --allow-unrelated-histories`, remove stray
remote-tracking branches and the remote, push, verify with `gh api
repos/kuhyx/testsAndMisc-archive/contents/<dir>`, **then** `git rm` from
`testsAndMisc`. Read `docs/superpowers/evidence/archive-setup-hosts-guard-2026-08-18.json`
or `archive-plagiarism-tools-2026-08-18.json` for the full worked example.
`testsAndMisc`. Read
`docs/superpowers/evidence/archive-setup-hosts-guard-2026-08-18.json` for
the full worked example.
## 6. Testing pattern to follow (for an actual split, if one gets picked)
## Testing pattern to follow (Phase 1 splits and Phase 3 new coverage alike)
The established pattern for `single_use/`-adjacent shell scripts with no
prior test coverage (used across three prior splits):
The established pattern for shell scripts with no prior test coverage:
- A `lib/tests/` directory sibling to the split libs.
- A `*_harness.sh` file (sourced, not executed) with `_t_pass`/`_t_fail`/