mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:47:02 +01:00
262ef38efe
238 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 262ef38efe |
Split wsg_grabber/net.py under the 250-line cap
net.py spoke HTTP for two very different things. The media transfer moves out: _download.py Transfer, download, _stream, _over_budget, _write_body _reject.py classify() -- status line and headers to an Outcome net.py keeps build_session and get_json and re-exports the transfer API through __all__, so downloader.py and test_net.py keep using net.download / net.Transfer / net.digest_of / net.size_of untouched. digest_of had to move down into _download rather than stay put: _stream needs it, and importing it back up from net was a real runtime cycle. net re-exports it. _rejection became classify because it is now imported across a module boundary. Note for the next split: ruff's F401 autofix stripped 11 dead imports here in one pass. __all__ is what stops it doing the same to a re-export. net.py 335 -> 109, _download.py 205, _reject.py 96. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 62ad1484a6 |
Split wsg_grabber/review.py under the 250-line cap
review.py held three separable concerns. Two move out:
_render.py emptiness, status_line, filename_line, title_line, render --
pure state-to-text, no toolkit and no transitions
_undo.py can_undo, on_undo, forget_last -- correcting the tally and
putting the on-screen video back at the front of the queue
review.py keeps the state transitions and re-exports all eight names through
__all__, so ui.py and test_review.py reach them at review.* exactly as before;
verified by resolving every name in __all__.
Both new modules reference SessionState under TYPE_CHECKING only, so there is
no runtime cycle. Also inlined _worker_message, a single-caller passthrough
that just returned event.message.
review.py 330 -> 196, _render.py 107, _undo.py 74.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
|
|||
| 8ce453690f |
Split wsg_grabber/cli.py under the 250-line cap
The `logs` subcommand's reporting -- show_logs() and summarise() -- is read-only analysis of the JSONL session logs and shares nothing with the scrape/review flow, so it moves to _logs_report.py. cli.py re-exports both through __all__ so cli.show_logs / cli.summarise stay valid for test_cli.py and the dispatch table. The PEP 484 `X as X` alias form does NOT survive here: ruff flags it PLC0414, autofixes it away, and then deletes the import as unused under F401 -- which would break cli.summarise with lint still green. Recorded in the handoff so it is not re-derived. cli.py 282 -> 247, new _logs_report.py 76. 277 wsg_grabber tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 77c6d9f815 |
Split wsg_grabber/catalog.py under the 250-line cap
catalog.py was 251 lines. Its five defensive accessors for the loosely-typed /wsg/ JSON -- _get, _as_list, _as_int, _as_md5 and the _MD5_SHAPE / int64 range constants they depend on -- form a cohesive unit distinct from the parsing logic above them, so they move to a sibling _coerce.py and become module-public there. No caller or test referenced the private helpers (downloader.py uses only the six public parse functions), so no re-export shim was needed and the public API is byte-for-byte unchanged. catalog.py 251 -> 175, new _coerce.py 87. Both modules stay at 100% branch coverage; 277 wsg_grabber tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 73b85f5326 |
Split poker-stakes' four oversized files under the 250-line cap
useGame.test.tsx (494) splits by describe-block into reducer/session/debt files; handEval.test.ts (269) splits at the evaluate7 boundary; useGame.ts (268) sheds its state and reducer into gameState.ts; handFlow.ts (252) sheds its betting-state helpers into handFlowBetting.ts. useGame.ts and handFlow.ts re-export what they moved, so no importer changed. The shared test helpers live in src/test/ rather than beside the source they exercise. That is load-bearing: vite.config.ts already excludes src/test/ from coverage, so no new exclusion entry was needed. Putting them next to the source first dropped coverage to 98.57% because v8 counted the fixtures' unused branches as production code -- fixed by moving them, not by excluding them. Verified: 220 tests still pass, coverage back to 100% on statements, branches, functions and lines, and npm run build succeeds. poker-stakes violations 4 -> 0; repo total 196 -> 192. invariants.test.ts hit its 5s timeout once mid-work. It passes standalone in 2.56s and on re-run, so it is a pre-existing contention flake under parallel coverage load rather than a regression here -- noted, not papered over. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 72a5e28456 |
Track poker-stakes so the file-length cap can reach it
The directory has existed on disk with 58 untracked files, four of them over the 250-line cap. An untracked file is invisible to every pre-commit hook, so those violations could not be fixed or gated while it stayed that way -- and its CI workflow was untracked too, so nothing ran against it. Landed with its already-written evidence artifact and a contract, since this crosses the 4-file threshold. Verified before staging: 58 plain text source/config files, no binaries, no node_modules, no build output. npm run check passes -- typecheck, eslint --max-warnings 0, and 220 tests at 100% statements/branches/functions/lines. Splitting its four oversized files is deliberately a separate commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 004b4b0b29 |
Cut CLAUDE.md 259 -> 141 lines: delete what hooks already enforce, fix what was wrong
This file loads into context on every turn of every session, so every line is
paid for repeatedly. Two thirds of it either duplicated a gate that already
fails the commit, or stated something untrue.
Deleted because a hook enforces it: the ruff/mypy/pylint/coverage settings,
"double-quote expansions" (shellcheck runs with no severity filter), absolute
imports, Google docstrings, type hints, and the inline-Python rule -- which
got a real gate in
|
|||
| 17b20e5fae |
Gate the no-inline-Python rule instead of asking people to remember it
CLAUDE.md's longest and most emphatic code rule -- never embed Python program logic in a shell script -- was enforced by nothing. A repo-wide scan found it had already been violated: install_plagiarism_tools.sh ran a 6-line NLTK download as an inline `python3 -c`, invisible to ruff, mypy, pylint, bandit and pytest, exactly the failure mode the rule exists to prevent. check_no_inline_python.sh flags heredocs and multi-line `python -c` while permitting the single-line availability probes the rule explicitly allows, so the ~250 shell files here produce zero false positives. Commented-out examples are skipped; otherwise the rule's own documentation would fail the hook. The extracted download_nltk_data.py also fixes a real defect: the inline block ignored nltk.download()'s return value, so a partial corpus download reported success. It now returns non-zero and the caller's `set -e` sees it. nltk is imported via importlib inside main(), not at module scope: it belongs to the plagiarism toolchain, not this repo, so it is absent from requirements.txt and unresolvable to pylint as a static import. No suppressions anywhere: T201 avoided with sys.stderr.write, INP001 with a shebang (matching sibling fast_count.py), E0401 with the dynamic import. pylint 10.00/10, ruff clean, mypy clean. Verified the gate fires rather than passing vacuously -- a staged canary with a real heredoc exits non-zero -- and that the extracted script runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY |
|||
| 95a1f62fc4 |
artgate: add 64px icon spec for the diffusion route
The diffusion route generates at SDXL's native 1024px, so a 32px icon is a 32x downscale. Detail that survives the generator does not survive that reduction -- a coin's struck emblem, a shield's radial pattern and a lantern's glass panes all mush at 32px and read clearly at 64px. max_colors is deliberately NOT scaled with the canvas. Measured over the 300 genuine 48x48 icons in ~/opengameart-downloads: p50=9 p90=13 max=14. A cap of 32 rejects 0.00% of that art and so does every larger cap, so hand-made art does not spend more colours on a bigger canvas. Raising it would only admit more un-quantized generator output. Gates 18/18: the 12 original bake-off subjects re-finished at 64px, plus six subjects the route had never seen (lantern, axe, boots, helmet, torch, mushroom). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF |
|||
| a0db432933 |
artgate: render 3D route with Standard view transform, not AgX
Blender defaults to the AgX view transform, a photoreal film emulation that rolls off highlights and desaturates. On the flat-lit item renders the 3D route produces, it turned gold into beige and washed out every material -- which is why row C read as unusable in the bake-off. Measured on an identical coin scene where only view_transform differed: mean saturation 0.282 (AgX) vs 0.787 (Standard), a 2.8x increase. This makes row C a fair test, not a good one: the geometry is still low-poly primitives and ring/bone remain nearly invisible at 32px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF |
|||
| e6e0ba5ab3 |
artgate: diffusion + 3D routes, full vector coverage, and the margin gate
Closes the remaining bake-off cells. The phase's real result was not a route
score but a MISSING GATE.
All twelve SDXL icons measured 100% opaque coverage -- square tiles with no
sprite silhouette -- and passed every gate in the harness. Gate-pass was
measuring pipeline hygiene, not usability. MARGIN_TOO_SMALL inverts the table
from a meaningless 12/12/12 tie to:
diffusion 0/12 (every output full-bleed)
vector 10/12
procedural 11/12
The two non-diffusion failures were true positives in MY shapes, not gate
error: the procedural sword pommel ran off the bottom edge and the vector meat
bone clipped the top. Both fixed; both routes back to 12/12.
Diffusion recovers to 12/12 with flood-fill background removal, but visual
inspection shows the fill eats into the art (bone reduced to a fragment, sword
nearly erased). Reported as inadequate rather than shipped -- proper matting is
needed before route (b) is a usable pipeline.
Blender exits 0 even when its embedded script raises: BLENDER_EEVEE_NEXT does
not exist in 5.2, and the failure produced a clean exit and no file. render()
now asserts the output exists, or the route would report phantom successes.
Model gating verified at execution time, never recalled: SDXL base and
nerijs/pixel-art-xl are ungated, FLUX.1-dev is gated=auto, and
runwayml/stable-diffusion-v1-5 now 307-redirects.
The finishing stage now generalises across three continuous-tone sources
(SVG raster, Blender render, SDXL): each is 0/N raw and N/N finished.
Verified: 114 tests, 100% branch coverage on 13 modules, zero suppressions,
all hooks pass. Note: a local pytest run masked a missing branch that the
pre-commit hook caught -- the hook's run is authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
|
|||
| 691a228499 |
artgate: procedural + vector routes, and the shared finishing stage
Closes the two CPU-only cells of the art bake-off and builds the bridge the diffusion cell will reuse. Pins the bake-off to the SAME twelve subjects item-icons measured, with its easy/hard split intact. Without this a route tested on distinctive silhouettes beats the 6/12 baseline for free and the decision table means nothing. It also makes the subject-routing rule falsifiable: a generator route is predicted to lift specifically the six human-prior failures, and if it does not, the rule is recorded as wrong. Route (a) procedural: all twelve subjects drawn deterministically from a 7-colour locked palette. Every shape gets a distinguishing feature up front, because item-icons measured that plain primitives read as placeholders however correct the geometry is. Machine gate: 12/12 -- which proves "not malformed", NOT quality. A procedural generator satisfies palette/alpha/canvas constraints trivially; the human contact sheet is the real verdict. Route (d) code-as-art: SVG emitted as text and rasterised by rsvg-convert, so no new Python dependency (cairosvg/svgwrite stay absent). The structural finding, measured on real rasteriser output rather than a synthetic stand-in: raw vector renders pass 0/5 -- every one fails ALPHA_NOT_BINARY, most also TOO_MANY_COLORS. After the finishing stage, 5/5. Continuous-tone routes are therefore GENERATORS FEEDING A FINISHER, not pixel-art generators. finish.py binarises alpha before quantising colour, mirroring the gate order: quantising first blends transparent pixels' arbitrary RGB into the palette. Verified: 92 tests, 100% branch coverage across all ten modules, zero suppressions, all hooks pass. The PLR2004 lint refactor was confirmed pixel-neutral by byte-comparing all 12 icons before and after (12/12 identical). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF |
|||
| da8ca6d827 |
artgate: deterministic exit-code gates for generated game art
Adds the route-independent spine of the planned art-generation bake-off: a harness that decides whether generated art is well-formed, so "non-sloppy" becomes machine-checkable instead of a judgement call. Findings that shaped the design, all measured rather than assumed: - Integer-scale invariance separates true pixel art (impurity 0.0000) from interpolated fakes (1.0000) exactly -- but passes trivially at scale 1, so it must never be trusted alone on native-resolution output. - Gate ORDER is load-bearing. A soft-alpha image has no fully-opaque pixels, so an opaque-only colour count returns 0 and silently passes. ALPHA_NOT_BINARY now runs first and COLOR_COUNT fails closed on an empty opaque set. - Counting must be over opaque RGB only: transparent pixels retain arbitrary RGB and inflated a naive RGBA count, rejecting correct art at 18/16. - Declaring a gate was not enough to run it. hand_painted.toml shipped declaring SILHOUETTE_LOW_CONTRAST with no implementation, so it parsed, ran nothing, and reported a pass. load_spec now fails closed on unimplemented gates, result-only codes, and gates missing their config. Thresholds are calibrated against 250 genuine 32x32 CC0 icons, not guessed: p50=7 p90=13 p99=26 opaque colours; cap 32 rejects 0.0% of real art while raw generator output measured 34403 colours. An initial cap of 16 wrongly rejected a real DCSS icon -- re-derived from the corpus rather than nudged. Verified: 57 tests, 100% branch coverage, zero suppressions, all hooks pass. 58/60 (96.7%) of real CC0 icons pass the full stack; both failures confirmed true positives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF |
|||
| 54054e7cf9 |
billsplit: use the shared SectionHeader instead of the local copy
`design_system`'s `SectionHeader` was built as the union of untools' `_SectionHeading` and billsplit's `_SectionHeader` -- its `defaultPadding` is `fromLTRB(md, md, md, xs)` specifically to reproduce billsplit's `(16, 16, 16, 4)`. The shared widget shipped and untools' donor was deleted, but billsplit's never was: the file has imported `package:design_system/design_system.dart` since the migration while still defining and using its own copy. That is a duplicate shipped rather than removed, which is the failure the extraction phases exist to prevent. Verified equivalent rather than assumed: padding `(16,16,16,4)`, glyph origin `(16,16)`, line height 24.0 and type size 16.0 are all identical to the retired widget. The one difference is that the shared version wraps the title in `Row`/`Expanded` to leave room for its optional trailing action, so the `Text` box fills the row instead of shrinking to content -- invisible for a left-aligned title with no action, but it does change hit-test width, so it is worth stating rather than calling a pure no-op. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno |
|||
| 40d2c96a77 |
billsplit: adopt the shared palette, retire the teal seed
Last of the four drifted apps. The theme was ColorScheme.fromSeed(teal) plus fifteen ad hoc colour literals that mostly bypassed it -- not one widget outside main.dart read colorScheme at all, so the seed barely affected what you saw. It now uses design_system-v0.1.0's buildLightTheme(), and the literals that have a real theme role read it off the scheme. Light, not dark, deliberately: the receipt screens carry black-on-white helper text, so inverting brightness would make it unreadable. The Colors.black54 sites are exactly those, now onSurfaceVariant. The error family maps cleanly -- Colors.red to error, red.shade50 to errorContainer with onErrorContainer text on it, and the swipe-to-delete background+icon to error/onError so the fill and the glyph drawn on it cannot drift apart. The four category dots (deepOrange/amber/blueGrey/teal) stay literals, with a comment saying why: they encode which category an item is, not a theme role, and a single-accent system has no four distinct hues to lend them. Mapping them onto primary/secondary/tertiary would make the categories stop being distinguishable, which is their only job. They are tracked in nielsen-audit.md as needing a categorical ramp. Note this decouples the default dot from the old seed -- both were Colors.teal, so they used to match by coincidence rather than by design. Verified: 60/60 tests, 100% line coverage across 11 lib files, analyze clean apart from four pre-existing unnecessary_unawaited infos. A temporary assertion pumped the real app and read primary=ffb8862e and surface=fff6f4f3 off a live Scaffold, confirming the palette changed rather than merely compiling. Colour literals: 15 -> 4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL |
|||
| 01778d4f05 |
Put focus_owner on the shared palette, and stop curfew hiding todo
focus_owner had drifted entirely off the frozen palette -- a cool blue #5B9DD9 accent on a #1B1D21 field -- while its own doc comment claimed it matched the other com.kuhy.* apps. It did not: every other app is on the gold #B8862E. lib/theme.dart now aliases design_system-v0.1.0, so the ~80 call sites keep reading the same names but the values come from one place and cannot drift again. The six constants duplicated verbatim in main.dart are deleted -- the old comment said they had been "lifted out" into theme.dart, but the originals were never removed, so there were two copies of a wrong palette. Separately: dev.kuhy.todo was in the day allowlist but not the night one, so a release build installed at 23:07 was hidden by the enforcer ~80ms later. It is now in night_allowed_packages. That is a deliberate loosening of the curfew's "answer the phone, reach a bank, handle an emergency" rule -- writing an idea down at night is the one thing that app does -- and it is recorded as such in both config files. Deploying the shell mirror of that list surfaced two pre-existing latent bugs in phone_focus_mode/config.sh: comment lines inside the double-quoted WHITELIST string referenced the night list (defined ~70 lines below) and an ALWAYS_BLOCKED that is never defined anywhere, so under set -u the entire deploy aborted with "unbound variable". Both are now written plainly, with a note saying why a dollar sign or a double quote must never appear inside that string. Verified on the Pixel 6a: analyze clean, 94/94 tests, colour literals 15 -> 2 (both Colors.transparent sentinels), and an ad-hoc assertion confirming kAccent is really ffb8862e rather than merely compiling. The installed APK shows CURFEW and the action links in gold, "Allowed apps 29 (16 at night)" (was 15) and "restored 1"; todo then launched and rendered at 23:19, inside the curfew window, with all 108 notes intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL |
|||
| f7e8c59e8f |
Add token_audit package: per-session token attribution and reporting
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 82e206116a |
Make the geofence tell the truth, and the phone explain itself
The enforcer failed in both directions: nothing blocked at home, and everything blocked at the office. The comparison was never inverted -- `metres <= threshold` was always right. Three separate causes: Only ACCESS_COARSE_LOCATION was declared. Android 12+ fuzzes coarse to a ~1-2 km grid against a 150 m fence, so a fix taken at home routinely landed outside it, read as AWAY, and restored every app the geofence exists to hide. FINE is now declared and self-granted by the device owner, along with coarse and background, re-asserted every pass so a reinstall cannot silently drop it. There were three states but two behaviours. LOCATION_UNKNOWN applies the identical sweep to AT_HOME -- correct fail-closed design, indistinguishable from outside. And nothing ever requested a fix: the pass read whatever another app happened to cache, so at the office with no navigation running the cache was empty. Passes now acquire a fix inside a location-typed foreground service, bounded at 20 s, and treat anything older than 30 min as no fix. That window is deliberately longer than the 15-minute cadence; a shorter one marks the newest cache stale on every pass of an idle phone and blocks everything. None of it was diagnosable. `adb logcat -d -s FocusOwner:V` came back empty while dumpsys showed 82 alarms had fired, and `run-as` is refused on the release build device owner requires. So the pass now writes a ring-buffered JSONL record read back in-app: reason, distance vs the fence, fix age/provider/accuracy, curfew state, and every hidden app with the rule that hid it. Coordinates are never recorded -- a distance is enough to debug a geofence, and this is a home address. Also fixed along the way: - Hysteresis keyed off `isEnforcing`, which is true under AWAY too because YouTube stays hidden, so the threshold was permanently radius+hysteresis. It now keys off whether the previous pass was inside the fence. - The scheduler's curfew-boundary alarms were documented but never scheduled; both callers invoked `scheduleNext()` bare, so 05:00 could land 15 min late. - Installing over the app cancelled its pending alarms, so `adb install -r` silently stopped enforcement until someone opened the app. MY_PACKAGE_REPLACED now re-arms it, verified on device without launching the app. - A wrong-typed log record threw instead of being skipped, and the exception escaped far enough to leave the page stuck busy -- which disables the release-device-owner button, the one control that must work when everything else has gone wrong. - `lib/enforcement.dart` was a second decision layer with no always-blocked concept, cited by KDoc as pinned by a `PolicyParityTest` that never existed. It would have made the new screen report YouTube visible while it is hidden. Deleted; the UI renders the Kotlin record rather than re-deciding. - Anchoring home accepted a stale or unknown-precision fix. A wrong home offsets the fence everywhere and forever, which is what produced a 766 m reading while standing at home. Policy: Play Store is allowed by day (not during curfew) so infakt can be installed and updated; the sweep is default-deny for third-party packages, so anything installed from Play is hidden on the next at-home pass. Tachiyomi's per-source extension apks are covered by a new prefix allowlist rather than an exact list that goes stale whenever an extension is installed. `export.py` stopped subtracting the allowlist from `blockable_system_packages`. Sweepable is not hideable: that list decides which system packages are eligible for a decision, and `isAllowed` is what protects them. Removing an allowed system package from the sweep freezes it in its last state -- it would have stranded Play hidden permanently the moment it was allowlisted. Verified on the Pixel 6a: FINE granted POLICY_FIXED, `applied AT_HOME` with a 0 m distance from a +/-11 m GPS fix, YouTube/Chrome/Firefox absent from `pm list packages -e`, Play Store, infakt, RunnerUp and all five Tachiyomi packages present. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjxL9kdAzZmGqQA5tMNrZ8 |
|||
| 47371ac9a1 |
Wire uBlock Origin into every real Chromium launch, alongside LeechBlock
install_ublock.sh patches the same wrapper install_leechblock.sh already targets, always rewriting --load-extension into a single comma-joined flag rather than appending a second occurrence — verified empirically that Chromium is last-wins across separate --load-extension flags and would otherwise silently drop LeechBlock. seed_ublock_storage.js previously hardcoded uBlock Origin's Chrome Web Store extension ID, but --load-extension (unpacked) loads get a synthetic, install-path-derived ID instead. It now reads the real ID back out of the profile's own Preferences file, with a --ext-path override for installers whose package path differs from Arch's. install_ungoogled_chromium_ubuntu.sh's UBLOCK_EXT_PATH was an unverified guess (/usr/share/webext/ublock-origin-chromium); corrected to the real Debian/Ubuntu package path confirmed via packages.ubuntu.com. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yfDYbaw1D1WhVMqWhsSDA |
|||
| 0c28f9988f |
Fix pacman whitelist matching for yay's -U package-file installs
is_blocked_package_name compared raw pacman -U <file>.pkg.tar.zst arguments against whitelist entries, which never matched since yay always installs AUR packages this way. Added strip_pkgfile_suffix() to derive the bare package name first, plus a regression test. Also swaps the browser whitelist entries to ungoogled-chromium (Chromium 151 dropped MV2, breaking uBlock Origin's full/classic build). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yfDYbaw1D1WhVMqWhsSDA |
|||
| a97ad66e1f |
Whitelist RunnerUp day and night, so it can always sync
RunnerUp needs to stay enabled after a run to export the TCX file screen-locker verifies against; being disabled by focus mode at home silently blocked that. It's straightforwardly beneficial, so treat it like the other always-available apps: present in both WHITELIST and NIGHT_WHITELIST. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SH7XBkpEqQX4KghHdgx9T |
|||
| 2d4d2a2eb2 |
Block by hiding apps, which the phone cannot undo
The network layer was the wrong place for this. Tested remotely today: opening RethinkDNS and switching off its device blocklists un-blocked YouTube in a few taps, and no device owner API can stop that -- an on-device filter app always owns its own rules. App hiding is different. 'pm unhide' from adb shell throws a SecurityException and the package stays hidden; only the device owner can reverse it, and reaching that costs a factory reset. So the block moves there, scoped to an explicit allowlist and the home geofence. Both whitelists are rewritten from the apps that actually exist on this phone. The old ones described the rooted Blackview. Every browser is excluded now, so at home a link does not open at all -- confirmed as intended. LAUNCHER_PACKAGE was still the Blackview's launcher, which is not installed here. The exporter refused to render the policy twice over it -- precisely what that check is for. The network layer stays a soft deterrent by choice: pinned and uninstall-blocked, but without lockdown, and its blocklists remain switchable. Private DNS is implemented but ships disabled. stunnel logged zero connections from the handset across two pin attempts, so the router does not forward 853. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| 62862c01ab |
Close the two holes that made the network block advisory
Both were measured, not theorised. `pm uninstall --user 0 com.celzero.bravedns` succeeded while the VPN was pinned and DISALLOW_CONFIG_VPN was set -- I ran it to check whether the provider was protected, and it removed the app, took the DNS filter with it, and left the always-on pin aimed at a package that no longer existed. The second hole was quieter: without lockdown, stopping the tunnel let traffic flow normally, so the filter only held while it was running. So the provider now gets setUninstallBlocked, re-asserted every pass, and the pin runs with lockdown. Lockdown is a real hazard and is deliberate: if the VPN app breaks, the device has no connectivity until ownership is released. That is only survivable because the release path lives in the enforcer app and needs no network. releaseDeviceOwner reads the pinned package before unpinning it, then unblocks its uninstall. Reversed, getAlwaysOnVpnPackage returns null after the unpin and the provider would stay unremovable forever -- the same trap the self-uninstall ordering already avoids. Home coordinates are provisioned. The pass now logs "applied AT_HOME" rather than LOCATION_UNKNOWN, so enforcement is finally geofenced rather than permanent-everywhere, driven by a real fix. `pm uninstall --user 0` on the provider now returns DELETE_FAILED_OWNER_BLOCKED. What this does NOT fix: RethinkDNS's blocklists are gone with the uninstall, since they live in app-private storage. The filter is running with no rules and youtube.com resolves normally until they are re-entered. And nothing here stops those rules being switched off from inside the app -- no device owner API can. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| 6836a32b43 |
Let the app set its own home, and lock VPN configuration
Two gaps left from the device owner work. The geofence had no way to be provisioned. push_home_location.sh stages the file through `run-as`, which returns "package not debuggable" on the release build device owner requires, and making the build debuggable to fix that would hand anyone with adb the ability to edit this app's state -- the bypass the whole design exists to close. So the app writes its own filesDir instead, from a button. Deliberately not an exported intent: anything that can set home from outside the app can set it somewhere else, and the geofence would then report AWAY forever with enforcement off. The coordinates are never logged either, since that is a home address and logcat is readable by anyone with adb. DISALLOW_CONFIG_VPN is now applied from the app, after the filter was confirmed working rather than before -- locking VPN configuration over a broken setup would pin the broken state in place. The open question was whether it also freezes RethinkDNS's own settings; it does not. Its blocklist selection was changed under the restriction without trouble, which is the answer that matters, because the alternative meant every domain rule had to be final before locking. The status page gained a Geofence row. "no home set - enforcing everywhere" is a large behavioural difference from a working geofence and was previously visible only by inspecting logcat. The widget tests caught a real bug rather than a test-only one: the two new buttons overflowed the column by 4px, and the control that must never be clipped off the bottom of the screen is "Release device owner". The body scrolls now. Verified after a reboot: youtube hidden, all 14 youtube domains at 127.0.0.1, google/gmail/play resolving normally, no_config_vpn still enforced, uninstall still refused. The geofence itself stays untested on hardware until the phone has a GPS fix to record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| db1d8f8247 |
Pin RethinkDNS as always-on VPN, the layer that reaches every app
Hiding the YouTube package stops the app. It does nothing about youtube.com in Firefox, in a webview, or in any client nobody has enumerated. This is that layer. setAlwaysOnVpnPackage from the device owner is not the mechanism that failed on 2026-08-09 -- that was `settings put secure always_on_vpn_app`, which writes successfully, survives a reboot, and is ignored by VpnManager. The DPM call takes effect: dumpsys now reports mAlwaysOnVpnPackage=com.celzero.bravedns. Lockdown stays off. It drops all traffic whenever the tunnel is down, and the wipe checklist names it together with the DISALLOW_* restrictions as the combination most likely to leave an unrecoverable device. DISALLOW_CONFIG_VPN is deferred for the same reason: locking VPN configuration before the filter is confirmed working would pin a broken setup in place. The exporter emits always_on_vpn_package only when the provider is also in never_disable_prefixes, and RethinkDNS was added there. Pinning a package the sweep can hide is the worst case -- the restriction would point at a hidden app and cost general connectivity, not just the filter. The pin is re-asserted every pass and, unlike the uninstall block, does not track the decision: the network filter must not lift when the geofence says AWAY. releaseDeviceOwner now unpins the VPN and clears the restriction before clearing ownership, for the same reason it already unblocked uninstall. Not yet active, and the commit does not pretend otherwise: the tunnel needs a one-time in-app VPN consent tap that adb cannot grant, and RethinkDNS is at first-run state because its wildcard rules live in app-private storage that the backup never reached. They have to be re-entered by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| b1de6f8d38 |
Take the geofence out of YouTube's reach, and pin infakt on
Leaving the house was an off switch. The AWAY branch returned early with packagesToShow = every installed package, so walking 150m from home restored YouTube -- which is the exact affordance device owner was provisioned to remove. always_blocked_packages is exempt from all four branches: AWAY hides them, the allowlist loop cannot show them, and the workout release skips them, because otherwise "go for a run" is the off switch instead. Chrome is included as a second route to the same content; Firefox stays, and carries the uBlock filters. Play is deliberately left geofenced so apps can still be installed away from home -- and a hidden package cannot be reinstalled from Play anyway. infakt moves from the allowlist to SYSTEM_NEVER_DISABLE. Allowlisting only held while it stayed in BOTH the day and night lists, and dropping it from either would silently make it hideable; isAllowed() consults the never-disable list before the curfew split, so it now holds under every condition. It is device-paired to a bank over SMS, so losing it strands the same re-authentication chain a hidden Messages app would. The exporter now refuses an always-blocked package that is not also blockable: the runner filters unopted system packages out of installedPackages, so such an entry would never be hidden and nothing would say so. Verified: 128 Python tests at 100% branch coverage, 16 Kotlin tests, the new ones canary-checked by breaking an assertion and confirming the suite actually fails. On device, youtube/music/chrome/vending are hidden=true, infakt hidden=false, uninstall still refused. The AWAY path itself is unit-tested but NOT verified on hardware: with no home_location.json every pass decides LOCATION_UNKNOWN, so the branch is unreachable on this phone until coordinates can be provisioned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| 6bf7343c0f |
Make the enforcer real: block YouTube, and refuse to be uninstalled
The device owner is provisioned, so enforcement stops being theoretical. Three things had to be true before it was safe to switch on. Release signing. A device owner can only be replaced by an APK with the same signature, and `dpm remove-active-admin` does not work on one, so a debug-signed device owner could never be updated -- the only exit would be another factory reset. focus_owner now uses the shared key.properties config the other apps use. No minify: an untested ProGuard pass that strips a DevicePolicyManager callback produces a device owner that cannot release itself. Uninstall blocking. Device owner hides Settings' uninstall button, but `adb uninstall` still worked, and that is the route that strands ownership with no holder. setSelfUninstallBlocked closes it, re-asserted every pass so it self-heals. The ordering in releaseDeviceOwner is not stylistic: unblock, THEN clear. Reversed, the block outlives the ownership required to lift it. The allowlist. config.sh describes the rooted phone, so the hide set was computed on the PC before any pass ran -- which caught pl.infakt.infakt in it. infakt is device-paired to a bank over SMS, so hiding it would have stranded the same re-authentication chain as hiding Messages. Verified on the Pixel 6a as a real device owner: hid 27, and youtube, youtube.music, chrome, vending and videos are all hidden=true, while phone, messages, keepass, mbank, revolut, infakt, mobywatel, settings and signal stay hidden=false. `adb uninstall` returns DELETE_FAILED_DEVICE_POLICY_MANAGER. All of it survives a reboot, which is exactly what `pm suspend` and `pm disable-user` failed to do here. restore_apps.sh reinstalls the 54 backed-up packages via install-multiple, since split APKs make a bare `adb install` fail. It skips the device owner's own package by name. Enforcement is permanent for now, not geofenced: push_home_location.sh delivers coordinates through `run-as`, which is dead on a release build, so every pass decides LOCATION_UNKNOWN and fails closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| eeaf9590da |
Let the sweep hide preinstalled apps, one named package at a time
The Device Owner sweep skipped every FLAG_SYSTEM package, which meant it could not touch the apps it exists to block: YouTube ships at /product/app/YouTube as a system app, and so does Chrome. Deleting the filter is not the fix. Measured on the Pixel 6a, that would expose 320 system packages of which 243 match no allowlist entry and no never_disable_prefix -- including com.android.cellbroadcastreceiver (emergency alerts), com.android.credentialmanager and com.android.devicelockcontroller. So system apps stay default-deny and blockable_system_packages is the opt-in. com.android.vending is on the list deliberately: leaving Play reachable makes every other removal a one-tap undo. The list lives in the Python exporter rather than config.sh because it is a safety boundary, and a shell typo should not be able to widen it. The exporter subtracts anything already allowed, so an asset can never state that a package is both allowed and blockable. Kotlin reads the field with a new optionalStringSet: an asset predating the field parses to the empty set instead of throwing, because a throw makes decide() skip the pass and silently disable enforcement. Verified on-device under a profile owner: hid 1 for YouTube, restored 1 when temporarily allowed, and cellbroadcastreceiver, credentialmanager, devicelockcontroller and settings all left hidden=false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| 553c395cff |
inFakt is SMS-only 2FA, so the wipe cannot strand the account
This was the last blocker on the Device Owner decision. Checked in the
web panel: "Aplikacja weryfikacyjna" is unconfigured, so no TOTP seed
exists for a wipe to destroy, and only "Wrazliwe" actions are gated --
"Logowanie" is unchecked, so plain login needs the password alone. The
registered SMS number lives on the SIM, which survives.
The browser-login approval tap is a Firebase push prompt
(FirebaseInstanceIdReceiver on the device). Push tokens are per-install,
so a reinstall re-registers; and if that ever misbehaved, SMS is still
the fallback.
Corrects the wipe checklist, which assumed TOTP secrets were "likely"
inside the KeePassDX database and treated the .kdbx as a seed backup.
Neither holds: the canonical .kdbx is on the PC, and there are no seeds.
Earlier advice in this session to fetch inFakt backup codes was likewise
for the OTP-app variant this account does not use.
What still does not survive is unchanged and unrelated to 2FA: the
banking pairing ("Usun powiazanie z telefonem") and the local app PIN,
re-paired via SMS like mBank.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxNrcMDcizNB3XrrYpZF23
|
|||
| 7b1a1656f7 |
Back up what adb can actually reach, and block YouTube in Firefox too
phone_backup.sh captures package lists (including what was purged for user 0), every third-party APK with its splits, /sdcard, all three settings namespaces, and the focus-mode state. Verified: 54/54 packages, 126 APK files, 5.1 GB, splits intact. It is deliberately not sold as a full-device backup, because on an unrooted phone it cannot be one: /data/data is unreachable, so logins, in-app data and device pairings do not survive. It writes a RESTORE-BY-HAND.md naming each of those, with a restore order that avoids a lockout -- mObywatel last, because activating it on a new device deactivates the old one. The first two runs pulled 1 of 54 APKs and then 6 of 54: `while read < file` with adb inside the loop lets adb drain the same stdin. Reading into an array first fixes it, and verify_backup now compares package count against the manifest rather than only checking that the files it did pull are valid zips -- integrity alone called the 1-of-54 run a pass. Also documents the uBlock Origin filters, which block YouTube in Firefox independently of RethinkDNS: force-stopping the VPN still leaves youtube.com blocked by ||youtube.com^. And records what this is worth -- Play offers a one-tap Install for YouTube, the VPN is a few taps from off, and the uBlock page has a Proceed button. Three independent layers, none of them tamper-proof; only Device Owner removes the off switch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JxNrcMDcizNB3XrrYpZF23 |
|||
| 10fff1d1a8 |
Block YouTube on the unrooted Pixel, and trust more than one phone
`pm uninstall --user 0` unregisters a package for one user while leaving the APK on the system image. It needs no root and no factory reset, and unlike `pm suspend` and `pm disable-user` -- both measured to be cleared by a reboot on this device -- it survives one. Applied to the YouTube app, YouTube Music, and Chrome; `--restore` puts any of them back with no download. focus_owner could not have done this: its sweep filters to third-party packages, and YouTube ships at /product/app/YouTube with FLAG_SYSTEM, so the Device Owner path would skip it even after the wipe. Verified on 23181JEGR08034 across two reboots: all three read installed=false, and in Firefox m.youtube.com, www.youtube.com, a /watch URL and a youtu.be short link all fail to connect while example.com loads. The web half of that is RethinkDNS domain rules, which live on the device and are not in this commit; docs/youtube-block-unrooted.md records how to rebuild them, and what this does NOT resist -- the VPN is a few taps from off, which only Device Owner can prevent. The trusted-device record becomes one file per device, so managing a second phone no longer evicts the first. Unenrolled devices still abort, and a changed model or build fingerprint aborts rather than falling through to the next record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JxNrcMDcizNB3XrrYpZF23 |
|||
| b73811c233 |
Add android_ui: drive Android apps by element, never by coordinates
Shared across every repo with an Android surface (screen-locker's workout_app, todo, dufs-cloud/app). Works against a plain release APK on a physical device over adb — no root, no emulator, no Flutter debug connection, no app changes. Driving an app by tapping pixel positions read off a screenshot fails in ways that look like success. All observed in one session (2026-08-10) verifying workout_app's Firebase restore: * A tap aimed at a password field landed with the field unfocused. The typed password went nowhere and NOTHING errored — the next screenshot just showed an empty box. type_into/type_into_field now re-read the field and raise. * Opening the soft keyboard moved a field from y=1572 to y=1319, so a coordinate captured one step earlier was already wrong. Every action re-resolves the element immediately before acting. * A widget behind the keyboard reports its LAID-OUT position (1549) not its real one (1939) — a 390px error that taps a letter key and reports success. keyboard_is_up() distinguishes "absent" from "behind the keyboard", and tap closes the keyboard first. * KEYCODE_BACK is a route pop in Flutter, not a keyboard dismiss: it navigated out of Settings and discarded typed credentials, twice. dismiss_keyboard tries ESCAPE then BACK and verifies via dumpsys input_method. * `input text` inserts at the cursor, so typing over a filled field produced "canary@example.com321krzychu@gmail.com". Fields are cleared first. * uiautomator returns ONLY the focused EditText nodes while a Flutter text field has focus, and stays that way across retries — so a naive retry loop never converges. Partial trees are retried and the best result kept. * An EMPTY EditText has no text, content-desc or resource-id, so a "must be labelled" filter drops the very field you need to fill in. Editable nodes are kept regardless and addressable by position. Ambiguity is an error rather than a silent first-match: `tap "Back"` correctly refuses when "OFFLINE BACKUP" is also on screen. Its own test suite caught a real bug in _escape: backslash sat inside the escape loop, so "&" became "\\&" and a password containing shell metacharacters would have been typed wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JupKvZCNKRmEeRQY3G1unG |
|||
| 590bc2004d |
ci: install jq for the shell tests, and probe for it explicitly
The claude_usage i3blocks tests failed on CI while passing locally. archlinux:base-devel ships no jq, and claude_usage.sh parses its cache with it, so the block degraded to its "no data" path and every percentage assertion failed. That degradation is correct at runtime — the bar should not break because a tool is missing — but it made a missing dependency look like a logic bug. Install jq alongside git and strace, and probe for it at the top of the section so the failure names its own cause. The probe runs jq rather than calling `command -v`: a jq that exists but cannot execute produces exactly the same "no data" output as an absent one, and `command -v` passes in that case — verified by stubbing a jq that exits 127. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
|||
| 91061edc14 |
feedback: local-only pacman package build, and ignore build artifacts
fee[dB]ack has no AUR entry, so install_feedback.sh builds a local package from the PKGBUILD tracked beside it, the same way hypersomnia-appimage and jackify-bin are installed here. The script only builds and then prints the `sudo pacman -U` line to run, since interactive sudo prompts belong to the user rather than an agent. The built .pkg.tar.zst is ~640 MB, so it is gitignored along with the Bottles prefixes and the archwiki_rag vector index — all three are local runtime state that is regenerable and must never enter the history (see memories/binary-files.md). The PKGBUILD and the installer are the source and stay tracked. Exclude PKGBUILDs from shellcheck. A PKGBUILD is a makepkg manifest, not a standalone script: it has no shebang by design, pkgname/pkgver/ source/sha512sums are read by makepkg, and pkgdir/srcdir are injected by it, so shellcheck flags every one as unused or undefined. The same reasoning already excludes zsh — the dialect is outside what shellcheck can judge — and the alternative was a directive on nearly every line. Also drops three stray files: a saved search-results page, a 0-byte file from a mistyped redirect, and a June i3 config backup superseded by git. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
|||
| 86565bdef6 |
archwiki_rag: offline semantic search over the Arch Wiki
Indexes the arch-wiki-docs dump that is already on disk and exposes it
to the knowledge-rag MCP server, so Arch questions can be asked by
symptom ("why doesn't my systemd user unit see DISPLAY under i3")
rather than by page title, with no network round-trip at query time.
The one Arch-specific MCP server in the wild fetches pages over the
network and bundles pacman operations that duplicate the local yay-mcp.
This does neither: it only searches, against a local corpus.
install.sh installs its own dependencies, wires the MCP config and
registers a pacman hook so the index follows package updates. The
generated index is machine-specific and regenerable, so it is
gitignored rather than committed.
_reindex_entry runs under the knowledge-rag interpreter, where
mcp_server is importable and this repo's packages are not. Resolving
that import with importlib.import_module states the deferral outright
instead of a function-level import statement that reads as an oversight,
and the backend failure path now re-raises as SystemExit, so the
exit-code translation is explicit rather than a swallowed exception.
Both replace lint suppressions; the package carries none.
pylint runs in its own venv, so beautifulsoup4 and markdownify are
declared there too — being in meta/requirements.txt does not make them
resolvable to the hook.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
|
|||
| cef72fbab0 |
syncyomi_guard: refuse a collapsed payload as the new good copy
TachiyomiSY 1.13.2 can report a restore as SUCCESS while writing almost nothing, then sync that near-empty library up and replace the only good copy. On 2026-08-09 a 14 MB payload holding 2182 manga became a 267 KB stub, and the real library survived only as stale pages in the SQLite write-ahead log. The upstream bug is Kotlin in an APK and cannot be fixed here, so this targets the fixable half: decode the payload, count what is actually in it, and refuse to treat a collapse as normal drift. A pure is-it-zero check would have caught this incident but not the harder case upstream describes, where a restore keeps some entries and drops the rest, so the threshold is proportional. Categories get their own rule because all 2182 manga survived the round trip and all 18 categories did not. Adds the setup script and a runbook covering the WAL-page salvage used to recover the library on the day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
|||
| f33fd03a16 |
focus_owner: keep the unhide path reachable, never hide the enforcer
setApplicationHidden drops a package from the default PackageManager enumeration even though it stays installed=true, so a sweep querying with flags 0 could never see anything it had already hidden. That made packagesToShow permanently empty: measured on device, a pass following one that hid three apps reported "hid 0, restored 0" and left them hidden with no way back. Query with MATCH_UNINSTALLED_PACKAGES instead. Hiding the enforcer itself is the other unrecoverable case, since it takes the escape hatch and the trigger with it. The exporter now injects com.kuhy.focus_owner into both the day and night allowlists so the decision layer cannot propose it, and the runner keeps its point-of-use skip to cover a policy asset rendered before this fix or hand-edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
|||
| 58c1df573c |
i3blocks: add Claude usage block, fix icon rendering, drop dead blocks
Add a [claude_usage] block showing the 5h and 7d rate-limit windows. It
reads ~/.claude/cache/limit-state/*.json, which Claude Code's statusLine
hook already writes to disk, so the data costs nothing to display: the
script only runs jq against a local file and makes no API call. Limits
are account-wide while the cache is sharded one file per project, so it
takes the newest file and ignores cwd. Percentages arrive as floats and
are truncated; a stale cache is labelled rather than shown as current,
and a rolled-over window reads "?%" instead of a misleading value.
Replace Font Awesome private-use-area icons with plain Unicode. Both
fonts named in the bar's pango string ("System San Francisco Display",
"FontAwesome") are not installed, so PUA codepoints fell through to
whatever font claimed them: ethernet rendered as a star, wifi as
Cyrillic, gpu as Hangul, disk as a chess pawn. Emoji resolve through
fontconfig consistently, which the pre-existing volume block already
relied on, and need no change to ~/.config/i3/config.
Remove the bluetooth, battery, wifi, activitywatch, warp and
network_monitor blocks: this desktop has no BT adapter, no battery, no
wireless interface, and no warp-cli. This also removes a rendering bug,
since activitywatch_status.sh and warp_status.sh were interval=persist
yet emitted three lines per update, leaking their colour line into the
bar as literal "#FF0000" text.
Fix ethernet reporting "down" on a live wired link:
find_ethernet_interface returned the first non-loopback interface, which
with docker installed is a br-* bridge (permanently down, and sorted
before enp*). It now requires a real /sys/class/net/*/device and prefers
an interface that is up.
Tests drop the assertions and fixtures for the removed blocks and add
claude_usage coverage (fresh, warn, critical, stale, rolled-over, null
reset, unparsable, missing dir), a guard that removed blocks cannot
return, a repo-wide check that no block script uses a PUA codepoint, and
an ethernet physical-NIC regression check. The fork budget is asserted
against a populated cache directory, which caught a stat call inside the
scan loop that cost one fork per cached project (170 -> 2 execs).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
|
|||
| 5d0caa9725 |
reverse_survivors: difficulty tiers
Haunting, Normal and Crusade scale enemy hp and speed, the survivor's health, your income, and the length of the night. Normal is exactly 1.0 on every multiplier — it is the identity tier, which is what lets every existing raw-value assertion keep passing untouched. Enemy speed now derives from a single helper, so difficulty and frenzy compose in one place rather than fighting at three call sites. Also fixes a bug this surfaced: handleRestart ignored the selected tier, so restarting after choosing Crusade silently reverted to Normal. The react-hooks purity rule flagged Date.now() in the new handler, and extracting restartSeed() exposed the omission. Endless mode is deliberately left out — fmtTime(Infinity) renders Infinity:NaN, which would cost a guard plus tests for a mode whose only content is the same game but longer. README rewritten for all four features, including the reversal of its old placement promise. 160 tests, 100% coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFyXexNjMQG9V7K8de17fc |
|||
| 2d66bfdd61 |
reverse_survivors: director powers — ambush, frenzy, rift
Ambush drops a unit beside the survivor, inside the ring its kiting AI would otherwise keep clear. Frenzy drives the whole horde faster and harder for six seconds. Rift pins every spawn to one edge for ten, so you can mass a flank instead of scattering. This reverses the README's old promise that placement is never yours to choose. The interesting decision turned out to be what a placement is worth, not being denied one — so placement is purchasable, never free: a premium for Ambush, a cooldown and a commitment window for Rift. A shared spendPower gate covers cooldown and affordability for all three, so the next power costs no new branches. EDGES became a Record keyed by a finite union rather than an array: under noUncheckedIndexedAccess an array index is possibly-undefined and would force an unreachable fallback. The unpinned spawn path still draws the same RNG values in the same order, verified against the old implementation across 50 seeds — existing seeded replays are unchanged. 145 tests, 100% coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFyXexNjMQG9V7K8de17fc |
|||
| 43b63ac266 |
reverse_survivors: survivor status effects (slow, suppress, bleed)
The warden mires, the artillery stifles, the leech unknits. All three need contact, so a survivor that kites cleanly takes none of them — debuffs punish positional failure rather than taxing existence. isOn/factor are written as arithmetic rather than comparisons: Math.min(1, Math.ceil(t)) is 1 while a timer is live and 0 once it expires, so consumers multiply instead of branching. The whole status module therefore contributes zero branches, which is what makes the 100% gate affordable for a system this cross-cutting. Statuses are applied unconditionally too; a 0-duration spec is a provable no-op. applyStatus uses Math.max, so re-applying refreshes rather than stacks. That caps a horde's total lockdown at one duration by construction — a structural guarantee, not a tuned number. 123 tests, 100% coverage. Branches 117 -> 119: the draw halo gate and the snapshot chip filter. The status logic itself adds none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFyXexNjMQG9V7K8de17fc |
|||
| f0712d3854 |
reverse_survivors: add splitter, artillery, warden and the leech boss
Splitter dies into three rushers, so killing it creates pressure rather than relieving it. Artillery outranges the survivor (460 vs its 260) and holds station beyond reach, the first unit its kiting AI cannot answer. Warden outruns it. Leech is a third boss. Two new EnemySpec fields carry this: spawnKind generalizes the hivemind's hardcoded 'rusher' minion, and splitCount drives on-death splitting. split() is called unconditionally and is a no-op at splitCount 0 — a guard branch would need two tests to satisfy the 100% gate and buys nothing. Child offsets are fixed rather than drawn from the RNG, so seeded replays are unchanged. 105 tests, 100% coverage. Branch count stays at 117: this adds none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFyXexNjMQG9V7K8de17fc |
|||
| b0c7baf22e |
Log why Chromium browsers die; whitelist librewolf
Thorium was "closing and dying randomly" with no evidence anywhere.
Two independent structural causes, both now fixed:
1. Crashpad was never enabled -- no crash_reporting consent key and an
empty crash DB, so no minidump was ever written.
2. stderr from a dmenu/.desktop launch reaches no terminal and no
journal. The only Thorium lines ever seen in the journal came from a
systemd-launched instance, which is why they survived.
browser-preexec-wrapper.sh now gives every Chromium-family launch its own
log under $XDG_STATE_HOME/browser-crash-logs: a VRAM/RAM/kernel/driver
snapshot before start, the browser's stderr, and on exit the exit code,
decoded signal, and a SECOND VRAM snapshot. Those two snapshots plus the
signal are the discriminator between the live hypotheses -- a VRAM death
shows NVRM/NV_ERR_NO_MEMORY or near-full VRAM at exit, a stale-binary
death shows SIGSEGV with the GPU idle. Root cause is deliberately NOT
asserted here; the next crash decides it from evidence.
Three details that are load-bearing rather than incidental:
- The exit record is written from a signal trap, not inline after
`wait`. If the wrapper itself is killed, an inline block never runs
and the log ends mid-stream with no verdict -- indistinguishable
from a browser that is still running.
- The browser is a direct redirect, NOT a pipe into a size filter: in
a pipeline `wait` reports the last element's status, discarding the
browser's signal, which is the one fact worth capturing.
- Logs are capped by bytes as well as count. Measured: ~2 MB per 45s
of navigation (65% VERBOSE1 net/TLS chatter), ~0 while idle. Count
alone would not bound disk use, and a full disk causes exactly the
random deaths under investigation.
setup_periodic_system.sh: rewrite Chromium "app mode" .desktop entries
(Exec=/opt/<browser>/...) to go through /usr/local/bin. These are
auto-generated and bypass the wrapper entirely, so neither the flags file
nor the logging applied to them -- five Thorium entries were found still
launching without the --disable-gpu-compositing mitigation added on
2026-07-25, two weeks after the fact. Runs on every install because
Chromium regenerates the files.
pacman_whitelist.txt: allow the exact package name `librewolf`. The
`librewolf` keyword stays blocked, as do all other Firefox forks. It is
LeechBlock-enforced via /etc/librewolf/policies (force_installed), so
this is not an unfiltered escape hatch; it is needed because Chromium's
MV2 sunset ends full uBlock Origin with all filter lists, which the Gecko
build keeps.
Verified: shellcheck clean, no lint suppressions; fake-browser SIGSEGV
and wrapper-SIGTERM both produce a verdict; trim watchdog bounds a
runaway log; kill -SEGV on the real /opt/thorium-browser/thorium wrote
"exit code: 139 / died on signal: 11 (SEGV)"; librewolf installs and
loads with uBO + LeechBlock active.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME5ZkgWjFvPZrUwMpVZM3e
|
|||
| a96605dc4b |
Wire the enforcement decision to the DPM hide call
The decision layer and setApplicationHidden were both built and tested but never connected -- the service logged a tick. Joining them meant porting the decision to Kotlin, because a background Service has no Flutter engine and starting one per alarm to answer a pure question would be slow and would add a failure mode at precisely the moment enforcement has to be reliable. That duplication is only safe if the two cannot drift, so the Kotlin suite restates the Dart cases one for one rather than testing something adjacent. It was checked for vacuity by asserting the hidden set contained a package that is not installed: 13 tests completed, 1 failed, and it was the right one. Show is applied before hide. If a pass is interrupted -- process killed, device sleeps -- the half that ran is the half that restores access rather than the half that removes it. The launcher, the protected prefixes and the app's own package are re-checked at the point of use as well as in the policy, because hiding the dialer would be unrecoverable from the device itself. An unreadable policy yields no decision rather than an empty allowlist, which would hide every app. The next run is scheduled in a finally, so an enforcement bug costs one tick instead of ending the chain. Note for the next session: JDK 26 is the system default and breaks the Android jlink transform; the Gradle tests need JAVA_HOME=/usr/lib/jvm/java-21-openjdk, which is what Flutter itself uses. NOT verified on device. Unit tests cover the decision, but no real hide/show pass has been observed -- a managed profile was created and the app made profile owner, and the UI confirmed the policy loaded, but the session ended before a pass was triggered. The profile was removed and user 0 verified clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| cc804dd311 |
Run enforcement from exact alarms, and re-arm it before first unlock
The decision layer needed a runtime. A polling loop was the obvious design and the wrong one: Doze can skip a poll indefinitely, and skipping the 05:00 boundary leaves the curfew allowlist applied into the morning -- the user finds their banking app missing with no obvious cause. The transitions are known ahead of time, so they get exact alarms. The service wakes, applies, schedules the next run and stops rather than sitting resident. BootReceiver takes LOCKED_BOOT_COMPLETED as well as BOOT_COMPLETED, because the latter does not arrive until first unlock. That is not theoretical: this phone sat at RUNNING_LOCKED for minutes after an earlier reboot in this session, with no network and no app able to start. Relying on BOOT_COMPLETED alone freezes enforcement state across a reboot -- safe for hiding, not for unhiding, which is the direction that strands you without a dialer. The schedule is re-armed before unlock; the pass itself waits until storage is readable. The first on-device run logged "exact alarms not permitted", and dumpsys showed what that costs: window=+11m14s, maxWhenElapsed=+25m51s, so a 15-minute fallback could slip to 26 minutes. SCHEDULE_EXACT_ALARM is user-revocable and denied by default on Android 16; USE_EXACT_ALARM is the permission for apps whose function is acting at specific times, and is granted at install. After the change: window=0, exactAllowReason=policy_permission, whenElapsed equal to maxWhenElapsed. The service reports "not DO/PO - no enforcement applied" and does nothing when unprovisioned, rather than failing quietly, and a failure inside a pass cannot stop the next run being scheduled -- an enforcement bug should degrade one tick, not end the chain. Not yet wired: the service logs a tick rather than calling setApplicationHidden with the decision. Both halves are tested; joining them is next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| d0fcda3a47 |
Provision home coordinates out of git; layer the workout signal; drop the test hook
The geofence needs HOME_LAT/HOME_LON, but those say where I live, so the committed policy asset is generated with --redact-home and the real values are pushed to app-private storage instead. push_home_location.sh greps config_secrets.sh rather than sourcing it -- that file holds other secrets and this script has no business evaluating them -- and refuses the shipped REDACTED_* placeholder with a message naming the file to fill in, rather than writing whatever the parse happened to land on. Private storage was chosen for its lifetime as much as its permissions: it does not survive an uninstall, so reinstalling forces re-provisioning instead of silently inheriting a stale home. The default umask left the file 666, so the script chmods it 600; the shell user gets Permission denied, and the /data/local/tmp staging copy is removed after the transfer. Every read failure returns null rather than throwing. Null reaches the enforcement layer as location-unknown, which already fails closed into enforcing -- so a corrupt coordinates file cannot become an unlock, and cannot crash the service either. Workout detection replaces a root-only SQLite read of StrongLifts' private database. The manual source is time-boxed rather than a plain toggle, because a workout mode left on by accident is indistinguishable from having no exception at all. The guarded wrapper catches broadly and times out: nothing about a workout probe failing should be able to stop enforcement running. The combined source can name which signal opened the exception, so the UI can explain an unlock rather than presenting it as magic. Only the two sources verified to exist on this device will be wired -- com.kuhy.workout_app exposes nothing readable and org.runnerup is not installed. HideTestReceiver is deleted. It proved setApplicationHidden persistence, which is what it was for, but it applied policy from an exported receiver and that has no place in a build that will later hold device owner. Confirmed absent from the rebuilt and reinstalled APK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| 1b7958143d |
Prove DPM app-hiding survives a reboot; build the enforcement decision layer
The whole Device Owner design assumed setApplicationHidden persists across a reboot, and that had never been tested -- while both shell primitives were measured to be cleared by one. Settling it looked like it needed a factory reset. It does not: a managed profile is removable and takes its profile-owner grant with it, so the DPM API can be exercised and then erased. pm create-user --profileOf 0 --managed -> user 10 dpm set-profile-owner --user 10 ... ... hide, reboot, re-check ... pm remove-user 10 hidden=true before the reboot, hidden=true after, agreed by dumpsys and by the DPM API itself. Afterwards: Device Owner Type -1, no restrictions, and Chrome in user 0 untouched. Scope limit, stated because it is easy to overclaim: this proves the API's hidden state persists under PROFILE OWNER inside a work profile. It does not prove a DEVICE OWNER can hide a given app in user 0, which is what the design actually needs -- DO there additionally cannot touch the launcher, the default dialer/SMS/IME or packages the system deems critical, and the call can return false silently. That must be tested in the first minute after provisioning, while clearDeviceOwnerApp() is still a cheap exit. On top of that, EnforcementDecision turns policy plus conditions into a hide/show set. It is a pure function, so the whole decision layer is tested without a device, a clock or a location provider. packagesToShow is deliberately the complete allowed set rather than a delta. Failing to hide something is a mild disappointment; failing to UNHIDE strands the user without a dialer or a bank at 09:00 because the phone rebooted during curfew. Applying the full set on every evaluation makes a missed run self-repairing, which matters because BOOT_COMPLETED does not fire until first unlock -- observed directly, the phone sat at RUNNING_LOCKED for minutes after a reboot with no network at all. The workout exception is expressed as packages rather than domains. The rooted system unblocked youtube.com in a hosts file; hiding an app cannot target a domain, so the package set is derived from the configured domains and config.sh stays the source of truth for intent. Signal sources were checked on the device before being built: StrongLifts is granted health.WRITE_EXERCISE and Health Connect is installed, so that path is viable; com.kuhy.workout_app exposes only a MainActivity and Flutter boilerplate; org.runnerup is not installed here at all. Only the two that can actually produce a signal will be wired. HideTestReceiver is temporary and must be deleted before any provisioned build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| 7cfe188bbe |
Fix engine pruning: disabled -> inactive (they were still running)
The previous commit's pruning did not actually take effect. Per enginelib,
"disabled" only sets the DEFAULT for a fresh visitor -- "will allow the user
to manually activate it in the settings" -- so any browser session with an
existing preferences cookie kept running every pruned engine. yandex images
was still throwing JSONDecodeErrors in the log minutes after being
"disabled". "inactive" is the key that actually removes an engine.
Converted 84 entries. Verified behaviourally rather than by reading config:
an images search now serves from google cse images ALONE (20 results,
unresponsive=[]), and no pruned engine appears in the logs after a restart.
brave and qwant deliberately keep "disabled". Sibling engines reference
their outgoing connection pool by name (network: brave <- brave.images/
.videos/.news; network: qwant <- qwant news/images/videos), and "inactive"
removes the definition entirely, so the container dies at startup with
KeyError: 'qwant'. "disabled" keeps the pool defined while hiding the engine.
Pool owners in this image: brave, qwant, yandex, piped, yacy.
Also fixes two status checks that could report a healthy instance as broken.
Public /search was returning 429 while loopback returned 200, which looked
like a regression but was a missing Accept-Language header in the request:
botdetection runs one check per header and rejects on the first failure.
Setting "debug: true" under general: makes SearXNG log the exact rejecting
method ("NOT OK (searx.botdetection.http_accept_language)"), which settled it
in one run after several wrong theories. wait_for_site and check_headers had
the same omission and are fixed; the full required header set is documented
at check_http.
Public path confirmed working end to end: general 200 with 21 results in
0.71s, images 200 with 40 proxied thumbnails in 0.58s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG
|
|||
| dc45779d38 |
Prune 84 SearXNG engines; images category now sub-1s
kuhy went through the /preferences engine table and selected every erroring engine (CAPTCHA / access denied / timeout / parsing error / crash) plus the slow tail, across all nine categories. Applied all of them, keeping only google cse images for the images category as requested. Result: unresponsive_engines is now [] in EVERY category. Per-category latency (q=linux): general 572ms, images 406ms, videos 1378ms, news 455ms, music 461ms, it 1698ms, science 1338ms, files 584ms, social media 1074ms. Images went 2.1s -> 406-900ms across 12 queries, still 20 results. The root cause was not google cse images being slow (1.1s): SearXNG waits for the SLOWEST engine in a category, so quark images at 2.9s set the floor. Cutting the others removed the ceiling. Three engines that were not in the list surfaced as unresponsive only under load and were pruned with the rest: brave.images, startpage images, kickass. Corrects a wrong claim from the first pass. I had said general search retained three working sources because !go returned 20 results and !bi returned 10. Upstream marks both google and bing "inactive: true", so they answer ONLY to an explicit bang and were never part of the merged view -- which google cse has been serving alone all along. The pruning therefore removed only engines already contributing zero, but general search does have a single point of failure, so status now prints the !go/!bi fallback when a live search returns nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG |
|||
| 84d1c6e897 |
Record browser CSP verification for SearXNG
Closes the one gap left open at deploy time. The strict CSP (default-src 'none') had only been validated by static asset analysis, because headless chromium produced empty output on this host and the browser extension was not connected at the time. Loaded the live instance in a real browser: front page, text search, image search and autocomplete all work, and the UNFILTERED console shows 4 messages total -- all benign [PLUGIN] calculator DEBUG lines from SearXNG's own bundle, zero "Refused to load" CSP violations. The "?" tiles in image results are upstream 404s, not blocked requests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG |