They were only reachable through test_changed.sh. Add them to the
shared-gates workflow (with pre-commit, and apt PyYAML for the
/usr/bin/python3 hook-id helper) and to CLAUDE.md's test command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
`full` ran every manual-stage hook in one pre-commit call, which takes
over 60 s in screen-locker and leetcode-guard. `step <hook-id>` runs one
manual hook on --all-files and records "<tree> <id>" in
$GIT_DIR/gate-steps (per-worktree); the step that completes the set
writes the stamp exactly as `write` does. Dirty trees and non-manual ids
are refused with exit 2. Hook ids come from scripts/gate_stamp_hooks.py
(PyYAML, /usr/bin/python3), which treats an absent default_stages as
"every stage", matching pre-commit, so the steps cover what `full` runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
sync() reads the pool, waits on the network, then writes its merged copy
back without a lock. A second write landing inside that window was
silently overwritten. Marking day A and then clearing day B within a
second or two lost the clear, so B stayed a sick day.
Every background sync now holds a module lock, and each write tries it
without blocking. A click that lands mid-sync shows "Syncing -- try again
in a moment." in the panel and calls on_saved with nothing. The phone's
picker avoids the same race by making the grid inert while it syncs.
The red-first test pins the lost write with a sync stub that reads, waits,
then writes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
`sickdays gui` opens a Tk window modelled on the phone's Sick days screen:
a Monday-first month calendar (cat-2 fill, a dot for kept lockers, past
days faded and inert), a five-switch panel for the clicked day, and the
"Marked sick" list with "Kept: ..." lines. A free day is refused with the
phone's wording. Each write starts sync_quietly() on a non-daemon thread.
The window class is "sickdays" and Escape closes it.
sickdays.gui.KeepPanel is the panel on its own, a plain tk.Frame with no
root, mainloop, grab or wm_* call, so screen-locker can embed it under its
lock-screen grab. `import sickdays` and the rest of the CLI never import
tkinter: the CLI loads the gui module by name only for `gui`, and a
subprocess test checks this.
The tests drive real Tk on a withdrawn root, and CI now runs them under
xvfb-run, as gatelock's CI does. Adds a sickdays.desktop launcher, which
install.sh installs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
A sick day may keep some lockers active: a locker stands down iff the day
is sick and the locker is not in that day's keep set. New record field
`keep` = [sorted, de-duplicated locker ids, hlc].
- add/remove write state, actor and keep on one tick (keep=[] on a clear),
so a stale keep cannot survive a clear + re-add.
- set_kept_lockers re-stamps only keep and actor; state keeps its clock, so
a keep edit never re-asserts sick over a concurrent clear. Ticks are
issued after every clock already on the record (clock-skew safe).
- Reading: no keep (0.1.0) or keep older than state -> keep nothing;
malformed keep -> every locker kept (fail closed, logged); unknown ids
ignored.
- API: stands_down, kept_lockers, set_kept_lockers, add_sick_day(keep=),
SickDay.keep, LOCKER_* / LOCKERS. is_sick_day unchanged.
- CLI: add --keep, keep DAY [LOCKER ...], list shows keep, check --locker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
Also fixes a ~5% flake in the hmac round-trip test (freedays and sickdays):
load_key strips whitespace, so a random key edged with a whitespace byte
never compared equal to the raw bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
sickdays: per-date pool, no cap or reason, add/remove today or later only,
import_legacy for the migration. LWW state flip per date (no tombstones),
Firebase RTDB sickdays-sync/devices/<uuid>/sick_days.json, HMAC audit trail.
Readers fail closed (unreadable pool = not sick, logged); writers refuse a
corrupt pool. Free-day cross-check via a lazily imported freedays predicate.
earned_time: resolve(..., sick=True) floors the final shutdown at 20:00
(capped at the ceiling); gaming, terms and base unchanged. Result dataclasses
moved to _resolution.py for the line cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
From TUTOR_QUOTA_FROM (2026-10-12) credit_units scales the tutor's deduped
day minute sum to min(60, minutes * 60 // quota); reward and penalty are
unchanged, every quota <= 60 so the switch is raise-only. Earlier days pay
one unit per minute as before. WORKDAYS is a literal copy of freedays'
(importing freedays pulls crdt-sync; earned_time is stdlib only), guarded by
a drift test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E57caQF4KXwEvqEnKtJiHM
Rows carry no earner name, so only the ledger path moves; the tutor copies
its data dir (old rows included). Pre-cutover AUTOMATION (anki quota),
LADDER and the waiver are unchanged. Same minutes, cap, ladder, TUTOR_FROM
and confirmed_on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YADHKXzZhhePzQ2rNoXFcK
AUTOMATION_TUTOR: 1 gaming min per unit (minute), max_units 60; TUTOR_LADDER
automation Rung(1, extra=1), rungs still sum 250 (23:00 / 18:50).
credit_units sums each row's units (ROW_UNITS: tutor detail.minutes, a row
without it = 15); a repeated entry_id pays once at its smallest count; an
invalid minutes field is refused by tutor_match. An all-block day resolves
exactly as 0.7.0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9cLaZkZaH7Zi2K63uWuoB
teatr-pw and teatr-pw-pytania (created ad hoc 2026-10-09) had no shim, hook or
'## Commands' section: the gate was only ever rolled out by hand. The
installer writes shim + pre-commit hook + CI workflow and bootstraps the
Commands section and scripts/test_changed.sh, exiting 1 while <FILL IN>
remains.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjyHmRrjZssaxXDWcGuCeQ
A repo with `.dep-freshness-no-workflow` at its root keeps the local pieces
(delegate script, pre-commit hook) but the installer no longer creates or
rewrites `.github/workflows/dependency-freshness.yml` there. teatr-pw
deleted its workflows on purpose (no GitHub Actions minutes on a private
repo; its pre-push hook runs `--all --strict`), and without this every
fleet sweep would put the workflow back.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8tUQzqQmiidbg8ZXmeQAg
TUTOR_FROM = 2026-10-10 (the day after the deploy). The automation tutor
keeps confirmed_on=None, so it stays "new".
Ladder floor: a rung comes off the floor only for a pure bonus (no
penalty_from) or an earner whose penalty has started -- the same
_penalised() rule (first_credits -> penalty_start, confirmed_on fail-closed
fallback, penalty_from without first_credits) the gaming base uses. Before
this, a never-credited tutor would have lowered 2026-10-10's floor by its
full 50-minute rung (19:50 -> 19:00) even with first_credits. Maturity can
only raise time: new property test against the 0.6.0 floor formula.
ruff 0.17.0 (dependency-freshness): CPY001 ignored -- personal repo, no
copyright policy.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdfmsGMht9ypyvxGPmNF1V
CI's earned_time pre-commit flagged tests/_maturity_rows.py: the local
--all-files run predated git tracking of the new files. Same exclude as
steam-backlog-enforcer's tests/_*.py helpers. No package code changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdfmsGMht9ypyvxGPmNF1V
How far to trust a gate is computed from its signed ledger: new / maturing /
mature (>= 14 verified credit days, first >= 21 days old), with a read-only
CLI table. Only signed, non-manual, paying credits count; a gate's own
re-evaluation grant (detail.grant_of, book-guard bonus: rows) counts at
amount 0.
Earner.confirmed_on (hand-set) is new. penalty_start = max(penalty_from, day
after first real credit, day after confirmed_on); resolve/base_for take an
optional first_credits map and can only delay a penalty, never add one.
Omitted, every day resolves exactly as in 0.5.0.
Fail closed: a gate with confirmed_on and no credit on record (unreadable
key/ledger, deleted or emptied ledger) falls back to max(penalty_from, day
after confirmed_on), so deleting a ledger cannot lift a penalty.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdfmsGMht9ypyvxGPmNF1V
freedays 0.3.0 pinned crdt-sync v0.9.1 exactly, so any consumer that
moved to crdt-sync 0.9.2 (screen-locker) could not resolve both in a
clean venv. No code change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
**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
From LADDER_FROM (2026-10-10) shutdown is read down from sleep: ceiling =
WAKE_MINUTES (07:00) - 8h = 23:00, floor = ceiling - every earner's rung
(workout 110, LeetCode 50, reading 30, Anki 25, Automation 25) = 19:00.
A second workout earns nothing more. Earlier days resolve exactly as in
0.3.0 (frozen 23:00 ceiling); gaming is unchanged for every input.
WORKOUT gets a signed ledger (~/.local/share/workout_locker/ledger.json,
written by screen-locker) and workout_match; a rest_day row counts only if
declared before its day began. New API: first_credit_at, credit_units,
day_window, shutdown_minutes_for, extra_shutdown_minutes_for,
shutdown_ceiling_for, on_ladder, LADDER/Rung, LADDER_FROM, WAKE_MINUTES.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrjUTo1yhgAsUUasdw4bpG