Commit Graph

1270 Commits

Author SHA1 Message Date
db243fe02e Cache prettier's verdict so a push stops paying Node startup
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
A push took ~13s. prettier is a pre-push hook precisely because Node's
startup dominates its runtime, and pre-commit chunks a large file list into
several invocations -- a 106-file push paid ten cold Node starts. ci-mirror,
the hook that looks expensive, was already 0.02s once its green-tree cache
hit.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
2026-08-30 15:49:12 +02:00
42e41319dd Drop the alarm leg from the morning routine
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
wake_alarm._alarm no longer exists: the PC's waking role was deleted in
kuhyx/wake-alarm, and the phone owns the alarm now. This removes ALARM_MODULE,
_run_alarm() and the --with-alarm flag, leaving one leg -- the workout lock.

Landing this in the same change as the deletion is the point. An orchestrator
left pointing at a module that no longer exists is exactly the failure this
repo has already had once: after screen-locker was extracted on 2026-05-28 the
reference here went stale and the workout lock died with ModuleNotFoundError on
every run for a month, while the alarm half kept working and hid it.

morning-routine.service loses --with-alarm and its comments now name what
actually starts it: wake-alarm-trigger.timer, at the phone's synced alarm time,
not the deleted systemd-sleep hook. Environment=DISPLAY=:0 stays -- the comment
credited it to the alarm's Tk window, but workout-locker.service needs it too.

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjfWdAyHbCubac7wJKJZW
2026-08-29 18:34:25 +02:00
ce33c935e4 Run the website's post editor as a loopback unit, no Caddy route
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
2026-08-29 17:50:03 +02:00
f70af95b61 app_icons: add the bowl-star glyph for restaurant-rater
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
A new app at ~/restaurant-rater rates restaurant dishes, so it needs a
family icon. shield-cutlery is diet-guard's and the accent is shared
fleet-wide, so the new glyph has to be distinguished by silhouette alone: a
filled star above an open bowl -- a dish, and a verdict on it.

The star is filled rather than stroked for the same reason as the nib and
the anvil: at 72px weight its five interior notches close up well above
MIN_NEGATIVE_SPACE and it renders as a pentagon at 48dp.

The bowl went through two passes. The first used shallow walls stopping at
y=736 and read unmistakably as a smiling mouth with the star for a nose --
two curves with a shape above them is a face unless the vessel is clearly
deeper than it is wide at the base. The committed version drops to y=752
between narrower walls, which reads as a bowl.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7BaCSV8FEtcNgVDVsRAJj
2026-08-29 16:35:30 +02:00
899b00ae75 Take pylint 4.0.8
Upstream released it between two CI runs today, which is enough to turn the
freshness gate red on a commit that touched no Python -- the documented and
expected behaviour of a gate that checks "newest stable" rather than "newest
stable as of whenever someone last looked".

The pre-commit pylint hook takes its version from the mirror rev, not from
this file, so nothing about which checks run changes here.

Also refreshes the isort note: 4.0.7 declared `isort<9`, 4.0.8 declares
`isort!=5.13,<10,>=5`, so the comment's specific bound was already stale.
Reworded to state the constraint's purpose rather than a version that moves.

Fixes the red main that ci-baseline-green is blocking commits on.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7BaCSV8FEtcNgVDVsRAJj
2026-08-29 16:19:28 +02:00
d255288e06 Tell the fix-forward path that it needs the flag twice
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The guard prints `CI_GREEN_SKIP=1 git commit ...` and stops there, but it runs
at two stages: at commit, and again inside the pre-push CI mirror's
clean-worktree `pre-commit` run, which sees the same still-red baseline. So the
documented escape gets you past the commit and into an identical wall at push,
with no hint and nothing in the output connecting the two.

Hit today landing the pre-commit cache-key fix: the commit went through, the
push did not. The variable is read from the environment and `ci_mirror.sh` does
not scrub it, so `CI_GREEN_SKIP=1 git push` carries through to the nested run --
which is now what the message says.

Verified both paths: with the variable set the script warns and exits 0, and
against today's green main it exits 0 silently. shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 12:09:02 +02:00
6eb70de0ba Key the pre-commit cache on the Python that was actually installed
The key said `py3.12` as a literal while the job installs `3.x`, which is
3.14.7 today. A `language: python` hook env is built by whichever Python
pre-commit ran under, so the cache kept handing back envs built on 3.12.14 and
the hooks kept running there -- invisibly, right up to the first syntax 3.12
cannot parse.

PEP 758 was that syntax. The previous commit's `except A, B:` reached pylint
and the debug-statement hook on cached 3.12 interpreters and came back as
"multiple exception types must be parenthesized", a failure with no local
reproduction: developer caches were built on 3.14, where the same pylint 3.3.2
and the same pylint 4.0.7 both parse the file fine. The syntax was never the
defect; the stale interpreter was.

`steps.setup-python.outputs.python-version` cannot drift from what was
installed, so this cannot recur on the next release either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 12:01:52 +02:00
7decc499cb Take coverage 7.16.0
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Already red before today's Python work touched the repo; the commit hook had no
reason to look at the requirements files, so only a whole-tree run saw it.
Bumped across every repo pinning 7.15.4 in the same pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:51:18 +02:00
adf2e402df Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading `meta/` against 3.10 -- three tools judging an interpreter
nothing runs any more, and ruff withholding the pyupgrade fixes that only
become safe at the newer target. `pin_ci_python.py` rewrites these three knobs.

What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That makes the source 3.14-only at the
syntax level, which is what `requires-python = ">=3.14"` already promised; kuhy
chose it deliberately over holding ruff a release back (2026-08-29).

Also drops a comment in `wsg_grabber/models.py` that the bump makes false: it
explained why the file used ``str, Enum`` instead of ``StrEnum`` under mypy at
3.10, sitting directly above a ``class Verdict(StrEnum)`` that had already
stopped obeying it.

ruff, ruff-format, mypy --strict and pylint all clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:50:39 +02:00
af068a22cf Build pygame from source so the pre-commit job runs newest Python too
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 20:25:31 +02:00
5a8aba7369 Keep the pre-commit job on 3.12, where pygame still has a wheel
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 18:51:11 +02:00
cd9bf57d38 Run CI on newest stable Python, which the pins now require
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 18:45:50 +02:00
6c648450d3 Drop the two dead pins that made the environment unsolvable
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 18:32:58 +02:00
2c63c031bc Restore the two-space gap before the typing_extensions pin comment
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 18:29:24 +02:00
d72bbf0da6 Latest stable dependencies, gatelock v0.7.2, and the gate
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 17:40:00 +02:00
1cf567977f Latest stable dependencies, gatelock v0.7.2, and the gate
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 17:38:25 +02:00
924630c029 Run SyncYomi from the local fork build and stop clobbering the Caddy edge
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 14:44:40 +02:00
2554a45c17 Document the TachiyomiSY fork that fixes the SyncYomi collapse
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 14:19:22 +02:00
b910c81f2a Pin .fvmrc to a Flutter version, not a channel
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
`{"flutter": "stable"}` names a moving target: two machines on the same commit
can build with different SDKs, and a freshness check has nothing to compare it
against. This is the only .fvmrc in the fleet, and it was the one file the new
dependency-freshness gate could not evaluate.

Purely declarative today -- nothing in any repo invokes `fvm flutter`, and fvm
has no SDKs cached, so the build path is unchanged and still resolves the
pacman-installed SDK through ~/.local/bin/flutter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
2026-08-28 13:03:16 +02:00
c5cd63fb80 token_audit: measure tool-call batching per API message
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Counting transcript records reports 1.00 tool calls per message no matter
what the model did: the harness writes one JSONL record per tool_use block,
all sharing message.id. A hand-rolled per-record count read the real 1.9%
batching rate as 0.0%, which nearly shipped as a headline finding.

Group by message.id instead, and report it, so the number comes from a
tested code path with the window handled by find_transcripts rather than an
ad-hoc timestamp. Three snapshot fields give next week's run a baseline.

Axes was at the 12-attribute cap, so the counters live in their own Batching
dataclass with the two ratios as properties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiSSDu4gKbp9GGxTTtXvYW
2026-08-28 12:22:21 +02:00
2a49ae31ef Register betting_sim with the shared icon generator
track-bars on the family charcoal/accent: rising bars with a plotted trend
point, which reads as odds movement.

linux:false because betting-sim's desktop target is a Chrome-wrapped web build
rather than the GTK embedder, so the hicolor set would go unread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDSQS61Vgb6wptLPT2Utbf
2026-08-28 10:00:29 +02:00
88ffa37fe1 Find Material text fields by their hint label
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
A Flutter/Material TextField publishes its label as `hint` and leaves text
and content-desc empty, so every field on such a form arrived as an
anonymous EditText. The workout app's manual-workout form is nine of them:
the only way to fill it was by screen position, which is precisely the
mis-tap-writes-the-wrong-field failure this tool exists to prevent.

`hint` ranks below text and content-desc but above resource-id. Material
clears the hint once content arrives, so a filled field still answers to its
real text and a caller can tell an empty field from a completed one.

The app-side alternative does not work and is documented as a trap: wrapping
the field in Semantics(label:) emits a second labelled node at the same rect,
and MergeSemantics collapses them by dropping the hint from Android's tree,
which leaves the form MORE anonymous than before. Widget tests cannot catch
that -- they assert on Flutter's SemanticsNode while this tool reads
Android's AccessibilityNodeInfo, so nine of them passed green on the build
whose on-device labels had just gone empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWq9MRemX9ZWvNmnhNWUP2
2026-08-27 20:39:46 +02:00
6c12467f2d ci: check out shared gates in pre-commit; add punchme icon
Some checks failed
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (push) Has been cancelled
Python tests / test (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
`Pre-commit checks` has been red on main since 2026-08-24: the md-naming
and file-length hooks are thin shims that exec the real gate out of the
utils repo, and a runner has no ~/utils, so both died with "shared gate
not found at /home/runner/utils/...". The dedicated md-naming.yml
already checks utils out into .utils/; this does the same and points
UTILS_ROOT at it, which is the override the shims already support.
CI_GREEN_SKIP is set because this commit IS that fix.

Also registers punchme (github.com/kuhyx/punchme) in the shared icon
family on the family accent, Android-only so linux=False. Its glyph is a
time card with a clipped corner, a column of four punched holes and
three ruled lines; the first draft put two holes above two wide slots
and read unmistakably as a face at every size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:31:32 +02:00
0b0b71688c Reindent a commented-out block to satisfy prettier
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The pre-push prettier hook has been failing on .pre-commit-config.yaml since
49d82511, which blocks every push regardless of what the push contains. The
only complaint is the indentation of the commented-out COMMITIZEN block.

Purely cosmetic and confined to comments: yaml.safe_load of the file before
and after compares equal, and both parse to the same 16 hook repos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KU9czAaz7wAkjyJ5uEB3t
2026-08-24 23:34:48 +02:00
e971bf73bb Add focus_owner and a padlock-closed glyph to the icon family
focus_owner (com.kuhy.focus_owner, the Device Owner enforcer) was the one
Flutter app never registered with the shared generator. It shipped a
hand-written vector adaptive icon added purely to unblock its build — the app
had no icon resource at all and every release AND debug build failed on AAPT
"resource mipmap/ic_launcher not found". That placeholder used #E8EAED, so it
was off-family as well as unregistered.

None of the existing 13 glyphs fits a device-policy enforcer, so this adds
padlock-closed: shackle stroked (inheriting the group's round caps), body
filled in the accent, keyhole punched back out in the field colour. A fully
stroked padlock loses its keyhole at 48dp and a fully filled one loses the
shackle's opening; splitting them keeps both.

Geometry was computed against style.py's constraints rather than eyeballed,
per the lesson recorded on _NOTE_ASCENDER: body 292..732 x 520..792, shackle
centreline r=132 about (512,520) so its outer top is 352 (inside the 232 safe
edge) and its legs land at 380/644, well within the body. The keyhole leaves
60px of body above it and 76px below the stem, both clear of
MIN_NEGATIVE_SPACE=36 — checked by rendering at 48px, where the hole survives.

The repo path points into ~/phone-focus-mode/focus-owner: focus-owner was
absorbed as a subtree on 2026-08-24 and both the standalone clone and its
GitHub repo are gone.

app_icons stays at 100% branch coverage; 44 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KU9czAaz7wAkjyJ5uEB3t
2026-08-24 23:23:28 +02:00
49d82511c3 Adopt the four markdown namespaces and gate them
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2026-08-24 15:55:46 +02:00
69937ffda8 Record the depth-cap campaign as finished, not mid-flight
Some checks failed
Pre-commit checks / pre-commit (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
The "Depth-cap progress" table still advertised 294 violations and a 207-file
"step D worklist", which stopped being true when the campaign completed. The
gate now reports zero:

  $ bash meta/scripts/check_directory_depth.sh --all
  check_directory_depth: all checked paths are within 2 directories   (exit 0)

A stale worklist is worse than no worklist: it sends the next session hunting
for 207 files that are not there. The table is hand-written --
report_live_state.sh generates the path inventory above it but never emitted
this section -- so regenerating the doc would not have fixed it.

Rewritten to state the guarantee (the directory-depth-cap hook enforces it on
every commit) and to point at the command rather than at a number, with the
original backlog kept as history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-24 13:30:26 +02:00
782a6320af Let --rollback work on a freshly installed machine
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The previous commit made hosts-blocker's install.sh register the guard-lib
instances itself, which is what finally gives a fresh machine real hosts
enforcement. It also created a class of machine that --rollback refused to
touch: the state dir it demanded, /var/lib/guard-lib-migration, is written by
the one-shot migration, and a machine that never had a legacy layer has no
such state. So --rollback exited 1 while the instances it exists to remove
were live -- every freshly installed machine had no escape hatch at all.

Rollback now proceeds whenever a guard-lib instance is registered, says
plainly that there is no legacy layer to restore, and still refuses when
there is genuinely nothing to roll back.

The second half is worse and was only visible by running it. The existing
re-assertion of chattr +i works by starting hosts-guard.service and
hosts-bind-mount.service, neither of which exists on a fresh machine, so
every systemctl cat skipped and the rollback finished with /etc/hosts holding
no immutable flag and no bind mount -- less protected than before it ran.
It now re-asserts immutability directly when no legacy unit did it.

Verified in a pristine vmbox guest whose guards came from
install_core_system.sh --all: dry-run and real rollback both exit 0, the
three instances are uninstalled, shutdown-schedule (a different feature) is
untouched, and /etc/hosts keeps ----i----------------- and all 178410 lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-24 12:57:24 +02:00
4c426eae0e Make install_core_system.sh work on a fresh machine
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Python tests / test (push) Has been cancelled
Two of the seven modules invoked python_pkg/ paths that stopped existing when
screen_locker and steam_backlog_enforcer were extracted into their own repos,
so the first thing the installer did on a fresh machine was fail. Nothing
installed guard-lib either, which meant the CORE "Midnight shutdown timer"
module always died with "guardctl not found on PATH".

Every module now resolves through install_module_from_repo, and
require_extracted_repo clones a missing sibling instead of only printing
instructions -- otherwise "clone it yourself" is not actionable on the bare
machine this script exists to set up. The clone drops privileges to the
invoking human via sudo -u, because an installer that re-execs under sudo
would otherwise leave a root-owned checkout in the user's home; a sandbox
cannot catch that, since the guest is root throughout. A clone that fails is
still a loud, fatal module failure, never a silent skip.

guard-lib is addressed as the "utils" repo: it is a subdirectory of
github.com/kuhyx/utils, so asking for "guard-lib" would resolve to
~/guard-lib and derive a clone URL that does not exist. It installs first,
before the modules that need guardctl.

Separately, the security hardening suite's screen_locker section hardcoded
$HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py. That path went
away with the extraction, so all five checks silently SKIPPED -- reporting a
clean run for tests that never executed, which is the exact failure mode
lib/extracted_repos.sh was written to prevent. It resolves the repo properly
now ($HOME is also wrong under sudo), and the assertions are rewritten against
the current implementation: the four symbols they grepped for no longer exist
anywhere, having been replaced by SPORT_CHOICES, SPORT_TABLE_TENNIS,
_validate_table_tennis and MANUAL_WORKOUT_REFLECTION_MIN_CHARS.

On this host that takes the suite from 18 passed / 1 failed / 5 skipped to
23 passed / 0 skipped. The task file's "18 passed / 6 skipped out of 17"
target was stale -- the suite is 24 checks now -- so it could not be matched
literally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-24 12:45:21 +02:00
db4d947351 Record the vendor-closure and broad-scan findings in the evidence
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Two post-push audits, both worth keeping.

vendor/common.sh sources three sibling libs unconditionally and only common.sh
had been vendored, so every entry point printed three "No such file or
directory" errors. Every suite passed anyway -- the functions in use are
defined above that block and a failed sibling source is non-fatal -- which is
exactly why it needed its own test. Fixed in digital-wellbeing c5eb028.

A grep across /etc, /usr/local and /usr/bin, broader than report_live_state.sh
(which reads only /usr/local/{bin,lib}, units and symlinks), found three
references. None from this work: pw_guard.sh's target still exists, and
dwm-session carries two pre-existing single_use/ paths. dwm is dormant and was
deliberately abandoned, so that one is reported rather than changed.

Also records that digital_wellbeing/data/ was safe to delete: never tracked on
any historical path, and no script in the unit references data/ at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 21:10:14 +02:00
cc999d04e4 Rewrite the restructure handoff as a closing report
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The campaign is done, so the file no longer opens with a prompt to paste. It
now records the closing numbers (0 depth violations, 0 stale live references,
949/1200 source files, 1090 tracked), what landed, what was deliberately left
undone and why, and the traps worth carrying forward.

The "deliberately NOT done" section names the two things that still look red
but are not regressions: the unwrapped discord/signal-desktop binaries, which
fail identically at baseline, and workout_locker, which belongs to the
separate screen-locker repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 21:02:03 +02:00
6b828d29aa Extract digital-wellbeing; the directory-depth cap now enforces
The last unit over the cap is now github.com/kuhyx/digital-wellbeing, cloned
at ~/digital-wellbeing, and `--warn` is gone from the directory-depth-cap
hook. `check_directory_depth.sh --all` exits 0 with zero violations, down from
46 at the start of the session. That was the point of the campaign.

History was filtered across all three paths the unit has lived at (63
commits); the current path alone holds 2.

The six suites that test this code lived in linux_configuration/tests/ rather
than beside it, so they were filtered separately (24 commits) and merged in as
a second root. A path filter of the unit alone would have left them here,
testing code that had left the repo -- and their names would still have been
in shell-tests.yml's hardcoded arrays, failing CI on missing files.

linux_configuration/lib/common.sh is vendored into vendor/ rather than trimmed
to the four functions actually used: set_actual_user_vars calls two of the
others, so a trim breaks silently. It is in vendor/ and not lib/ because
test_music_parallelism.sh copies the unit's own lib/ into a scratch worktree
while stubbing the shared lib; in one directory the real common.sh overwrote
the stub, and the suite silently measured the wrong library.

Running the callers caught what shellcheck and grep both missed:
check_and_enable_services.sh died with "DW_REPO: unbound variable" because the
definition sat below its first use. It now runs clean and reports ok for every
extracted-unit check.

Two long-standing live defects fixed as a consequence. The pacman-wrapper
drift manifest embeds ABSOLUTE paths and had named deleted scripts/ paths
since the flatten, so the check reported a permanent false "stale or
tampered"; it now verifies, with unchanged hashes proving the extracted bytes
are identical. And the last two stale live references are gone: 8 -> 0.

The two pre-commit excludes for setup_midnight_shutdown.sh are dropped rather
than moved. lsattr shows nothing in the unit is chattr +i any more, and the
fixer hooks leave the file alone when allowed to run.

Removing 80 shell files also shrank the jscpd denominator enough that the
virtualenv/tqdm clones inside the GENERATED .ci-mirror-venv/ tipped the repo
to 2.09% and blocked this commit, which adds no duplication. That directory is
the same vendored third-party shell the ignore list already excludes as
.venv/, just under another name; jscpd walks the working tree rather than the
index, so being untracked did not spare it. Excluded by name: 1.74%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:57:51 +02:00
981823e3b9 Delete the dead .hippo memory store
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
161 tracked files, 3.2M. All 158 episodic entries have retrieval_count 0 --
the store was written to and never once read from. Last write 2026-05-28, the
tool is uninstalled, and nothing references it: the only code hit for "hippo"
anywhere in the tree is the blocked domain "wordhippo" in
leechblock_defaults.json.

No .gitignore entry was needed after all. The handoff anticipated one for
hippo.db, which was untracked only because *.db is ignored -- but with the
whole directory gone there is nothing left to ignore.

Tracked files: 1346 -> 1175.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:42:03 +02:00
309f4c8d42 Repair live self-repair broken by the flatten and extractions
Five live mechanisms were silently pointing at paths that no longer exist.
None of them announced it; each degraded to a log line or a missing file.

- hosts-file-monitor.service is ACTIVE and watches /etc/hosts for tampering,
  but its embedded installer path was the pre-flatten scripts/ one. It logged
  "Hosts install script not found" 37 times: it detected tampering it could
  not repair.
- dns-blocklist-refresh.service (enabled, next run 00:01) named a deleted
  single_use/features/ path, as did media-organizer.service under utils/.
- setup_periodic_system.sh read all TEN of its templates from
  periodic_background/system-maintenance/, which has held nothing but
  untracked .pyc and .coverage residue since system-maintenance was
  extracted. Every install of a service, timer or script was reading a file
  that was not there.
- /etc/sudoers.d/hosts-install-no-passwd granted NOPASSWD on the old hosts
  installer path.

Running the fixed dns unit then caught a bug the static checks missed: under
systemd there is no SUDO_USER and `id -un` is root, so the new resolver
returned /root/hosts-blocker and the service died with "Feed generator not
found". extracted_repo_home() now handles the bare-root case by falling back
to the owner of the lib file itself. Verified across all four invocation
modes: as the user, via sudo, as root with SUDO_USER unset, and with an
explicit *_DIR override.

The service now reports "Feed refreshed and dnsmasq reloaded (189038 lines)".
/etc/hosts is untouched throughout -- still 176888 lines, still chattr +i,
still bind-mounted by its guard.

Live artifacts were patched via validated temp files, never in-place: the
sudoers drop-in was checked with `visudo -cf` before install, and every
original is backed up under /var/tmp/*.pre-{extract,fix}.

Stale live references: 8 -> 2. The 2 that remain name digital_wellbeing
paths and can only be fixed when that unit is extracted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:39:59 +02:00
f72455662c Extract the hosts blocker into its own repo
linux_configuration/periodic_background/hosts/ is now
github.com/kuhyx/hosts-blocker, cloned at ~/hosts-blocker. Clears 5 of the 46
directory-depth violations; the remaining 41 are all digital_wellbeing, so
--warn stays on the depth hook until that unit moves too.

History was filtered across ALL FOUR paths the unit has lived at (hosts/,
linux_configuration/hosts/, linux_configuration/scripts/periodic_background/
hosts/, and the current one). Filtering on the current path alone would have
preserved 1 commit out of 51 -- the flatten moved it recently, so a naive
single --path silently truncates the history to the flatten commit and still
looks fine in `git log | head`.

No source line changed: all 15 files are byte-identical to the copies removed
here. install.sh already resolved its lib/ via BASH_SOURCE.

The six callers now resolve the checkout through a new shared helper,
linux_configuration/lib/extracted_repos.sh, which reads the invoking user's
home from SUDO_USER's passwd entry rather than $HOME -- several of these
scripts re-exec under sudo, where $HOME is /root. A missing checkout fails
loudly with a clone instruction; it is never a silent skip, which is the
failure mode that left screen-locker's self-repair dead for five months after
the 2026-05-15 reorg.

The live blocker is untouched: /etc/hosts keeps its chattr +i, and the guards
load their plugins from /usr/local/share/guard-lib-plugins/ (byte-identical by
md5), not from a repo path.

Verified: 25 tests pass from ~/hosts-blocker; shellcheck clean on the helper
and all six callers; sudo resolution prints /home/kuhy/hosts-blocker;
test_setup_dns_blocker.sh passes; report_live_state.sh --check is byte-identical
before and after, so no new stale references.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:30:18 +02:00
fd967e37e6 Prettier-format INSTALLER_FIX_TASK.md
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
It was untracked until this rename, so the pre-push prettier hook had never
seen it. Table separators and one blockquote marker; no prose changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:14:00 +02:00
9543432ece Collapse three NEXT_SESSION_* files into one handoff
Three files starting NEXT_SESSION_ meant neither I nor a future agent could
tell which one to open: a prompt, its reference doc, and an unrelated
installer task all competed for the same name.

Now one file per job, named after the job. RESTRUCTURE_HANDOFF.md carries
the prompt and its reference in a single paste; the header says where to
start and what is superseded. The installer work becomes
INSTALLER_FIX_TASK.md — still a real task, no longer pretending to be the
next session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:13:10 +02:00
042acc820b Record the red-baseline deadlock in the handoff
ci-baseline-green also runs inside ci-mirror's clean worktree, so a red main
blocks the very commit that fixes it. CI_GREEN_SKIP=1 has to be set on the
push, not just the commit. Cost a failed push to discover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:11:09 +02:00
ac8e098c03 Point the diagnostics bats suite at the flattened path
Shell tests went red on my last push. test_network_diagnostics.bats sourced
its scripts through $REPO_DIR/scripts/diagnostics, a level the flatten
removed, so setup() failed and all 30 tests errored.

Latent rather than new: the previous run was green only because ci-mirror
aborted before the bats stage. Fixing the concurrency OOM let the gate reach
this suite for the first time since the flatten.

Grepped the tree for the removed scripts/ and single_use/ levels; this was
the last one in tracked code.

Also rewrites the two handoff docs around the units that remain.

CI_GREEN_SKIP=1 because this commit is the fix for the red baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:08:40 +02:00
2a1bb60d9f Serialise the heavy ci-mirror gates when memory is tight
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The push kept failing with "1798 passed" immediately followed by "Killed".
The suite was never the problem: it peaks at ~0.29 GiB against its own 2G
cap. ci_mirror.sh runs pre-commit --all-files and the pytest gate
concurrently, and the pre-commit tree peaks at ~1.75 GiB (mypy and pylint
repo-wide, invisible to /usr/bin/time because it measures only the parent).
The pair wants ~2 GiB at once; the pytest scope has MemorySwapMax=0, so on
a loaded machine it is the one that dies.

Concurrency is now conditional on MemAvailable. Above 4 GiB the fast path
is unchanged. The probe lives in ci_mirror_mem.sh, following the existing
ci_mirror_range/ci_mirror_shell split, which also keeps ci_mirror.sh under
the line cap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:02:59 +02:00
aa4e3cd3ae Record the guard repoint as done; hosts extraction is unblocked
Item 1's "the classifier blocks me" note was stale. Ran in vmbox first,
then on the host one guard at a time with a DNS check between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 19:53:40 +02:00
9aeeb005dd Derive the lib prefix in the music_parallelism drift guard
The pre-push gate was failing on a layout that is actually correct. The
script sources ../../lib/common.sh, which is right for where it sits after
the flattening; the test's guard still hardcoded ../lib/common.sh from
before the move, so it reported drift against itself.

The prefix is now read out of the script under test, so the two cannot
desynchronise the next time the tree moves. Verified non-vacuous: rewriting
the script to ../../../lib/common.sh in a throwaway worktree still fails
the guard, and it now names the real prefix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 19:53:17 +02:00
7cb23e8a81 Prettier-normalize two docs the pre-push hook was already failing on
Both were non-conforming before this session and block every push, not
just this one, since the hook runs over all files. The diff is entirely
table-separator widths and emphasis markers; no prose changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 19:42:18 +02:00