Fixes the three defects PLAN.md section 6 lists. The previous-note glob is
constrained to the date pattern, so a .sync-conflict- sibling can no longer
be selected as the file to carry forward, which is the likeliest origin of
how those conflicts spread. The retitle is line 1 only, so a body line like
"meeting moved to 2026-09-08" is no longer silently advanced a day. The dead
`&& cd -` is gone.
Pull before the edit, push after. Error handling is shaped by the CrowdSec
rules in section 5 rather than by generic retry logic: a 401 stops dead with
a message, a 404 means the timer has not run yet so it creates once and
never polls, and anything else degrades to editing the local copy.
The offline fallback shells out to the same prune.py the server rollover
uses, installed to ~/.local/lib/todo, so the two cannot drift.
Since SFTPGo ignores If-Match, the precondition is enforced client side by
comparing the ETag captured at pull against a HEAD before the PUT. A remote
that moved mid-edit produces a .sync-conflict- sibling and a non-zero exit
rather than an overwrite. Real merging is phase 5; this just makes phase 2
incapable of losing an edit.
The credential comes from rbw and reaches curl over stdin, never argv, where
any other user's ps could read it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU5ZFfQFtDTFqqhvMTWdGH
It round-tripped the pruned text through a shell variable, and command
substitution strips every trailing newline while printf added exactly one
back, so any file whose pruned output ended in more than one blank line
diffed against a reshaped copy of itself. Comparing files directly instead
brings review.sh to 270 removals across 118 files, matching validate.py,
which derives the same number from the drop set rather than from a diff.
The counting was wrong too: a removed `- [x] foo` is `-- [x] foo` in a
unified diff, so the `^-[^-]` pattern skipped every checkbox it was meant
to be counting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU5ZFfQFtDTFqqhvMTWdGH
Never modifies an existing note. It only creates today's file when that
file is absent, so the blast radius is one new file and re-running is a
no-op.
Two bugs that only a real run on the server could surface. tempfile
creates 0600 and os.replace preserves it, so notes would have landed
private in a tree where everything is 0644 and both sftpgo and syncthing
read; the previous note's mode is now carried over rather than hardcoded.
And --dry-run fell into a degenerate branch once today's file existed,
emitting a bare heading exactly when a preview is most wanted, so it now
always previews from the newest note that is not today.
The unit is sandboxed, and that was verified against the NFS mount rather
than assumed: writes to the notes directory succeed, and writes to its
parent are refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU5ZFfQFtDTFqqhvMTWdGH
The rule PLAN.md describes and the rationale it gives for that rule
disagree on one shape that turns out to be common. 235 checkboxes in the
corpus sit under a plain bullet used as a group label, and by the letter
of the rule those have no checkbox ancestor, so a finished one would be
dropped on its own. But section 4 already argues the opposite case for
checkbox parents: a finished item under an unfinished group is context,
not cruft, and a label like `- Trellis "Ergonomics" Fixes` is a group in
every sense that matters.
Resolved in favour of the rationale, which collapses both cases into one
rule. A top-level bullet is any bullet with no bullet ancestor in its
section. It is dropped, with its subtree, exactly when that subtree holds
at least one checkbox and all of them are [x]. Prose bullets hold none and
so are never eligible, which is the property section 4 wanted.
Validated across all 228 notes with zero invariant failures: no heading,
no unfinished item and no checkbox-free subtree is ever removed, and no
group label is left with its children gone. 270 lines would be dropped
across 118 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU5ZFfQFtDTFqqhvMTWdGH
Phase 0. Resolve the four Syncthing conflict files and push the laptop's
228 notes to mainframe, both sides now hashing identically.
Settle the If-Match question PLAN.md deferred to phase 1: SFTPGo's WebDAV
returns ETags but ignores the precondition, so a PUT carrying a bogus
If-Match still writes. The client takes the HEAD-compare-then-PUT fallback.
Chasing a PUT/HEAD ETag mismatch turned up a NAS whose clock was 124
seconds fast with no time daemon installed, which NFS was stamping onto
every file mtime under /docs. Installed and enabled chrony there. The
mismatch survives the fix at ~1ms, so it is a write-path race in SFTPGo
that the broken clock had been amplifying, not a protocol defect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU5ZFfQFtDTFqqhvMTWdGH
Extends ~/.local/bin/todo to Android without turning it into an app. Notes stay
plain markdown in ~/docs/notes/todo; this repo holds the tooling.
Key finding: files.rcjohnstone.com already serves /home/connor/docs over
authenticated WebDAV, and sftpgo and syncthing mount the same host directory, so
today's note is already reachable at a public TLS URL. No new service, hostname,
Caddy block, or credential store is needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NctNN59e573MxYdNWzNHUw