Fourth module of the tool/ coverage task, and the first of the four large
ones. Split into checks and loaders/main() up front rather than discovering
the 250-line cap afterwards -- 245 and 225 lines.
Every check is asserted on its *message*, not merely on having raised. The
whole reason this tool exists is that the misconfigurations it catches are
indistinguishable once data has started moving, so a check firing with the
wrong explanation sends the reader to the wrong place. That includes the
wrong-region 404, which answers with a correctUrl body rather than an error
and would otherwise pass preflight, pass sign-in against a different host
entirely, and only break mid-migration.
main()'s stated contract is pinned too: cheapest-first ordering, and that it
short-circuits -- a typo must be reported without a network round trip, so
the later network checks must not run at all.
The module's config paths are module-level constants pointing into the real
~/.config/crdt-sync, so the fixture redirecting them at tmp_path is autouse:
a test that forgot it would read and report on the user's actual credentials.
Confirmed afterwards that ~/.config/crdt-sync was not touched.
crdt_sync 293 -> 333 passed, still 100%. 4 of 7 tool/ modules done.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Keeps the handoff accurate mid-task: the module table now carries per-file
status and commits, the ordering note no longer sends a reader back to work
already done, and the baseline is 293 rather than the original 259.
Adds the two traps this session hit that the prompt's own list did not carry
-- `ruff check` alone is insufficient because the hook applies unsafe fixes
CI then rewrites the file with, and S105/S106 match the binding name rather
than the value -- plus the observed ~1.3 test-lines-per-module-line ratio,
which means each of the four remaining modules needs a planned split rather
than a discovered one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Third module of the tool/ coverage task, and the last small one.
Patched at the module's own boundary (firebase_client_for, FirebaseConfig.load
and sync_log), so nothing touches Firebase. The assertions are deliberately
about the *shape* handed to sync_log rather than about sync_log working --
test_sync.py already covers the latter. This tool exists so
crdt_sync_dart/tool/interop_check.dart has a Python-written record to find, so
the things worth pinning are the ones that would make the two halves silently
miss each other: the _interop/devices prefix, the pydev device id, the py-rec
id with its exact value and Hlc, the library's own codec rather than a local
reimplementation, and the in-memory revision store that keeps the seed
repeatable after the Dart side deletes both devices' data.
crdt_sync 286 -> 293 passed, still 100%. 3 of 7 tool/ modules done; the
config flip stays last.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
`crdt-sync pre-commit` went red on the previous two commits: pytest is only
referenced in annotations in the _cli split, so TC002 moves it into the
type-checking block.
Missed locally because I ran `ruff check` directly, while the hook passes
`--fix --unsafe-fixes --exit-non-zero-on-fix`. TC002's fix is classed unsafe,
so a bare `ruff check` reports nothing and CI still rewrites the file. The
lesson generalises past this file: for this repo, `pre-commit run --all-files`
is the check, not `ruff check` -- the same trap the prompt already recorded
for re-export pruning.
Verified after the fix: 18 tests, tool/google_id_token.py still 100% branch,
and `pre-commit run --config crdt-sync/.pre-commit-config.yaml --all-files`
clean end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Second module of the tool/ coverage task. No network and no browser: the
callback server is replaced wholesale so consent resolves instantly, and
requests.post is mocked at the same boundary test_firebase_auth.py uses.
All six TokenError paths are covered, since each names a different
misconfiguration and the module docstring exists because they are easy to
mistake for one another -- notably the fixed-vs-free port distinction, where
a Web client matching redirect URIs exactly fails with redirect_uri_mismatch
before the account picker ever appears. Also asserts the server is closed on
the failure path, since a leaked listener makes the next run fail to bind.
Split across two files because the cap hook rejected the first attempt at 273
lines: the flow tests here, main()'s argument parsing and exit code in
_cli.py, which imports the shared fixture values rather than redeclaring them.
That is the gate doing its job on this session's own output.
Three things the tooling caught, all fixed rather than suppressed:
- S105/S106 match on the binding *name*, not the value, so any constant
called SECRET/TOKEN/PASSPHRASE trips them however obviously fake it is.
Renamed around the rule the way test_firebase_auth.py names _ID_1.
- mypy rejected the previous commit's log_message test for asserting on the
return of a `-> None` function -- a tautology. Rewritten to make a real
request and assert stderr stays empty, which is the only thing that
distinguishes the silenced handler from the stock one.
- ruff pruned the imports that moved out during the split; verified it took
only those, and that the suite is still green afterwards.
crdt_sync 268 -> 286 passed, still 100%. Both modules verified at 100% branch
individually; the config flip stays last.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
First module of the tool/ coverage task. It is the one the task was raised
for: _start_callback_server's runtime path had never been exercised, and it
is the only tool/ module with no network dependency, so it needs no mocking.
Driven over real HTTP against 127.0.0.1 on a port the module picks, rather
than with a faked request object. The branches worth protecting only exist
once a socket is involved -- particularly the /favicon.ico fetch that must be
answered without being recorded, since treating it as the callback retires the
server before Google ever redirects and surfaces to the user as a consent
timeout. Both serve-loop exits are covered: a result arriving, and the
deadline passing (monkeypatched negative rather than waiting out 300s).
Writing them surfaced a teardown race, not a bug in the module: calling
server_close() while the worker is still inside handle_request() pulls the
socket out from under it and raises on the thread. Invisible normally and
deterministic under --cov, whose tracer widens the window -- so the tests
retire the thread through the loop's own exit condition before closing, via
one _retire helper. Verified over 6 consecutive --cov runs.
crdt_sync 259 -> 268 passed, still 100%. tool/ is not yet in the coverage
source, so the config flip stays last, per the prompt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Keeps the prompt as the handoff artefact it was written to be, since task 2
(all of crdt-sync/tool/ at 100% branch coverage) may outlast a session and the
file is what stops the next one redoing finished work. Not deleted, per its
own instruction to strike through instead of removing while any task is open.
The finished sections are rewritten rather than merely struck through, because
the prompt's diagnosis was wrong in two places and a future reader would
inherit the error: web-ui-tests was failing on a trailing newline that tsc
does not emit (not a stale dist/), masked locally by tsc -b's incremental
cache; and README.md still holds the per-stack design-system patterns -- what
moved out was the operability and screen-size material.
Also records what the ~/utils working tree cannot show: tasks 3 and 4 edited
~/.claude, committed there as e0cf24a. And notes finish.sh at 309 lines as an
open finding outside the four tasks -- it blocks nothing today, since the hook
gates staged files only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
`web-ui-tests` has been red since 6714d13 and the cause was not a stale dist/:
the content is byte-identical. Every one of the 16 files differed only by a
trailing newline that tsc does not emit.
Where the newline came from: at 03bf1ed the subproject config had no exclude,
so pre-commit's end-of-file-fixer appended a newline to each generated file.
eaa5683 added `exclude: ^web_ui/dist/` to stop that recurring, but did not
revert the newlines already committed -- leaving a dist/ that no `pnpm build`
can ever reproduce, and therefore a job that could never go green.
It looked fine locally because `tsc -b` is incremental: with a warm
tsconfig.tsbuildinfo it skips emit entirely, so `pnpm build` was a no-op and
printed no diff. CI has no cache, always emits, and always failed. Deleting
tsbuildinfo reproduces the failure locally in one command.
Verified: after deleting tsbuildinfo and all of dist/, a from-scratch
`pnpm build` reproduces exactly what is committed here -- `git diff
--exit-code -- dist` is clean, which is the assertion CI makes. pnpm lint
passes and pnpm coverage is 84 tests, 100% statements/branches/functions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
crdt-sync/firebase-debug.log had been sitting untracked since 2026-08-11. The
file itself is disposable tool exhaust, but it not being ignored is the actual
bug: the CLI rewrites it on every failed command, so it reappears in
`git status` indefinitely and is one stray `git add .` away from landing a
1931-line log in the history.
Covers the sibling logs the same CLI writes (firestore, database, and the
rotated firebase-debug.N.log form), since they appear the same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
todo used to be grouped with the Flutter-only apps that authenticate in-app
on the phone and therefore need no desktop session. That stopped being true
when it grew a desktop wrapper (lib/desktop/wrapper_server.dart) with its own
Firebase REST client, which needs a seeded session exactly like the Python
daemons do.
Both files the new comment cites were confirmed present today. The credential
cache at ~/.config/todo/firebase_auth.json has the same shape as every other
entry -- FirebaseCredentials.fromJson in crdt_sync_dart parses the same
{id_token, refresh_token, expires_at} that credential_store_for writes -- so
the only thing that was missing is the DEFAULT_APPS entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
The cap work is done, but four things were deliberately left and the session
that found them hit the length gate at 1159 turns. Written as a self-contained
prompt so a fresh session can pick them up without re-deriving anything:
1. web-ui-tests is red (stale web_ui/dist/, needs `pnpm build`) — red since
6714d13, which predates the cap work.
2. crdt-sync/tool/ has no coverage. Scoped honestly: the trigger was
_oauth_callback.py, but `source = ["crdt_sync"]` means adding `tool`
pulls in 7 modules / ~1350 lines, and this repo forbids omitting packages
from coverage. Ordered so partial progress is useful, with the config
flip last so it cannot block unrelated commits.
3. The three dirty working-tree items — including seed_session.py, which is
real intentional work (adds `todo` to DEFAULT_APPS), not junk to discard.
Its claim that ~/todo grew a desktop wrapper was verified today.
4. The unified-design-system skill still describes README.md as holding the
per-stack patterns, which moved to operability-patterns.md.
Carries the traps that cost time this session: run the CI-pinned ruff 0.15.2
rather than whatever `ls | head -1` finds, and use `pre-commit --all-files`,
which is what catches the autofixer pruning re-export imports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
operability.md pointed at README.md#pointer-free--small-screen-per-stack-patterns,
written before that section was moved out of README into
operability-patterns.md. The link now targets the file that holds the content.
My earlier link check only validated the *file* part of each link and silently
ignored the fragment, which is why this survived. Re-checked with the anchor
compared against the target file's actual headings; all links in the directory
now resolve, fragment included.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
All 46 files are under the cap and the gate is in place. The survey figures in
the body are the 2026-08-14 snapshot — they were already stale when this
started (39 listed, 46 actual) — so the note says to re-run the gate rather
than trust them, and keeps the rest as the record of what was asked for.
Also picks up a pre-existing markdown-formatter change to the file that was
sitting unstaged in the working tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
My local pre-commit cache holds three ruff versions and I had been resolving
`ls ... | head -1`, which picks 0.14.5 — CI pins 0.15.2, and the newer version
flags eight things the older one does not (unused imports left behind in
_syncargs.py and tool/_oauth_callback.py, import ordering, an empty
TYPE_CHECKING block). Re-ran against the pinned binary; `ruff check` is now
"All checks passed!" under the same version CI uses.
259 tests at 100% branch coverage, `pre-commit run --all-files` clean for this
config, and `python3 -m tool.google_id_token --help` still runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
The hook entry was an absolute /home/kuhy/utils path, copied from a config
written for ~/.claude where it only ever ran locally. On an Actions runner
that is exit code 127 — "No such file or directory" — so gatelock-precommit
and crdt-sync-precommit failed on every commit of this refactor while the
hook silently checked nothing. Now repo-relative, matching the sibling hooks
(`entry: scripts/check_subproject_scope.sh`): pre-commit always runs from the
git root, so it resolves the same in both places.
Fixing that made the CI configs runnable locally, and `--all-files` then found
what per-file runs had missed — ruff pruning imports that exist *for* their
re-export. `import crdt_sync` was broken three separate ways (FileSyncStateStore,
encode_key, FileCredentialStore). Each affected module now states an explicit
__all__ so the autofixer cannot take them again, and the two files that pushed
past the cap once __all__ was added are split further:
_firebase.py 260 -> 247 + _fberrors 25
_sync.py 257 -> 188 + _syncargs 91
Test patch targets followed the code they patch: gatelock._outputs.shutil ->
_xrandr.shutil, gatelock._arbiter.Path -> _claims.Path.
gatelock and crdt-sync both pass `pre-commit run --all-files` in full;
389 and 259 tests at 100% branch coverage; the root gate exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Requirement 4 of "make it permanent": the pre-commit hook only ever sees the
files in a commit, so it cannot catch a violation that arrives by a path the
hook never ran on — a merge, a push from a machine without hooks installed, a
rebase that resurrects a file. This runs `check_file_length.sh --all` over the
whole tree on every push and PR to main.
Deliberately no `paths:` filter, unlike the other ten workflows: the cap
applies to every file in the repo, so scoping this job to a subdirectory is
the one change that would reintroduce exactly what it exists to catch.
Added last, because until the final doc split it would have failed on push.
Verified against a clean checkout of HEAD in a throwaway worktree — the exact
command the job runs exits 0 — and the commit gate re-tested in both
directions after file_length's own internals were refactored: a staged
261-line file fails (exit 1, no commit object), a 200-line one commits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
tokens.md 260 -> 219 + operability.md 69
nielsen-audit.md 258 -> 154 + shipped-record.md 122
README.md 257 -> 172 + operability-patterns.md 95
Worked the requested order rather than cutting straight to a split.
(a) Automate: the categorical ramp's four constraints were restated in prose
with the measured margins hand-copied (ΔE 34.6/26.0/25.6/27.4, etc).
ramp_check.py prints every one of those numbers and fails if any
regresses, so the prose now points at the command instead of duplicating
its output. The two consequences the numbers do not state — why the ramp
is mid-toned, and why lightness falls monotonically — stay in prose.
(b) Dedup: measured, not assumed. Zero paragraphs are shared between the
three files; README already delegates the rules to tokens.md and shows
only per-stack code. Nothing to remove.
(c/d) Split on meaning: tokens.md is "frozen values", so the behavioural
requirements that *use* those tokens moved to operability.md, with the
per-stack code for them in operability-patterns.md. nielsen-audit.md is a
live grid, so completed phases moved to shipped-record.md — following the
existing phase3-record.md convention, not a new one.
The focus-ring token row had to stay in tokens.md: palette_check.py parses
that file, and moving the row made the check fail with "MISSING md:focus-ring"
— a token it cannot see is a token nothing compares across the four stacks.
The rationale lives in operability.md and the row links to it.
palette_check and ramp_check both at 0 failing checks; every relative link
between the docs resolves.
`bash scripts/check_file_length.sh --all` now exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
sync_settings_widget.dart 68 — the public StatefulWidget and its callbacks
sync_settings_sections.dart 132 — the two sections the screen composes
sync_settings_screen.dart 192 — the State: loading, connecting, build
Uses `part`/`part of`, not imports: _FirebaseSection and _BackupSection are
library-private, so a sibling file cannot import them at all — extracting them
as ordinary files would not compile.
Two shapes tried and rejected on the way: an `extension _BackupActions` on the
State (an extension cannot call the protected setState), and moving build()
out of the class (it is an @override and has to stay). Both reverted from HEAD
rather than patched around.
flutter analyze clean, 29 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
firebase_auth_rest.dart 333 -> 238 + firebase_credentials 71
+ firebase_auth_error 31
firebase_client.dart 283 -> 207 + firebase_keys 60 + firebase_errors 19
sync.dart 259 -> 141 + sync_state 121
Mirrors the split just made on the Python side, module for module, so the two
implementations stay legible against each other: credentials with their store,
key escaping with its escape table, the error type with the response parser
that builds its message.
Every new file is added to lib/crdt_sync.dart. A src/ file that is not in the
barrel compiles and tests green while silently dropping the symbol from every
consumer's import, so the export list is part of the split, not an afterthought.
280 tests pass and `dart analyze` is clean — including the four consumers
(firebase_config, secure_credential_store) whose imports had to follow the
moved types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
_sync.py 456 -> 245 + _syncstate 131 + _pull 106 + _revisions 34
_firebase_auth 412 -> 242 + _credentials 146 + _autherrors 69
_firebase.py 336 -> 250 + _fbkeys 100
google_id_token 316 -> 232 + tool/_oauth_callback 105
_github.py 255 -> 238 + _gherrors 30
revision_of became its own leaf module because both the push and pull halves
need it; without that the two would have imported each other. _fbkeys holds
the RTDB key escaping — the escape tables moved with the functions that read
them, since the pair has to round-trip exactly or a device silently stops
seeing its own writes.
crdt-sync/tool/ is outside the coverage source, so the split there has no test
net: verified by running it the documented way instead
(`python3 -m tool.google_id_token --help` prints its usage, and
`from tool.google_id_token import TokenError, fetch_id_token` — what
seed_session.py does — still resolves).
An AST pass over each pair caught what the suite would not have: _is_revoked
needs the error type, so it stayed with it rather than following the
credentials; _TERMINAL_AUTH_REASONS and _CONSENT_TIMEOUT_SECONDS had to be
carried to their new callers.
259 passed at 100% branch coverage. crdt-sync/tool/seed_session.py and
firebase-debug.log are pre-existing local changes and are deliberately not in
this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
_arbiter.py 397 -> 246 + _claims 241
_escape.py 300 -> 230 + _escape_types 82
_claims holds the claim document and everything that decides whether one is
still owned: taking the advisory lock IS the liveness proof, so reading a
claim and reaping a dead one are the same operation and belong together.
_arbiter keeps the policy — who outranks whom, and whether this app may arm.
An AST pass over every module in the package (names loaded vs. names defined
or imported) caught two things a green suite would not have:
- _try_lock and fcntl moved with the locking code, so the tests' patch
targets had to move too;
- OutputSource was left behind by the earlier _outputs split and only
resolved because of `from __future__ import annotations` — a latent
NameError the moment anything evaluated that annotation.
gatelock is now at 0 files over 250 lines: 389 passed, 100% branch coverage,
the same 58 public names, and palette_check still at 0 failing checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
_output_types.py 62 — Output, OutputRect, OutputScan (data only)
_randr.py 157 — the optional python-xlib backend and its Protocols
_xrandr.py 112 — the `xrandr --query` subprocess fallback
_outputs.py 137 — the enumerator that picks between them
The two backends both needed the value types, which would have made the
dependency circular. The first attempt papered over that with function-local
imports; extracting the types into a leaf module removes the cycle instead, so
every import is top-level again — and, importantly, the tests' existing
`patch("gatelock._outputs.scan_xrandr")` targets keep working, which the
lazy-import version had silently broken.
_RR_CONNECTED moved with the RandR code and _XRANDR_TIMEOUT_S/_OUTPUT_LINE/
_GEOMETRY with the xrandr code, each carrying its comment. An earlier pass
tried to relocate those constants with a regex, which ate part of an unrelated
function body; that attempt was reverted and redone with asserted line
boundaries plus an AST check for unresolved names in both halves.
389 passed at 100% branch coverage; gatelock.__all__ still the same 58 names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
_recovery.py 321 -> 242 + _recovery_types 47 + _grabwatch 84
_surfaces.py 380 -> 250 + _placement 136 + _surface_types 54 + _textmirror 70
Two guards had to be repaired first, because both would have gone quiet
rather than red:
1. palette-drift CI was ALREADY BROKEN by the earlier _window.py split.
palette_check.py reads LockConfig's colour defaults from a hardcoded
gatelock/_window.py, and LockConfig had moved to _config.py — so it parsed
0 tk tokens and reported "10 failing check(s)" where HEAD reports 0. Fixed
the path, and made parse_tk raise instead of returning {} when LockConfig
is not found: the empty dict is what let every tk comparison pass
vacuously while the drift guard was off for the whole stack.
2. test_recovery_invariants' symbols() read one hardcoded filename, so
splitting _recovery.py would have narrowed the "no weakening call"
invariant — the one that exists because of the 2026-07-25 unlock — to
whichever half kept the name, with the suite still green. It now globs the
whole _recovery*/_surfaces* family and fails if the glob matches nothing.
Verified it discriminates: a throwaway _recovery_probe.py calling
restore_vt_switching made the test fail, and removing it restored green.
GrabWatch is a small object rather than a free function because holds_grab
and its once-per-cause warning share state; _placement holds the geometry
questions and the re-assertion pass, which can only ever add visibility.
_surfaces keeps an explicit __all__: the autofixer pruned the mirror_text_widgets
re-export once and `import gatelock` broke, so the intent is now stated.
389 passed at 100% branch coverage; gatelock.__all__ is the same 58 names as
HEAD; palette_check back to 0 failing checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
aur-publish/publish.sh 439 -> 148 + lib/aur_registration 107
+ lib/aur_verify 122 + lib/aur_publish_one 107
guard-lib/lib/file_guard.sh 350 -> 140 + file_guard_unlock 214
file_length/config.py 252 -> 45 + _tables 202 + exemptions 77
The libs are sourced, never executed: they read the readonly config the entry
script declares, and a `readonly` re-run inside a twice-sourced lib aborts
under `set -e`. Verified by function parity rather than by eye — publish.sh
resolves the same 17 functions as before the split, guardctl the same 15 —
plus `shellcheck -x` clean on every file and a real `--help` / bad-option run.
file_length is the gate's own code, so a broken import there turns the gate
into a no-op. Split three ways to avoid an import cycle without a `# noqa`:
_tables holds the data, exemptions the rules that read it, config re-exports
both so `from file_length.config import is_generated` still resolves. The
first attempt was wrong — EXCLUDED_DIRS was left behind and every invocation
raised — caught because the gate was re-run in both modes rather than
assumed. Now verified: exit 0 on a clean file, exit 1 on an over-cap one,
--explain intact, and the same 61 exemption decisions as HEAD.
Also deletes baseline-2026-08-14.json and SURVEY-2026-08-14.txt: the task
forbids a baseline file, and nothing in the tree read either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
firebase_auth_rest_test.dart 525 -> 153 + _google 158 + _tokens 206 + _uid 141
sync_revisions_test.dart 451 -> 223 + _peers 228 + _backends 158
+ _order 165 + _persistence 215
sync_settings_screen_test 457 -> 207 + _backup 143 + _google 145
mirror_store_test.dart 452 -> 209 + _reads 172 + _fallback 196 + _revisions 244
firebase_client_test.dart 374 -> 198 + _writes 227
firebase_config_test.dart 360 -> 138 + _client 148 + _client_google 164
device_auth_test.dart 307 -> 128 + _poll 201
github_client_test.dart 291 -> 143 + _writes 201
sync_test.dart 269 -> 160 + _skips 175
Two traps this hit, both invisible to a line count:
- _FakePersistence is declared *after* main(), so a naive cut at the last
brace dropped it; sync_revisions_persistence_test.dart failed to load
until it was carried across. Audited every other split file for trailing
top-level declarations — this was the only one.
- Copying the preamble into each sibling left helpers the sibling never
uses. `dart analyze` flagged 12; all removed rather than left as dead
code, and the analyzer is now clean.
crdt_sync_dart 280 passed, sync_settings_ui 29 passed, github_device_auth
analyze clean — all matching a clean HEAD worktree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
test_firebase_auth.py 439 -> 170 + _google 159 + _tokens 159 + _session 175
test_sync_revisions.py 406 -> 163 + _peers 162 + _backends 216
test_firebase.py 369 -> 212 + _writes 221
test_mirror.py 364 -> 178 + _fallback 229
test_config.py 355 -> 139 + _credentials 112 + _client 233
test_github.py 301 -> 127 + _writes 219
test_sync.py 271 -> 195 + _skips 130
Cut at concern boundaries, not line counts: test_sync_skips.py holds the five
cases where a peer's pushed file is missing, corrupt, wrong-shaped or our own
— one story about what sync_log declines to merge — rather than an arbitrary
half of the original class.
259 passed at 100% branch coverage, verified against a clean HEAD worktree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Each file split at a class or concern boundary, never mechanically:
test_arbiter.py 432 -> 222 + _lifecycle 183 + _release 201
test_outputs.py 418 -> 187 + _randr 204 + _enumerator 126
test_scrollable.py 374 -> 231 + _into_view 197
test_escape.py 343 -> 175 + _budget 216
test_window_surfaces 341 -> 110 + _lifecycle 104 + _preempt 182
test_surfaces.py 282 -> 167 + _focus 154
test_window.py 269 -> 143 + _grab 139
Test counts verified against a clean HEAD worktree, per file group rather
than in aggregate: arbiter 49/49, outputs+escape 84/84, window+surfaces
72/72, scrollable 18/18. The whole suite is 389 passed at 100% branch
coverage, identical to before.
The one non-mechanical part: _reply/_randr_display are module-level helpers
defined *between* classes, so they did not travel with the header and had to
be moved to the RandR file by hand — six tests failed until they did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
The file's own docstring described three layers; they are now three files:
test_recovery_invariants.py (52) — the static AST check that _recovery.py
contains no weakening call at all
test_recovery_reassertion.py (218) — what a tick re-asserts: grab, focus, VT
test_recovery.py (208) — the 2026-07-25 monotonic-sequence walk,
tick mechanics, and scheduling
Splitting the classes made pylint's R0801 fire: both halves built a
RecoveryLoop with the same RecoveryCollaborators block. Rather than suppress
it (banned) the shared construction became conftest.build_loop(root, config),
which the `loop` fixture now uses too — so the tests that need a non-default
config ask for one instead of restating the wiring. The output scans both
halves drive moved to conftest alongside it.
389 passed, 100% branch coverage — same tests, same count, three files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
A refactor without a gate silently regrows, so this is the part that makes the
cap stick. Until now the repo had no root .pre-commit-config.yaml at all --
`git commit` ran zero hooks, and the five subproject configs only covered their
own directories.
Adds a root config wiring scripts/check_file_length.sh as a local hook, and
carries the same hook in each of the five subproject configs so committing
with one of those is gated too. Staged files only, so an unrelated commit
never breaks on a violation it did not introduce; exit 1, not a warning; no
baseline and no allowlist, since those are suppressions.
Verified in a throwaway worktree, both directions:
- a staged 261-line file fails the commit (exit 1, HEAD unchanged, no
commit object created)
- a staged 200-line file commits normally (exit 0)
The 44 files still over the cap cannot block this: the hook only ever sees
what a commit touches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Cut along concerns rather than line counts, so each file answers one question:
_scrollfit.py (180) — does the content fit, and what layout follows?
_scrollkeys.py (176) — key/wheel bindings and the six scroll commands
_userintent.py (106) — was this focus move the user's doing, or the app's?
_scrollable.py (243) — the widget wiring and the public surface
_scrollfit takes an explicit FitTarget bundle rather than the surface, matching
the ArmingCollaborators pattern; UserIntentTracker owns the two fields that
answer "did the user just act?", which is the guard that keeps the viewport
from scrolling itself under someone who touched nothing.
The 45-line module docstring — the three Tk defaults this viewport exists to
prevent, and the failure behind each — moved to docs/scrollable-viewport.md
rather than being trimmed. It is load-bearing rationale, so it is referenced,
not deleted.
389 passed, 100% branch coverage — unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
_window.py was the repo's largest file and could not be read in one piece.
Split along the seams the file already marked out, keeping the public API
and every existing import path intact:
_config.py (215) — LockConfig, the design tokens and preset resolution
_arming.py (178) — the way up: VT-disable, backdrop, surfaces, input grab
_preempt.py (118) — naming a blocked grab's holder, standing a weaker one down
_hooks.py (51) — the LockWindowHooks Protocol, pure interface
_window.py (243) — construction, focus, and teardown
_arming takes an explicit ArmingCollaborators bundle rather than the window
itself, mirroring the existing RecoveryCollaborators; setup() returns whether
VT was disabled instead of mutating the window. That leaves no reach-back into
private members (ruff SLF001 clean) and makes the arming sequence testable
without a window.
LockConfig is re-exported from _window, so `from gatelock._window import
LockConfig` — which five sibling modules and the tests already do — still
resolves. Tests that patched moved symbols were retargeted to their new home;
the dead _maybe_preempt delegate, whose only callers were tests, is gone.
389 passed, 100% branch coverage — identical to before the split.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Upstream clone at ~/Mapperatorinator is a third-party sibling (MIT,
github.com/OliBomby/Mapperatorinator), driven by subprocess from
~/osu-automapper and never vendored or modified.
Verified both directions: is_vendored() returns True for a 2538-line upstream
file (which would otherwise fail the cap) and False for ~/osu-automapper's own
sources, which stay gated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iJHHN18dud8dXpjd7ff3R
gatelock is not the docstring problem its siblings are -- 312 of its 394
findings are pytest fixture idiom (redefined-outer-name, protected-access),
so the prompt makes that the explicit fork to settle with the user first,
citing screen-locker's tests hook as the existing precedent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
BREAKING: sync_log's flat keyword arguments are replaced by SyncTarget,
LogCodec and RevisionTracking. Callers change from eleven loose keywords to
four positional arguments; every consumer repo is updated in the same pass.
Eleven parameters on one function was the last real pylint finding in the
package, and the grouping is the fix rather than a workaround: the three
bundles are the three things a tick actually needs -- where to sync, how to
serialize, and whether to track revisions -- and each documents its own
fields. SyncTarget.own_ids also moves the "this device plus its pre-migration
id" rule onto the type that owns it, instead of recomputing it inline.
Internals follow: _pull_remote_logs takes a _PullContext instead of eight
positionals, and the push half splits into _merge_peers/_push, which is what
clears the 21-locals finding.
The two duplicated in-memory fakes merge into one FakeStore in conftest, as
does the no-bulk-read wrapper around it -- the shared half was the fiddly
prefix slicing, which is exactly where two copies drift.
crdt_sync now scores a clean 10.00/10 on pylint. Two suppressions remain,
both scoped and explained at the line: asserting on a private attribute IS
the timeout test, and reversing merge_record's operands IS the commutativity
test.
259 tests, still 100% branch coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
pylint's C1803/C1804: `assert not merged` over `assert merged == {}`, in the
eight places where the comparison was to an empty literal. Takes the package
from 9.90 to 9.93.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
pylint scored the suite at 8.58/10, and 277 of the ~300 findings were missing
docstrings -- every one of them in crdt_sync/tests/, none in library code.
ruff exempts tests from the docstring rules, so the two linters disagreed and
the score sat well below the bar.
Rather than silence pylint to match ruff, every test class, test function and
stub method now says what it pins. Stub arguments that exist only to match a
real signature are underscore-prefixed, which is the documented way to say
"deliberately unused" and clears the unused-argument findings too.
No behaviour change: 259 tests, still 100% branch coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
crdt-sync-v0.8.0 was already pushed at the previous commit; moving a
published tag is worse than spending a patch number.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
The keyword has a default, so it adds no branch and 100% branch coverage
stays green whether or not it is threaded through to the client at all --
the gate is blind to exactly the thing the change was for. Two tests pin it:
an explicit value reaches the client, and omitting it keeps the shared
default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
Every request went through the module-level requests.get/put/post/delete
helpers, so each one paid a fresh DNS+TCP+TLS handshake. Measured today
against the real backends that is 95ms/req to api.github.com and 52ms to
Realtime Database -- on a 27-request diet-guard tick, most of the wall clock.
Each transport module now binds a pooling requests.Session to the name
`requests`. A Session is a drop-in for those helpers, so all 9 call sites and
all 16 tests that patch `<module>.requests.<verb>` are untouched; the real
module stays available as `_requests` for the RequestException/Response
references. Sessions are per-module rather than shared because the tests use
sequence-consuming side_effect lists, where one module could otherwise eat
another's queued response.
Transient failures now retry, restricted to GET: a PUT here carries a
Contents-API sha, and replaying one whose response was merely lost would
write against a stale precondition on a remote the phone also reads.
Also thread timeout_seconds through firebase_client_for, which accepted none,
so a caller's tighter budget silently reached the GitHub backend only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
preempt_weaker_holder defaulted to True, so *any* app building a plain
hard LockConfig would SIGTERM a lower-ranked incumbent. wake_alarm
(RANK_WAKE_ALARM=300) builds exactly that and outranks screen_locker
(200), so on any morning wake-alarm-trigger.timer fired while the workout
locker was armed, it stood the enforcement lock down: machine unlocked,
no workout logged, next re-arm up to 30 minutes away.
Default to False and let screen_locker opt in explicitly, which is the
direction that actually motivated the feature (diet_guard held the grab
for hours on 2026-08-21). Nothing can now evict an enforcement lock
without asking for that power by name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
Under i3 (not Xvfb), a managed tk.Tk() toplevel's geometry() request
was silently rewritten to the tiled pane size instead of the
requested WxH, so the content never actually overflowed and every
assertion built on "does this overflow" failed -- 11 tests across
test_fitcheck.py/test_scrollable.py. Fix: overrideredirect the
throwaway root, matching how every production lock window is already
built and for the identical reason (a WM must not be able to rewrite
geometry a fit/scroll check depends on).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
wait_for_turn only helps a weaker locker arriving after a stronger
one; it does nothing when the weaker one is already the incumbent.
On 2026-08-21 diet_guard (rank 100) held the grab for ~3 minutes
against a newly-arriving screen_locker (rank 200), which just
retried and logged instead of ever making progress.
Fix: when a grab is blocked by a holder that outranks-us-not, SIGTERM
it once. The holder's own signal handler already converts SIGTERM
into its normal close() teardown (VT restore, arbiter release,
app-specific on_close) -- this reuses that path rather than adding a
new one, and never signals upward (never disturbs an incumbent that
outranks us), so it cannot become a way to bypass a stronger lock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
wait_for_turn/stronger_claims already fixed leetcode_guard's grab-race
with screen_locker; screen_locker and diet_guard never got the same
fix, so a stray re-arm race between them was still live (confirmed
2026-08-21: diet_guard held the global grab 07:04-09:07 while
screen_locker built a competing window and retried its own grab
forever instead of queueing).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
Each prompt is now also copied to <repo>/prompts/dopamine-ux-*.md so a repo can
be opened and the prompt run directly. ~/utils/dopamine-ux/ remains the source
of truth; corrections go here and are re-distributed.
Adds the completion protocol every copy now carries: on finishing, the session
deletes its repo copy in the implementation commit and appends a DONE row to the
completion log in 00-INDEX.md. Partial completion is never recorded as done --
the file is edited to state what remains instead, since a stale prompt is
indistinguishable from a pending one.
Also fixes a codespell violation (re-declared -> redeclared) caught by
diet-guard's pre-commit hooks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FdApz7EZ1zDfvtvvi1idDv
Nine copy-pasteable per-repo prompts derived from "The Dopamine Architecture"
(london-post.co.uk), plus an index and a verification gate.
The four mechanics (variable rewards, micro-animations, sound, near-miss) are
aimed at kuhy's own stated goals and gated by two tests: is the displayed state
true, and is the variance real. Corollary that shapes every prompt: reward the
recording, never the number recorded -- rewarding a calorie target teaches
under-eating rather than honest logging.
Surveys found the fleet has exactly one animation across four Flutter apps, no
motion tokens in any stack, and a populated screen-locker streak that no
renderer reads. Two constraints are recorded as must-not lines because a naive
implementation would violate them: screen-locker rewards must never reduce
enforcement (a banked skip credit was deliberately removed), and wake-alarm's
UI sound layer must never touch the desktop alarm daemon.
verify_prompts.py checks all 146 cited paths and 30 cited symbols, so a prompt
pointing at a renamed function fails loudly instead of wasting a session.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FdApz7EZ1zDfvtvvi1idDv
An implementation plan under docs/superpowers/plans/ and an append-only log
under docs/superpowers/sessions/ are frozen records of one past session. They
are read whole or not at all, so splitting one rewrites a historical record
for no reading benefit -- the same reason generated files are exempt.
Scoped deliberately: specs/ and contracts/ stay capped, because those are
living documents that get re-read and edited.
is_session_artifact() is kept separate from is_vendored() so the gate reports
an accurate reason -- these are kuhy's own files, not third-party code.
Relaxes the gate only, so no already-wired repo can newly fail. Verified:
~/todo still exits 0 on --all; testsAndMisc drops 198 -> 197 violations
(plans/ exempt, specs/ still caught) in both absolute and relative modes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
`ts_core` (@kuhyx/ts-core) and `github_device_auth` close the two clusters
the audit flagged as genuinely missed. Six shared packages now, not four.
Both extractions turned up the same lesson, worth writing down: the copy
that looks canonical is not always the one to take. Each cluster had a
behaviour living in exactly one repo that a naive "extract the tidiest
version" would have silently deleted --
* diet-guard's configurable device-flow endpoints, without which its
desktop web build cannot authenticate at all (no CORS headers on
GitHub's endpoints)
* wake-alarm's transient-socket retry, without which an approved grant
is lost if GitHub closes the connection at the wrong moment
Both are now in the shared package, so all four apps gained the fix the
other three never had. Same shape in ts_core: the RNG ships two interfaces
rather than one reconciled shape, because the generated sequence is the
contract and re-rolling every saved world would be a worse outcome than
the duplication.
sims3-clone is recorded as the one consumer still pending -- another
session had uncommitted work there, so migrating it would have entangled
two sets of changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
wake-alarm's copy carried a fix the other three lacked: `pollForToken`
swallows a transient socket failure and keeps polling, because GitHub can
close the connection at the exact moment the user authorizes and a phone
can drop its network mid-poll. Without it, an approved grant is lost to a
blip and the user has to start the whole flow again.
Extracting only todo's version would have silently deleted that fix from
wake-alarm. Taking it into the package instead gives it to all four apps.
Catching `http.ClientException` is sufficient, and avoiding `dart:io` here
is deliberate: diet-guard's desktop **web** build imports this package, and
naming `SocketException` would break that compile. Checked rather than
assumed -- a probe against a dead port shows package:http raises
`_ClientSocketException`, which implements *both* `ClientException` and
`SocketException`, so the single catch covers IO and web alike.
24 tests, 100% line coverage, clean analyze.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
diet-guard's copy was not cosmetic drift from todo's: it takes the
device-code and token URLs as parameters, because GitHub's device-flow
endpoints send no CORS headers and its desktop **web** build has to route
both through a local proxy (`github_client_factory_web.dart` passes them).
Shipping only todo's hardcoded version would have broken that build the
moment diet-guard adopted the package.
So the package takes the more capable variant: `deviceCodeUrl`/`tokenUrl`
default to GitHub's own URLs, leaving every other consumer unaffected, and
`githubDeviceCodeUrl`/`githubTokenUrl` are exported so callers can
reference the defaults rather than retyping the literals.
23 tests (two new ones pin both the defaults and an override actually
being used for the request), 100% line coverage, clean analyze.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
todo, home_inventory, diet-guard and wake-alarm each carried the OAuth
device-flow client and the modal that drives it -- ~650 lines, with two
pairs byte-for-byte identical (todo/home_inventory's clients, and
diet-guard/wake-alarm's dialogs).
Deliberately its own package rather than part of `sync_settings_ui`. That
package is Firebase-only by an explicit 2026-08-12 decision recorded as
"do not re-add a GitHub section without kuhy asking again", so folding a
GitHub flow into it would reverse a standing decision. The GitHub mirror
in these apps is legacy-but-load-bearing code that was explicitly not
authorised for deletion, so it gets its own home instead of being merged
into the Firebase path or dropped.
One behaviour change, made once here instead of four times: the dialog
catches `on Object`, not `on Exception`. An `Error` is not an `Exception`
in Dart, so `ArgumentError` -- notably from URI parsing -- escaped
`_poll()` and left the dialog spinning on "Waiting for authorization…"
forever with nothing shown. That is the same trap that silently killed the
Firebase sync tick across these apps in 2026-08. A test pins it with a
mock that throws an Error rather than an Exception, so it cannot regress
back to `on Exception` unnoticed.
20 tests, 100% line coverage, clean under very_good_analysis, with the
same CI gate the other Dart package holds. Consumers are migrated
separately so each app's own suite gates its own swap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno