ReachWeaver 0.13.0 release notes
Released on 2026-10-21.
Approval covers more of what is actually sent, and stays withdrawn when you withdraw it. A Mastodon post’s visibility, language and content warning and a Postiz post’s server and integration are bound into the approval; an exact repeat of material already shared on the same account is refused at send time unless you approved it with --allow-repeat; unapprove and cancel are recorded in the approval ledger, so a restored backup cannot send a withdrawn post. New owner-only review walks your drafts one by one in your own terminal. Preflight retries appear in the worker log. 119 commands (1 more than 0.12.0, none removed). No live adapter is added; dry-run, emergency-stop, credential isolation and reconciliation are unchanged.
Upgrade notes.
- Approval ledger:
unapproveandcancelnow append awithdrawnline toapproval-ledger.jsonl. Existing ledgers stay valid and nothing needs re-approval. A version older than this one reads a ledger with awithdrawnline as broken and refuses every send and approval (approval_ledger_unavailable), so do not downgrade after anunapproveorcancel; if you must, restore the ledger from a backup taken before the upgrade, together with the database. - Worker log:
publish --log-jsoncan now write apreflight_retryline, and therun_endcountsobject has a new key,preflight_retries.schema_versionstays 1 (both changes only add); a parser that rejects unknown events or keys needs updating. - Mastodon and Postiz posts approved with 0.12.0 or earlier must be re-approved once. Approval now binds a Mastodon account’s
visibility,languageandspoiler_textand a Postiz account’spostiz_base_urlandpostiz_integration_id. Older approvals carry no such binding and are not grandfathered:publish --liverefuses them withapproval_send_options_missing_reapproval_requiredbefore any request, and they stay approved. Runreachweaver health(it lists the Mastodon ones underapproval_rebind_required), then for each:unapprove POST_ID,show POST_ID,approve POST_ID(andscheduleagain if it was scheduled). Their stored hashes are unchanged; other platforms need nothing. - Exact repeats are now refused at send time. If two copies of the same video, image or wording are approved on one account, only the first goes out; the other is
SKIPPED ... repeat_of_shared_materialand stays approved until youcancelit or re-approve it with--allow-repeat. An approval made with--allow-repeatbefore this version carries no bound flag; re-approve it with--allow-repeatif it should still be sent. - Opening a database adds two nullable columns (
posts.approved_send_options,posts.approved_allow_repeat); older code ignores them.
Security
Section titled “Security”- A withdrawn approval can no longer come back from a database backup (security decision item 12, owner-approved 2026-10-21).
unapproveandcancelof a post that carries an approval nonce now append onewithdrawnline for that(post id, nonce)to the hash-chained approval ledger, under its lock and before the row changes.claim_sending, the pre-claimcheck_approval_bindinggate and the ledger’s ownconsumetreat a withdrawn nonce like a consumed one and refuse with the fixed codeapproval_withdrawn_reapproval_required(oneapproval_replay_blockedevent,last_errorset, nothing claimed, no identity preflight, no request). Before this, a database restored from a backup taken while a post was approved would send it on the next worker run even though you had withdrawn it.healthand the dashboard list such a row asattention. Normal use is unchanged: re-approving afterunapprovedraws a fresh nonce and sends; the documented recovery (unapprove,show,approve) works and adds no second line. Each(post id, nonce)holds at most one ledger line. A withdrawal is never refused because of the ledger: if the line cannot be written (missing, unreadable or broken ledger) the post is still withdrawn, theunapprove/cancelevent recordsledger=unrecorded, and the CLI prints aWARNINGon stderr. Residual: the ledger is still writable by the same Unix user. Tests:tests/test_approval_ledger.py(WithdrawalTests, 11 offline tests). approve --allow-repeatrecords itsrepeat_overrideevent only with a granted approval. The event used to be written before the reply, identity, confirmation and approval checks, so a refused approve left an override in the audit log for a post that was never approved. It is now written after the approval, in the same transaction, and names the approval it belongs to (approval_sha256=prefix, the same one theapproveevent carries). Tests:tests/test_repeat_guard.py.reviewis owner-only. The command catalog tags itowner_onlyandinteractive(socontextanddescribelist it among the owner-only commands), the plugin’s agent guard refuses it in every invocation form (review-packet,review-commentandreview-statusare not affected), and the MCP server’s never-exposed list includes it. It refuses to start, before reading the database or printing anything, unless stdin and stdout are terminals and/dev/ttyopens, and it reads every answer from/dev/tty, never from stdin. Like the typed hash prefix this is friction, not a boundary: a local process can drive a pseudo-terminal. Repositories that keep Claude Code deny rules in.claude/settings.jsonshould add matchingreviewrules.- Mastodon visibility, language and content warning are bound into the approval (owner-approved 2026-10-21). They come from
accounts.json, and the approver saw them inshow, but until now a later edit (for exampleunlistedwith a content warning changed topublicwithout one) changed the post that was sent without a new approval.approvenow stores a SHA-256 fingerprint of the three effective values (visibilitydefaults topublic) inposts.approved_send_options, bound into the payload under the new tagreachweaver.storage.approval/options-1, and prints them.publish --liveand the claim compare it with the account they send with and refuse a difference withapproval_send_options_changed_reapproval_requiredbefore any request (post stays approved, oneapproval_binding_failedevent); restoring the approved values or re-approving lets it go. The send gate refuses apublish()call whose account entry differs from the claimed row, so the request uses exactly the bound values.approverefuses a Mastodon post whose account entry cannot be loaded (approval_send_options_unavailable).showreports the bound values and whether they still match. Replies were already covered by their bound target. New modulereachweaver/send_options.py. - The Postiz server and integration are bound into the approval (owner-approved 2026-10-21, same mechanism). Re-pointing a Postiz account’s
postiz_base_urlorpostiz_integration_idafter approval refuses the post before any request, includingGET /integrations.postiz_api_prefixis not bound. - The repeat guard now also runs at send time (owner-approved 2026-10-21, decisions D1-D4).
claim_sending, under its lock, after the account hold and before the budgets, refusesrepeat_of_shared_materialwhen the post’s video, an image or its exact wording matches material the same account has published, is sending, has inneeds_review, or recorded withmark-shared. Nothing is written, no budget is used and the post stays approved/scheduled; the CLI loop, the Postiz and TikTok runners and the dry run run the same check before the identity preflight, with the fixed worker log reasonrepeat_of_shared_material.approve --allow-repeatis now bound (posts.approved_allow_repeat, tagreachweaver.storage.approval/repeat-1), so it passes the send-time check for that approval only, is withdrawn byunapprove, and an edited value is a hash mismatch. Therepeat_overrideaudit event is never read as permission and is now written only after the approval succeeded (before, a later refused check could leave it behind).approveprints a note when an exact copy is also queued on the same account. - Every new payload key is present only when its column holds a value, so all existing approval hashes stay valid. Tests:
tests/test_send_binding_repeat.py. Existing suites that approve Mastodon or Postiz posts throughstorage.transitionnow pass the account’s fingerprint asapprovedoes (tests/gate_support.bound_send_options), and fixtures that published several identical posts on one account now use distinct wording. No change to the dry-run default, emergency stop, credential isolation, ambiguous-send reconciliation or the ledger.
- Guided review loop (
reachweaver review, #11, owner-approved 2026-10-21). Walk the drafts one by one, oldest first, in your own terminal instead of copying post ids by hand: for each draft it prints the same review sheet asshowand the result ofcheck, then asksa(approve this one post),s(skip),c(cancel, after you typeCANCEL) orq(quit).aruns the normalapprovecommand for that post id, with all its checks and, whenapproval-mode --confirmation tty_hash_prefixis on, the typed hash prefix for every post; the post must still be exactly the row that was shown. An empty or unknown answer never approves; end of input quits. It never batches, never passes--allow-repeat, and leaves reply drafts out.--campaign,--account,--approver,--persona,--optout,--sourcesand--verify-identity --onlinenarrow it or are passed to each check and approval. The summary lists the exact ids approved, skipped, cancelled, refused and not reached, and the next command. New modulereachweaver/review_loop.py; docs recommended setup; teststests/test_review_loop.py.approveitself is unchanged. - Docs: recommended approval settings for a computer you sit at (recommended setup, security checklist): turn on
approval-mode --confirmation tty_hash_prefixand an approval expiry withapproval-mode --max-age-hours N(for example 168); the owner approval key is deferred until a separate worker host exists. No code change. - Docs: preconditions before enabling TikTok, YouTube or a Meta adapter (first live pilot): TikTok status polling must re-read the emergency pause and the inbox-delivery handling in
queue-healthand the account hold is decided together; YouTubequotaExceededmust record a provider cooldown; Meta error codes andappsecret_proofare decided after the first Meta receipt. Nothing is built now; none of these send paths can run today. - Docs: spend ceilings are a precondition for any paid-API adapter such as X (worker operations). No monetary ceiling is built while no paid adapter exists.
Changed
Section titled “Changed”- Preflight retries show up in the worker log (issue #26 retry metrics, security decision item 3, owner-approved 2026-10-21). Each retry of the read-only identity check before the claim (
ratelimit.preflight_with_backoff: the nativepublish --liveloop, the Postiz backend and the TikTok runner) now writes onepreflight_retryline withpost_id,account_id,platform,attempt(1 or 2),delay_msand a sanitizedcode, andrun_endcounts them incounts.preflight_retries.trace POST_ID --log FILEshows them. Log only: nothing is written to the database, and the retry count, delay, pause checks and claim are unchanged; a failing log write is dropped and never stops a preflight. Without--log-jsonno observer is passed at all. Tests:tests/test_retry_metrics.py(12 offline tests).
