From eb46b365cf4d595ed9882a7034bfff3cb2c5dfea Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Tue, 18 Aug 2026 23:03:35 +0200 Subject: [PATCH] NEXT_SESSION_PROMPT.md: phased CI-green + 250-cap + gates + shell-coverage campaign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- NEXT_SESSION_PROMPT.md | 361 +++++++++++++++++++++++++++-------------- 1 file changed, 236 insertions(+), 125 deletions(-) diff --git a/NEXT_SESSION_PROMPT.md b/NEXT_SESSION_PROMPT.md index 3d2f6450..91ac6d79 100644 --- a/NEXT_SESSION_PROMPT.md +++ b/NEXT_SESSION_PROMPT.md @@ -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 `) 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 ` 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 ` 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 ` 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 [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 ` 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/-.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 ` + 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 [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 ...`** — 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 --path --path `, 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/`, **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`/