The previous commit skipped the poweroff settle when `ssh_rc != 255`, on the
reasoning that a successful poweroff kills the ssh connection and ssh reports
255. That holds for the ssh transport and for nothing else. `_run_transport`
falls back to the serial console whenever ssh does not answer -- and the
README calls serial "the transport of record for shutdown tests" -- where the
exit code comes from serial_exec.py and is 0 even while the machine is already
on its way down.
Result, reproduced: `VMBOX_TRANSPORT=serial vm run <vm> 'sudo systemctl
poweroff'` reported "still running" for a guest that had cleanly powered off,
with SHUTDOWN{guest:true} in events.jsonl and "reboot: Power down" in the
serial log. The one thing this tool exists to get right, silently wrong. My
five verdict re-tests all went through ssh, so none of them caught it.
Grepping the serial log for shutdown markers instead does not work either:
they land a second or two after the command returns, so the check loses the
race. The fix waits a short grace period (default 3s) for any sign of a stop
before committing to the 30s wait. Warm runs go from 0s to 3s; that is the
honest price of a correct verdict.
Re-tested: clean poweroff, reboot and panic on BOTH transports, plus
tests/destructive_demo.sh 7/7.
Also, per "a fix that needs a human to remember a flag is not a fix": the X
session wait is now automatic on the branch that actually boots the guest --
the only one that can race tty1's autologin -- instead of requiring
VMBOX_X_WAIT. Warm runs never reach that branch. The env var remains an
override. README numbers corrected to the measured 14s/3s and ~100s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
`vm run` took 41s cold / 30s warm to run `true`. Both were vmbox waiting on
itself. Now 12s / 0s, with all five shutdown verdicts re-tested unchanged.
- _run_settle waited up to 30s for the kernel's "reboot: Power down" marker
UNCONDITIONALLY, including on the majority of runs that leave the guest up,
where that marker is never going to appear. It now settles only when the
guest has actually stopped, or when ssh returned 255 (what a successful
poweroff looks like from the host).
- The ssh readiness polls used a 5s ConnectTimeout. QEMU's user-mode
networking ACCEPTS the forwarded connection while the guest still has
nothing on :22, so each probe blocked for the full timeout instead of
failing fast: a guest reachable at ~10s was reported up at ~45s. Fixed in
both the sandbox and build paths, via an env override -- ssh honours the
FIRST value for an option, so a second -o could not have worked.
Making runs fast exposed a race: sshd now answers BEFORE tty1's autologin has
started Xorg, so the X environment could be missing on the first run after a
boot. `VMBOX_X_WAIT=<seconds>` waits for the session; it defaults to 0 so an
ordinary run never pays for a graphical session it is not using.
Build: the host's /var/cache/pacman/pkg is offered to the build guest
read-only (`readonly=on` AND an `ro` mount, because it is the real host cache)
as an extra CacheDir after the guest's own writable one, so a package the host
lacks still downloads rather than failing the build. This removes the 396 MiB
the build used to fetch. Host cache verified byte-for-byte identical across
four builds: same file count, same size, same mtime.
Build time itself moved only 103.8s -> 100.3s. Per-phase timings (also added
here) show why: 47s of it is the build guest's cloud-init first boot, and that
is NOT datasource probing -- `cloud-init analyze blame` totals 2.27s. Left
unexplained rather than guessed at; SESSION_RESULTS.md records the evidence.
Also: `vm scp`, which lib/ssh.sh could always do but the CLI never exposed,
and provision.sh split at the 250-line cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Testing boot_recovery produced a guest that cannot boot: boot-repair read
/boot/vmlinuz-linux and initramfs-linux.img on this BIOS/GRUB guest as
"orphaned kernel files shadowing the ESP" and deleted them, leaving the
sandbox at a GRUB prompt. That is a legitimate sandbox outcome -- destroying
the guest is what the tool is for -- but vmbox handled it by hanging.
Two independent unbounded waits, both fixed:
- vm_wait_ssh counted only its `sleep 2` calls, ignoring the up-to-5s
ConnectTimeout each failed probe spends inside ssh. A nominal 150s wait ran
past 400s (measured, then cut short by the harness). It now works to a wall
clock deadline: the same guest is now reported unreachable in 156s.
- _run_recover_rc reboots the stopped guest to read the recorded exit code,
and fell through to an unbounded serial console read: 180s for a prompt
that never comes plus 300s for a command that never runs, all AFTER the
verdict had already been printed. Recovery is best-effort by definition, so
both bounds are now short (60s/30s) and overridable.
serial_exec's timeouts become env-overridable to make that possible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
README documented `bats tests/test_vmbox.bats` covering "name validation,
meta handling, serial rotation and the full verdict table", and the handoff
made it a done-condition, but the file did not exist. Written to that spec:
39 tests, no VM boot, well under a second.
VMBOX_HOME is redirected to a per-test mktemp dir, so the suite can create
and delete VM state without any chance of clobbering a live sandbox's overlay
or pidfile; one test asserts every path helper stays inside it.
The verdict cases encode the distinctions the design turns on rather than just
exercising the function: guest:true with a truncated serial log is DIRTY (3)
and not a clean poweroff, POWERDOWN alone proves nothing and must read as
"none", an unrecognised shutdown reason fails closed at 6 rather than passing
silently, and events.jsonl is read last-event-first so a previous boot's
poweroff is never mistaken for this run's outcome.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
The image ships Xorg + i3 on tty1 precisely so locker and i3 tests can run,
but `vm run` executes through `sh -c`, which reads no profile. Every X tool
therefore landed with no DISPLAY and no XAUTHORITY and failed with
"Could not determine i3 socket path" -- indistinguishable from "this sandbox
has no X server". testsAndMisc's i3-configuration installer failed that way;
with the environment resolved it exits 0 and i3-msg reports {"success":true}.
XAUTHORITY is the awkward half: startx generates /tmp/serverauth.XXXXXXXX
with a fresh random suffix every boot, so it cannot be hardcoded. Resolve it
from the running Xorg's own -auth argument, falling back to a newest-first
glob.
The `[ -f ]` guard on the source line is load-bearing, not defensive
dressing. In POSIX sh, `.` on a missing file is a SPECIAL BUILTIN error that
exits the shell outright, and `|| true` does NOT rescue it -- written that
way, every `vm run` against a base image predating this snippet silently
produced no output at all. Found by doing exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Three changes from kuhy's follow-up:
- An explicit standing instruction that when vmbox breaks the next session
FIXES it -- not works around it, not reports it as a limitation, not quietly
falls back to testing on the host. Bugs found by real use are the return on
having built this.
- Guidance to attach to an existing sandbox or create one as appropriate,
rather than tearing down and rebuilding per command (`vm reset` is ~2s).
- A build-speed job with measurements rather than guesses: the last build
pulled 197 packages / 396 MiB over the internet and spent 45s waiting on
cloud-init, while the host's 114 GB pacman cache already holds every one of
the packages checked. Exposing that cache read-only is the obvious first
move; trimming base-devel and shortening the cloud-init wait follow.
The smoke gate stays regardless -- a faster build that fails open would be a
bad trade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Two jobs, deliberately ordered. First: run real linux_configuration work
through the sandbox (the 8 Arch-only CI tests, night-lockdown, boot_recovery,
the hosts guard) and fix vmbox wherever it breaks -- it has only ever been
driven by one session against a handful of scripts, so the ~1090-file surface
will find things.
Second, and only if the first says vmbox is trustworthy: the CLAUDE.md rule
that routes script testing here by default. A rule pointing at a flaky tool is
worse than no rule, so that ordering is the point. Four questions have to be
settled before writing it -- what triggers it, whether it replaces or precedes
the host run (see feedback-verify-real-deployment-path), the hardware escape
hatch, and keeping it short since CLAUDE.md is always-on context.
Carries forward the traps that cost real time this session: diagnose an
unreachable guest with `systemctl list-jobs`, never grep a bash -x trace for
the failure, and fix the image live over the serial console rather than
rebuilding once per hypothesis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Captured a real 1280x800 PNG of a sandbox's i3 session straight from qemu's
framebuffer -- no VNC or SPICE viewer installed on the host. This is the path
locker and fullscreen-gate tests use, and it is safer than testing those on
the host, where a fullscreen locker can hijack input and steal focus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
`vm reset` killed the VM and immediately recreated the overlay, without
waiting for the process to actually exit. The next boot then read a
half-detached file and failed with "sandbox did not become reachable" -- a
symptom that appears one step AFTER the cause and says nothing about it.
Reset itself was always correct (verified by hand: after
`rm -rf --no-preserve-root /etc /usr`, a reset guest reports
NAME="Arch Linux" and has /usr/bin/bash back). This only bit when a reset
followed a still-shutting-down VM closely enough to race it, which is exactly
what the destructive demo does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Measured between two sandboxes: 3 packets transmitted, 3 received, 0% packet
loss, 0.23 ms average.
The previous commit called this broken. It was not: the dgram transport worked
all along, but the odd/even pairing computed the wrong peer port for even
indexes, so a VM pointed `remote.port` at a port nothing had bound. Both ends
came up, addresses were right, and zero frames crossed -- indistinguishable
from a dead transport, and nothing in any log said otherwise. The tell was
`ss -lunp`: the bound ports were 12349 and 12350 while the guest was sending
to 12348. The test then compounded it by using indexes 4 and 5, which belong
to DIFFERENT pairs (4 pairs with 3, 5 pairs with 6).
Pairing is (1,2), (3,4), (5,6)... Two sandboxes created back to back are wired
together, which is the common case.
mcast= remains genuinely unusable here and is documented as such so nobody
re-tries it: lo has no MULTICAST flag and there is no multicast route, so qemu
accepts it and drops every frame.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
The peer-address service read a `vmbox.peer=` kernel argument that the
launcher never passed -- the reader was written and the writer forgotten -- so
eth1 came up DOWN with no address on every guest. It now derives the address
from the NIC's own MAC (the host allocates 52:54:00:be:ef:<index>), which
removes the coupling entirely: nothing has to be passed in, so the launcher
and the guest cannot disagree about which index a VM has.
That fixed the addressing but NOT the connectivity. `mcast=` cannot work on
this host at all -- `lo` carries no MULTICAST flag and there is no multicast
route, so qemu accepts the option and silently drops every frame, which is the
same "flags accepted, nothing happens" trap as POWERDOWN. A listen=/connect=
socket hub over loopback TCP was tried next: the hub port listens, both
guests bring eth1 up with the right addresses, and ARP still fails.
Documented as a known limitation rather than papered over. Sandboxes work
fully on their own; two guests cannot talk to each other yet.
Also corrects the destructive demo: a running Linux SURVIVES `rm -rf /usr`
because processes hold their binaries through open file handles, so sshd keeps
answering. The honest proof of destruction is that the machine cannot boot
again, so the demo now reboots it and asserts it does not come back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Ran kuhy's real setup_midnight_shutdown.sh inside a sandbox -- the installer
whose own output says "There is NO disable option - this is protected by a
monitor service". It created /etc/shutdown-schedule.conf under guard-lib's
immutable protection and enabled shutdown-timer-monitor.service,
day-specific-shutdown.timer and shutdown-timer-monitor-watchdog.timer.
The host was provably untouched afterwards: base image checksum OK, /etc/hosts
still carrying its +i flag, and the host's own shutdown-schedule.conf still
stamped 11:21 that morning rather than the 21:19 when the guest wrote its copy.
Two properties of that installer are now documented, both found by running it:
it is interactive (`read -r -p 'Do you want to proceed?'`, so pipe `echo y |`
since vm run closes stdin deliberately), and it is subcommand-driven, so a
bare invocation prints usage and changes nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
9p exports a directory TREE and does not follow a symlink pointing out of it.
The share directory held `testsAndMisc -> /home/kuhy/testsAndMisc`, so the
guest mounted /mnt/hostrepo successfully, listed the symlink, and then got
"No such file or directory" following it -- the target path does not exist
inside the guest. Measured in a live sandbox, not assumed.
`vm share <path>` now bind-mounts the directory into the share and remounts it
read-only, so the guest sees real contents while still being unable to write
to the host copy. `vm unshare <name>` reverses it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
The destructive demo asked the guest to confirm its own destruction, one step
after deleting /usr -- which removes bash, sshd and agetty. Nothing would have
been left to answer, so the check would have FAILED on a step that actually
succeeded. It now proves breakage from the host (the overlay grew, and the
guest can no longer answer at all), which is the same evidence the shutdown
verdict relies on and for the same reason: a corpse cannot report its own
death.
Step 3 also no longer recreates the sandbox. `vm reset` in step 4 is the claim
under test; recreating first would have quietly tested something easier.
README's verdict table now carries the MEASURED qmp events per guest command
rather than the inferred ones, and states the finding that motivates the whole
design: ssh returned an identical "Connection closed by remote host" for the
poweroff, the reboot AND the panic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
`_run_exec "$name" "$@"` sat on its own line under `set -euo pipefail`, so a
non-zero return aborted the script before the verdict was ever computed. ssh
returns 255 when its connection dies -- which is exactly what a SUCCESSFUL
guest poweroff causes. The single case this tool exists to report on was the
one case that silently printed nothing.
Caught by the acceptance demo, not by the unit tests: verdict_report was
correct in isolation the whole time and passed 8/8 there. Only running the
real CLI against a real poweroff exposed the wiring.
Verified after the fix, end to end:
==> Running in 'fixtest': sudo systemctl poweroff
Connection to 127.0.0.1 closed by remote host.
ok VERDICT: clean poweroff -- guest powered itself off and reached
'reboot: Power down'
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
shutdown_verdict_demo.sh exercises every branch of the verdict table against a
real guest -- clean poweroff, reboot-instead-of-poweroff, kernel panic, and a
command that leaves the machine running. The failure branches matter as much
as the success one: a verifier that has only ever returned "pass" is untested.
destructive_demo.sh is the acceptance test for the tool as a whole: destroy
/etc and /usr in the guest, assert the guest really is broken, reset with one
command, assert it is pristine again, then assert the HOST is untouched
(/etc/hosts attributes unchanged, base image hash unchanged, /etc and /usr
still present).
Measured on the first real run, which confirms the design's central claim:
QMP reported SHUTDOWN {"guest": true, "reason": "guest-shutdown"} and the
serial log reached "reboot: Power down", so the verdict was "clean poweroff"
with exit 0 -- while ssh saw only "Connection closed by remote host", which
would have been indistinguishable from a crash or a dropped link.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Root cause, confirmed causally rather than inferred: masking
systemd-timesyncd (which vmbox does so `-rtc base=` stays pinned, since
timesyncd otherwise resets the guest clock to real time) left
systemd-time-wait-sync waiting for a sync that can never arrive. That unit sat
in "start running" forever and every other job queued behind it as "start
waiting" -- sshd included -- so the guest never reached multi-user.target. It
booted to a login prompt and looked perfectly healthy while being permanently
unreachable. The fix is one line: mask both units together.
Four earlier hypotheses were each disproven by measurement. What finally
worked was asking the guest instead of reading absence-of-log-lines:
`systemctl is-system-running` returned "starting", and `systemctl list-jobs`
put systemd-time-wait-sync at the head of the queue. Stopping it made sshd
active and bound :22 within seconds, and ssh from the host then succeeded.
Rule for next time: a unit that is enabled, has satisfied dependencies, and
neither starts nor fails is QUEUED -- check list-jobs first.
The smoke gate added earlier did its job here: it caught the unreachable image
and refused to seal it, keeping the sandbox so its logs survived.
Also fixes serial_exec markers to be plain alphanumerics -- shell
metacharacters get echoed and line-wrapped by the tty, which made them
unreliable to match and produced spurious timeouts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Committed by accident: running systemd-ssh-generator --help during
investigation made it treat '--help' as its output path and write generator
units there. Not part of vmbox.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
The Arch cloud image leaves sshd enabled but never started -- no start line,
no failure line, dependencies satisfied. Four hypotheses (NTP, cloud-init
clean, a socket-disable, a blocking start) were each disproven by measurement,
so rather than keep paying a ~10 min rebuild per guess this adds the transport
the tool should arguably have used all along.
For the case vmbox exists for, serial beats ssh on the merits: it keeps
printing straight through the poweroff that kills an ssh connection, and it
works when sshd does not. `vm run` now picks ssh when it answers and the
serial console otherwise, and recovers the guest exit code either way. Guests
get a second serial port (ttyS1) on a unix socket with a root autologin, kept
separate from ttyS0 so driving a shell never interleaves with the append-only
log the verdict is read from.
serial_exec.py waits for an explicit per-command end marker rather than a
fixed sleep. That is not a detail: an earlier probe used a 4s window, the next
command was typed while the previous one still ran, and the tty echoed that
input -- which reads exactly like output in a transcript and produced a
confident but wrong conclusion. It also strips ANSI CSI and systemd's OSC 3008
shell-integration blocks, verified against a real captured boot log.
Also: a vmbox-sshd-kick unit that forces sshd up after multi-user.target as
belt and braces, and the smoke-test sandbox renamed to a name that actually
passes validate_vm_name -- "__smoke" was rejected, so the gate silently never
ran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
Much of testsAndMisc/linux_configuration cannot be tested on the host: 15
files use `chattr +i` (so a bad test is not simply revertable), the shutdown
installers write units that power the machine off, and boot_recovery installs
pacman hooks. The repo's own CI says as much -- its Ubuntu job runs only the
side-effect-free tests, noting the rest "drive pacman, /etc, systemd or adb".
vmbox runs them in a throwaway Arch guest and verifies the result from the
HOST, which is the part that is not obvious: a successful shutdown kills the
connection carrying the report, and ssh returns 255 for a clean poweroff, a
crash and a dropped link alike. So the verdict comes from two artefacts that
outlive the VM -- a QMP event log written by a recorder attached at launch
(attach later and qemu has already exited), and the serial console log.
Five outcomes are distinguished: clean poweroff (SHUTDOWN guest:true plus a
serial tail reaching "reboot: Power down"), dirty stop, guest-reset (rebooted
instead of powering off), GUEST_PANICKED, and hung. A POWERDOWN event proves
nothing -- it means the request was delivered, not that anything acted on it.
Reset is a qcow2 backing-file trick: the golden image is built once and sealed
(chmod 444 plus a sha256 sidecar verified on every launch, since booting the
base would silently invalidate every overlay), and each sandbox is a thin
overlay, so reset is rm + recreate. No root anywhere -- user-mode networking
with per-VM forwarded ssh ports, and a socket multicast segment for VM-to-VM.
The guest clock is pinned and re-applied on every launch, because the shutdown
installers gate on a 21:00-05:00 window; NTP is masked in the image or it
silently undoes the pin. Repos are cloned from a read-only share scoped to an
explicit directory, never $HOME, and credentials are sentinel values.
`vm build` ends by booting a throwaway sandbox off the sealed image and
refusing to ship if ssh does not answer. Several distinct faults produced an
identical "boots fine, connection reset" symptom; checks inside the build VM
only prove state at seal time, so the gate has to be an end-to-end boot.
Tests: 8 bats cases over the verdict table and name/meta/serial handling;
shellcheck and ruff clean; every file within the 250-line cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
database.rules.json carries YOUR_FIREBASE_UID on purpose -- this repo is
public and the uid is the one value that must not be in it. But nothing
stopped anyone publishing the template as-is, and rules pinned to a uid that
matches no account deny every read and write. The symptom is every device
failing at once with a bare permission error, which reads like an outage
rather than a bad paste.
render_rules.sh substitutes the uid from ~/.config/crdt-sync/firebase.json
and fails closed on anything unsafe: a surviving placeholder, missing
`auth != null`, a missing uid pin (that one matters -- `auth != null` alone
lets any stranger who self-registers through the public Web API key read
everything), or output that is not JSON.
Verified: its output is byte-identical to the deployed
~/.config/crdt-sync/database.rules.json, and the live database still answers
an unauthenticated read with 401. All three failure paths exercised.
Also corrects the README, which described the in-repo copy as "the deployed
rules, with the real uid filled in" -- it never was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
lyricanki's Linux desktop cannot use google_sign_in (no Linux
implementation) and its loopback replacement is dark until a Desktop OAuth
client exists. Hiding the button there makes the screen silently offer less
on one platform, which reads as a bug.
googleUnavailableReason renders a *disabled* button plus the reason. That is
not the "control that can never succeed" trap: the trap is a live-looking
button that reports a fake cancellation, and a null onPressed cannot be
tapped at all. Null stays the default, so every existing host is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
An optional '' default reads as "this parameter is usually irrelevant", and
it made `avoid_redundant_argument_values` fire in the first app to pass ''
explicitly -- pushing that app toward deleting the argument, which is exactly
the line that must stay for when the Desktop client id arrives.
Requiring it forces each app to state which halves it configured. Passing ''
still means "not configured"; it just has to be said out loud.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Five apps had copied google_sign_in_backend.dart; home_inventory's copy was
byte-identical to todo's. lyricanki was about to become the sixth, which is
the mistake this package exists to prevent. So the plugin call moves here.
Desktop is the new half. google_sign_in ships android, ios and web
implementations only -- there is no google_sign_in_linux -- so lyricanki's
real GTK build could never have used the plugin, and "one tap" would have
meant "one tap on the phone, type a password on the PC". The OAuth loopback
flow covers it: bind a socket, open the system browser, catch the redirect,
exchange the code with PKCE. Both halves return a Google ID token of the same
shape, so signInWithGoogle cannot tell them apart.
googleSignInSupported takes the client id rather than reading a const. In
todo the emptiness check was a second conjunct at the call site, and moving
the fetcher here without it would have let a forgetful app ship a button that
always reports "cancelled" -- todo's own file documents that failure. Taking
the value as an argument makes the check impossible to drop.
The decision (which flow, or none) is pure and covered; the two one-line
delegations that reach a platform channel and a real browser are the only
suppressions. 83 tests, 100% coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Two different git refs for the same package name cannot coexist in one
resolution, so an app taking both sync_settings_ui and crdt_sync_flutter --
which is the intended pairing, and what lyricanki now does -- failed version
solving while this stayed pinned to v0.10.0.
v0.11.0 only widens what FirebaseAccount.tryParse accepts, so nothing here
changes behaviour: 29 tests green, analyze clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Deleting prompts/dopamine-ux-01-motion-tokens.md (the completion protocol) left
00-INDEX.md's "Distributed copies" table citing a path that no longer exists, so
verify_prompts went from 3 pre-existing failures to 4. Exactly the dangling
anchor 44bc12b fixed after the doc split.
Asserts the path ABSENT rather than dropping the row, which keeps the check
two-directional: a finished prompt reappearing in a repo is itself a failure,
because the next session would re-run work that is already done. Verified both
ways -- recreating the file fails the check, removing it passes.
This recurs for 02-09, so the comment says to move a row as each completes.
Back to the same 3 failures as before this session, all in ~/screen-locker and
all pre-existing (prompts 04/05).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
flutter_secure_storage 9 pins flutter_secure_storage_windows 3, which pins
win32 5 -- and share_plus 13 needs win32 6. Any app depending on both
(lyricanki, the first consumer) failed version solving outright, and pub's
suggested way out was downgrading share_plus, which would undo a just-shipped
Android share fix.
home_inventory already runs 10.3.1, so this moves toward the fleet rather than
away from it. 35 tests still green on the newer plugin.
Released as v0.1.1 rather than re-cutting v0.1.0: that tag is already pushed,
and moving a published tag breaks anyone who resolved against it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
All four `done` conditions hold: motion.md exists at 121 lines, the 250-line
cap and palette_check both exit 0, and both tags are cut (design_system-v0.2.0,
web_ui-v0.3.2).
Deletes the distributed prompt copy per the protocol in 00-INDEX.md -- a
finished prompt left in a repo is indistinguishable from a pending one, and the
next session re-runs it. The canonical copy in dopamine-ux/ stays as the record.
Prompt 01 gated the other eight; 02-09 are now unblocked, and 06/07/08 have the
literal tag refs they were waiting on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dopamine-ux prompt 01. The token layer froze colour, spacing, radius and type,
but every animation in every app picked its own duration and curve out of the
air -- so "200ms ease-in-out" and "180ms ease-out" both existed and neither was
wrong, because nothing said what right was.
unified-design-system/motion.md is the freeze: four durations, three curves,
a haptic vocabulary mapped onto Flutter's HapticFeedback, and three sound cues,
each with its one-line rationale. Shipped as AppDuration/AppCurve in Dart and
--duration-*/--ease-* in CSS, so a consumer imports the scale instead of
transcribing it.
Three things worth stating, because they are the parts easy to get wrong:
- Reduced motion collapses the *tokens themselves* (CSS @media, Flutter
disableAnimations), so no consumer has to remember to branch.
- Haptics and sound are NOT motion and are deliberately left alone by that
preference. Suppressing animation is not a request to lose confirmation that
a tap landed; the sound opt-out is a separate, user-facing setting.
- Reward the recording, never the number recorded. The haptic fires on logging
a meal, not on hitting a calorie target -- the latter rewards eating less
rather than recording honestly, and corrupts the app's own data.
gatelock stays untouched: Tkinter has no animation framework, so motion there
is marked `n/a (tk)` rather than shipping a dead constant that reads as a
supported feature.
Tests assert the frozen values on both sides, and that the reduced-motion block
collapses every non-zero step -- verified by mutation: deleting one collapse
line fails the suite. flutter analyze / tsc / eslint clean, 68 Dart and 88 TS
tests pass, palette_check and the 250-line cap still exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`crdt_sync` is pure Dart so it can run from CLI tools and headless systemd
jobs, which means it takes closures instead of depending on
flutter_secure_storage and google_sign_in. Every Flutter app then wrote the
same ~390 lines to supply them, and five apps ended up carrying near-identical
copies of firebase_backend.dart.
Those copies drifted. `todo` gained an opt-out flag and a seeded-session path
that home_inventory and wake_alarm never received, so the same bug was fixed
in one copy and left live in the others -- which is the actual cost being paid
here, not the line count.
This package owns that glue once: the keystore adapter, the persisted
per-install device id, the account store and a one-call bootstrap. A new app
needs a SyncApp const and `openSync(kApp)`.
The wrapper routes are a parameter rather than a constant, because todo's copy
hardcoded its own `~/.config/todo/firebase_auth.json`. An extraction only one
app could adopt would be worse than the copies.
Also documents what made this hard to find: crdt_sync_dart's README covered
only the *old* GitHub transport and never mentioned the Firebase surface at
all, and sync_settings_ui -- a complete drop-in settings screen -- had no
README whatsoever.
The five existing apps stay on their copies and on crdt_sync_dart-v0.10.0.
Migration is forward-only and those fallbacks are load-bearing; converging
them is a separate task, and the README says so.
35 tests, 100% line coverage, no suppressions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
Both the Google one-tap path and the seeded-session path deliberately write
`FirebaseAccount(email: ..., password: '')` -- on those devices the refresh
token in the keystore is the credential, and there is no password to store.
`tryParse` rejected exactly that blob. So a device signed in with Google
wrote an account marker it could never read back: every later launch saw
"not configured", fell through to the mirror, and left a perfectly good
refresh token unused in the keystore beside it.
Only widens what parses, so no caller that previously got an account back
can regress. Every call site already guards `password.isEmpty` and passes
null for it -- meaning "no password on this device" -- which is precisely
the case this now lets them see. An empty *email* stays rejected: it
carries no identity at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
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
Per the file's own closing instruction -- a finished prompt left in the repo
is indistinguishable from a pending one, and the next session would re-run it.
1. web-ui-tests green (53f2816). Not a stale dist/: 16 files differed only
by a trailing newline tsc does not emit, left by end-of-file-fixer before
the dist/ exclude existed, and masked locally by tsc -b's incremental
cache.
2. All of tool/ at 100% branch coverage, with source = ["crdt_sync", "tool"]
in effect (6d64f9e). 259 -> 432 tests.
3. The three dirty items resolved: seed_session committed (9d638fb),
firebase-debug.log gitignored and deleted (e0eef3d), staged/ applied into
~/.claude (e0cf24a there) and removed.
4. The design-system skill pointer corrected, in ~/.claude e0cf24a.
Two things this leaves open, deliberately, neither part of the four tasks:
~/.claude/scripts/finish.sh is 309 lines and over the cap (it blocks nothing
today -- the hook gates staged files only), and phone_deploy.sh's
build/install/launch tail is still unexercised because it needs a real APK
install on the phone.
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
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