mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 22:54:01 +01:00
143d260c1c
166 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 143d260c1c |
Park phone DNS filtering: Private DNS fails closed, so the PC cannot host it
Measured on the Pixel 6a, twice: with private_dns_mode=hostname and the resolver unreachable, `ping example.com` returns "unknown host". Android's Private DNS does not fall back to the carrier's resolver -- hostname mode is RFC 8310's strict profile, and failing to establish TLS on 853 is a hard error. So a resolver living on this PC means "PC off" equals "phone has no internet", not merely "no filtering". That is disqualifying for a daily driver, and it disqualifies the whole home-hosted delivery path regardless of how well it works while the machine is up -- which it does; it was verified end to end. DNS_TRUSTED_DOT_HOST/IPS go back to empty, which restores the previous enforcer behaviour byte for byte. No code revert is needed, which is exactly why the flag was built to default empty. Two things found while testing are worth keeping. The nftables input chain is policy drop and admitted only ssh over wg0, so binding stunnel to the tunnel address was not sufficient on its own: ICMP passed while TCP 853 was dropped, which presents confusingly as "ping works but the port does not". That rule is now added explicitly and scoped to wg0. And setup_phone_wireguard.sh automates the client setup the tunnel needed, verifying handshake, reachability and DNS rather than assuming success, and deleting the pushed private key afterwards. The deeper reason the home endpoint could not work even with the tunnel up: dns.kuhy.duckdns.org resolves to the public IP while stunnel listens only on 10.8.0.1, and with AllowedIPs=10.8.0.0/24 that routes outside the tunnel to a closed port. Android never opened a connection at all. Split-horizon DNS does not fix it either, since on mobile data the phone uses the carrier's resolver. Research also ruled out the hosted-hybrid option that looked most attractive: no provider can ingest a 185k-domain list (NextDNS none, AdGuard 1-5K, ControlD 10K), and the free tiers fail OPEN -- they stop filtering past 300k queries a month while still resolving, which for a self-control tool is the worst failure mode available. Details and the remaining options are in the parked doc. The stunnel endpoint is left running deliberately: WireGuard-only, verified unreachable from LAN and WAN, and it makes the next attempt testable immediately. Removal commands are documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| e3d5e1dd18 |
Serve the blocklist over DoT and hand the policy to the app as an asset
Two gaps stopped the unrooted path working end to end. The phone had no way to read the focus policy, and the blocklist was only reachable as a hosts file or over plain UDP/53 on the LAN -- while the rooted enforcer actively rejected every DoT connection, so a self-hosted resolver could not have worked even if one existed. focus_owner now loads a generated policy.json instead of hardcoding anything, so config.sh stays the single source of truth across Python, shell and Kotlin. The committed asset is rendered with --redact-home and a test fails if home coordinates ever appear in it. An unknown schema_version is rejected outright: silently accepting a newer schema means enforcing a policy the app has misread, and a misread allowlist is one that blocks the dialer. dns_enforcer.sh rejected ALL port 853 traffic and pinned Private DNS off, every 20 seconds. That is right when DoT is a bypass channel and exactly wrong when DoT is the enforcement channel, which is what an unrooted phone needs -- it has no hosts file to fall back to. With DNS_TRUSTED_DOT_HOST set it now ACCEPTs the trusted resolver before the blanket REJECT, and pins Private DNS to that hostname so the user cannot pick an unfiltered one. Rule order is load-bearing: iptables takes the first match, so an ACCEPT appended after the REJECT would never be reached. expected_rule_count() counts the new rules, because a mismatch there makes chain_intact() rebuild the chain every 20s -- a silent fork storm that also drops the connection each time. With the variable empty the behaviour is byte-identical, so the rooted phone is untouched until this is deployed. setup_dot_resolver.sh terminates DoT with stunnel rather than Caddy: DoT is a raw TLS stream and stock caddy:2.8 has no layer4 module, so using Caddy would have meant an xcaddy rebuild of a container serving five live sites. It reuses the certificate Caddy already renews, so nothing competes for port 80. It binds to the WireGuard address, not 0.0.0.0 -- a public DoT endpoint is an open resolver, and over the existing tunnel it needs no port-forward, no firewall rule and no rate limiting. Verified: example.com resolves, pornhub.com returns 0.0.0.0, TLS 1.3 against the LE cert, and both the LAN and public addresses refuse. The Device Owner checklist is planning only. Checking the device rather than the config settled the one open risk: Google Wallet is not installed and no NFC payment default is set, so the unconfirmable tap-to-pay question does not apply here. The same check found that Aegis, MS Authenticator, Oracle IDM and PKO IKO are absent -- they are in config.sh's whitelist, which describes the rooted phone, not this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| f636d2b21b |
Make focus policy and blocklist usable without root; fix workout CDN unblock
phone_focus_mode is built for a rooted phone: the hosts layer is a read-only
bind mount over /system/etc/hosts, chosen so that even root-in-a-terminal
cannot overwrite it. None of that ports to a stock locked device. This splits
out the parts that are device-independent so one policy can drive both.
python_pkg/focus_policy parses config.sh rather than copying it, so the shell
config stays the source of truth and the two cannot drift. It rejects the
shipped REDACTED_* coordinate placeholder instead of defaulting to (0,0),
which would move "home" to the Atlantic and quietly disable the location gate.
blocklist.py converts the hosts file to domains for a DoT resolver or a local
VpnService. Three properties of the real 194k-line file make the obvious parser
wrong, and all three are handled: generate_hosts_file.sh *comments out*
deliberate unblocks rather than deleting them, so stripping '#' re-blocks
facebook/messenger/linkedin/4chan; the StevenBlack header carries loopback and
IPv6 housekeeping that would blackhole localhost; and ~1060 upstream annotation
tokens ('#[adware.zeno]', '"spam') render as address=/"spam/, which makes
dnsmasq refuse to start -- taking DNS down rather than blocking anything.
Verified with dnsmasq --test over all 185681 generated domains.
strip_workout_hosts.py had the same class of bug on the live rooted phone:
exact set membership meant the allowlisted googlevideo.com never matched the
dynamic r*---sn-*.googlevideo.com hosts YouTube actually streams from, so the
workout unblock was only partly working. Now suffix-matched (17 lines stripped,
was 14). The three new tests were checked against the original code in a
throwaway worktree; two of them fail there, so they pin the fix rather than
passing vacuously.
focus_owner is a device-owner app that deliberately does nothing yet. It ships
clearDeviceOwnerApp() implemented and tested first, because dpm
remove-active-admin does not work on a device owner: an app that locks the
device down before proving it can let go leaves a soft-brick recoverable only
through fastboot on a locked bootloader. Built, installed and launched on the
Pixel 6a; dumpsys afterwards confirms Device Owner Type -1 and no user
restrictions, i.e. the phone is unchanged.
Measured while scoping this, and not yet reflected in any enforcement code:
on Android 16 both pm suspend and pm disable-user are cleared by a reboot, so
shell-level app blocking is per-session only. setApplicationHidden is assumed
to persist under Device Owner but is untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
|
|||
| f1590cbb64 |
Pin prettier, shfmt and zsh too, not just shellcheck
|
|||
| 23fdfcf867 |
Pin shellcheck in CI so local and CI agree on what a finding is
Root cause behind the outage |
|||
| 9dd9aeb50e |
Fix the 11 shellcheck findings that kept CI red since b83ce5d
"Pre-commit checks" has failed on every push since
|
|||
| 1b43ba093c |
mtk_root: build the rooting toolkit before the phone lands
Prep for a Ulefone Armor X12 Pro (MediaTek MT6765) that has not arrived, validated read-only against the Pixel 6a that has. Partition discovery never hardcodes boot vs init_boot. It reads /dev/block/by-name/, derives the slot scheme from observed _a suffixes, and picks the ramdisk carrier by preference order init_boot then boot over what is actually present. The AOSP launch-version rule explains why that ordering is right; the code consults the filesystem, not the API level. One path therefore serves both a Pixel (no init_boot, falls to boot_a) and a 13-launch MediaTek device (init_boot_a taken). Patching the wrong one yields a phone that will not boot. Classification refuses rather than guesses: ULEFONE needs a positive MediaTek signal AND a vendor match, with no "not a Pixel therefore Ulefone" fallback. Carrier lock is always reported UNDETERMINED, because sys.oem_unlock_allowed, ro.oem_unlock_supported and settings global oem_unlock_allowed read identically whether a device is carrier-locked or merely has the toggle off; asserting a negative would wrongly rule out a path. No flashing script, deliberately. Automating irreversible partition writes defeats the confirmation steps that catch a wrong partition or missing slot suffix. Three bugs found by running the code rather than only linting it: - SIGPIPE: `adb version | head -1` under pipefail exited 141 and killed 00-preflight.sh mid-run - 20-dump-stock.sh exited silently when no recon had run, because find on a nonexistent cache dir fails under set -e - the read-only static scan had a hole: stripping all quoted strings to avoid false positives on printf text also hid `adb shell "reboot bootloader"`. Now only printf/echo/cat arguments are stripped, and a negative self-test asserts the scan fails on injected commands Also narrowed a world-writable udev catch-all (idVendor=="*", MODE="0666") that granted every local process read/write access to every USB device; commented with a backup, adb connectivity re-verified afterwards. Validated: 58/58 offline fixture tests; 00-preflight.sh HOST READY (12/12); 10-recon.sh read-only against the Pixel reports PIXEL, boot_a, A/B, no init_boot, path C-or-D indeterminate, with the device unchanged afterwards. Untested and labelled as such in READY.md: the init_boot branch on real hardware, mtkclient over BROM, and the whole flash/verify sequence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7 |
|||
| b83ce5d3c7 |
Fail the build on any shellcheck finding; fix the 222 the old gate hid
Some checks failed
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
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:
- Five scripts in single_use/misc/testsAndMisc-bash/ sourced
$SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
at that path, so all five failed at runtime.
- setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
(which ends in `exit 0`) and then `exit 1` for an unknown command. The
exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
- Six i3blocks scripts had a `# shellcheck source=` directive missing the
scripts/periodic_background/ path segment. The stale path had silently
disabled source-following for all of them.
- diagnose_arch_performance.sh word-split a newline-joined file list; when
the find came back empty, awk got no file operands, fell through to
reading stdin, and the diagnostic hung.
- lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
`return 0` while documenting "Returns: 0 if handled" -- a contract no code
path could satisfy.
- deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
level, and config.sh can repoint it at the on-device path
/data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.
The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.
17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.
Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.
The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.
third_party/ is excluded -- vendored upstream, matching the prettier hook.
Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 79829ec8a2 |
Diagnose the pacman first-hook stall; fix the drift that hid a stale wrapper
pacman intermittently freezes at its FIRST PreTransaction hook - 5 abandoned
transactions since 2026-07-29, stalls of 4 minutes to overnight, always fine on
retry. This investigates it and repairs the real defects found on the way.
The stall is NOT fixed, and deliberately so: no verified defect was found, so
nothing pacman-side was changed.
- Not guard-lib. /etc/guard-lib/targets was empty, so that hook script
provably exits in milliseconds. The failures share POSITION (first hook of
the transaction), not identity: one stall hit the hosts hook back when that
one ran first.
- Not reproducible on demand. 65 real transactions (40 idle, 25 under 2.6 GB
of memory pressure) produced zero stalls. Against the historical 4-in-56
rate that is ~0.9% likely, so the trigger is conditional on something a
synthetic load does not recreate. Memory pressure is untested at the load
that actually occurred - not refuted.
So diagnose_pacman_hook_stall.sh drives real transactions with a watchdog, and
its --watch mode passively captures /proc/<pid>/{stack,wchan,syscall}, the
process tree, meminfo and PSI from the next REAL stall. No pacman-side fix
should ship before that dump exists.
Three real defects were found and fixed:
1. The deployed pacman wrapper was 19916 B against a 27510 B source, never
sourcing the shared pacman_lock_lib.sh that makepkg_wrapper already used.
It hid because check_pacman_wrapper() only tested that files EXIST. Both
installers now write a drift manifest covering source AND installed hashes;
the checker replays it with sha256sum -c and fails closed when it cannot
verify. Manifest paths are absolute - relative ones resolve to nothing under
systemd's cwd=/ and would have looped reinstalling forever.
2. The guard-lib migration (
|
|||
| 4d9f486176 |
billsplit: build a real APK by dropping file_picker for file_selector
billsplit had never produced an Android APK. file_picker 11.0.3 — the latest release on pub.dev — is wedged under AGP 9 in both directions: it skips applying the Kotlin Gradle Plugin when AGP >= 9, so with built-in Kotlin off its own Kotlin never compiles (`cannot find symbol: FilePickerPlugin`), and with built-in Kotlin on its buildscript leaks KGP onto the global classpath via rootProject.allprojects, which AGP then rejects for every module. The app's Gradle config was never at fault: android/app/build.gradle.kts and settings.gradle.kts are byte-identical to habit_stack, home_inventory and diet_guard_app, which all build. Only the dependency differed, so the fix is the dependency — the toolchain stays on the house-standard AGP 9. Ported both call sites to the official file_selector. Its save flow returns a destination and leaves the write to the caller, where file_picker wrote the bytes inside the plugin; that write now goes through an injectable seam because a real dart:io future never completes inside testWidgets' fake-async zone, so a widget test could otherwise never observe the resulting snackbar. The test double moves from a MethodChannel mock to a FileSelectorPlatform fake (file_selector speaks Pigeon, so a channel mock would not intercept it), and a plain non-widget test covers the real on-disk writer to hold 100% coverage without an ignore. The launcher icon is now verified from the built artifact: the ic_launcher assets unzipped from the APK render as the charcoal + gold perforated receipt, not the stock Flutter beach. Peak build footprint is 1.86 GiB, inside budget. Also rewrites ci_mirror.sh to run its gates in a disposable worktree at HEAD. `pre-commit run --all-files` stashes the live tree, and in a shared checkout that made an unrelated agent's uncommitted work collide with hook auto-fixes, failing the push while blaming an innocent hook — and putting that work inside a stash window, which memories/git-rules.md forbids. A clean worktree needs no stash and is a truer mirror of CI, which never sees a dirty file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 287512f011 |
phone_focus_mode: reach the phone over adb without touching it
Android's Wireless debugging is deliberately ephemeral: the listener dies when you leave the settings screen or the device sleeps, and it picks a new random port each time. In one session it offered 35545, then 40401, then 43781, and vanished entirely twice. Every deploy therefore started with tapping through Settings and reading a port off the screen. enable_persistent_adb.sh waits for any such transient window, then uses it to pin adbd to port 5555 - the port deploy.sh has always assumed - and installs a Magisk service.d boot script so the same happens on every boot. The phone is on DHCP with no reservation, so the script identifies it by MAC rather than trusting a hardcoded address: a ping sweep populates the neighbour table and the address is looked up there. That beats a router reservation because it needs no router access and works on any network. One bug worth naming: the first version took the first `device` line from `adb devices`. Pairing also leaves an mDNS _adb-tls-connect._tcp entry for the same phone, so it reported port 5555 as reachable while nmap showed it closed. It now matches the exact ip:port. Verified: nmap showed 5555 closed then open across the switch; a run against a deliberately wrong IP reported "phone moved" and connected; and after `adb reboot` the script connected with no arguments and no interaction, with /proc/uptime at 149s and service.adb.tcp.port reading 5555, so the boot script really ran. shellcheck clean. Trade-off accepted by the user: adbd on a fixed port is reachable to anything on the LAN, though it still requires an authorised key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| ed1a224ce7 |
Format last three markdown files; cap billsplit's Android build memory
Brings the repo to prettier-clean across all 525 tracked md/json/yaml files. The markdown edits are cosmetic: escaping `L*` so it stops rendering as emphasis, normalising `*em*` to `_em_`, and aligning tables. billsplit/android/gradle.properties shipped from the Flutter template asking for `-Xmx8G -XX:MaxMetaspaceSize=4G` — more than this machine has. Lowered to 768m/512m and, more importantly, cut the number of JVMs: Kotlin compiles in-process, no persistent daemon, no parallel execution, one worker. Also repairs a regression an earlier prettier pass introduced. That pass pretty-printed test/fixtures/sample_receipt.json, but `accepts bare JPK payload and bare JWS token` reads the fixture as raw TEXT with a regex matching `"data":"..."` with no space after the colon. The inserted space made the match null and the null-check operator threw. Comparing the parsed JSON for equality had passed and proved nothing about that raw-text contract. The fixture is restored to its original minified bytes — a real e-paragon arrives as single-line JSON — and test/fixtures/ is now in .prettierignore so captured payloads stay byte-faithful. The regex also tolerates whitespace now and asserts the match is non-null with a reason, so the same change would fail readably instead of crashing. Verified against both the minified and the pretty-printed fixture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 5b654fc286 |
app_icons: finish the epopeja_karta entry; whitelist it on the phone
Registers the new Epopeja character-sheet app (~/epopeja_karta, its own
repo at kuhyx/epopeja_karta) in the shared icon family and lets it run
under focus mode.
The quill-nib glyph and the initial APPS entry already landed in
|
|||
| c600760e71 |
Move billsplit Flutter app into the monorepo
billsplit was a standalone Flutter app in ~/Downloads with one commit and no git remote — that directory was the only copy of its history. Three things the move needed beyond copying the tree: - The root .gitignore silently swallowed 32 of its files (*.png, *.ico, *.manifest). Ignored files do not error on `git add`, they just vanish, so this would have committed an app that cannot build. Added a trailing block of ! overrides plus .binary-allowlist (which did not exist yet) — the documented in-repo exception for binaries a build genuinely needs. The ../testsAndMisc_binaries/ symlink scheme does not apply: those directories are gone, and a symlinked PNG breaks a fresh clone. - billsplit/.github/workflows/ci.yaml was inert once nested; GitHub only reads workflows at the repository root. Relocated with a working-directory and a billsplit/** path filter. - The app's coverage gate moved to python_pkg/billsplit_coverage/ so every Python file stays under python_pkg/, and gained a test suite at 100% branch coverage as that location requires. billsplit/ now holds only Dart. Also replaces the stock Flutter icon with a family icon from the shared generator. The glyph is a filled silhouette because the family's 72px stroke cannot carry a fine torn edge, and the split is an evenodd-cut perforation so the holes stay transparent on the foreground and monochrome layers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 2b3c1a69f7 |
fix(hooks): make git hooks and their toolchain mandatory and fail-closed
A completely absent `prettier` binary went unnoticed long enough for unformatted markdown to reach main. Two independent causes, both fixed here. No pre-push hook existed. The three hooks declaring `stages: [pre-push]` in .pre-commit-config.yaml — prettier, ci-mirror, pytest-coverage — therefore never ran locally at all. Adds one. The pre-commit hook failed open. shfmt and shellcheck were wrapped in `if command -v`, and the entire pre-commit framework was skipped with a warning when its binary was missing. A silently skipped gate is indistinguishable from a passing one, so a missing tool now aborts. - lib/common.sh: require_tool tries `sudo -n pacman -S` and aborts with the exact command when a tool cannot be installed non-interactively; verify_toolchain applies it to all eight binaries the hooks invoke. - The pre-push hook resolves git's stdin refs into --from-ref/--to-ref. Without a range, `pre-commit run --hook-stage pre-push` checks *staged* files, and nothing is staged at push time, so prettier would have received zero files and passed vacuously. - ensure_hooks_path repairs core.hooksPath drift on every hook run. - meta/scripts/install_hooks.sh covers the one case the hooks cannot self-heal — a fresh clone where the config was never set, so nothing runs. `--check` reports state and exits non-zero when incomplete. Note `pre-commit install` must never be used here: it writes into .git/hooks/, which git ignores entirely while core.hooksPath is set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| a47191673a |
feat(reverse_survivors): migrate Reverse Survivors game into the monorepo
Moves the standalone React 19 + Vite 8 + TypeScript 6 game from ~/Downloads/reverse/reverse-survivors into reverse_survivors/, so it is version-controlled, indexed, and covered by CI instead of sitting untracked outside any repository. - Relocates the project's nested .github/workflows/ci.yml to the repository root as .github/workflows/reverse-survivors-ci.yml. GitHub only reads workflows from the root, so the nested copy would have been inert — same treatment billsplit received. The workflow is path-scoped to reverse_survivors/** and runs npm ci / check / build on Node 22. - Sets cache-dependency-path on actions/setup-node, since defaults.run.working-directory applies only to run: steps and not to actions; without it the npm cache looks for a lockfile at the root. - Keeps npm and the existing package-lock.json. TypeScript stays pinned at 6.0.3 deliberately: typescript-eslint 8.65 caps its peer range at <6.1.0 and TS 7 drops the JS compiler API that typed linting needs. - Adds a reverse_survivors/ row to the Repository Layout table in README.md and CLAUDE.md. Verified in place: npm ci, npm run check (95 tests, 100% statements / branches / functions / lines), npm run build, and a dev-server smoke check serving /src/main.tsx. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 8e24ff093f |
Add boot-repair: offline recovery for a stale EFI System Partition
On 2026-07-30 a kernel upgrade left the machine in systemd emergency mode on three consecutive boots. The upgrade replaced /usr/lib/modules/<old> with <new>, but the new kernel never reached the ESP, so the firmware kept booting the OLD vmlinuz whose module tree had just been deleted. Nothing modular could load after pivot-root — including vfat, which meant /boot itself could not be mounted, local-fs.target failed, and systemd dropped to an emergency shell. pacman reported the transaction as successful. Root cause: pacman-git with ParallelDownloads > 1 segfaults every forked alpm hook child, silently skipping 90-mkinitcpio-install and depmod. boot-repair is self-contained (no repo libs — it must run in the emergency shell) and fully offline (missing modules are recovered from /var/cache/pacman/pkg). It repairs by default; --dry-run inspects. It detects and fixes: read-only root, noauto/nofail fstab drift, orphaned kernel files shadowing an unmounted ESP, unloadable vfat, unmounted ESP, missing modules.dep, ESP kernel != installed tree, stale initramfs, and ParallelDownloads > 1. Two pacman hooks make the failure impossible rather than merely recoverable: 05-boot-mounted-guard aborts a kernel transaction when the ESP is unmounted (AbortOnFail), and 99-boot-autorepair fixes ESP drift after the transaction, before the machine is ever rebooted. Safety: a module tree counts as usable only if it has BOTH vmlinuz and kernel/, so the metadata-only wreckage this bug leaves — which sorts newer — is never chosen as the target. Files under /boot are deleted only while it is not a mountpoint AND contains no EFI/ or loader/; deleting after mounting would destroy the real kernel. Verified: - 31/31 fixture tests (--root against throwaway trees, no privileges) - 11/11 reversible live tests on the real machine: unmounted the real ESP, planted shadow files, confirmed they were deleted and the ESP then mounted with EFI/, loader/ and kernel 7.1.5-arch1-2 intact - Gate proven against a real transaction with the ESP unmounted: "failed to commit transaction", no packages upgraded, nothing written - shellcheck + shfmt clean, pre-commit passing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 89ed9bc95d |
wsg_grabber: scrape /wsg/, download every new video, triage with keep/pass
New package. Walks every thread on 4chan's /wsg/, downloads each video it has
not seen, and reviews them one at a time: mpv embedded in a Tk window via --wid
with Keep/Pass/Undo directly beneath the picture.
Dedupe is structural rather than a convention. Every post carrying a file also
carries md5 -- base64 MD5 of the whole file -- so it is the sqlite primary key.
An already-seen video costs zero bytes to skip and a byte-identical repost in
another thread collapses onto the existing row. Verified against a live file:
the API's Nz9OEKdMuMZEdYE6eLTKmA== matched base64(md5(bytes)) exactly.
Undo is unbounded and survives quitting: the verdict trail lives in the index,
not in memory, so yesterday's decisions can still be taken back.
No downloaded video is ever deleted. A pass moves the file to trash/ and leaves
it there; files.apply_move refuses to overwrite an existing destination, so the
guarantee holds by construction rather than per caller.
Three bugs only running it could find, each caught from evidence rather than
guesswork:
- the video stayed black: a <Configure> handler set mpv's geometry, fighting
the automatic sizing --wid already does
- playback never advanced: refresh() ran every tick and re-sent loadfile ~7x
a second, restarting the video continuously
- it froze on one video around the 26th while filenames kept advancing: mpv
pushes an event stream down the same socket and nothing drained it, so the
buffer filled and mpv stopped reading commands. Isolated to command #35
without a drain thread; all 60 honoured with one.
A five-lens review then found nine more, all reproduced before fixing: a failed
mpv start stranded the non-daemon worker forever; undo could clobber a file in
incoming/; a NUL-bearing md5 poisoned the index permanently; an oversized int
killed the worker with no event; gzip bombs on both hosts were accepted; the
board endpoints were fetched unconditionally forever; and Session.missing could
corrupt the undo trail.
mpv runs as a subprocess over its JSON IPC socket rather than via python-mpv,
which resolves libmpv at import time and would fail on CI while passing here.
Third-party surface is requests alone, already declared.
277 package tests, 100% branch coverage on all 22 modules, no suppressions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 0fc3788d7a |
app_icons: home_inventory has a Linux desktop target
Its desktop app is a Chrome wrapper around the Flutter web build, and it ships a .desktop entry plus a hicolor icon theme -- so it genuinely has a Linux target. With linux=False, `generate --linux-out` skipped it silently and reported success, which meant desktop/icons/ in that repo could not be regenerated at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 45f4311993 |
app_icons: add home_inventory storage-box glyph; whitelist it on the phone
The new ~/home_inventory Flutter app needs two things from this repo: a family launcher icon, and a focus-mode whitelist entry so the daemon does not pm disable-user it ~1s after launch. The glyph is an isometric carton — a hexagon plus the three edges meeting at the near-top corner. A front-on rectangle with a lid seam was tried first and rejected on the contact sheet: a seam plus a divider reads as a browser window, not a box. Body is 488x440, not a rounder 500x440, because STROKE_WIDTH straddles the path and adds 72px to each rendered dimension; 488 is the widest body whose ink still fits SAFE_BOX. The generator warned about exactly this at 500. Accent stays the one shared family accent — apps differ by glyph, not colour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 9ab005f18f |
Point the hook test at the extracted session_autopsy, add the artifacts
The gate caught this: test_session_autopsy_hooks reads the DEPLOYED hook under
~/.claude, so repointing that hook to kuhyx/mcp-servers broke an assertion still
expecting `python_pkg.session_autopsy`. Exactly what a deployment-path test is
for — a test against a stale in-repo copy would have stayed green.
The replacement is stronger than what it replaces: it asserts both the new module
path AND that "python_pkg" is absent, so regressing to the old location fails
rather than passing quietly. The mutation checker confirms all 12 tests are still
killed by at least one mutation.
Also adds the evidence/contract artifacts for the extraction. They were written
for
|
|||
| db9c8921a2 |
kcd2 dice solver: lock deploy_build.sh so two runs cannot publish half a bundle
deploy_build.sh has two entry points — setup_kcd2_dice_solver.sh calls it
directly, and the post-commit hook starts a unit that runs it — and nothing
serialised them.
When they overlap, the second run's `vite build --emptyOutDir` truncates
dist.staging while the first run's rsync is still reading it. rsync exits 24,
`set -e` aborts before assert_mount_live, and dist/ is left holding a fresh
index.html that references assets which never arrived, with the previous
bundle's files already deleted by --delete-after.
Nothing downstream notices, which is what makes it worth fixing rather than
tolerating: `try_files {path} /index.html` answers the missing chunks with the
SPA HTML, so the browser is handed HTML where it expects JavaScript and no 404
reaches any log (verified against the live site: /assets/does-not-exist.js
returns 200 text/html). The hook's `|| true` then swallows the unit failure, so
the commit succeeds and the site is quietly broken.
Reproduced with a throttled rsync: rc=24, live left with index.html and two of
six chunks. With flock, two concurrent runs each publish a complete bundle.
Also tightened assert_mount_live, which announced success in three cases where
it had verified nothing: docker absent, container not running, and a failed
`docker exec` — the last because docker reports OCI runtime errors on stdout,
so `2>/dev/null` does not suppress them and the guard's non-empty test passed
on a 184-byte error string. It now requires an actual inode and says plainly
when the mount was not verified.
Found by the working-diff review these same changes had skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 9d7751cebb |
kcd2 dice solver: serve it at dice.kuhy.duckdns.org, fit all 43 dice, move inventories between devices
The solver only ran behind ./run.sh, had one media query in the whole
stylesheet, and kept the inventory in a 46vh scroll box that showed maybe a
third of the 43 dice. Setting it up on the PC did nothing for the phone.
Deployment
dice.kuhy.duckdns.org, behind the existing gitea-caddy edge. DuckDNS already
wildcards *.kuhy.duckdns.org (checked against 1.1.1.1 and 8.8.8.8), so no
dashboard step. deploy_build.sh builds to dist.staging and rsyncs into dist/
with --delete-after --delay-updates; plain --delete removes stale assets
before writing the new ones, which 404s a client mid-load. A post-commit hook
starts a MemorySwapMax=0 systemd user unit, so a rebuild can neither slow a
commit nor thrash zram.
Three traps found by driving the live URL in a browser rather than curling it:
a site address of "127.0.0.1:8089" reads as a HOSTNAME, so Caddy enabled
auto-HTTPS and started an HTTP->HTTPS redirect listener that bound *:80 next
to the edge -- SO_REUSEPORT would then split inbound HTTP and intermittently
break ACME for every domain here. The same address also made a Host matcher,
so the edge's forwarded Host matched nothing and served an empty 200 while the
local health check passed. And watching dist/'s own inode could never detect a
detached bind mount: rsync into an existing directory never changes it. Each
now has a guard that asserts positively rather than failing open.
Dice list
All 43 on screen at every width, no inner scrollbar. Column count and size
tier are chosen together from a container query on the panel -- not the
viewport, which mispredicts by 2x at the point the columns split. Two
invariants govern it: the list fits above the fold, and the name column stays
wide enough to tell Painter's die B, G and R apart. The left column holds the
grid alone, so it is sized to fill the height it owns: one --die-row value
drives the type, buttons and bar strip, giving a 52px row and 44px buttons on
a 1296px-tall screen and 20px rows on a phone.
Three safe-design rules are broken inside .die-list deliberately: sub-16px
body text, square icon buttons, sub-8px intra-cell gaps. 43 items with a name,
a chart and a counter in one viewport is arithmetically incompatible with 16px.
Import / export
A file, the clipboard, or a link carrying the inventory in the URL fragment.
One validator behind all four entry points including localStorage; it keeps
what it can and reports what it dropped, so a die added in a later game patch
costs you that entry, not the inventory. A link never silently overwrites what
is saved here -- it offers Keep or Discard, and only a link that actually
carries dice is applied at all, because one encoding a count of zero parsed
cleanly, blanked the screen, disabled Clear and left Keep writing the emptiness
over the visitor's own loadout.
session_autopsy
parse.py (551) and report.py (504) were over the repo's 500-line limit, so
test_file_length has been failing on main since
|
|||
| 5ffd035c21 |
session-autopsy: measure turn efficiency, the dominant token cost
Cache read is charged per API round-trip on the whole conversation so far, so a long session's cost is driven by TURN COUNT, not message length. One measured session spent 267M cache-read against 856k output — a ratio of 290:1. The report priced tokens but never counted the turns spending them. Across the 263 stored transcripts this now shows 33,508 tool turns at an 8% batching rate, of which 4,437 (13%) decided nothing: polling a counter, running a linter, checking whether a push landed. At 606k average context re-read per turn that is ~2.7B cache-read tokens on turns a script could have taken. - turns.py classifies a command as poll / lint_test / vcs_check / substantive, deliberately conservative: state-changing commands (git commit, push) count as substantive even though they are scriptable, so the reported waste is a floor. A turn is only mechanical when EVERY command in it is. - Batching is grouped by requestId, not by transcript line: one API response can span several lines, and counting per line reports every turn as single-tool (0% batching where the real rate is 12%). - records.py gains TurnEfficiency (schema v3, additive — old records read as zeros) with derived rates written into to_dict so readers need not recompute. - report.py gains a "Turn efficiency" section pricing each mechanical kind. - _TurnTracker owns both the totals and the open turn, after pylint flagged _Accumulator over its attribute budget; _Accumulator.turns is now a property, so every existing caller is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 071963b92b |
Fix: stop the Flutter SDK reading your own repo as itself inside git hooks
`git commit`/`git push` export GIT_DIR to every hook they run, and the
flutter tool shells out to git to identify its own SDK. `git -C
"$FLUTTER_ROOT" rev-parse HEAD` in bin/internal/shared.sh does not beat an
inherited GIT_DIR -- `-C` only changes directory, GIT_DIR overrides
repository discovery outright:
$ git -C /opt/flutter rev-parse HEAD
058e0af2c2... <- Flutter's HEAD
$ GIT_DIR=~/diet-guard/.git git -C /opt/flutter rev-parse HEAD
84bd550331... <- diet-guard's HEAD
So inside a hook, flutter reports the project as the SDK:
Flutter 3.47.0-0.1.pre • channel main • https://github.com/kuhyx/diet-guard
pub then resolves the SDK as 0.0.0-unknown and every version constraint
becomes unsatisfiable. flutter also writes that into its own version cache,
so the wrong answer outlives the hook and follows the next caller into an
unrelated repository -- which is why the identical command passes when you
re-run it by hand and fails again on the next push.
diet-guard's pre-push gate hit this today. Clearing the variables per call in
each hook works, and screen-locker and diet-guard now do, but only for as
long as every future hook author remembers -- deployment hygiene, not a fix.
This installs a wrapper ahead of /usr/bin on PATH that strips them for every
caller in every repo, covering both flutter and dart. It removes its own
directory from PATH before exec so the Flutter entry point's internal `which
flutter` cannot loop back into it, and it verifies itself by running flutter
against a throwaway poisoned GIT_DIR and failing unless the real SDK comes
back.
Not patching bin/internal/shared.sh: that file belongs to the flutter-bin
package, so the next update would silently restore the bug. Upstream already
unsets these variables in content_aware_hash.sh and update_engine_version.sh,
just not in the version path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| d91c0ab0e9 |
fix(i3): repair the dead idle-inhibitor autostart and make it a singleton
The i3 `exec` line pointed at
/home/kuchy/linux-configuration/scripts/utils/turn_off_auto_idle_screen_shutdown.sh
- a typo'd home ("kuchy") *and* a path that stopped existing when the repo
was reorganised in
|
|||
| 5ab9a806eb |
session_autopsy: handled candidates leave the nudge; drop explain-diff
Per user verdicts: the ⚙ statusline glyph told them nothing (removed on the ~/.claude side; the plain-words SessionStart line is the channel) and explain-diff is not wanted at all (skill deleted there, verdict 'dropped'). The count now means 'significant AND not yet handled' — resolving a candidate visibly shrinks it (7 -> 4), and handled table rows stop re-suggesting a review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| a07405c05f |
session_autopsy: badge counts only significant candidates; zero-token aliases
The ⚙ statusline badge counted all 182 candidates — meaningless. It now counts candidates ≥100k est weekly tokens (7 today): a number small enough to mean 'worth your time'. token-savers.zsh gives the compiled scripts shell entry points (finish-gate / phone-deploy / autopsy / yay-clean) so the deterministic halves cost zero LLM tokens. Adds the first explanation doc produced by the newly compiled explain-diff scaffold. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 7f706adb11 |
Make the hook tests' non-vacuity re-verifiable, and gate it
|
|||
| 63a5124920 |
session_autopsy: test the hooks, the untested half of the analyzer
|
|||
| a83b5abc27 |
python_pkg: track session_autopsy, finishing its interrupted refactor
The transcript analyzer behind /compile-candidate lived only in the working tree. Untracked meant unlinted — pre-commit only inspects files handed to it — so nothing had ever checked it, and it had been abandoned mid-refactor with every parse raising AttributeError: '_Accumulator' object has no attribute 'first_timestamp'. The accumulator had been moved onto the grouped records dataclasses, but three increment sites and parse_session still read flat attributes that no longer existed. parse_session now hands over acc.meta and acc.counts directly; only subagent_count comes from elsewhere, since no transcript line carries it. Two modules broke the 500-line limit, so they were split along seams that were already there. signatures.py takes the text-signature helpers — traces.py was importing them from parse.py, making parse an accidental hub, and now neither imports the other. stats.py takes the pure number and time helpers. parse.py 521 -> 458, detectors.py 559 -> 467. The structural lint findings were refactored, not configured away. _counter_candidates took eight arguments; the five naming which counter field to mine and at what thresholds became a frozen _CounterSpec, so both call sites are one line. _extract_skill carried in_span and a line budget as loop variables across thirteen branches; a _SpanBuffer owns that state now, leaving the walk at four. record() took twelve flat kwargs and now takes one override per record block, with session_meta() so a partial meta override keeps the standard timestamp — a bare SessionMeta(slug=...) would have nulled started_at and quietly changed the per-week arithmetic. All 36 call sites rewritten by an AST pass. tests/factories.py moved into conftest.py, where every other package here keeps shared builders and where name-tests-test does not object. 965 tests pass at 100% statement and branch coverage, every hook is green, and no suppression was added anywhere. `candidates` and `traces skill-finish` both run against the real corpus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 11ff8e6a26 |
kcd2_dice_solver: Balatro's joker replaces the one, confirmed
The mapping was marked UNVERIFIED because no published source states which pip the grinning face covers. The player, who owns the game, confirmed it is the one — the same face the Devil's head die replaces. So the die carries no plain single pip at all, which is exactly why picking the joker alone counts as a 1. Comment and README only; wildcardFaces was already [1]. The uniform side weights stay flagged as the one remaining assumption, since nothing publishes them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 1b0a8dff46 |
python_pkg: the file-length test was blind to its own CI venv
It walked the working tree and skipped vendored trees by hard-coded directory name. `.venv` was listed; `.ci-mirror-venv` — created inside the repo by the pre-push CI mirror — was not, so the walk descended into it and died with a UnicodeDecodeError on a joblib fixture that is deliberately encoded big5. A crash, not a report: every real violation was hidden behind it. Venvs are now found by their pyvenv.cfg marker, so any venv is pruned whatever it is named, and the walk prunes in place instead of visiting thousands of third-party files to filter them afterwards. Lines are counted from bytes, so a line count cannot depend on an encoding. third_party/ joins the vendored skip set — it holds upstream MCP servers, which this repo's 500-line rule never governed. Two regression tests come with it: a venv under an unlisted name holding an oversized, undecodable module is pruned, and the line counter handles big5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 4aae92ae09 |
kcd2_dice_solver: Balatro's die has one joker face, not six
The wiki's dice-effect row for Balatro's die is `{{Dice effect|d|d|d|d|d|d}}`
— six *unfilled placeholder* cells that render as "d%". A previous pass read
those six `d`s as six wildcard faces, which made the die resolve every throw in
the player's favour: EV 8000, bust 0%, and the solver forced it into every
loadout it could. Only one face is the joker (Jimbo's grinning face, visible on
the item icon), and no source publishes the die's side weights at all.
The two joker faces also do not score alike, and the scorer was treating them as
one thing. Per the wiki, a Devil's head "matches any combination but never
scores on its own", while Balatro's joker counts as a 1 when picked alone. So
there are now two substitute slots: a Devil's head is spent inside the
combination it completes rather than resolved to a plain face, which is what
stops it being held as a lone 1. Hands with no substitute keep the original
single-pass code path.
Balatro's die: EV 8000 -> 771.2, bust 0% -> 3.09%; a lone devil's head scores
100 -> 0 and is no longer a legal hold. The two dice now tie exactly on a single
throw of six and differ only mid-turn (813.7 vs 808.3 points per turn), which is
where "may be held alone" actually pays.
Which pip the joker replaces is unpublished; the 1 is assumed, matching the
Devil's head die the game compares it to, and marked UNVERIFIED in the data.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 4f2c82fee7 |
Add overnight power-save to night-lockdown; harden dnsmasq with a watchdog
Caps CPU (amd-pstate-epp) and GPU (nvidia-smi power limit) during the existing night-lockdown window instead of migrating services to a Raspberry Pi (tried, abandoned after SSH auth failed). A more aggressive GPU PCI-unbind option is built but left disabled by default. Also fixes an unrelated live incident found while testing: dnsmasq (this PC's LAN DHCP/DNS server) had died with zero error logged, breaking WiFi for every device on the network. Added a watchdog timer mirroring the existing shutdown-timer-monitor-watchdog pattern, and whitelisted the Tapo smart plug app so the overnight power reading is checkable during the phone's curfew window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| a7596d0442 |
bucket_catch: introduce a real design-token system, fix reversed gradients
Fixes every violation in DESIGN_AUDIT_TODO.md's bucket_catch frontend section. The app previously had no shared tokens at all (index.css was a 12-line reset) and 19 hardcoded hex colors duplicated independently across 6 *.module.css files. New :root token block in index.css: bg gradient stops, a drag-state gradient, the indigo/pink accent pair collapsed to one canonical --gradient-accent direction, text/surface colors, success/error, 2 consolidated muted-gray roles (down from 4 unrelated shades), a 4px spacing scale, and a radius scale. All 6 component files migrated onto it. Fixes the real reversed-gradient-stop bug the audit named (PuzzleResult.module.css .grade was pink->indigo vs every other heading's indigo->pink) plus a second instance of the same bug the audit's prose didn't cite by line number (ModeSelect.module.css .puzzleStart button) -- both now reference --gradient-accent so this class of bug can't recur. Also fixes: .preset's border contrast (was under 3:1), cardDesc's sub-16px font-size, 4 buttons' off-ratio padding (now exact 2x), 2 large icons' full-contrast rendering next to muted text (now dimmed), the explicitly-cited off-grid spacing values, and ModeSelect's gridInput background color which had drifted from its DropZone sibling's. Verified: 145/145 tests at 100% coverage (CSS-only, confirms no functional regression), a real vite build succeeds, re-verified identical in an isolated worktree at HEAD, and pixel-checked the production build via a headless-Chrome screenshot (gradient direction now matches between heading and button; preset chip borders clearly visible). `npm run lint` (tsc -b + eslint) was already broken on a clean checkout before this change (1 pre-existing tsc error in vite.config.ts, 46 pre-existing eslint errors in untouched test/lib files) -- confirmed via an isolated worktree at the parent commit, left alone as out of scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 27456c431a |
chore: fix prettier formatting on the kcd2 evidence JSON
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 517fbe5d3b |
kcd2_dice_solver: warm-tint the unified-design-system palette, fix design audit violations
Fixes every violation in DESIGN_AUDIT_TODO.md's kcd2_dice_solver section: 15px root font-size (root cause of both the sub-16px body-text floor and the off-grid rem spacing scale), 2 hardcoded tier colors bypassing the custom-property system (also separates their brightness from --accent/ --good, and removes an accidental cool tint from tier-silver), the largest text widening tracking instead of tightening it, near-invisible border contrast (~1.4:1 -> >=3:1 against both bg and panel), inconsistent ad-hoc border-radii collapsed into a 2-step scale with nested elements using the smaller radius, and two off-ratio button paddings corrected to an exact 2x. Verified: lint clean, 210/210 tests at 100% coverage, production build, re-verified identical in an isolated worktree at HEAD (narrowly staged around this monorepo's unrelated dirty state), and pixel-checked via a real `vite preview` production build through a headless-Chrome screenshot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 7f2c1e19df |
app_icons: auto-sync pubspec.yaml's adaptive_icon_background
style.BACKGROUND changing doesn't help if flutter_launcher_icons reads a stale value from pubspec.yaml's own independent config -- that's exactly what just happened across all 6 apps during the warm-palette rollout: the generated PNG assets were correct, but Android's actual home-screen icon (the adaptive icon: background color + foreground drawable, not the legacy ic_launcher.png mipmap) stayed on the old color, invisible until a physical device check caught it. sync_pubspec_background() now rewrites the line in place before every run_flutter_launcher_icons() call, so this can't silently drift again. Verified: 43/43 tests (4 new), 100% line+branch coverage on generate.py. |
|||
| f1320e1f7e |
app_icons: warm-tint the shared icon family palette
Charcoal field #1B1D21 -> #211D1B, accent #4C7DF0 -> #B8862E across all 6 apps in the family, matching the unified-design-system palette rework already applied to diet-guard/dufs-cloud/screen-locker/gatelock. Both are exact byte-reversed mirrors of the old values, so relative lightness is unchanged -- only the tint shifts from blue toward red/brown. Also fixes stale prose in README.md left over from an earlier accent- unification pass (claimed each app had its own accent; they've shared one since that pass). Verified: 39/39 tests, preview contact sheet reviewed, all 6 repos' assets regenerated (diet-guard's rebuilt APK pixel-verified on device). |
|||
| 88dabb08b2 |
Apply prettier formatting to new kcd2_dice_solver files
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 3babda92d2 |
Track kcd2_dice_solver app and add run.sh dependency installer
The Vite/React/TS dice solver had no git history yet. Commit it alongside a run.sh that ensures Node/pnpm are present, installs deps, and starts the dev server with a single command. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| ae458d92cf |
app_icons: register habit_stack with a new chain-link glyph; whitelist it
New Flutter app (~/habit_stack) gets an on-style launcher icon from the shared family: two interlocking stadium-ring outlines, sized so the 72px stroke leaves a genuinely hollow interior at launcher size. Accent matches the app's own ThemeData seedColor (#66BB6A), not already claimed by a sibling app. Also whitelists com.kuhy.habit_stack in phone_focus_mode/config.sh (both the always-on productive-apps list and the night/curfew health-tool list) and redeploys, since an unwhitelisted com.kuhy.* app is killed within ~1s and would otherwise look like an install failure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 5d4d3b9e24 |
night-lockdown: stop firing the morning alarm on a fixed hour
The unlock script started morning-routine.service on every staggered trigger (05:00/05:15/05:30/06:00/07:00) on alarm days. That was a workaround for the always-on lockdown removing the hibernate/resume event the alarm used to ride on — reasonable when nothing else could fire it. Something else fires it now. wake-alarm-trigger.timer ticks every minute and starts morning-routine.service at the time the phone synced, with a catch-up window for a suspended or just-booted machine. Keeping both meant two independent triggers, one of which ignores the phone entirely. That is not hypothetical: on 2026-07-20 the phone was set to 08:00 and the alarm went off at 07:00, started here, with no wake-alarm-trigger "Firing" line in the journal at all. The 08:00 fire then landed as a no-op because this 07:00 run had left morning-routine.service still active. Drop maybe_start_morning_alarm() and both call sites, plus the config that went dead with it (ALARM_DAYS_DOW, MORNING_ROUTINE_UNIT, the alarm_started_date marker). MIN_WAKE_AFTER_LOCKDOWN_HOURS stays — the wake-floor trigger uses it. The lockdown's actual job — console/GUI restore, unmute, unmasking the login surface — is untouched. The only starters of morning-routine.service left are wake-alarm-trigger.timer (primary) and the system-sleep hook (resume backstop). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| 5faed23977 |
security-hardening: recognise the guard-lib shutdown guard
Test 10 only knew the retired shutdown-schedule-guard.path, so it reported a skip while guard-file@shutdown-schedule.path was actively protecting /etc/shutdown-schedule.conf. That understated the real protection in the same way tests 2, 3 and 5 did before the last commit. It now accepts the guard-lib unit and passes. The disabled-is-intentional branch stays for hosts running neither unit. Host: 19 passed / 0 failed / 5 skipped. Container: exit 0. |
|||
| 2d4331c703 |
security-hardening: check guard-lib units, not the retired ones
The suite reported four failures on this host and none of them meant protection was missing. Tests 2, 3 and 5 checked hosts-guard.path, hosts-bind-mount.service and nsswitch-guard.path. The 2026-07-17 guard-lib refactor replaced those with templated units — guard-file@hosts.path, guard-bind-mount@hosts.service, guard-file@nsswitch.path — which are all active. That is also why the legacy units stop a second after boot: they are superseded, and the old nsswitch guard retriggered itself into systemd's start limit fighting the new watcher. Each check now accepts either generation, and the bind-mount check also accepts the read-only mount on /etc/hosts, which is the real invariant. Test 10 now treats a *disabled* shutdown-schedule-guard as intended rather than broken: the poweroff flow it protected was replaced by night-lockdown-enter.sh. An enabled-but-inactive unit still fails. That check also had a shell bug — `systemctl is-enabled` prints the state and exits non-zero when disabled, so `|| echo disabled` appended a second line and never matched. Verified protection is genuinely intact rather than assuming it: /etc/hosts is mounted ro,noatime and both it and /etc/nsswitch.conf carry the immutable flag. Host now reports 18/0/6. A container with faked host markers still reports 10 failures, so the suite has not been softened into always-green. No system state was changed. |
|||
| ee740bf92e |
Run all 14 shell tests in CI
The last three exclusions are gone. test_i3blocks_efficiency.sh only ever needed strace and ptrace. It went unverified earlier because Docker's bridge network has no DNS on this machine, so pacman could not install strace; --network=host works, and the test passes. The job installs strace and adds SYS_PTRACE. test_hosts_guard_pacman_integration.sh asserted three symbols the guard-lib refactor renamed (pacman_hooks_manage_hosts_guard -> pacman_hooks_manage_guard_lib, and similar). The feature was intact; only the assertions were stale. test_security_hardening.sh now separates live-host checks from repository checks. Off-host the live ones skip, so it is green in a container while still running its repository checks — and on the real host it still fails loudly when a guard unit is down. That last part needed care: the first detection attempt used `systemctl is-system-running`, which exits non-zero on a degraded system — precisely the state a failed guard unit produces. It masked all four real failures. Detection now tests for /run/systemd/system plus the installed guard payload, which unit health does not affect. Verified: all 8 arch tests pass in a container; the same security-hardening file still reports 4 failures on this host. |
|||
| 896d7b1d15 |
Run Arch-only shell tests in a container; harden worktree stubbing
Adds an arch-tests job on archlinux:base-devel so the pacman wrapper, makepkg wrapper and hosts-guard tests finally run somewhere. They need an Arch userland and root, which a disposable container provides. Four more tests still referenced pre-reorganisation paths and could never find their targets; repairing them took the container set from two passing to five. All five were verified with --network=none, so the job installs nothing beyond git (base-devel ships no git, and actions/checkout needs it). Also removes the silent-failure mode behind the earlier music_parallelism confusion. The test now derives its temp-worktree path from the script's real position instead of hardcoding the depth, checks up front that ../../lib/common.sh resolves to the stub, and has the stub write a marker asserted after every run. Previously a reorganisation made the script source the real /usr/local/lib/common.sh, so the test measured the live system and failed as though the daemon were broken. Three tests stay out, each for a different reason, documented in the workflow: test_i3blocks_efficiency.sh needs strace and could not be verified locally (containers here have no DNS); test_hosts_guard_pacman_integration.sh fails a real assertion worth investigating; test_security_hardening.sh asserts live host state and can never pass in a fresh container. |
|||
| e7c3300e80 |
Repair two stale shell tests; CI now runs all six
Both tests excluded from CI last commit turned out to be stale rather than catching regressions. Neither covered script needed changing. test_music_parallelism.sh built its temp worktree at scripts/digital_wellbeing/, but music_parallelism.sh resolves its library as $SCRIPT_DIR/../../lib/common.sh — two levels up, correct for the real scripts/periodic_background/digital_wellbeing/ location. From the shallower path that resolved outside the worktree, so the stubbed common.sh was never sourced and the script fell through to the real /usr/local/lib/common.sh on the machine. The test was exercising the live library and ignoring its own MOCK_FOCUS_ACTIVE, which is why idle mode looked like it waited 15s instead of 30s. The daemon was correct all along. test_usage_monitoring_installer_efficiency.sh demanded a literal 'sleep 60'. The logger template no longer polls: it sleeps once until midnight via seconds_until_next_day, then blocks on wait for the pmon child — strictly fewer wakeups than the assertion required. Replaced with assertions on that property so a return to polling still fails. Allowlist extended from four tests to six. |
|||
| 93a466c8e4 |
Run shell tests in CI and repair three broken by a reorg
The shell tests under linux_configuration/tests/ never ran anywhere: no workflow executed them, so they rotted silently. Three referenced script paths that moved under scripts/periodic_background/ in an earlier reorganisation, and test_android_guardian_service.sh created its temp worktree at a different path from the one it copied into, so its cp could never succeed. The new workflow runs the bats suites plus an explicit allowlist of side-effect-free tests. Roughly eight others drive pacman, /etc, systemd or adb and need root plus an Arch container, so they stay out. Repairing the paths uncovered two genuine failures, deliberately left out of the allowlist because they look like real regressions in the scripts they cover rather than stale assertions: - test_music_parallelism.sh expects idle mode to wait IDLE_CHECK_INTERVAL (30s); the daemon waits 15s, so it wakes twice as often as intended when idle. - test_usage_monitoring_installer_efficiency.sh expects the logger template to sleep between day-rollover checks. Both need a fix in the covered scripts, not in the tests. |