kartka's headless agent wrapper serves ~/.config/kartka-agent/
firebase_auth.json once per launch as /sync-credentials, and its weekly
source evaluator reads credential_store_for("kartka-source-eval"), which
otherwise falls back to the retired password. One run with
--app kartka-agent --app kartka-source-eval seeds both from one consent
tap. Both also join the default all-app run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2brk4NNnRyTnt7cD2QF2f
The sufler PC TTS relay reads ~/.config/sufler/firebase_auth.json via
credential_store_for("sufler"), like tutor, so it needs a seeded session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jw52sNWquCp34dy3rj19GD
pre-commit 4.7.0, pylint 4.1.3 and ruff 0.17.2 across crdt-sync,
earned_time, freedays, gatelock, music_theory and sickdays; numpy 2.5.4 in
music_theory (requirements, pyproject and the mypy/pylint hook deps). The
ruff-pre-commit rev follows to v0.17.2 in those configs and vmbox's, so the
hook lints with the version requirements.txt pins. No Python lockfiles exist.
All six suites at 100% coverage; every package's pre-commit config green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
**FileCredentialStore.save wrote every save through one fixed `<cache>.tmp`.**
Two processes share a credential cache (a long-running stream and a periodic
sync), so one could rename the other's half-written temp file into place and
leave a truncated refresh token behind. Each save now gets its own mkstemp
file in the same directory -- created 0600 from the outset, so the token is
never world-readable -- and replaces the cache atomically. A failed write
unlinks its temp file and leaves the previous cache untouched.
Tests cover: no temp file left after a save, distinct temp names per write,
and an interrupted write keeping the old credentials with no leaked temp.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
~/src/daily-limits' user timer publishes today's limits for its phone app
and answers its requests, with a Firebase session of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcxgxMaQHMSGWzLRpNSkuP
ts_core and web_ui depend on `link:../ts_config`. pnpm links the directory
and installs nothing inside it, and a flat config imports its plugins from
the preset's own node_modules, so CI died on "Cannot find package
'@eslint/js'" while the local tree (where ts_config had been installed by
hand) passed. Both workflows now `pnpm install` in ts_config first, and
trigger on ts_config/** too so a preset change re-verifies its consumers.
The same push turned the dependency-freshness job red on five pins that had
aged underneath it (ruff 0.16.8 in four requirements.txt,
flutter_secure_storage 11.2.0 in crdt_sync_flutter); bumped, pubspec.lock is
ignored in that package so nothing else moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnAYyNH1Sm4iJNidfYbrZJ
The 8.0 bar tolerated two points of debt and let the score regress
unnoticed. Adopting the fleet's source-only pylint scope needed
ignore-paths, not ignore = ["tests"]: ignore matches only the basename
of what pylint is handed, and pre-commit hands it explicit file paths,
so a directory name there never fired under the hook.
What the scope change left in source: runtime imports moved above the
TYPE_CHECKING blocks, the consent-flow CLI plumbing shared between
google_id_token and seed_session instead of duplicated, the token
exchange split out of google_id_token to stay under the 250-line cap,
and http.server's do_<METHOD> names admitted by method-rgx since the
stdlib dispatches on that exact spelling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
The three Python packages pinned ruff twice at different versions:
requirements.txt at 0.16.7, .pre-commit-config.yaml at v0.15.2. Only the
older one ever ran, so `select = ["ALL"]` was enforced two minor versions
behind. Measured before aligning: v0.15.2 reports 0 findings, every 0.16.x
reports CPY001 on each file -- 70 crdt-sync, 23 freedays, 71 gatelock, and
no other rule.
Resolved by adding the notice rather than ignoring the rule, so the gate
keeps its teeth.
Two gatelock modules sat at exactly the 250-line cap, so a one-line header
tipped them to 251. Both are split along a real seam rather than shaved:
_widget_fills.py colour arithmetic over LockConfig, no tkinter import
_surfaces_focus.py selects among existing surfaces, never mutates them
The focus split keeps _surfaces.py's documented containment (it stays the
only module that creates, moves or destroys windows), and the static guard
in test_recovery_invariants.py globs _surfaces*.py, so the new sibling is
covered with no list to extend. widgets.py re-exports the fills helpers,
which keeps gatelock.widgets the single public surface its tests import.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kMvSMtsVMB3d2uoZ56pbp
Latent drift, not a regression: the crdt-sync pre-commit workflow is path
filtered on crdt-sync/**, so it had not run since the file was written. The
requirements bump in 2c195cd triggered it for the first time and it failed
on I001 immediately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kMvSMtsVMB3d2uoZ56pbp
Run through Claude Code's `!` there is no stdin to paste into, and the
console dialog's copy button has already put the value on the clipboard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pBgaXSJKyY3DarYpDatDF
seed_session failed with invalid_client on 2026-09-11: the secret in
~/.config/crdt-sync/oauth_client_secret had worked on 2026-08-12 with the
same client id, so it was rotated in the console since. There is no API
for it (no gcloud command, none in the Firebase CLI), so the irreducible
step is copying it from the console page. refresh_oauth_secret.sh opens
that page, reads the paste without echo, rejects anything that is not a
GOCSPX- secret before writing, and runs the seed at once so a wrong paste
fails here rather than on the next timer tick.
xdg-open is detached from stdin: the spawned browser inherited the
terminal and swallowed the paste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pBgaXSJKyY3DarYpDatDF
Two things the pool needed to be real rather than merely correct.
**freedays no longer depends on gatelock.** It imported
gatelock.log_integrity for ~15 lines of HMAC, but importing gatelock runs
its __init__, which imports tkinter for the lock-window machinery. So a
headless policy library had acquired a hard dependency on a GUI toolkit,
and `import freedays` failed anywhere without Tk:
ImportError: libtk8.6.so: cannot open shared object file
A vmbox run caught it. The host masked it completely -- Tk is installed
here for the lockers -- and so did CI, whose own comment claimed "no
display needed" while the job would have needed libtk. freedays/_hmac.py
now does the signing with hashlib and hmac.
The format is deliberately unchanged (HMAC-SHA256 over compact
sorted-key JSON), so nothing has to be re-signed: all 8 entries already
in the real audit trail, written by gatelock's implementation, verify
against the new one. That was checked, not assumed.
This also dissolves the version diamond that shaped the app rollout:
freedays now pins only crdt-sync, so it can never conflict with whatever
gatelock tag an app carries.
**The pool now syncs on a schedule.** A `sync-quiet` subcommand plus a
systemd user timer every 15 minutes, because "it syncs" was otherwise
only true if you typed `freedays sync` yourself -- a day marked on the
phone would sit there. sync-quiet exits 0 when there is no network or no
credential, on purpose: a unit that goes red every time the laptop is
offline is a unit you learn to ignore, and a real bug still reaches the
journal through the library's logging.
install.sh gives freedays its own venv rather than borrowing an app's --
the pool is shared by five apps, so binding its CLI to one of them would
make "is freedays installed?" depend on something unrelated, and
uninstalling that app would take the command with it.
Verified in vmbox first (systemd), then on this host: the timer is
enabled and scheduled, and the service *finishes* rather than failing
when there is no remote.
seed_session.py gains freedays, without which the one command that can
give the pool a Firebase session would reject it outright (--app is
choices-validated) and the pool would stay local-only with nothing
saying why. That file was already exactly at the 250-line cap, so the
app list and its rationale move to tool/_seeded_apps.py.
freedays: 109 tests, 100% branch coverage. crdt-sync: 433 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fifth gatelock sibling (github.com/kuhyx/home-guard) calls
firebase_client_for("home_guard") from its systemd-run gate, so it needs
~/.config/home_guard/firebase_auth.json seeded like every other daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both libraries had already moved `requires-python` to 3.14, but ruff, mypy and
pylint were still grading 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), a dead `timezone` import left behind by it, and ruff's
stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`.
That last one 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).
These two are libraries the apps install, so the floor matters more here than
anywhere else in the fleet -- every consumer already pins >=3.14.
Per project: ruff, ruff-format, mypy --strict and pylint clean, crdt-sync 432
tests and gatelock 389 tests, both at 100% line and branch coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
**The gate could not see the shared Python libraries at all.** They are
consumed as PEP 508 direct references --
`crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.5.1#subdirectory=crdt-sync`
-- which parsed as an unpinned PyPI package that does not exist on PyPI. That
answer is undeterminable, not stale, so wake-alarm sat on gatelock-v0.4.1
against v0.7.1 and crdt-sync-v0.5.1 against v0.9.0 with a green gate. They are
git-tag dependencies now, judged the same way the Dart libraries are.
A ref that is a branch or a SHA still parses as before: there is no version
in it to compare.
The line is found textually when the key indexer cannot help. A TOML
dependency array holds bare strings with no `key =`, so these findings would
otherwise report line 0 and make the fix a search.
Both libraries also had their dependency upgrades land after their newest tag
(81d2493), so anything moving to "latest tag" would have taken the pre-upgrade
constraints. Cut 0.9.1 and 0.7.2; version bump only.
315 tests, 100% branch coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
Sixteen `>=` floors become exact pins across the two Python packages, taking
five majors with them: mypy 1.13 -> 2.3.1, pylint 3.3 -> 4.0.7, pytest 8 -> 9,
pytest-cov 5 -> 7, bandit 1.7 -> 1.9.4, plus ruff 0.15 -> 0.16.5 and
requests/types-requests. A `>=` floor is satisfied by everything newer, so it
told a checker nothing about whether these repos were actually current.
Verified in a clean venv per repo, which is what both CI workflows do
(`pip install -r requirements.txt`): gatelock 389 tests at 100% coverage under
xvfb, crdt-sync 432 tests at 100%. No test change was needed for any of the
five majors.
**Open, and deliberately not in this commit:** `.pre-commit-config.yaml` pins
these same tools AGAIN via `rev:` (ruff v0.15.2, mypy v1.13.0, pylint v3.3.2),
independently of requirements.txt, and the gate does not read `rev:` pins at
all -- so the linters CI actually runs are still on the old majors. Bumping
them is a separate and much larger job: ruff 0.16 raises 71 findings here
(CPY001 alone accounts for many, since this repo selects ALL) and mypy 2.x
raises 60, mostly stricter implicit-re-export rules. One major per commit, and
that is several.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
While trimming that file under the cap I had rewritten the parametrize as
`[*lg._EXPECTED_ISSUERS]`, which asserts only that the module agrees with
itself: deleting a spelling from _EXPECTED_ISSUERS would have removed the case
that tests it and left the suite green.
Confirmed by mutation rather than by inspection -- dropping the bare
"accounts.google.com" from the module now fails
test_both_google_issuer_spellings_are_accepted[accounts.google.com], where
before it would have passed. Google uses both forms, so accepting both is a
real contract worth pinning.
432 passed, still 100%, verified with HOME redirected at a temp directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
CI caught this: 8 tests failed on the runner with "/home/runner/.config/
crdt-sync/password could not be read". They passed here for the worst possible
reason -- that file exists on this machine, so the tests were reading the
user's actual sync password.
_CONFIG is a real FirebaseConfig, and link_google() calls
config.read_password(), which falls back to PASSWORD_FILE when given no path.
The linking tests now pass a MagicMock(spec=FirebaseConfig) with read_password
stubbed, which is what the one test that already asserted on the password was
doing; that test now shares the helper instead of keeping its own copy.
Adding the helper pushed the file to 265 lines, so the token-inspection tests
move into test_tool_link_google_token.py -- a three-way split matching the
module's three concerns (inspect / link / CLI) at 196, 82 and 223 lines.
Also fixes a mypy finding in the seed_session ordering test, which used
`list.append(...) or "value"` to record call order in a lambda. append returns
None, so the expression only worked by accident; replaced with named functions.
Verified the way the earlier local run could not: HOME pointed at an empty
temp directory, reproducing the runner. 432 passed, 1333 statements and 234
branches at 100%. ~/.config/crdt-sync/password untouched (mtime still Aug 4).
Worth keeping as a rule: for code that reads ~/.config, a green local run
proves nothing unless HOME is redirected first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Finishes the tool/ coverage task. seed_session.py is the seventh and last
module, split into seed_apps and main() at 224 and 222 lines.
The tests pin the two structural decisions the module's docstring argues for,
since neither is visible from its output: each app gets its own exchange and
its own credential store (one session copied across them would have every app
refreshing the same rotated token and invalidating the others), and
verification runs as a *second pass* after all sessions exist -- checking each
app as it is written would pass one that a later exchange then invalidates,
which is the exact failure being guarded against. Asserted by recording the
call order and requiring seed,seed,verify,verify.
Also covers the partial-failure path, which is the difference between a
copy-pasteable rerun and a hunt through ~/.config: SeedError carries the apps
that verified, and the rerun line lists the remaining ones and only those.
With every module at 100%, the config flip lands last as planned:
addopts += "--cov=tool"
source = ["crdt_sync", "tool"]
Done condition met -- 432 passed, 1333 statements and 234 branches at 100%,
tool/__init__.py included. `import crdt_sync` and `python3 -m
tool.google_id_token --help` both still work after the ruff autofixes, and
`pre-commit --all-files` is clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Sixth module of the tool/ coverage task, split into token inspection/linking
and _read_token/main() at 249 and 224 lines.
The failure this script exists to prevent is silent -- linking onto a new
account succeeds at the auth layer and is then denied every read and write by
rules that pin one uid -- so the two central assertions are made on the
request body rather than inferred from the response: the existing account's
idToken is always sent (omitting it turns accounts:signInWithIdp into "sign in
or sign up"), and a mismatched localId is a hard error whose message names the
console unlink step.
Also pins the deliberate exception handling in main(): LinkError, ConfigError
and FirebaseAuthError each return 1, while an unexpected RuntimeError is left
to propagate. That is the stated reason the handler names three types instead
of catching Exception -- a tidy "FAIL" line for an unanticipated bug would
hide it -- and it is only a real guarantee if something asserts it.
crdt_sync 365 -> 400 passed, still 100%. 6 of 7 tool/ modules done; only
seed_session.py remains before the config flip.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Fifth module of the tool/ coverage task, split into the GitHub-reading half
and main() at 216 and 248 lines.
This is a one-shot migration whose worst outcome is reporting success while
having moved the wrong bytes, so the tests pin the decisions that prevent
exactly that: reading blobs by sha rather than by path (the contents API
would return whatever is at that path *now*, which need not be what the tree
listing described), refusing a truncated tree instead of migrating part of
the repo, never fetching a skipped path, emitting the report before the first
write, and failing the run when the byte-for-byte read-back disagrees --
including the partial case, where one bad file out of two is a failed
migration rather than a 50% success.
Also fixes the S105 finding the previous commit's preflight tests carried:
`_PASSWORD` trips the hardcoded-credential rule on the binding name, the same
trap this task's notes already record. Renamed rather than suppressed. The
blanket rename briefly caught the module attribute `_PASSWORD_FILE` too,
which the suite caught immediately.
crdt_sync 333 -> 365 passed, still 100%. 5 of 7 tool/ modules done; the two
remaining are link_google.py and seed_session.py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
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
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
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
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
_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
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
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
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