Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.
TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
Moving bump_patch_version.sh into .pre-commit-config.yaml introduced three
bugs, all reproduced in clean clones of HEAD before fixing and re-verified
after:
1. DATA LOSS. pre-commit stashes unstaged changes and restores them with
`git apply`. The bump rewrites pubspec.yaml inside that window, so an
uncommitted edit near the `version:` line made the restore conflict:
the commit aborted AND the developer's edit vanished from the working
tree, recoverable only from $PRE_COMMIT_HOME. Only non-mutating checks
belong in that file.
2. A commit rejected by the file-length gate still had its version bumped
and staged, riding into the next successful commit. `always_run: true`
with no `fail_fast` meant "ordered last" did not mean "runs last only
on success" -- which is exactly what the comment above it claimed.
3. `files: ^pubspec\.yaml$` narrowed the crdt_sync override check from
unconditional to staged-only, so a poisoned pubspec arriving by pull or
merge was invisible to every later commit that did not touch pubspec.
The hand-written hook this replaced checked it every time; that
narrowing quietly widened the hole the check exists to close.
The fix keeps the framework for the read-only gates and puts the mutation
back outside it: install_hooks.sh now writes a wrapper that runs
pre-commit's generated hook first and bump_patch_version.sh only if it
passed. Plus fail_fast, and always_run on the crdt check.
Also fixes three stale references this refactor created: a load-bearing
layout comment citing the renamed _buildRaw, bin/todo_desktop.dart naming
two split-away test files, and CLAUDE.md giving two different suite
runtimes.
Verified: unstaged pubspec edit preserved through a commit; a rejected
commit leaves nothing staged; a merged-in override is caught on an
unrelated commit. 313 tests, 100% coverage, gate exits 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CLAUDE.md 925 -> 165. 789 of those lines were a verbatim paste of upstream's
flutter/flutter rules.md -- 85% of an always-loaded file given over to a
document nobody edits. It moves to five topic files under docs/, linked
rather than @-imported: inlining it back would pass the cap while keeping it
in context on every turn, which is the cost the cap exists to remove.
Every `###`/`####` heading in the upstream block survives the split; the two
that do not are its own wrapper headings, replaced by the per-file titles
(verified by diffing the sorted heading lists).
desktop-performance-findings.md 278 -> 73 + 216. It was already narratively
two-part; the author signposted "Part 2" explicitly at the seam.
Also updates two things that had drifted: the suite is 313 tests at 100%
coverage (not 238), and Conventions now documents the cap, where the gate
lives, and the two patterns used to split without breaking callers
(re-export barrels, and `part` files where a private State must stay in one
library).
`bash ~/utils/scripts/check_file_length.sh --all` now exits 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Launching the app died with an unhandled SocketException whenever anything
already held the wrapper port -- from `todo` and from `./run.sh` alike, since
run.sh execs the same entry point. What was holding it was a wrapper from an
earlier session that had outlived its window: /proc/<pid>/exe read
"/opt/todo/bin/todo_desktop (deleted)", i.e. install_arch.sh had replaced the
binary underneath a process that kept serving anyway.
That orphan is manufactured by the handoff branch in bin/todo_desktop.dart.
When the launched browser exits within 5s the launcher assumes it handed the
URL to an existing window and keeps serving rather than pulling the server out
from under it -- right at that moment, but nothing ever reaps the server once
that window closes. It then survives an upgrade and blocks every later launch.
The port cannot simply move: the browser keys localStorage (the GitHub token)
and IndexedDB (the notes) by origin, so a fallback port would present an empty,
logged-out app. lib/desktop/port_guard.dart therefore decides who owns 8730 and
acts: a wrapper with no window attached is stale, so it is terminated
(SIGTERM, escalating to SIGKILL) and the port rebound with the current build; a
wrapper that still has a window is joined, leaving exactly one server; anything
else is named and refused. Reaping is what makes an upgrade self-healing, so
install_arch.sh needs no restart step of its own -- one mechanism, not two.
It fails closed at every step. `ss` hides the owning pid for another user's
socket, and that is a real branch rather than an impossible one: it resolves to
"refuse", never to a blind kill or a retry loop.
Two things measurement contradicted, both of which had to change the design:
Re-running the Chrome command does not focus the existing --app window, it
opens a second one (verified on Thorium: the process returns in ~180ms and the
window count goes 1 -> 2). So the join path opens a window instead of printing
and exiting; both windows share the one server and one IndexedDB, and typing
`todo` to get nothing visible would be the worse outcome.
Chrome also rewrites its own argument vector into a single space-joined string,
so /proc/<pid>/cmdline for a browser contains no NUL separators at all. Exact
token matching therefore never fired, and the first run of the live-window case
reaped the server under an open window. Both matchers now tolerate either
layout. The direction of that error matters and is commented as such: a false
negative kills a server someone is using, a false positive merely opens a
spare window.
The decision logic lives in lib/ rather than in bin/, which is
coverage:ignore-file -- otherwise the one path that has to work would be the
only untested code in the repo. Host access sits behind an injected SystemProbe,
and the ss parser is additionally exercised against genuine `ss` output by
binding a real socket, since a fixture cannot catch a format change.
Verified by running it, not only by tests: orphaned dev wrapper, orphaned AOT
wrapper from the installed package, live window, a foreign owner (python
http.server -- named, exit 1, and confirmed still alive afterwards), ./run.sh,
and plain `todo`. 238 tests, 100% line coverage held.
CLAUDE.md picks up the port guard, and drops two stale claims: the suite is 238
tests taking 1-2min, not 191 tests in ~5s -- that drift predates this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audited the template against 514 real Claude sessions (1503 top-level
prompts) joined to the 49 notes in BACKLOG.md. Sections were judged on
whether the agent actually read them, not on fill rate — four of them
said "leave blank if none" in their own helper, so a low fill rate was
the section working correctly.
Measured over the 22 note/session pairs:
estimate filled 22, referenced 1 (4%)
done filled 22, quoted back 4 (18%)
refs filled 14, URLs fetched 5 (35%)
tech the agent opened pubspec.yaml/CLAUDE.md anyway in 54%
ask 100% ask rate with it, 92% without — no measurable effect
Kept: title, what, where, must, done. Dropped: tech (into `where`),
ask, nice and never (into `must` as `optional:` / `must not:` lines),
depends, estimate. Renamed `refs` -> `read first`. Added `verify`, the
only addition: "test it yourself, on the phone" was the single most
repeated correction in the corpus, and 3 of the 7 filled `never`
sections were already smuggling it in.
Fixes a latent data-loss bug found while testing this. parse() treats
an unknown `## heading` as content on purpose (values may contain
subheadings), so a legacy `## tech` block was folded into the preceding
section's value AND the note still reported conforming — opening any
old note in the stepper would have swallowed it on the next save.
NoteTemplate.retiredLabels now makes those notes explicitly
non-conforming, so they open raw and untouched.
tool/migrate_backlog.dart converts an old export (export -> migrate ->
import). It is idempotent and gated: it throws if a rewritten note does
not conform or if any source line vanishes. All 49 notes migrated on
the desktop and merged to the phone (0 new, 25 updated, 24 unchanged).
Also formats the few files dart format had never been run over, and
extracts a local in frame_stats_io.dart so the reflowed line stays
under 80 chars.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the move off Flutter's Linux embedder, which manages ~20fps at 4K
against ~144fps for the same Dart code in Chrome (measured end-to-end: the real
app, fullscreen at 4K, sustains 144fps scrolling a 300-note list).
`linux/` is gone. Its icons and .desktop installer move to `desktop/`, since
those are still needed and are not Flutter platform scaffolding. install_arch.sh
now builds the web bundle plus an AOT wrapper; run.sh does the same for
development.
Four things this took to get right, each found by actually installing and
launching the package rather than assuming:
- `dart compile exe` refuses to build the package: transitive dependencies
(sqlite3, objective_c) have native build hooks. `dart build cli` handles them,
and emits bin/ + lib/, so the wrapper resolves its assets at ../web.
- `options=('!strip')` is load-bearing. makepkg stripped the AOT executable
(7.0M -> 5.7M), discarding the embedded snapshot; the result still ran but was
the bare Dart VM printing a usage message instead of the app.
- No hard browser dependency. Declaring `depends=('chromium')` made makepkg
install it, and this machine has a policy that uninstalls chromium three
seconds later, which then failed dependency resolution. The wrapper discovers
whatever browser exists (Thorium included) and TODO_BROWSER overrides.
- The server no longer dies with the browser process. Chrome exits immediately
when handing off to an instance that owns the profile -- or when a stale
SingletonLock is left behind -- which killed the server out from under a live
window.
Chrome gets --class=todo so WM_CLASS matches the .desktop StartupWMClass and
the taskbar shows the app icon rather than a browser icon.
Verified: `todo` launches, serves, opens a window titled todo, and recovers the
backlog through the wrapper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
todo-sync, diet-guard-sync, and screen-locker-sync were folded into
one private github.com/kuhyx/syncs monorepo, each keeping its own
subdirectory as the Contents-API path prefix. Update the default
owner/repo, the changeset path prefix, and add a persisted-value
migration so an already-installed device that saved the old
'todo-sync' repo name picks up 'syncs' on next load.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Installs the official flutter/skills and dart-lang/skills packs into
.agents/skills/ and appends Flutter's AI rules.md to CLAUDE.md, so
Claude Code has task-specific playbooks alongside the dart MCP server
(registered separately at user scope).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NqCvrbFnoNNmqCwZKntBK
Initialised from this session: documents the data/sync/ui layers, the CRDT
sync model, the structured note template, build/test commands, the repo-specific
git rule (always push to main, never open PRs), and the widget-testing patterns
that keep the suite fast and at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>