ReachWeaver 0.15.0 release notes
Released on 2026-10-21.
One place to write, share and learn. The browser composer reopens, autosaves and attaches media, and “Mark as posted” makes a post you shared by hand measurable. share now also makes copy-ready Bluesky, Mastodon, Telegram and LinkedIn posts, and account-add writes a valid account entry without a secret. Your own results feed hashtag ideas, follow-up thresholds, suggest and calendar-plan. platform-risk shows how dependent you are on each network, and eu-review-evidence lists the human review recorded for each published post. Media posts now need an explicit AI-label answer at approval where a platform requires a label (owner-approved; see Upgrade notes). 129 commands (4 more than 0.14.0, none removed). No live adapter is added.
Security
Section titled “Security”published_manualis born terminal:transitionhas no edge into or out of it (approve, unapprove, cancel and schedule refuse it),reconcilerefuses it, andpending,claim_sending,publish(dry run and--live, with or without--post-idor--scheduled-only), the Postiz and TikTok runners, the media/video setters,queue-health,dead-letterand the approval-ledger audit never select it. Every approval column of the record is NULL. Covered bytests/test_manual_published.py. No send path, network endpoint or credential use was added.- AI-content label precondition for posts with media (owner-approved 2026-10-21, security review decisions D1-D4 and D6; D5 deferred).
approveof a post with images or a video computes theai-label-checkdecision for the post’s own platform from its stored declarations (EU Art. 50 left out, D4) and refuses withai_label_unansweredwhen it isrequiredorunknown(D2), until you passapprove --ai-label applied|not_needed. Text-only posts are never blocked (D1);checkandshowprint the advice and, where relevant, the EU Art. 50 hint (advisory, never blocks). For a post with media, the declarations and the answer are bound into the approval hash under the add-on tagreachweaver.storage.approval/media-3(D3), so changing one after approval refuses the send; theapproveaudit event recordsai_label_decision=... ai_label=....claim_sending,check_approval_binding,schedule,publish(dry run) andhealthre-run the same decision on the bound values and the bundled registry, with no network, and refuseai_label_unansweredwhen a needed answer is not bound. The registry file is not bound.healthwarns (ai_disclosure_registry_aging, attention, never blocks) when the AI-disclosure rules are more than 90 days old (D6). Nothing is added to the send path: no platform label field is set. - Browser composer write path (
share --web, owner-approved 2026-10-21): every new route (/api/packages,/api/package/open,/api/package/duplicate,/api/mark-posted,/api/draft/load|save|clear,/api/media) needs the loopback Host, the per-run token and the exact Origin; JSON bodies stay at 96 KiB, a photo/video at 256 MiB with theshare --attachextension allowlist. Package names must matchshare-YYYYMMDD-HHMMSS-<12 hex>, media ids 16 hex digits, and every path is resolved and confirmed to sit directly inside its private 0700 folder; symlinks are refused and never followed (O_NOFOLLOW,O_EXCL). An update needsconfirm: trueand the revision the editor loaded (409 otherwise) and is refused once any network is marked posted. The only database write isstorage.record_manual_publish(terminalpublished_manual); nothing can approve, schedule or send. The read-only dashboard (ui) is unchanged and still refuses every write method. Covered bytests/test_share_browser_composer.py.
platform-risk(new, read-only): how much of your reach depends on one platform. Per account, per persona and overall, the share of your recorded reach per platform:--metric views|impressionsat a matched post age (--age-hours, default 168; nearest exact snapshot, windowed ones skipped, never summed), else the latest exact value labelledlatest_not_age_matched. When a platform in a scope has no measured post, the scope falls back to post counts (published posts plus recorded manual shares) and saysFALLBACK. Concentration: top-platform share, HHI (0-1 and points) and effective platforms, withconcentrated/few/spreadlabels that are editorial thresholds. Owned channels (site,blog,newsletter,email,--owned ID, owner-stated--owned-reach NAME=N/--owned-posts NAME=N) give an owned share. Advice lines are hypotheses with a way to check them;--site/--setupreusesocial-web-plan’s canonical home.--json(kindreachweaver.platform-riskv1). Database openedmode=ro, never created; no network, queue or send path. Guide: docs/PLATFORM-RISK.md.reachweaver/platform_risk_registry.json(new, shipped in the wheel and the skill bundle). Sourced risk flags, each with an official URL, the exact quote and checked date 2026-10-21: X pay-per-use API pricing, LinkedIn API Terms 3.1 on automated posting, Reddit’s separate agreement for commercial Data API use, and the TikTok USDS Joint Venture (2026-01-23) retraining the US recommendation algorithm on US user data. Owned channels and concentration bands are labelled editorial. Validated byplatform_risk.validate_registryandtests/test_platform_risk.py.--market US|global|PLATFORM@MARKEToncalendar-plan,distribution-planandplatform-risk. When TikTok is in the plan, amarket_noticesentry flags that TikTok’s US recommendation algorithm sits with a separate US joint venture since 2026-01-23, cites the announcement and asks you to re-verify with your own US audience data (appliestrue, false or null when no market is given). It only flags and cites: slots, rankings and--applyare unchanged, and no US/global difference is claimed.eu-review-evidence(new, read-only): human-review evidence for the EU AI Act Art. 50(4) exemption. For published posts (--since,--account,--platform,--post-id,--limit): who approved and how the name was obtained, when, the approved payload hash, a SHA-256 of the stored text, the opt-in human-confirmation token, the approval-ledger line for the post’s nonce (consumed time, hash match) withledger.audit’s integrity status, and the authorship declaration when the database has an authorship column (absent columns are handled). Each post isrecorded,partialormissingwith its gaps. Every format (text,--json,--format markdown) leads with “Evidence of human review recorded by ReachWeaver; not legal advice; wording from a secondary source (artificialintelligenceact.eu); verify on EUR-Lex.” The database is openedmode=roand the ledger read through its own read-only chain check: nothing is created, repaired, migrated or sent. Docs: AI-CONTENT-LABELS.md. Tests:tests/test_review_evidence.py.sharefor Bluesky, Mastodon, Telegram and LinkedIn (owner decision 2026-10-21). These four text networks, which ReachWeaver can also send to through its API, are now offered as copy-only packages inshare,share --guideandshare --web(the browser marks them “Copy and post yourself · or connect it to send from ReachWeaver”). Each package is checked with the ruledraftandcheckuse (policy.validate_post), shows that rule as its text limit, adds an offline Bluesky link/tag preview, and says: “Copy and post it yourself; to send it from ReachWeaver instead, connect the account and use draft → review.” Share still writes no database, send queue or post row (tested by hashing an existing database before and after) and records nothing;mark-sharedstays the way to record a by-hand post.bskyis accepted as an alias.- Per-network caption fitting in
share(reachweaver/share_tailor.py, deterministic, no AI). When one network’s copy is over its limit, it is shortened at the last word that fits, ends with “…” and is marked SHORTENED in the plan, OPEN-FIRST.md and that network’s CHECKLIST.md, naming any hashtags that were cut. Limits are the ones ReachWeaver already applies: thedraft/checkrule for the four API networks, the sourced Threads, Instagram, TikTok and YouTube-description limits inmanual_export.FACTS, and the conservative Facebook Pages cap. Networks with no known limit keep the text as written, with a note when it exceeds ReachWeaver’s internal guardrail. Hashtags are never moved (no sourced rule exists). New flagshare --no-shortenkeeps an over-long copy, which then blocks as before. - Offline media checks in
share --attach. Without a media manifest, each attached photo or video is probed from its header (media.check_file_for_targets, new) and checked with the same sourced rows asmedia-check(container, size, duration, aspect ratio, dimensions, codecs, frame rate) for each network that has a table (Bluesky, Mastodon, Instagram, TikTok, LinkedIn, YouTube Shorts, DEV). Findings appear asMEDIA PROBLEM/Media advicelines and inmanual-export.json(share.media_checks) and never block; values the source does not state or an unreadable header are listed as not checked. No ffprobe or other tool is used. Alt text, captions and rights still need a manifest. account-add PLATFORM(new command, offline): appends one correctly shaped public entry for bluesky, mastodon, telegram, linkedin, instagram, threads, tiktok, youtube or facebook toconfig/accounts.json(or--accounts, inside the workspace when one is active). It asks in a terminal for the public details still missing, or takes--handle,--server,--field KEY=VALUE,--id,--env-prefix,--persona;--dry-runwrites nothing. It never asks for, accepts or writes a secret (secret-named fields are refused), prints the credential variable names to set yourself and points toconnect --guide. The candidate file is validated by the unchangedaccounts.load_accounts; duplicates (id or identity), overlappingenv_prefix, example placeholders and a malformed Telegramchat_idare refused, an unedited example entry of that network is filled in place, the previous file is kept asaccounts.json.bakand writes are atomic. With--personathe wrong-account guard applies (unknown persona, a username the profile records under another persona, or a profile conflict is refused).startmentions it for placeholder and missing accounts files. Tests:tests/test_account_add.py,tests/test_share_everywhere.py.- Measure posts you shared by hand.
mark-shared --url LINK(owner-approved 2026-10-21) now also creates a measurable record: a post with the new terminal statuspublished_manual(“posted by hand”) holding the platform, the account (optional), the text, the link and the time you posted it (--at, default now; more than 10 minutes in the future is refused). It prints the post id.metrics,import-metrics,report(including--since),insights,journaland the dashboard’s Posted and Results pages include it, labelled “posted by hand”.metrics-fetch --onlinereads its public counters when the link is a Mastodon link on the configured server and handle, or a Blueskybsky.app/profile/did:.../post/...link by the configured handle (derived offline; no new endpoint). The same link recorded twice for a platform is refused. Without--url,mark-sharedis unchanged.storage.record_manual_publish(...)is the one entry point, for the browser composer’s planned “Mark as posted” button. Guide: docs/MANUAL-EXPORT.md#measure-posts-you-shared-by-hand. Tests:tests/test_manual_published.py(synthetic data, no network). reportJSON gainsposted_by_handper platform (how many ofpublishedwere posted by hand), Markdown shows “N (M posted by hand)”, CSV gains a last columnposted_by_hand.insightscohorts gainposted_by_hand.showgains aposted_by_handblock (provenance, declared time, link) for such a record.- Learning loops: your own results wherever ReachWeaver recommends. These additions use the
insightsmethod: interactions at about 7 days, one account at a time, n and median against the rest,observationortoo little data. They are read-only and advisory. No scheduling, approval or send path changed (docs/INSIGHTS.md).discovery-planshowsyour_resultsper recommended tag when a local database already exists. It opens the database read-only and never creates it.--account IDlimits the results to one account. Scores and order never change (score_adjustment: null), and docs/DISCOVERY-OPTIMIZATION.md explains why.followup-suggestwithout--rulessets one “took off” rule per account atceil(2 x your median)interactions, labelled “derived from your N posts”. It compares only the same snapshot source within 192 h, needs 10 or more posts with a result, and prints a friendly message when the data is too thin.--rulesworks as before.experiment-next --readout FILE/--readout-dir DIRreads savedexperiment-readout --jsonfiles, with labels unchanged and one readout per experiment.--resultsstill works, and its output is unchanged.suggestwith a database adds P3 “What works for you” items (observational_only, with n and medians). When a bundled registry fact is aging or stale, it also adds one P3 research-freshness item.- A
calendar-planproposal gainsyour_results: your 6-hour blocks and weekdays that did better, next to the block of each proposed slot. It never picks a slot, and--applyis unchanged.
registry-age,healthanddoctornow coverai_search_registry.json,ai_disclosure_registry.json,social_web_registry.jsonandplatform_risk_registry.json(nine registries).ai-declare POST_ID(offline, writes a draft only): record your own answers about a draft:--authorship human|ai_assisted|ai_generated(who wrote the text) and--realistic-synthetic yes|noper image or video (--attachment Nfor one;clearremoves an answer). Stored in new nullablepostscolumns, keyed by each attachment’s SHA-256 so a replaced file has no answer.ai-label-check --post-id,checkandshownow use them. See docs/AI-CONTENT-LABELS.md.- Browser composer workflow (
share --web). Saved packages are listed and can be reopened into the editor (captions per network, title, community note, destinations and attachments), duplicated into a new package, or updated in place after an explicit confirmation; “Save as new package” never overwrites. The editor draft is autosaved tostate/composer/draft.json(0600, editor fields only) with a visible Saved indicator, and Undo reverts the last change. Photos and videos can be picked in the browser: they are kept instate/composer/media/, copied into the package’smedia/folder on save, and each preview card shows the offline header checksshare --attachcomputes. Mark as posted: per network of a saved package, paste the native post link (and optionally the time you posted); the composer callsstorage.record_manual_publishand shows the post id and “posted by hand”. Networks show exported or posted per package. New modulereachweaver/share_composer_store.py.
Changed
Section titled “Changed”shareno longer refuses Bluesky, Mastodon, Telegram and LinkedIn with “uses the separate draft, check, approve, publish flow”: they get copy-only packages (above). Sending from ReachWeaver is still only the separate draft, review and approval flow; human-only (Hacker News and others) and moderator-only (Reddit) venues still get notes only.manual-exportis unchanged and still coversmanual_onlydestinations only.share --attachfiles are no longer described as “not validated”: OPEN-FIRST.md and the plan report the offline header checks.journal: a record made withmark-shared --urlis listed once (sourceposted_by_hand, statuspublished_manual), not twice;--no-by-handleaves it out too. The repeat guard compares only itsmark-sharedentry (same account scope as before), never the record itself, and a record is never reported as a repeat of its own entry.data-purge: thepublished_content_daysredaction also coverspublished_manualtext.schema-exportlistspublished_manualposts as unmapped (no canonical state; noPublishJobor approval is invented).reportopens the database read-only (mode=ro). A missing database prints the same empty report plus a note on stderr and is never created. An older schema is refused with a hint to runinitand is never migrated. The output is otherwise identical.share --webnow writescomposer.json(the editor fields) and, when media are attached, amedia/folder into each package it saves;share --webpasses the global--dbto Mark as posted.
Upgrade notes
Section titled “Upgrade notes”- Schema:
shared_materialgains the nullable columnpost_id(added automatically by the usual migration). Theposts.statusCHECK of a database created before this version does not allowpublished_manual; the firstmark-shared --urlrebuilds thepoststable once (SQLite’s documented table-rebuild procedure in one transaction: every row and column is copied, approval hashes and MACs stay valid,foreign_key_checkmust pass, otherwise everything rolls back) and logs aposts_status_check_widenedevent. Nothing changes until then. Older ReachWeaver versions were not tested against a database that holdspublished_manualrecords; back upstate/before downgrading. - The database gains three nullable
postscolumns (authorship,realistic_synthetic_json,approved_ai_label), added bymigrate()on first open; older code ignores them. With all three NULL the approval payload is byte-identical, so every existing approval keeps its hash. - A post with images or a video approved before the upgrade is not grandfathered (same rule as the approval nonce and send options): if its platform decision is
requiredorunknown, the send is refused withai_label_unansweredand the post stays approved or scheduled;healthlists it under re-approval. Re-approve it once:reachweaver unapprove POST_ID,reachweaver show POST_ID, then approve with--ai-label applied|not_needed(or recordai-declareanswers that settle it). Text-only approvals are unaffected. Every live adapter enabled today is text-only, so no enabled live send changes. - Rollback: after a downgrade, an approval that bound declarations or an answer (
approval/media-3) no longer matches the payload older code computes, so it is refused withapproval_hash_mismatch_reapproval_requireduntil re-approved (fails closed), as with earlier binding changes.
