Skip to content

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.

  • published_manual is born terminal: transition has no edge into or out of it (approve, unapprove, cancel and schedule refuse it), reconcile refuses it, and pending, claim_sending, publish (dry run and --live, with or without --post-id or --scheduled-only), the Postiz and TikTok runners, the media/video setters, queue-health, dead-letter and the approval-ledger audit never select it. Every approval column of the record is NULL. Covered by tests/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). approve of a post with images or a video computes the ai-label-check decision for the post’s own platform from its stored declarations (EU Art. 50 left out, D4) and refuses with ai_label_unanswered when it is required or unknown (D2), until you pass approve --ai-label applied|not_needed. Text-only posts are never blocked (D1); check and show print 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 tag reachweaver.storage.approval/media-3 (D3), so changing one after approval refuses the send; the approve audit event records ai_label_decision=... ai_label=.... claim_sending, check_approval_binding, schedule, publish (dry run) and health re-run the same decision on the bound values and the bundled registry, with no network, and refuse ai_label_unanswered when a needed answer is not bound. The registry file is not bound. health warns (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 the share --attach extension allowlist. Package names must match share-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 needs confirm: true and the revision the editor loaded (409 otherwise) and is refused once any network is marked posted. The only database write is storage.record_manual_publish (terminal published_manual); nothing can approve, schedule or send. The read-only dashboard (ui) is unchanged and still refuses every write method. Covered by tests/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|impressions at a matched post age (--age-hours, default 168; nearest exact snapshot, windowed ones skipped, never summed), else the latest exact value labelled latest_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 says FALLBACK. Concentration: top-platform share, HHI (0-1 and points) and effective platforms, with concentrated/few/spread labels 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/--setup reuse social-web-plan’s canonical home. --json (kind reachweaver.platform-risk v1). Database opened mode=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 by platform_risk.validate_registry and tests/test_platform_risk.py.
  • --market US|global|PLATFORM@MARKET on calendar-plan, distribution-plan and platform-risk. When TikTok is in the plan, a market_notices entry 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 (applies true, false or null when no market is given). It only flags and cites: slots, rankings and --apply are 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) with ledger.audit’s integrity status, and the authorship declaration when the database has an authorship column (absent columns are handled). Each post is recorded, partial or missing with 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 opened mode=ro and 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.
  • share for 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 in share, share --guide and share --web (the browser marks them “Copy and post yourself · or connect it to send from ReachWeaver”). Each package is checked with the rule draft and check use (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-shared stays the way to record a by-hand post. bsky is 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: the draft/check rule for the four API networks, the sourced Threads, Instagram, TikTok and YouTube-description limits in manual_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 flag share --no-shorten keeps 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 as media-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 as MEDIA PROBLEM / Media advice lines and in manual-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 to config/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-run writes 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 to connect --guide. The candidate file is validated by the unchanged accounts.load_accounts; duplicates (id or identity), overlapping env_prefix, example placeholders and a malformed Telegram chat_id are refused, an unedited example entry of that network is filled in place, the previous file is kept as accounts.json.bak and writes are atomic. With --persona the wrong-account guard applies (unknown persona, a username the profile records under another persona, or a profile conflict is refused). start mentions 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 status published_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, journal and the dashboard’s Posted and Results pages include it, labelled “posted by hand”. metrics-fetch --online reads its public counters when the link is a Mastodon link on the configured server and handle, or a Bluesky bsky.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-shared is 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).
  • report JSON gains posted_by_hand per platform (how many of published were posted by hand), Markdown shows “N (M posted by hand)”, CSV gains a last column posted_by_hand. insights cohorts gain posted_by_hand. show gains a posted_by_hand block (provenance, declared time, link) for such a record.
  • Learning loops: your own results wherever ReachWeaver recommends. These additions use the insights method: interactions at about 7 days, one account at a time, n and median against the rest, observation or too little data. They are read-only and advisory. No scheduling, approval or send path changed (docs/INSIGHTS.md).
    • discovery-plan shows your_results per recommended tag when a local database already exists. It opens the database read-only and never creates it. --account ID limits the results to one account. Scores and order never change (score_adjustment: null), and docs/DISCOVERY-OPTIMIZATION.md explains why.
    • followup-suggest without --rules sets one “took off” rule per account at ceil(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. --rules works as before.
    • experiment-next --readout FILE / --readout-dir DIR reads saved experiment-readout --json files, with labels unchanged and one readout per experiment. --results still works, and its output is unchanged.
    • suggest with 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-plan proposal gains your_results: your 6-hour blocks and weekdays that did better, next to the block of each proposed slot. It never picks a slot, and --apply is unchanged.
  • registry-age, health and doctor now cover ai_search_registry.json, ai_disclosure_registry.json, social_web_registry.json and platform_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|no per image or video (--attachment N for one; clear removes an answer). Stored in new nullable posts columns, keyed by each attachment’s SHA-256 so a replaced file has no answer. ai-label-check --post-id, check and show now 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 to state/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 in state/composer/media/, copied into the package’s media/ folder on save, and each preview card shows the offline header checks share --attach computes. Mark as posted: per network of a saved package, paste the native post link (and optionally the time you posted); the composer calls storage.record_manual_publish and shows the post id and “posted by hand”. Networks show exported or posted per package. New module reachweaver/share_composer_store.py.
  • share no 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-export is unchanged and still covers manual_only destinations only.
  • share --attach files are no longer described as “not validated”: OPEN-FIRST.md and the plan report the offline header checks.
  • journal: a record made with mark-shared --url is listed once (source posted_by_hand, status published_manual), not twice; --no-by-hand leaves it out too. The repeat guard compares only its mark-shared entry (same account scope as before), never the record itself, and a record is never reported as a repeat of its own entry.
  • data-purge: the published_content_days redaction also covers published_manual text.
  • schema-export lists published_manual posts as unmapped (no canonical state; no PublishJob or approval is invented).
  • report opens 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 run init and is never migrated. The output is otherwise identical.
  • share --web now writes composer.json (the editor fields) and, when media are attached, a media/ folder into each package it saves; share --web passes the global --db to Mark as posted.
  • Schema: shared_material gains the nullable column post_id (added automatically by the usual migration). The posts.status CHECK of a database created before this version does not allow published_manual; the first mark-shared --url rebuilds the posts table once (SQLite’s documented table-rebuild procedure in one transaction: every row and column is copied, approval hashes and MACs stay valid, foreign_key_check must pass, otherwise everything rolls back) and logs a posts_status_check_widened event. Nothing changes until then. Older ReachWeaver versions were not tested against a database that holds published_manual records; back up state/ before downgrading.
  • The database gains three nullable posts columns (authorship, realistic_synthetic_json, approved_ai_label), added by migrate() 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 required or unknown, the send is refused with ai_label_unanswered and the post stays approved or scheduled; health lists 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 record ai-declare answers 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 with approval_hash_mismatch_reapproval_required until re-approved (fails closed), as with earlier binding changes.