feat(ci): add journal_io raw-mechanic gate; migrate 4 journal-data stragglers
Add scripts/check_journal_io_mechanic.py: a static AST check that flags raw
durable-write / locked-RMW mechanics (os.replace, filesystem Path.replace,
mkstemp+manual-replace, flock LOCK_EX RMW) against owner journal data anywhere
outside solstone/think/journal_io/. Unlike the import-binding access lint, this
gate SCANS owners — they may import journal_io primitives but must carry no
hand-rolled mechanics. Seeds with an empty allowlist; wired into make ci via
install-checks alongside the access and layer-hygiene checks.
To seed the gate empty and green, migrate every remaining journal-data straggler
to journal_io (byte-identical output, equivalent locking):
- link/paths.py: state/token/totp writes -> write_json (token/totp keep 0o600)
- convey/chat_stream.py: chat.jsonl -> atomic_replace (ensure_ascii=False house
pattern); declared the L2 owner of chronicle chat/<seg>/chat.jsonl
- activities.py: _write_jsonl_records -> atomic_replace; locked_modify ->
hold_lock (drops the bare-LOCK_EX blocking RMW + max_retries retry loop)
- importers/shared.py: chronicle import.<source>/imported.jsonl -> atomic_replace
(a 4th straggler the original inventory missed; surfaced in prep)
locked_modify now raises journal_io.LockTimeout on contention; surface it in
sol's voice at the activities CLI write boundary via a new ACTIVITIES_BUSY
reason, mirroring the entities ENTITY_BUSY pattern. Tests reassert locking and
byte-fidelity invariants instead of the old flock/mkstemp internals.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>