The guard prints `CI_GREEN_SKIP=1 git commit ...` and stops there, but it runs
at two stages: at commit, and again inside the pre-push CI mirror's
clean-worktree `pre-commit` run, which sees the same still-red baseline. So the
documented escape gets you past the commit and into an identical wall at push,
with no hint and nothing in the output connecting the two.
Hit today landing the pre-commit cache-key fix: the commit went through, the
push did not. The variable is read from the environment and `ci_mirror.sh` does
not scrub it, so `CI_GREEN_SKIP=1 git push` carries through to the nested run --
which is now what the message says.
Verified both paths: with the variable set the script warns and exits 0, and
against today's green main it exits 0 silently. shellcheck clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
The key said `py3.12` as a literal while the job installs `3.x`, which is
3.14.7 today. A `language: python` hook env is built by whichever Python
pre-commit ran under, so the cache kept handing back envs built on 3.12.14 and
the hooks kept running there -- invisibly, right up to the first syntax 3.12
cannot parse.
PEP 758 was that syntax. The previous commit's `except A, B:` reached pylint
and the debug-statement hook on cached 3.12 interpreters and came back as
"multiple exception types must be parenthesized", a failure with no local
reproduction: developer caches were built on 3.14, where the same pylint 3.3.2
and the same pylint 4.0.7 both parse the file fine. The syntax was never the
defect; the stale interpreter was.
`steps.setup-python.outputs.python-version` cannot drift from what was
installed, so this cannot recur on the next release either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
Already red before today's Python work touched the repo; the commit hook had no
reason to look at the requirements files, so only a whole-tree run saw it.
Bumped across every repo pinning 7.15.4 in the same pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading `meta/` against 3.10 -- three tools judging an interpreter
nothing runs any more, and ruff withholding the pyupgrade fixes that only
become safe at the newer target. `pin_ci_python.py` rewrites these three knobs.
What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That makes the source 3.14-only at the
syntax level, which is what `requires-python = ">=3.14"` already promised; kuhy
chose it deliberately over holding ruff a release back (2026-08-29).
Also drops a comment in `wsg_grabber/models.py` that the bump makes false: it
explained why the file used ``str, Enum`` instead of ``StrEnum`` under mypy at
3.10, sitting directly above a ``class Verdict(StrEnum)`` that had already
stopped obeying it.
ruff, ruff-format, mypy --strict and pylint all clean at the new target.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
`{"flutter": "stable"}` names a moving target: two machines on the same commit
can build with different SDKs, and a freshness check has nothing to compare it
against. This is the only .fvmrc in the fleet, and it was the one file the new
dependency-freshness gate could not evaluate.
Purely declarative today -- nothing in any repo invokes `fvm flutter`, and fvm
has no SDKs cached, so the build path is unchanged and still resolves the
pacman-installed SDK through ~/.local/bin/flutter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
Counting transcript records reports 1.00 tool calls per message no matter
what the model did: the harness writes one JSONL record per tool_use block,
all sharing message.id. A hand-rolled per-record count read the real 1.9%
batching rate as 0.0%, which nearly shipped as a headline finding.
Group by message.id instead, and report it, so the number comes from a
tested code path with the window handled by find_transcripts rather than an
ad-hoc timestamp. Three snapshot fields give next week's run a baseline.
Axes was at the 12-attribute cap, so the counters live in their own Batching
dataclass with the two ratios as properties.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiSSDu4gKbp9GGxTTtXvYW
track-bars on the family charcoal/accent: rising bars with a plotted trend
point, which reads as odds movement.
linux:false because betting-sim's desktop target is a Chrome-wrapped web build
rather than the GTK embedder, so the hicolor set would go unread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDSQS61Vgb6wptLPT2Utbf
A Flutter/Material TextField publishes its label as `hint` and leaves text
and content-desc empty, so every field on such a form arrived as an
anonymous EditText. The workout app's manual-workout form is nine of them:
the only way to fill it was by screen position, which is precisely the
mis-tap-writes-the-wrong-field failure this tool exists to prevent.
`hint` ranks below text and content-desc but above resource-id. Material
clears the hint once content arrives, so a filled field still answers to its
real text and a caller can tell an empty field from a completed one.
The app-side alternative does not work and is documented as a trap: wrapping
the field in Semantics(label:) emits a second labelled node at the same rect,
and MergeSemantics collapses them by dropping the hint from Android's tree,
which leaves the form MORE anonymous than before. Widget tests cannot catch
that -- they assert on Flutter's SemanticsNode while this tool reads
Android's AccessibilityNodeInfo, so nine of them passed green on the build
whose on-device labels had just gone empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWq9MRemX9ZWvNmnhNWUP2
`Pre-commit checks` has been red on main since 2026-08-24: the md-naming
and file-length hooks are thin shims that exec the real gate out of the
utils repo, and a runner has no ~/utils, so both died with "shared gate
not found at /home/runner/utils/...". The dedicated md-naming.yml
already checks utils out into .utils/; this does the same and points
UTILS_ROOT at it, which is the override the shims already support.
CI_GREEN_SKIP is set because this commit IS that fix.
Also registers punchme (github.com/kuhyx/punchme) in the shared icon
family on the family accent, Android-only so linux=False. Its glyph is a
time card with a clipped corner, a column of four punched holes and
three ruled lines; the first draft put two holes above two wide slots
and read unmistakably as a face at every size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pre-push prettier hook has been failing on .pre-commit-config.yaml since
49d82511, which blocks every push regardless of what the push contains. The
only complaint is the indentation of the commented-out COMMITIZEN block.
Purely cosmetic and confined to comments: yaml.safe_load of the file before
and after compares equal, and both parse to the same 16 hook repos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KU9czAaz7wAkjyJ5uEB3t
focus_owner (com.kuhy.focus_owner, the Device Owner enforcer) was the one
Flutter app never registered with the shared generator. It shipped a
hand-written vector adaptive icon added purely to unblock its build — the app
had no icon resource at all and every release AND debug build failed on AAPT
"resource mipmap/ic_launcher not found". That placeholder used #E8EAED, so it
was off-family as well as unregistered.
None of the existing 13 glyphs fits a device-policy enforcer, so this adds
padlock-closed: shackle stroked (inheriting the group's round caps), body
filled in the accent, keyhole punched back out in the field colour. A fully
stroked padlock loses its keyhole at 48dp and a fully filled one loses the
shackle's opening; splitting them keeps both.
Geometry was computed against style.py's constraints rather than eyeballed,
per the lesson recorded on _NOTE_ASCENDER: body 292..732 x 520..792, shackle
centreline r=132 about (512,520) so its outer top is 352 (inside the 232 safe
edge) and its legs land at 380/644, well within the body. The keyhole leaves
60px of body above it and 76px below the stem, both clear of
MIN_NEGATIVE_SPACE=36 — checked by rendering at 48px, where the hole survives.
The repo path points into ~/phone-focus-mode/focus-owner: focus-owner was
absorbed as a subtree on 2026-08-24 and both the standalone clone and its
GitHub repo are gone.
app_icons stays at 100% branch coverage; 44 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KU9czAaz7wAkjyJ5uEB3t
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.
TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
The "Depth-cap progress" table still advertised 294 violations and a 207-file
"step D worklist", which stopped being true when the campaign completed. The
gate now reports zero:
$ bash meta/scripts/check_directory_depth.sh --all
check_directory_depth: all checked paths are within 2 directories (exit 0)
A stale worklist is worse than no worklist: it sends the next session hunting
for 207 files that are not there. The table is hand-written --
report_live_state.sh generates the path inventory above it but never emitted
this section -- so regenerating the doc would not have fixed it.
Rewritten to state the guarantee (the directory-depth-cap hook enforces it on
every commit) and to point at the command rather than at a number, with the
original backlog kept as history.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The previous commit made hosts-blocker's install.sh register the guard-lib
instances itself, which is what finally gives a fresh machine real hosts
enforcement. It also created a class of machine that --rollback refused to
touch: the state dir it demanded, /var/lib/guard-lib-migration, is written by
the one-shot migration, and a machine that never had a legacy layer has no
such state. So --rollback exited 1 while the instances it exists to remove
were live -- every freshly installed machine had no escape hatch at all.
Rollback now proceeds whenever a guard-lib instance is registered, says
plainly that there is no legacy layer to restore, and still refuses when
there is genuinely nothing to roll back.
The second half is worse and was only visible by running it. The existing
re-assertion of chattr +i works by starting hosts-guard.service and
hosts-bind-mount.service, neither of which exists on a fresh machine, so
every systemctl cat skipped and the rollback finished with /etc/hosts holding
no immutable flag and no bind mount -- less protected than before it ran.
It now re-asserts immutability directly when no legacy unit did it.
Verified in a pristine vmbox guest whose guards came from
install_core_system.sh --all: dry-run and real rollback both exit 0, the
three instances are uninstalled, shutdown-schedule (a different feature) is
untouched, and /etc/hosts keeps ----i----------------- and all 178410 lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Two of the seven modules invoked python_pkg/ paths that stopped existing when
screen_locker and steam_backlog_enforcer were extracted into their own repos,
so the first thing the installer did on a fresh machine was fail. Nothing
installed guard-lib either, which meant the CORE "Midnight shutdown timer"
module always died with "guardctl not found on PATH".
Every module now resolves through install_module_from_repo, and
require_extracted_repo clones a missing sibling instead of only printing
instructions -- otherwise "clone it yourself" is not actionable on the bare
machine this script exists to set up. The clone drops privileges to the
invoking human via sudo -u, because an installer that re-execs under sudo
would otherwise leave a root-owned checkout in the user's home; a sandbox
cannot catch that, since the guest is root throughout. A clone that fails is
still a loud, fatal module failure, never a silent skip.
guard-lib is addressed as the "utils" repo: it is a subdirectory of
github.com/kuhyx/utils, so asking for "guard-lib" would resolve to
~/guard-lib and derive a clone URL that does not exist. It installs first,
before the modules that need guardctl.
Separately, the security hardening suite's screen_locker section hardcoded
$HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py. That path went
away with the extraction, so all five checks silently SKIPPED -- reporting a
clean run for tests that never executed, which is the exact failure mode
lib/extracted_repos.sh was written to prevent. It resolves the repo properly
now ($HOME is also wrong under sudo), and the assertions are rewritten against
the current implementation: the four symbols they grepped for no longer exist
anywhere, having been replaced by SPORT_CHOICES, SPORT_TABLE_TENNIS,
_validate_table_tennis and MANUAL_WORKOUT_REFLECTION_MIN_CHARS.
On this host that takes the suite from 18 passed / 1 failed / 5 skipped to
23 passed / 0 skipped. The task file's "18 passed / 6 skipped out of 17"
target was stale -- the suite is 24 checks now -- so it could not be matched
literally.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Two post-push audits, both worth keeping.
vendor/common.sh sources three sibling libs unconditionally and only common.sh
had been vendored, so every entry point printed three "No such file or
directory" errors. Every suite passed anyway -- the functions in use are
defined above that block and a failed sibling source is non-fatal -- which is
exactly why it needed its own test. Fixed in digital-wellbeing c5eb028.
A grep across /etc, /usr/local and /usr/bin, broader than report_live_state.sh
(which reads only /usr/local/{bin,lib}, units and symlinks), found three
references. None from this work: pw_guard.sh's target still exists, and
dwm-session carries two pre-existing single_use/ paths. dwm is dormant and was
deliberately abandoned, so that one is reported rather than changed.
Also records that digital_wellbeing/data/ was safe to delete: never tracked on
any historical path, and no script in the unit references data/ at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The campaign is done, so the file no longer opens with a prompt to paste. It
now records the closing numbers (0 depth violations, 0 stale live references,
949/1200 source files, 1090 tracked), what landed, what was deliberately left
undone and why, and the traps worth carrying forward.
The "deliberately NOT done" section names the two things that still look red
but are not regressions: the unwrapped discord/signal-desktop binaries, which
fail identically at baseline, and workout_locker, which belongs to the
separate screen-locker repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The last unit over the cap is now github.com/kuhyx/digital-wellbeing, cloned
at ~/digital-wellbeing, and `--warn` is gone from the directory-depth-cap
hook. `check_directory_depth.sh --all` exits 0 with zero violations, down from
46 at the start of the session. That was the point of the campaign.
History was filtered across all three paths the unit has lived at (63
commits); the current path alone holds 2.
The six suites that test this code lived in linux_configuration/tests/ rather
than beside it, so they were filtered separately (24 commits) and merged in as
a second root. A path filter of the unit alone would have left them here,
testing code that had left the repo -- and their names would still have been
in shell-tests.yml's hardcoded arrays, failing CI on missing files.
linux_configuration/lib/common.sh is vendored into vendor/ rather than trimmed
to the four functions actually used: set_actual_user_vars calls two of the
others, so a trim breaks silently. It is in vendor/ and not lib/ because
test_music_parallelism.sh copies the unit's own lib/ into a scratch worktree
while stubbing the shared lib; in one directory the real common.sh overwrote
the stub, and the suite silently measured the wrong library.
Running the callers caught what shellcheck and grep both missed:
check_and_enable_services.sh died with "DW_REPO: unbound variable" because the
definition sat below its first use. It now runs clean and reports ok for every
extracted-unit check.
Two long-standing live defects fixed as a consequence. The pacman-wrapper
drift manifest embeds ABSOLUTE paths and had named deleted scripts/ paths
since the flatten, so the check reported a permanent false "stale or
tampered"; it now verifies, with unchanged hashes proving the extracted bytes
are identical. And the last two stale live references are gone: 8 -> 0.
The two pre-commit excludes for setup_midnight_shutdown.sh are dropped rather
than moved. lsattr shows nothing in the unit is chattr +i any more, and the
fixer hooks leave the file alone when allowed to run.
Removing 80 shell files also shrank the jscpd denominator enough that the
virtualenv/tqdm clones inside the GENERATED .ci-mirror-venv/ tipped the repo
to 2.09% and blocked this commit, which adds no duplication. That directory is
the same vendored third-party shell the ignore list already excludes as
.venv/, just under another name; jscpd walks the working tree rather than the
index, so being untracked did not spare it. Excluded by name: 1.74%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
161 tracked files, 3.2M. All 158 episodic entries have retrieval_count 0 --
the store was written to and never once read from. Last write 2026-05-28, the
tool is uninstalled, and nothing references it: the only code hit for "hippo"
anywhere in the tree is the blocked domain "wordhippo" in
leechblock_defaults.json.
No .gitignore entry was needed after all. The handoff anticipated one for
hippo.db, which was untracked only because *.db is ignored -- but with the
whole directory gone there is nothing left to ignore.
Tracked files: 1346 -> 1175.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Five live mechanisms were silently pointing at paths that no longer exist.
None of them announced it; each degraded to a log line or a missing file.
- hosts-file-monitor.service is ACTIVE and watches /etc/hosts for tampering,
but its embedded installer path was the pre-flatten scripts/ one. It logged
"Hosts install script not found" 37 times: it detected tampering it could
not repair.
- dns-blocklist-refresh.service (enabled, next run 00:01) named a deleted
single_use/features/ path, as did media-organizer.service under utils/.
- setup_periodic_system.sh read all TEN of its templates from
periodic_background/system-maintenance/, which has held nothing but
untracked .pyc and .coverage residue since system-maintenance was
extracted. Every install of a service, timer or script was reading a file
that was not there.
- /etc/sudoers.d/hosts-install-no-passwd granted NOPASSWD on the old hosts
installer path.
Running the fixed dns unit then caught a bug the static checks missed: under
systemd there is no SUDO_USER and `id -un` is root, so the new resolver
returned /root/hosts-blocker and the service died with "Feed generator not
found". extracted_repo_home() now handles the bare-root case by falling back
to the owner of the lib file itself. Verified across all four invocation
modes: as the user, via sudo, as root with SUDO_USER unset, and with an
explicit *_DIR override.
The service now reports "Feed refreshed and dnsmasq reloaded (189038 lines)".
/etc/hosts is untouched throughout -- still 176888 lines, still chattr +i,
still bind-mounted by its guard.
Live artifacts were patched via validated temp files, never in-place: the
sudoers drop-in was checked with `visudo -cf` before install, and every
original is backed up under /var/tmp/*.pre-{extract,fix}.
Stale live references: 8 -> 2. The 2 that remain name digital_wellbeing
paths and can only be fixed when that unit is extracted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
linux_configuration/periodic_background/hosts/ is now
github.com/kuhyx/hosts-blocker, cloned at ~/hosts-blocker. Clears 5 of the 46
directory-depth violations; the remaining 41 are all digital_wellbeing, so
--warn stays on the depth hook until that unit moves too.
History was filtered across ALL FOUR paths the unit has lived at (hosts/,
linux_configuration/hosts/, linux_configuration/scripts/periodic_background/
hosts/, and the current one). Filtering on the current path alone would have
preserved 1 commit out of 51 -- the flatten moved it recently, so a naive
single --path silently truncates the history to the flatten commit and still
looks fine in `git log | head`.
No source line changed: all 15 files are byte-identical to the copies removed
here. install.sh already resolved its lib/ via BASH_SOURCE.
The six callers now resolve the checkout through a new shared helper,
linux_configuration/lib/extracted_repos.sh, which reads the invoking user's
home from SUDO_USER's passwd entry rather than $HOME -- several of these
scripts re-exec under sudo, where $HOME is /root. A missing checkout fails
loudly with a clone instruction; it is never a silent skip, which is the
failure mode that left screen-locker's self-repair dead for five months after
the 2026-05-15 reorg.
The live blocker is untouched: /etc/hosts keeps its chattr +i, and the guards
load their plugins from /usr/local/share/guard-lib-plugins/ (byte-identical by
md5), not from a repo path.
Verified: 25 tests pass from ~/hosts-blocker; shellcheck clean on the helper
and all six callers; sudo resolution prints /home/kuhy/hosts-blocker;
test_setup_dns_blocker.sh passes; report_live_state.sh --check is byte-identical
before and after, so no new stale references.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
It was untracked until this rename, so the pre-push prettier hook had never
seen it. Table separators and one blockquote marker; no prose changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Three files starting NEXT_SESSION_ meant neither I nor a future agent could
tell which one to open: a prompt, its reference doc, and an unrelated
installer task all competed for the same name.
Now one file per job, named after the job. RESTRUCTURE_HANDOFF.md carries
the prompt and its reference in a single paste; the header says where to
start and what is superseded. The installer work becomes
INSTALLER_FIX_TASK.md — still a real task, no longer pretending to be the
next session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
ci-baseline-green also runs inside ci-mirror's clean worktree, so a red main
blocks the very commit that fixes it. CI_GREEN_SKIP=1 has to be set on the
push, not just the commit. Cost a failed push to discover.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Shell tests went red on my last push. test_network_diagnostics.bats sourced
its scripts through $REPO_DIR/scripts/diagnostics, a level the flatten
removed, so setup() failed and all 30 tests errored.
Latent rather than new: the previous run was green only because ci-mirror
aborted before the bats stage. Fixing the concurrency OOM let the gate reach
this suite for the first time since the flatten.
Grepped the tree for the removed scripts/ and single_use/ levels; this was
the last one in tracked code.
Also rewrites the two handoff docs around the units that remain.
CI_GREEN_SKIP=1 because this commit is the fix for the red baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The push kept failing with "1798 passed" immediately followed by "Killed".
The suite was never the problem: it peaks at ~0.29 GiB against its own 2G
cap. ci_mirror.sh runs pre-commit --all-files and the pytest gate
concurrently, and the pre-commit tree peaks at ~1.75 GiB (mypy and pylint
repo-wide, invisible to /usr/bin/time because it measures only the parent).
The pair wants ~2 GiB at once; the pytest scope has MemorySwapMax=0, so on
a loaded machine it is the one that dies.
Concurrency is now conditional on MemAvailable. Above 4 GiB the fast path
is unchanged. The probe lives in ci_mirror_mem.sh, following the existing
ci_mirror_range/ci_mirror_shell split, which also keeps ci_mirror.sh under
the line cap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Item 1's "the classifier blocks me" note was stale. Ran in vmbox first,
then on the host one guard at a time with a DNS check between them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The pre-push gate was failing on a layout that is actually correct. The
script sources ../../lib/common.sh, which is right for where it sits after
the flattening; the test's guard still hardcoded ../lib/common.sh from
before the move, so it reported drift against itself.
The prefix is now read out of the script under test, so the two cannot
desynchronise the next time the tree moves. Verified non-vacuous: rewriting
the script to ../../../lib/common.sh in a throwaway worktree still fails
the guard, and it now names the real prefix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Both were non-conforming before this session and block every push, not
just this one, since the hook runs over all files. The diff is entirely
table-separator widths and emphasis markers; no prose changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Caught by the pre-push prettier hook. Only this file is touched: the two
other files it flags were already non-conforming before this session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
A gate that blocks a commit with no tooling to clear it just moves the
problem to whoever hits it. --prune stages the removal of the oldest
artifacts, keeping the newest 20 per directory plus the template, and
leaves the commit to the operator.
Running it caught a bug that reading it would not: sorting on %cs (date
only) ties every artifact committed the same day, so sort fell back to the
path and the prune deleted alphabetically - it picked one of today's
newest files instead of the oldest. Sorts on %ct with -rn now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Items 4 and 5 are done. Item 6 gets a recommendation against, with the
measurement behind it: the doc's ~2,780-line estimate was right and my
first grep was wrong (filenames only, 400 lines), but the two lib families
are parallel evolutions rather than copies, on 20% test coverage against
hardware I cannot reach.
Adds item 7: 3.7 GB of untracked build residue from the extractions, with
the checks showing it holds no source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The prune commit's evidence noted that "a recurring prune or a hard cap
would be the durable fix if this recurs" - which is advice, not a fix. The
tree would have regrown at one artifact per commit and been noticed months
later, exactly as it was this time.
check_repo_size.sh now caps evidence/ and contracts/ at 60 files each: the
headroom the prune bought, against 21 today. main accumulates failures
rather than returning on the first, so tripping both the source ceiling and
the artifact cap reports both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
These accumulate roughly one per commit and were the largest single driver
of tracked-file growth: 695 artifacts, 487 of them from 2026-08 alone.
Kept the 20 most recent of each plus both templates. A count cap rather
than a time window, because the growth is bursty - a 7-day window would
still have kept 393 files and bounded nothing. Selection is by git commit
date, not by filename: the corpus mixes -2026-05, -20260604 and undated
slugs, so a filename filter would have mis-sorted ~48 files.
Safe to delete: nothing reads the historical corpus. Every reference in
meta/scripts is either template.json or validation of files staged in the
current commit, and the three hooks still pass. sessions/ is append-only
and untouched.
Tracked files 1993 -> 1340.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The depth cap had 104 violations. 57 are structural: whole projects with
their own layout (C/, dwm/, artgate/), a tool split into modules to satisfy
the line cap, prose trees, and directories whose name an external format
dictates (systemd, hooks, workflows, patches, fixtures). Those are exempted
the same way lib/ and tests/ already were.
The other 47 all sit under periodic_background/{hosts,digital_wellbeing},
which can only come down by extracting those units - blocked on a manual
guard repoint. So the hook runs --warn: the number stays visible on every
commit without blocking unrelated work. Enforcement is one flag away.
Also guards the argument order: `--all --warn` used to ignore --warn and
exit 1, which as a hook entry would have blocked every commit.
check_repo_size.sh is the companion gate - depth stops one subtree growing
a taxonomy, this stops the repo absorbing everything. It counts tracked
files (git ls-files, never find: the tree holds ~30k untracked extraction
leftovers) minus vendored and hook-generated trees: 1049 against a 1200
ceiling, printed every run so the threshold is auditable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Depth 556 -> 104, tracked files 2295 -> ~1980. What remains is the guard
repoint (blocked on a permission I cannot grant myself), the two
subsystems behind it, and wiring the gates once the structural
violations are exempted by name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
check_file_length.sh required a dot in the filename, so every
extensionless executable was silently uncapped. is_source_file() now
reads the first two bytes and accepts a shebang, which catches real
scripts while still skipping data files like i3/config.
That immediately surfaced boot_recovery/boot-repair at 756 lines --
three times the largest capped shell file in the repo, uncapped purely
because its name has no dot in it. Split along its own section markers
into lib/{discovery,repair_fs,repair_kernel,modes}.sh, leaving the entry
script at 218.
The report block stays inline on purpose. It is top-level code with exit
calls rather than function definitions, and sourcing it changed control
flow: 30 of 42 tests failed. That is recorded in the lib headers so the
next reader does not retry it.
No vmbox pass for this one, deliberately. boot-repair is UEFI/ESP tooling
and the guest is BIOS/GRUB, so a sandbox green would be actively
misleading -- the vmbox skill says so itself. The evidence is the fixture
suite, which drives the real script against synthetic root trees: 42
passed before, 42 passed after.
verify_shell_split.sh reports 31 of 32 functions byte-identical. The one
difference is clean_shadow_files, where quoting $ESP_MOUNTPOINT fixed a
genuine SC2086 that only became visible once the function left the
monolith -- shellcheck could no longer see the readonly declaration and
stopped assuming the expansion was safe.
lib/tests/run_all.sh delegates to that same suite rather than standing up
a second, weaker harness: these libraries need the entry script's strict
mode and globals, so testing them in isolation would test a fiction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
i3 config was filed under periodic_background, which it never was. It is
now linux_configuration/i3/ (config + installer) and
linux_configuration/i3blocks/ (the status scripts), both at depth 2.
misc/testsAndMisc-bash/ was a directory named after the repo it already
lived in, so its contents moved up to misc/. Its stray .gitignore came
along, keeping the media rules working from the new location; its
.vscode/tasks.json did not, because it invoked Bash/transcribe.sh, a path
this repo dropped long ago.
Most of this commit is the references the previous flatten missed. Its
regex skipped anything with a variable prefix, which left 16 files --
including check_and_enable_services.sh and install_core_system.sh, the
orchestrators -- still naming $CONFIG_DIR/scripts/periodic_background/...
It had also rewritten three i3 paths into scripts/i3blocks/, a directory
that has never existed; the i3blocks suites caught that by failing.
13 more symlinks broke on the i3 move. Repointed, and cpu_monitor/volume/
disk were run again to confirm the bar still reports.
Depth 139 -> 104. Roughly 47 of the remainder sit in digital_wellbeing
and hosts, still deferred. The rest encode real structure -- a C project,
Python package directories, artgate's routes/styles, systemd unit dirs --
so flattening them further would destroy information rather than
taxonomy.
Verified: 1798 tests at 100%, both i3blocks suites pass, the shell
coverage gate is clean, and the relocated media rules still ignore what
they should while test_fw.srt stays tracked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.
Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.
The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:
- 52 relative `source` lines for lib/common.sh and lib/android.sh. The
correct prefix differs per file: 40 were ../../ and 12 were ../../../,
so a blanket replace would have fixed one set and broken the other.
- REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
each walking up four levels from a directory now two levels shallower.
This caused a real regression -- the fixes suite went red with
"log_warn: command not found" -- caught by running it and comparing
against the pre-move baseline worktree.
- conftest.py's sys.path entries, which took the pytest gate down with
14 collection errors, and the `# shellcheck source=` directives, which
took shellcheck down with SC1091.
16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.
Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
github.com/kuhyx/phone-focus-mode
The two halves go as one repo: focus_policy parses phone_focus_mode's
config.sh into the typed policy that focus-owner consumes, and nothing
else references it. 77 commits of history, plus the two Python suites
from linux_configuration/tests/ and the six mutation fixtures from
meta/scripts/fixtures/, all of which named phone_focus_mode paths.
Running the suites -- not reading them -- turned up a real defect the
extraction would otherwise have inherited. config.sh sources the
untracked config_secrets.sh, and ctl_libs_harness.sh never pinned
HOME_LAT/HOME_LON, so the distance assertions were checking against
whatever real coordinates that file happened to hold. The suite passed on
this machine and could not run anywhere else, and a real home location
was load-bearing in a pass/fail condition. The harness now pins a pair
4300 m south of the coordinate its own case seeds, and config.sh sources
the secrets file only when it exists. 136/136 pass with no secrets file
present.
Before making the repo public I scanned the whole history for the known
home coordinates and for any committed config_secrets.sh: zero hits, and
the file was never tracked. The new repo also gained a .gitignore, since
none came across in the filter-repo pass.
test_magisk_service.sh hangs. It hangs identically in the pre-extraction
baseline worktree and appears in no CI list here, so the new repo's CI
skips it with that reason rather than pretending it passes.
check_python_location.sh loses its phone_focus_mode exception now that
the directory is gone; every tracked .py still passes it. The .gitignore
rules for config_secrets.sh and the keystore STAY -- the working copy is
still on this machine and still holds a real location.
Depth 262 -> 248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
test_usage_monitoring_installer_efficiency.sh left with
system-maintenance, but shell-tests.yml keeps TWO hand-maintained lists
and I had only cleaned the Arch-specific one. The side-effect-free list
still named it, so ci-mirror rejected every push since that extraction.
Cross-checked the whole workflow this time: every test_*.sh it names now
exists on disk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp