Records prose done, kcd2 down to four source files, and that a bad TypeScript seam compiles clean and only fails at runtime via an import cycle - so the suite is the check there too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
12 KiB
Resume: enforce the 250-line file cap
Paste this whole file to a fresh Claude session opened in
~/testsAndMisc. It is self-contained. Continuesrefactor_claude_todo.md, which is still the spec — read it too, but the decisions below override it where they differ.
Where things stand
92 files over 250 lines (was 183 — exactly half cleared). Everything is
committed and pushed to main at 776e04d; the tree is clean.
python_pkg/ is DONE — 50 violations → 0. Do not reopen it. Cleared one
package per tranche, pushing after each: brother_printer (15→0, suite pinned
at 402), code_tutor (13→0, 216→217), then android_ui, app_icons,
token_audit, focus_policy, random_jpg (57/44/81/137/14), then
wsg_grabber's six test modules (277).
linux_configuration is 84 → 49, in eleven batches. All 12 prose files
are done (8 split, 2 deleted as completed-work records on the user's ruling).
kcd2_dice_solver is 12 → 4, all four remaining being source files. The
shell pattern is settled and scripted — read docs/shell-split-recipes.md
before touching another one.
Four deferred for shared state across the seam in kcd2_dice_solver and
linux_configuration alike (see the table below); the TypeScript ones are
search.ts (findBestSet ⇄ hillClimb) and badgeValue.ts (valueBadge ⇄
format). In TS a bad seam compiles clean and fails at runtime — an import
cycle leaves a constant undefined at module-init. Always run the suite.
Earlier sessions had already done: the ~/utils exemptions, the
no-inline-python hook, CLAUDE.md, poker-stakes/, and every wsg_grabber
source file.
git log --oneline --grep='cap' is the authoritative list.
What is left
| Directory | Count |
|---|---|
linux_configuration |
49 |
phone_focus_mode |
12 (do LAST) |
kcd2_dice_solver |
4 |
focus_owner |
11 |
billsplit |
6 |
reverse_survivors / meta / docs |
4 each |
.github / bucket_catch |
2 each |
Four linux_configuration files are deliberately over the cap, all for
one reason: state crosses the seam. Pass it explicitly first, then split;
never suppress. See "When to give up on a seam".
| File | Why |
|---|---|
steam_compatibility.sh (663) |
CACHE_MAP assoc array written in a lib, read by main |
diagnose_pacman_hook_stall.sh (493) |
assigns LAST_ELAPSED the caller reads, reads PACMAN_BIN it defines |
clean_audio.sh (419) |
probe-result globals threaded through two libs |
setup_passwordless_system.sh (374) |
no main(); top-level block must be kept by hand |
The pre-commit shellcheck hook lints each lib in isolation, with no view
of the caller's globals. That is stricter than shellcheck -x on the entry
script and is the check that decides whether a seam is real — run
pre-commit run shellcheck --files <all touched> before believing a split.
Next: the remaining main()-shaped shell scripts, then
kcd2_dice_solver / focus_owner / billsplit (TS+Dart+Kotlin, untouched so
far), then the four prose files — zero execution risk.
The live worklist
Do not work from a list in this file — it goes stale. Run:
bash ~/utils/scripts/check_file_length.sh --all
Every count here is a snapshot and will be wrong by the time you read it. Run the script. The table above tells you where the bulk sits, nothing more.
Do the source files in a package before its tests. Proven on wsg_grabber
and again on all seven python_pkg packages: the test files then split along
seams the source already established, instead of you inventing a new boundary
for them. On brother_printer this meant four of the five source splits needed
no test edit at all.
Decisions already made (do not re-ask)
- Full clearance to
--allexit 0. When context runs out, write a fresh handoff like this one rather than stopping mid-way. - CI checks only files in the push/PR range, not
--all. docs/superpowers/plans/**+sessions/**are exempt — already done.poker-stakes/is tracked — already done.- For prose: automate what can be automated, delete the automated lines, keep only HOW, never WHY.
- Shell verification is
bash -n+shellcheck+systemctl catpath checks. Never execute enforcement scripts that mutate the phone or the live system. - One commit per logical unit.
- Editing test files to follow moved code is allowed (2026-08-16), bounded
by: no assertion changed, nothing deleted/skipped/weakened, and an identical
pass count before and after. See
docs/python-split-recipes.md. Do not re-ask this — an earlier session wrongly treated tests as untouchable and stalled onbrother_printerbecause of it. - Per-commit gate is
pre-commit run --files <changed>; the realgit commitis the full gate (it adds jscpd). Push per directory tranche, not per commit. - When a seam splits code covered by a per-file-ignore, fix the lint —
do not copy the ignore onto the new file (user's call, 2026-08-16).
Done for
code_tutor/cli.py:S607fixed withshutil.which,PERF203by extracting the probe so the poll loop carries notry/except. Both entries deleted frommeta/pyproject.toml; the repo now has two fewer suppressions. Thewhich()fix added a branch, so a covering test was written (216 → 217) rather than excluding it. Expect the same question on any remaining per-file-ignore you split through — ask, don't assume.
The constraint that will bite you
Wire the file-length gate LAST, only once --all already exits 0.
meta/scripts/ci_mirror.sh:109 runs pre-commit run --all-files as a
pre-push hook, and .github/workflows/pre-commit.yml:105 does the same in
CI. Registering the hook while violations remain makes every git push fail
until the last file is fixed. Do not scope around it with exclude:/files: —
that is the allowlist the spec forbids.
Final commit, once and only once the tree is clean:
- id: file-length
name: file length <= 250 lines
entry: bash /home/kuhy/utils/scripts/check_file_length.sh
language: system
Plus .github/workflows/file-length.yml modelled on
~/todo/.github/workflows/file-length.yml (checks out kuhyx/utils into
.utils), scoped to changed files. Then prove it fails: stage a deliberately
251-line file, confirm git commit aborts, delete it.
Language recipes
docs/python-split-recipes.md— the seam-selection rule, the__all__re-export form, theTC001trap, the mixin shape, and the coverage behaviour of new sibling modules.python_pkgis done; read it if a stray.pyturns up, or for the seam rule, which generalises to any language.docs/shell-split-recipes.md— read this before the nextlinux_configurationsplit. What replaces the pass count when there is no test suite, which scripts escape the repo, the lib shape that satisfies both shellcheck and the shebang hook, and six traps found the hard way. The stubbed-run check is scripted atmeta/scripts/verify_shell_split.sh; run it on every split whose script ends inmain "$@".
Each rule in both cost a failed gate run or a revert to establish.
Split recipes
- Shell — extract into
lib/*.shsourced by a thin entry script. Seedocs/shell-split-recipes.mdfor the lib shape; note it corrects the old advice to repeatset -euo pipefailin every lib. - Python — extract cohesive helpers into sibling modules; keep the public API stable by re-exporting.
- TS/Dart — extract components; re-export from the original path so no importer changes.
- Tests — split by describe/test-group. Coverage must not drop, and
verify by the pass count, not a green run: a split that loses a whole
module still reports "passed".
In
python_pkg, shared fixture builders go in the package'stests/conftest.py— thename-tests-testhook rejects any other non-test_*.pyfile undertests/, so a_helpers.pywill fail the gate.python_pkg/syncyomi_guard/tests/conftest.pyis the precedent, andmeta/pyproject.tomlomits*/tests/*from coverage by path so nothing there is counted as production code (the opposite of thepoker-stakestrap below). Prefer one shared conftest over anotherpartNfile: the repo already carries four, and each new one that redeclares the same fixture scaffolding pushes jscpd toward its 2% duplication threshold — which only fires on the realgit commit.
Never game the cap: no one-lining, no deleting tests, no moving code into an exempt extension, no suppressions.
Never edit files while a push is running
git push runs pre-commit, which stashes unstaged changes and restores them
afterwards. Editing a file in that window makes the restore fail with
patch does not apply, and the push aborts — while git push | tail still
reports exit 0, because the pipe masks it. This cost two silent failed pushes
and one silently reverted split.
Rules that follow:
- Stage (
git add) before runningpre-commit run --files ...on a work-in-progress tree, or the same stash cycle can revert the edit. - Push only with a clean tree, and do not start other file edits until it returns.
- After any push, confirm with
git status --short --branchthat it does not still sayahead N. Never trust the exit code alone.
Traps that cost time in the last session
- Test fixtures and coverage. When splitting a test file, shared helpers
must go where the coverage config already excludes them (in
poker-stakesthat issrc/test/**, matched by path). Putting them beside the source makes the coverage tool count them as production code and the 100% gate fails. Move the file; do not add acoverage.excludeentry. end-of-file-fixeraborts commits. Files built withsed/catoften lack a trailing newline. The hook fixes it and then fails the commit ("files were modified by this hook"). Justgit addagain and re-commit.- Formatters silently revert edits. A PostToolUse formatter stripped an
added import twice. After editing,
grepthe file to confirm the change survived before running anything. - Contracts. Staging ≥4 code files requires a fresh
docs/superpowers/contracts/*.jsonin that same commit, on top of the per-commitdocs/superpowers/evidence/*.json. Required keys:title,objective,acceptance_criteria,out_of_scope,verifier. Validate withpython3 meta/scripts/validate_contract.py <file>. invariants.test.tsin poker-stakes flakes on its 5s timeout under parallel coverage load. It passes standalone in ~2.6s. Not a regression — do not "fix" it by weakening the test.- Long commands: background anything over ~60s rather than blocking.
phone_focus_mode — highest blast radius, do it LAST
Two silent failure modes:
deploy.shhas a hardcoded flat push list (search foradb_cmd push, around lines 374–383) with nolib/subdirectory. Every new split lib must be added there in the same commit, or the phone sources a missing file at runtime — gate green, tests green, device broken.config.sh(556 lines) is sourced by 11 scripts. Keepconfig.shas the entry point that re-sources its own parts, so all 11 callers keep working untouched.
Static check after each split: grep deploy.sh's push list and every
source/. line in phone_focus_mode/*.sh and confirm each new lib appears.
Suggested order
- The 48 near-misses (251–300) — mechanical, biggest count drop.
python_pkg(50) —pytest-coveragegates each changed package on commit.linux_configuration(84) — largest; check systemd unit paths still resolve.kcd2_dice_solver,focus_owner,billsplit, the rest.phone_focus_mode(13) — with the guard above.- Gate wiring + CI workflow — last.
Done condition
cd ~/testsAndMisc && bash ~/utils/scripts/check_file_length.sh --all # exit 0
pre-commit run --all-files # passes
Plus: a staged 251-line file makes git commit fail.