Commit Graph

294 Commits

Author SHA1 Message Date
038017d9eb wg firewall: replace only inet filter (no docker restart), persist DoT 853 via ALLOW_DOT
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cqv4ckcaStUFpUkcJKmmNz
2026-10-04 21:16:16 +02:00
83c7bf9664 i3blocks shutdown countdown: minute-precision schedule (*_MINUTES keys)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:38:24 +02:00
f0d50d7375 i3blocks: issue_dashboard shows merged issues awaiting their opener
The issue dashboard now keeps merged-but-open issues (Merge only tags the
opener, kado-no-akari #21) out of `todo` and writes them as `awaiting` in
status.json. The block shows them as "· ⏳ N" between "✓" and "✗", only when
N > 0, without changing the colour (they wait on the opener, not kuhy).
`awaiting` is optional, so the old server's status.json still renders
numbers rather than grey. Still zero forks on the hot path. Prunes the oldest
evidence artifact to stay at the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WXd5qap8DWqZCS9mjh3CzH
2026-10-02 20:59:36 +02:00
deaf3ba80a DNS: dnsmasq bind-dynamic, no ordering cycle, stunnel Wants=; retire pc_startup check; repo paths under ~/src
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-02 18:02:44 +02:00
63465cffac i3blocks: issue_dashboard block
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Python tests / test (push) Has been cancelled
[issue_dashboard] shows what the local issue dashboard (~/src/issue-dashboard)
knows, from the status.json its server rewrites on every refresh:
"📋 <to do> · ⚙ <agents working> · ✓ <ready to merge>", green when something
is ready to merge, red with "· ✗ <n>" when an agent or a proof failed, grey
"📋 dashboard" when the file is missing or stale (server not running). Zero
forks on the hot path; a click opens issue-dashboard-desktop, which reuses or
starts the server and opens its window. Behaviour + fork-count test in
tests/test_i3blocks_issue_dashboard.sh. Prunes the oldest evidence artifact
to stay at the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WXd5qap8DWqZCS9mjh3CzH
2026-10-02 17:03:47 +02:00
a3728824a1 i3blocks: book_guard block; gatelock 0.9.0 and newest-stable pins
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
[book_guard] shows this month's paper-book pace from the state.json
book-guard publishes (green on pace, yellow behind but unlocked, red when
locked or the timer is disarmed, orange when missing/stale); a click opens
book-guard-desktop -- the same way [screen_locker] opens its UI. Behaviour
test in tests/test_i3blocks_book_guard.sh.

meta/requirements.txt: gatelock 0.9.0 (the shared morning-session reader),
plus today's newest stable for anthropic, cryptography, flake8-bugbear,
geopandas, howlongtobeatpy, pyflakes, selenium, mypy, pylint and ruff.
Prunes the oldest evidence artifact to stay at the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbkN4ZB4aUPJPzR2prRan
2026-10-02 14:22:43 +02:00
07810fb270 Home Assistant setup + allow-homeassistant firewall option (Viomi V3 LAN control)
setup_homeassistant.sh runs a digest-pinned Home Assistant container with
host networking and installs hass-xiaomi-miot v1.1.5, so the Viomi V3
(viomi.vacuum.v13) is controllable over the LAN from the HA companion app.
setup_wireguard_ssh.sh gains a persisted ALLOW_HOMEASSISTANT flag that opens
tcp/8123 to the LAN subnet and wg0 only. Prunes the oldest evidence file to
stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGnk48tUeJteJWEQ7UhKDd
2026-09-27 17:12:22 +02:00
ae99698d5b i3blocks: gaming budget block (time left, used/total, unearned bonuses)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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
New [gaming_budget] block reads steam-backlog-enforcer's /api/budget every
30s: "🎮 2h00 left · 6h00/8h00 +📖1h". Colours follow the enforcer warn_at
(1h/30m/10m), blocked is red, a dead server shows the cached value as stale.
Unearned bonus sizes come from the enforcer config via jq --slurpfile.

Prunes the oldest evidence artifact to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CD7nSVE2dKvByG8hdSi3gm
2026-09-26 17:22:54 +02:00
ec2da0b575 helium: unlock the keyring through secretstorage; anchor the running-browser check
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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
`secret-tool lookup` is non-interactive — on a locked keyring it returns
nothing and shows no dialog — and a busctl Unlock+Prompt pair cannot work
either, because Secret Service prompt objects die with the D-Bus
connection that created them. secretstorage keeps one connection:
get_default_collection().unlock() pops the gcr dialog and blocks until it
is answered. Verified on the real profile: 5 v11 rows -> v10, then with
the keyring re-locked Helium (through the wrapper, so the flags file and
LeechBlock apply) lists all 5 logins and keeps every row.

`pgrep -f /opt/helium-browser-bin` also matched the invoking shell when
its argv mentioned the path, so the guard refused on an idle machine;
now anchored to `^/opt/helium-browser-bin/`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8YGXVZdNUG79q5DGsdMm3
2026-09-20 18:29:49 +02:00
c193408ccd helium: full uBlock Origin (MV2) via Helium, profile cloned, keyring prompt fix
Chromium 151 removed the last Manifest V2 code paths and the Web Store
purged uBlock Origin on 2026-08-31, so ungoogled-chromium 153 cannot load
the full blocker any more. Helium (GPL-3, ungoogled-chromium base) bundles
uBO 1.75.0 as a component extension and keeps MV2 alive.

- installers/install_helium.sh: pacman deps, AUR via yay or makepkg
  (+validpgpkeys import), /usr/local/bin/helium-browser ->
  browser-preexec-wrapper so LeechBlock NG and the hosts refresh still
  apply, --clone-profile with a downgrade check and running-browser
  refusal, then the verifier. Passed in vmbox from scratch, then host.
- installers/verify_helium.py: Xvfb + DevTools exit-code gate — uBO
  background_page + manifest_version 2, every /etc/chromium/policies key
  on chrome://policy (Helium reads that dir natively), adblock-tester >= 95.
- installers/migrate_helium_passwords_to_basic.py: lightdm autologin
  leaves gnome-keyring locked, so the 5 v11 logins make Helium prompt on
  every launch; this re-encrypts them as v10 and only then enables
  --password-store=basic.
- register helium-browser in aur_packages.txt and the periodic browser
  wrapper/service lists; pylint hook gets websockets + cryptography;
  codespell learns the vendor name; anthropic 1.6.0 -> 1.7.0 (gate red);
  oldest evidence artifact pruned to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8YGXVZdNUG79q5DGsdMm3
2026-09-20 18:07:34 +02:00
cce6e2c048 g29 shifter: no notification, no retry loop -- fail visibly and stop
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
kuhy: "I do not want it to work like this, I want it to just work
normally." PS4 mode (046D:C260) cannot be supported on this PC (no kernel
driver, SDL lg4ff has no c260 either, so FFB is dead there too) and the
selector is physical. Keep the mode detection so the journal and --status
name PS4 mode instead of "not found"; drop the desktop notification and
the 5-minute Restart loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVfC2T6DyvCFqse9A99zef
2026-09-18 10:56:52 +02:00
3ae53499e6 g29 shifter: detect PS4 mode (046D:C260) instead of failing silently
On the 2026-09-15 and 2026-09-17 boots the wheel enumerated as 046D:C260
(PS3/PS4 selector in PS4 position, bound by hid-generic). The BPF udev
rule, the ensure unit and its hotplug rule all keyed on C24F, so the unit
failed once with "no G29 found" and nothing was said; 1st/3rd stayed on
the firmware's bad decode until the selector was flipped a day later.

- fixes/g29_wheel_mode.sh (new, sourced): classify the HID bus as
  native/ps4/compat/absent; root-safe desktop notification via
  /run/user/*/bus + sudo -u.
- --ensure: absent -> exit 0; ps4/compat -> exit 1 + critical
  notification; native -> attach. --status prints the mode first.
- g29-shifter-bpf.service: Restart=on-failure, RestartSec=5min so a
  notification lost before the session bus exists at boot is re-sent.
- 99-g29-shifter-ensure.rules: also fires on C260.
- tests/test_fix_g29_shifter.bats: 13 tests against a fake sysfs tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVfC2T6DyvCFqse9A99zef
2026-09-18 10:47:18 +02:00
b3f3f55863 install_new_lg4ff: regenerate the initramfs only when it carries the other copy
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
The dkms pacman hook already rebuilds every preset on install and remove,
so the unconditional mkinitcpio -P cost 20 s twice per run on the host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SS8M5wk88nJTXxDWz61KKk
2026-09-14 22:15:43 +02:00
7e2d78c7db Add the G29 shifter HID-BPF fix (1st/3rd engage reliably)
Live on this host since 2026-09-11 but never committed. The wheel firmware
decodes the shifter's hall sensor itself and this unit's top row rests at
Y 180-193, below the ~195 engage threshold, so 1st/3rd often did not
register. g29_shifter.bpf.c re-decodes those two gates from the raw X/Y
bytes with hysteresis (engage >= 160, release <= 140) and ORs the bit in
before hid-logitech parses the report; it never clears a bit, so the
firmware's working gates cannot regress. fix_g29_shifter.sh builds,
attaches and installs it persistently (udev-hid-bpf rule + a systemd
safety-net unit pulled in on every re-enumeration); g29_shifter_capture.py
records and grades raw captures per gate.

Two files were over the 250-line cap: the test file is split at the
record/CLI boundary with the report builders moved into a conftest fixture
(the naming hook allows only test_*.py in tests/), and the grader's
docstrings are tightened. No behaviour change to the installed fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SS8M5wk88nJTXxDWz61KKk
2026-09-14 22:12:31 +02:00
2d54765f6f Read the load average through LOADAVG_FILE so the probe tests are host-independent
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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
The "flags nothing on a quiet machine" case read the real /proc/loadavg and
went red whenever a game was running on the host (22/16 during a BeamNG
session), and the "flags a load at/above thread count" case had to SKIP
whenever the host was idle. Same override pattern as JOURNALD_CONF_DIR: the
harness writes a fixture, both cases now assert deterministically, and a
missing file degrades to 0 as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SS8M5wk88nJTXxDWz61KKk
2026-09-14 21:55:06 +02:00
c81e4a9446 Add G29 force-feedback doc and a new-lg4ff installer
BeamNG.drive 0.39 native lost G29 FFB because SDL3's HIDAPI lg4ff driver
advertises an effect-status query it cannot answer (12,201 failures per
session in beamng.log). DOCS-g29-ffb.md records the diagnosis, the narrow
SDL_JOYSTICK_HIDAPI_LG4FF=0 hint vs the broad one, and the launch-option
%command% bug that had gamemoderun passed as a game argument.

install_new_lg4ff.sh swaps the in-tree hid-logitech for new-lg4ff via DKMS
(dkms.conf installs it under the same name in updates/dkms/), regenerates
initramfs presets that bundle the module, and offers --status/--revert.
Passed install/status/revert in vmbox; host run pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SS8M5wk88nJTXxDWz61KKk
2026-09-14 21:43:28 +02:00
ce3c4213b0 Arch journal cap: 300M -> 4G, vacuum only past the cap
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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
300M held about two days on kuhy's desktop: one Steam boot wrote 480k
journal lines and the evidence for a curfew failure two weeks earlier was
already gone when it was needed (2026-09-13). The three Arch copies of
the journal tweak (arch_hardware, arch_perf_fixes, arch_perf_report) move
to a 4G cap, and "large"/vacuum now mean past that cap — a 2G journal is
deliberate retention, not a finding. Tests follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mLnUeoJ3mUwGRTowTUH6d
2026-09-13 10:55:43 +02:00
db243fe02e Cache prettier's verdict so a push stops paying Node startup
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
A push took ~13s. prettier is a pre-push hook precisely because Node's
startup dominates its runtime, and pre-commit chunks a large file list into
several invocations -- a 106-file push paid ten cold Node starts. ci-mirror,
the hook that looks expensive, was already 0.02s once its green-tree cache
hit.

A file whose content, prettier version and prettier config are all unchanged
since its last clean check cannot have become unformatted, so the verdict is
now cached and the file never reaches Node. Only clean verdicts are recorded,
so a formatting regression is always re-checked; the key includes
`prettier --version` and every config file, so an upgrade or a config edit
invalidates the cache wholesale. The systemd-run memory cap is unchanged.

Measured on the same push range: 13.0s -> 3.3s total, of which ~0.6s is
hooks and the rest is git's own network negotiation. On a 40-file list the
runner itself goes 0.652s -> 0.057s, starting no prettier process at all.

endurain/README.md and copilot-instructions.md are reflowed: the ~
reorganisation lengthened paths inside them, which changed prettier's
markdown table alignment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 21:03:57 +02:00
f15f00f3fe Re-point paths for the ~ reorganisation (home-tidy)
Also bumps the 16 meta/requirements pins the freshness gate flagged
(anthropic 1.5.0, numpy 2.5.3, pytest-randomly 5, flake8-bugbear 26.9.9,
ruff 0.16.7, pyright 1.1.414, selenium 4.49.0, hypothesis 6.168.0, lxml
6.1.3, pipdeptree 4.2.5, the two types-* stubs) and Flutter 3.47.3 in
meta/.fvmrc. One test still expected the pre-move ~/mcp-servers path.

The pre-commit hook fed .zsh files to shfmt and shellcheck, neither of
which parses zsh (SC2148/SC1071), so no zsh file could be committed since
the hook was tightened on 2026-08-22; those now get `zsh -n` instead. The
hook's own shfmt pass also staged its reformat of 8 shell files.

CI_GREEN_SKIP because main is red on exactly the dependency-freshness
finding this commit fixes, which is the documented use of that flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 20:50:19 +02:00
0cea82cab5 feat: the nftables drift check now runs itself at boot
`verify` gated on the two invariants whose breach cut every bridged
container off the internet on 2026-08-29 and again after the 2026-08-30
reboot -- but nothing invoked it, so it reported drift only when a human
thought to ask, and the outage it exists for is exactly the one nobody
thinks to ask about.

nftables-drift.service now runs it after nftables.service, docker.service
and network-online.target at every boot, plus a daily Persistent timer. A
breach is a failed unit in `systemctl --failed`, not a log line: a
self-check that detects a failure has to gate, not warn.

Proven in ~/utils/vmbox across two real reboots -- drifted /etc leaves the
unit failed with the diff and the missing docker0 accept in its journal,
matching /etc leaves it active with no false positive -- then installed and
run green on the host. Not yet observed across a host reboot.

Also bumps the gatelock pin to v0.7.3, which is what turned main's
dependency-freshness job red; CI_GREEN_SKIP is set because this commit is
that fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
2026-08-30 15:49:12 +02:00
89a44b58b2 feat: gate on nftables drift so the docker cut-off cannot come back silently
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The forward-chain fix from 334c0cd9 only ever reached the live ruleset by
hand; /etc/nftables.conf kept the bare default-drop chain, so the next
reboot cut every bridged container off again and the Signal bot went deaf
a second time. Nothing compared generated against persisted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:56:24 +02:00
2713ae9d66 Put the screen-locker status back on the i3 bar
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The GTK tray icon was the only status surface, and it had been commented out
of ~/.config/i3/config since 2026-08-21. With nothing in the bar, the locker
miscounted the week for a fortnight and the machine never said so.

Shows "<count>/<minimum> · <gaming budget>", read from the local status API
(screen-locker-web.service on 127.0.0.1:8770); clicking opens the full UI.

Three things it deliberately will not do quietly:
- a dead status server renders "status server down" in red, or the last known
  value marked (stale) in orange -- never a blank block. That server is what
  the gaming budget reads, so its absence costs two hours;
- a disarmed or unscheduled timer overrides the compliance colour entirely
  and shows "DISARMED" in red. A locker that cannot fire is the worse
  problem and the one that hides;
- clicking only opens the UI. There is no bar action that can clear a lock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
2026-08-29 22:55:24 +02:00
334c0cd904 fix: stop the wireguard firewall from silently cutting every container off
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
`write_nftables_ruleset` emitted a forward chain with `policy drop` and no
rules at all. That chain runs at the same netfilter hook as Docker's own
DOCKER-USER/DOCKER-FORWARD chains, so it vetoed them: every bridged
container lost outbound networking while the host itself stayed online, so
nothing looked broken.

It cost the Signal bot three silent hours today. signal-cli lost its
connection to Signal at 15:32:50 and looped on reconnect for 2.5h, while
the bot kept answering /healthz with 200 -- `/v1/groups` still served from
local state, so every surface said healthy.

`flush ruleset` also deletes Docker's own tables, and dockerd only rebuilds
them on its next network event, so restore_docker_rules() now restarts
docker right after a successful apply.

Verified in vmbox (Arch guest, docker installed, alpine probe):
BASELINE=reachable -> OLD=blocked -> NEW=reachable, sshd active throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjfWdAyHbCubac7wJKJZW
2026-08-29 18:34:25 +02:00
ce33c935e4 Run the website's post editor as a loopback unit, no Caddy route
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
2026-08-29 17:50:03 +02:00
61abbf8cf9 Force-recreate the website container so config and dist changes apply
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Two failure modes compose cannot see, both of which report a healthy
container while it serves 404s:

  1. The Caddyfile is a bind mount, so its contents are not in the config
     hash compose compares. Editing it changes nothing observable, compose
     says Running, and the new directives never load.
  2. A bind mount resolves once, at start. Replacing dist/ rather than
     emptying it leaves the container on the old unlinked inode: /srv reads
     empty and every request 404s.

Hit both at once on a real deploy today -- the container was Up 6 hours,
Caddy logs clean, /srv empty, curl 404. Reproduced by rm -rf dist (inode
84695030 -> 84695031) and fixed by the patched function in one run.

wait_for_website also stops using curl -sf, which collapses every non-2xx
into the same "did not become reachable". It now reports the status it saw,
because unreachable and answering-404 are different bugs and the message
named only the first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7BaCSV8FEtcNgVDVsRAJj
2026-08-29 16:35:30 +02:00
516acc243f Serve prerendered blog posts, not the SPA shell, from the website container
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The personal-website repo now prerenders each post to
dist/blog/<slug>/index.html. Caddy try_files does not resolve a directory
to its index on its own, so with the single-term directive every post URL
fell through to the SPA shell: right content once React booted, wrong
<title>, and no link preview. Verified against caddy:2.8 in Docker both
ways before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7BaCSV8FEtcNgVDVsRAJj
2026-08-29 16:19:28 +02:00
adf2e402df Grade against 3.14 too, not just run on it
`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
2026-08-29 11:50:39 +02:00
924630c029 Run SyncYomi from the local fork build and stop clobbering the Caddy edge
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 14:44:40 +02:00
49d82511c3 Adopt the four markdown namespaces and gate them
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
2026-08-24 15:55:46 +02:00
782a6320af Let --rollback work on a freshly installed machine
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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
2026-08-24 12:57:24 +02:00
4c426eae0e Make install_core_system.sh work on a fresh machine
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Python tests / test (push) Has been cancelled
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
2026-08-24 12:45:21 +02:00
6b828d29aa Extract digital-wellbeing; the directory-depth cap now enforces
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
2026-08-23 20:57:51 +02:00
309f4c8d42 Repair live self-repair broken by the flatten and extractions
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
2026-08-23 20:39:59 +02:00
f72455662c Extract the hosts blocker into its own repo
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
2026-08-23 20:30:18 +02:00
ac8e098c03 Point the diagnostics bats suite at the flattened path
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
2026-08-23 20:08:40 +02:00
9aeeb005dd Derive the lib prefix in the music_parallelism drift guard
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
2026-08-23 19:53:17 +02:00
7cb23e8a81 Prettier-normalize two docs the pre-push hook was already failing on
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
2026-08-23 19:42:18 +02:00
0fb3e3eee2 Cap extensionless scripts, and split the one that was hiding
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
2026-08-23 19:13:40 +02:00
08aac8d7d1 Flatten i3-configuration and misc/testsAndMisc-bash
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
2026-08-23 19:02:18 +02:00
4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
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
2026-08-23 18:53:30 +02:00
a5209b6878 Extract phone_focus_mode together with focus_policy
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
2026-08-23 18:32:58 +02:00
fe8629e104 Extract android_guardian; heavy_job_lock goes to its installer
github.com/kuhyx/android-guardian, cloned to ~/android-guardian

periodic_background/utils/ held two unrelated things: the Magisk module
and heavy_job_lock.sh. Only the module was a unit, so the directory is
split rather than moved.

heavy_job_lock.sh goes to digital_wellbeing/, NOT to ~/utils. Measured
rather than assumed: of its consumers only install_pacman_wrapper.sh
reads the repo copy, while pacman_wrapper.sh, makepkg_capped.sh and
meta/scripts/run_with_heavy_lock.sh all read the installed
/usr/local/bin copy. A library belongs with whoever installs it, and
exporting it would have added a cross-repo edge for one caller.

Fixed a bug that predates the extraction: update_android_hosts.sh set
GUARDIAN_MODULE_DIR to "$SCRIPT_DIR/android_guardian", a sibling that
never existed at that path -- packaging could not have worked from this
checkout at all. It now reads ${ANDROID_GUARDIAN_DIR:-~/android-guardian}
and require_guardian_module() gates build_module(), the single funnel
every packaging path goes through, so a missing checkout names the clone
command instead of failing inside a cp four helpers deep.

Verified both directions: with no checkout the guard fires with
instructions; with one, the module builds -- 182331 blocked domains, a
1.1 MB zip. The subsequent adb push failed on device-side storage, which
this commit does not touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:00:58 +02:00
a1f8044a4a Extract system-maintenance, the isolated subsystem
github.com/kuhyx/system-maintenance, cloned to ~/system-maintenance

First of the four live subsystems, and the one with no cross-unit source
edges, so it establishes the pattern for the harder three. Passed in
vmbox first -- the installer ran from an extracted-layout copy in the
guest and detected its distro without any monorepo path -- then on the
host.

Its 21 Python and shell test files lived in linux_configuration/tests/,
reaching bin/ through a shared conftest. Twenty moved with the code and
got their own conftest; the shared one stays, since it serves several
script directories that are not going anywhere.

test_shutdown_timer_monitor.sh stays here. It is a pair test: it checks
that digital_wellbeing's installer template and the monitor it installs
still agree, so it belongs to whichever half owns the installer. It now
reads the monitor from ~/system-maintenance and SKIPs when that checkout
is absent.

An intermediate version of that test fell back to /usr/local/bin, which
HUNG -- the installed copy is stale and differs from source, so the test
was comparing an installer template against a binary it never generated.
Falling back to it would have been worse than skipping, so the fallback
is gone and the comment says why.

Also removed test_hosts_file_monitor.sh from shell-tests.yml's explicit
list: it left with the subsystem, and CI would have failed on the missing
file rather than quietly skipping it.

Verified: 136 tests pass in the new repo, 202 still pass here, the pair
test passes against the checkout and skips without it, and both the shell
coverage gate and shellcheck are clean. Depth 294 -> 267.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:55:29 +02:00
6b5fe8e7a7 Extract mtk_root, with the libraries that lived outside it
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
github.com/kuhyx/mtk-root

Unlike the app extractions, mtk_root could not travel alone: its four
libraries and three of its four test suites sat in scripts/lib/, one
directory up from the stages that source them. Taking only the directory
would have orphaned eight files here and left the new repo unable to
source anything, so all twelve moved in one filter-repo pass. The
mtk_root/ + lib/ sibling layout is preserved, so every
`source "$SCRIPT_DIR/../lib/..."` resolves with no call site edited.

Two things the new repo needed that the monorepo provided implicitly:

  lib/tests/run_all.sh  -- the copy here is shared with the non-mtk suite
                           and stays. Without a new one, shell-tests.yml's
                           glob over */lib/tests/run_all.sh would have
                           stopped covering these suites on a green build.
  .shellcheckrc         -- external-sources + source-path=SCRIPTDIR, which
                           is what lets shellcheck run with no severity
                           filter and still pass on the ../lib/ sources.

mtk_harness.sh stays here despite the name: test_common_datetime.sh
sources it. Removing it broke that suite outright, which running the
suite caught immediately. Its header now describes what it actually
serves; the misleading name is kept on purpose, since renaming would
churn its one consumer for nothing.

Verified: 84 tests pass in the new repo with shellcheck clean, 55 still
pass here, and check_shell_coverage.sh --all exits 0 with the four
mtk_root allowlist entries gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:39:27 +02:00
a0459b99a0 Extract reverse_survivors, kcd2_dice_solver and focus_owner
Three self-contained apps move to their own public repos with full
history (8, 14 and 30 commits, each matching the monorepo count):

  github.com/kuhyx/reverse-survivors
  github.com/kuhyx/kcd2-dice-solver   (cloned to ~/kcd2-dice-solver)
  github.com/kuhyx/focus-owner

Each gained a CI workflow. Only reverse_survivors had one here; the other
two were never gated by anything, which is part of why they no longer
belong in a repo whose CI is shared.

What stayed: setup_kcd2_dice_solver.sh, which deploys the solver rather
than being part of it. It now builds from ~/kcd2-dice-solver, and the
post-commit hook it generates drops its `git diff -- kcd2_dice_solver/`
filter -- in a standalone repo every commit is solver code, so the old
filter could never match again.

focus_owner's two references to python_pkg.focus_policy now name the
phone-focus-mode repo, where that generator is headed.

Running the live kcd2 unit -- rather than reading it -- found three real
breakages that no amount of grepping would have: pnpm ignoring the
package.json "pnpm" field, pnpm refusing to purge node_modules without a
TTY, and the Docker container still bind-mounting the deleted monorepo
path. All three are fixed; the unit exits 0 and
https://dice.kuhy.duckdns.org/ serves the freshly built bundle.

Depth violations 417 -> 303.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:34:02 +02:00
0b6df7b003 Revert the deliberate gate-probe break
Reverts the temporary broken assertion used to prove the pre-push gate
refuses a red shell suite (it did: exit 1).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 15:08:46 +02:00
04cc5fa23e TEMP probe: deliberate shell-test break 2026-08-23 15:06:15 +02:00
76b1d74388 Revert the deliberate gate-probe break
Reverts 58b4cef8, a temporary broken assertion used to verify the
pre-push gate refuses a red shell suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 15:03:29 +02:00
58b4cef85a TEMP: break shell test 2026-08-23 14:58:14 +02:00
b399fab56d Take the fixes/lib suite from 110s to 37s, same 645 assertions
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
Three changes, none of which touch what a single test asserts:

thorium_repairs.sh's `sleep 4` becomes `sleep ${THORIUM_STARTUP_WAIT:-4}`
and the harness sets 0.2s. Three cases were each paying four real
seconds to watch a stubbed browser stay up; the stub sleeps 30s, so the
`kill -0` check is unchanged. That file: 17.0s -> 5.6s.

_t_hide was the rest of it. It walks PATH and creates a symlink per
executable -- ~13.5k here, 4.6s a build -- and the suite has ~16
distinct tool sets. Now the full farm is built ONCE and `cp -al`
hard-links it per set, with only the hidden names deleted from the copy.
The cache also moved out of $TEST_TMPDIR, which is per-file, into a
run-scoped dir run_all.sh creates and removes, so it survives between
files.

110s -> 98s (sleep) -> 85s (shared cache) -> 37s (hard-linked farm).

645 OK, 0 FAIL throughout. The cases that only pass when a tool is
genuinely unfindable -- install_pyroveil's missing-dependency arms,
fix_aggressive without sqlite3 -- still pass, which is what proves the
farm is still hiding rather than quietly returning a full PATH.

lib_test_core.sh hit exactly 250 lines, so _t_hide and the farm move to
lib_test_path.sh (194 + 67).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:00:13 +02:00