Skip to content

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: unapprove and cancel now append a withdrawn line to approval-ledger.jsonl. Existing ledgers stay valid and nothing needs re-approval. A version older than this one reads a ledger with a withdrawn line as broken and refuses every send and approval (approval_ledger_unavailable), so do not downgrade after an unapprove or cancel; if you must, restore the ledger from a backup taken before the upgrade, together with the database.
  • Worker log: publish --log-json can now write a preflight_retry line, and the run_end counts object has a new key, preflight_retries. schema_version stays 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, language and spoiler_text and a Postiz account’s postiz_base_url and postiz_integration_id. Older approvals carry no such binding and are not grandfathered: publish --live refuses them with approval_send_options_missing_reapproval_required before any request, and they stay approved. Run reachweaver health (it lists the Mastodon ones under approval_rebind_required), then for each: unapprove POST_ID, show POST_ID, approve POST_ID (and schedule again 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_material and stays approved until you cancel it or re-approve it with --allow-repeat. An approval made with --allow-repeat before this version carries no bound flag; re-approve it with --allow-repeat if it should still be sent.
  • Opening a database adds two nullable columns (posts.approved_send_options, posts.approved_allow_repeat); older code ignores them.
  • A withdrawn approval can no longer come back from a database backup (security decision item 12, owner-approved 2026-10-21). unapprove and cancel of a post that carries an approval nonce now append one withdrawn line for that (post id, nonce) to the hash-chained approval ledger, under its lock and before the row changes. claim_sending, the pre-claim check_approval_binding gate and the ledger’s own consume treat a withdrawn nonce like a consumed one and refuse with the fixed code approval_withdrawn_reapproval_required (one approval_replay_blocked event, last_error set, 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. health and the dashboard list such a row as attention. Normal use is unchanged: re-approving after unapprove draws 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, the unapprove/cancel event records ledger=unrecorded, and the CLI prints a WARNING on stderr. Residual: the ledger is still writable by the same Unix user. Tests: tests/test_approval_ledger.py (WithdrawalTests, 11 offline tests).
  • approve --allow-repeat records its repeat_override event 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 the approve event carries). Tests: tests/test_repeat_guard.py.
  • review is owner-only. The command catalog tags it owner_only and interactive (so context and describe list it among the owner-only commands), the plugin’s agent guard refuses it in every invocation form (review-packet, review-comment and review-status are 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/tty opens, 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.json should add matching review rules.
  • 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 in show, but until now a later edit (for example unlisted with a content warning changed to public without one) changed the post that was sent without a new approval. approve now stores a SHA-256 fingerprint of the three effective values (visibility defaults to public) in posts.approved_send_options, bound into the payload under the new tag reachweaver.storage.approval/options-1, and prints them. publish --live and the claim compare it with the account they send with and refuse a difference with approval_send_options_changed_reapproval_required before any request (post stays approved, one approval_binding_failed event); restoring the approved values or re-approving lets it go. The send gate refuses a publish() call whose account entry differs from the claimed row, so the request uses exactly the bound values. approve refuses a Mastodon post whose account entry cannot be loaded (approval_send_options_unavailable). show reports the bound values and whether they still match. Replies were already covered by their bound target. New module reachweaver/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_url or postiz_integration_id after approval refuses the post before any request, including GET /integrations. postiz_api_prefix is 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, refuses repeat_of_shared_material when the post’s video, an image or its exact wording matches material the same account has published, is sending, has in needs_review, or recorded with mark-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 reason repeat_of_shared_material. approve --allow-repeat is now bound (posts.approved_allow_repeat, tag reachweaver.storage.approval/repeat-1), so it passes the send-time check for that approval only, is withdrawn by unapprove, and an edited value is a hash mismatch. The repeat_override audit event is never read as permission and is now written only after the approval succeeded (before, a later refused check could leave it behind). approve prints 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 through storage.transition now pass the account’s fingerprint as approve does (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 as show and the result of check, then asks a (approve this one post), s (skip), c (cancel, after you type CANCEL) or q (quit). a runs the normal approve command for that post id, with all its checks and, when approval-mode --confirmation tty_hash_prefix is 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, --sources and --verify-identity --online narrow 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 module reachweaver/review_loop.py; docs recommended setup; tests tests/test_review_loop.py. approve itself is unchanged.
  • Docs: recommended approval settings for a computer you sit at (recommended setup, security checklist): turn on approval-mode --confirmation tty_hash_prefix and an approval expiry with approval-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-health and the account hold is decided together; YouTube quotaExceeded must record a provider cooldown; Meta error codes and appsecret_proof are 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.
  • 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 native publish --live loop, the Postiz backend and the TikTok runner) now writes one preflight_retry line with post_id, account_id, platform, attempt (1 or 2), delay_ms and a sanitized code, and run_end counts them in counts.preflight_retries. trace POST_ID --log FILE shows 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-json no observer is passed at all. Tests: tests/test_retry_metrics.py (12 offline tests).