Skip to content

ReachWeaver 0.2.0 release notes

Released on 2026-10-11.

  • Clone-to-safe-preview onboarding and a versioned release gate (issue #27 slice). docs/QUICKSTART.md takes a new user from clone to a one-post dry-run preview and two manual export packages with no credentials. docs/TROUBLESHOOTING.md maps the exact error messages to fixes. docs/SECURITY-CHECKLIST.md covers the first live send, each release and incidents. reachweaver/onboarding.py audits both research registries: every platform without a live adapter has a manual alternative (manual_export, draft_export, native_manual) or an explicit human_only/unsupported marker, and Reddit and Hacker News stay human-only. scripts/release_gate.py (make release-gate) checks version consistency, this changelog, required documents, README coverage of every CLI subcommand, relative links, the skill bundle, the blocker table and a passing offline test suite. tests/test_onboarding.py runs the quickstart commands from the document against a fresh copy of the repository with an empty environment and sockets blocked. examples/manual-variant.threads.json is the quickstart’s manual-export input.
  • Unsupported feature-claim review flags (issue #10 slice). claims-check and campaign-preview flag a draft that claims a feature (“adds”, “supports”, “integrates with”, “now has” and similar) when the claimed feature’s key words are absent from the evidence the variant cites. The result is a needs_human_review warning with category: feature and missing_terms; staging and export are not blocked. See docs/CLAIM-GROUNDING.md.
  • baseline-eval (issue #16 slice). A read-only comparison of personalized slots, variant campaigns or saved readouts against calendar-plan’s generic, non-personalized baseline over an explicit --since/--until period, using seeded percentile-bootstrap intervals, permutation p-values and Holm–Bonferroni across every comparison in a run. Results are labelled explore/exploit and split into correlational and randomized lists, and small samples report insufficient_data instead of 0. The strict sign-test gate is unchanged. See docs/TIMING-EVALUATION.md.
  • media-storyboard (issue #18) turns one campaign brief into an offline scaffold: a vertical-video storyboard (storyboard.json and storyboard.md), an SRT subtitle skeleton with valid timing (plus WebVTT for WebVTT-only targets), a media-check manifest with a LinkedIn image draft and the vertical video, and an image spec per platform built from the sourced constraint rows. Scene copy, captions, subtitle text and alt text are {{write: ...}} placeholders and rights_confirmed starts false, so media-check blocks until a person writes them. No media or copy is generated and no network is used. See docs/MEDIA-ASSETS.md.
  • Crop and UI-overlay safe-area checks (media-storyboard --check). Text boxes outside a target’s safe area are errors and focal boxes are warnings, for a centered-crop (cover) or letterbox (contain) fit. The built-in TikTok, Instagram Reels and YouTube Shorts zones are labelled unverified; --safe-zones FILE accepts owner-sourced numbers. Generation stays opt-in: local tools cost nothing through ReachWeaver, and a paid provider bills the user directly. ReachWeaver has no generation integration.
  • Offline security regression suite (issue #26 slice). tests/test_security_regressions.py covers poisoned feeds, injected drafts and comments, fake-token and personal-data leaks across reports and exports, and dependency-compromise guards. Open findings are tracked as expected failures and listed in docs/THREAT-MODEL.md.
  • Work merged into main under version 0.2.0 before this changelog existed, as recorded in the README and architecture: source-bound campaigns (campaign-check, campaign-stage, campaign-export); exact-payload approval binding (#7); rolling per-account live budgets and queue-health (#11); the fail-closed capability registry (#6) and X/Reddit/Pinterest go/no-go decisions (#24); canonical schemas and schema-export (#4); credential hygiene audit and threat model (#5); Bluesky and Mastodon adapter hardening (#12, #13); LinkedIn member text adapter (#14); UTM links, attribution and public-counter metrics-fetch (#15, #25); timing evaluation and preregistered growth experiments (#16); engagement triage drafts (#17); offline media-check (#18); manual-export for Threads, Instagram, Facebook Pages, YouTube and TikTok (#19-#23); sources-scan and signal-brief (#9); claims-check and campaign-preview (#10); landing-check (#25); health, dead-letter, opt-out list, caps file and privacy lifecycle (#26); optional Postiz backend (#30); field pilot protocol and pilot-report (#27); the worldwide distribution planner; full mypy coverage (#8).

Account and credential hardening (issue #5, owner-approved 2026-10-09). Offline and mock-tested only (tests/test_hardening.py); not exercised on a real host or account.

  • load_accounts also refuses an account whose env_prefix derives or overlaps a credential no account owns (REACHWEAVER_GITHUB_TOKEN, read by sources-scan --online); credentials-audit reports it as reserved_credential_variable. Example: a Mastodon prefix REACHWEAVER_GITHUB would otherwise read the GitHub token.
  • Breaking: publish --live also requires REACHWEAVER_ENV=prod, in addition to --live and REACHWEAVER_ALLOW_LIVE=1, for native adapters and the Postiz backend. policy.require_live_gate runs before any account is loaded, any credential is read or any request is made. Set the marker only on the production host. doctor, health, privacy-status, credentials-audit and the dry run show REACHWEAVER_ENV as dev, staging, prod, unset or unrecognized; other values are never echoed.
  • Breaking: overlapping env_prefix values are refused. load_accounts fails closed, naming the two account ids, when two accounts share an env_prefix or when one account’s credential variable name equals or begins another’s, over every adapter suffix and the Postiz _POSTIZ_API_KEY suffix (accounts.env_prefix_conflicts). Configs that shared a prefix, including several Postiz accounts sharing one key variable, must give each account its own prefix.
  • systemd LoadCredential= support. One accessor, accounts.credential/require_credential, now serves every credential read (publishers, connections, LinkedIn, Postiz, metrics fetch, GitHub sources, health, audit). With $CREDENTIALS_DIRECTORY set it reads only $CREDENTIALS_DIRECTORY/<NAME> and never the environment: absolute path, directory not group/world-writable, O_NOFOLLOW regular file, at most 64 KiB, UTF-8, one trailing newline stripped. Errors name the variable only. File values are redacted by privacy.redact like secret environment variables. Without the directory, environment variables work exactly as before.
  • credentials-audit reads through the same accessor and adds credential_source, runtime_environment and the findings overlapping_env_prefix, unsafe_credential_file, unsafe_credentials_directory and env_credential_shadowed. health reports credential_source per account and globally, and a credential_unreadable blocked reason.
  • .claude/settings.json denies Claude Code Bash commands that run approve or publish --live or contain REACHWEAVER_ALLOW_LIVE=1 or REACHWEAVER_ENV=prod (a harness rule, not a ReachWeaver control).
  • The inert systemd example adds LimitCORE=0, UMask=0077 and commented LoadCredential= / production-only REACHWEAVER_ENV lines. Account onboarding, the threat model and the security policy are updated; the security policy adds a private e-mail fallback for vulnerability reports.
  • media-check subtitle URL warnings show only scheme, host and path (?… marks a hidden query or fragment); a translated cue’s ?token= query string or user:password@ no longer reaches the report.
  • Release gate: human-only rows can no longer drop their distribution_id. The human-only ⇒ blocked rule ran only when a scorecard row had distribution_id, so deleting it from the hacker_news row let that row claim offline_mock_tested. A row whose id or capability_id names a human-only distribution-registry entry must now carry that entry as distribution_id and be blocked (scripts/release_gate.py; ReleaseReadinessScorecard in tests/test_onboarding.py).
  • media-check translated subtitles. A URL that a translated cue adds and its source cue lacks is now a warning (a link injected during translation is a phishing risk). Digit runs are normalised as text instead of str(int(run)), which raised ValueError past Python’s 4300-digit limit; leading zeros are ignored and long runs are shortened in findings. An SRT index such as ² or ① (accepted by str.isdigit(), rejected by int()) crashed the check and is now a “numeric index” error, and an over-long timestamp hour field is a malformed timestamp instead of an int()/float() overflow. Locale tags compare case-insensitively (RFC 5646), so an en-us track is the source of an en-US asset and target_locales matching ignores case. See docs/MEDIA-ASSETS.md.
  • calendar-plan --audience-activity DST fall-back collision. When a fall-back in the planner zone mapped two activity hours to the same local slot (UTC CSV, as_of 2026-10-21, Europe/Rome: Sunday 00:00 and 01:00 UTC are both 02:00), the lower-ranked hour’s provenance overwrote the top hour’s and one top hour was lost. audience.derive_slots now keeps the higher-ranked hour, warns with the dropped hour, and fills from the next-ranked hour; calendar.propose_calendar also keeps the first provenance per window. See docs/CALENDAR-PLANNER.md.
  • Similarity tokenizer and zero-width spaces in Thai, Lao, Khmer and Myanmar. Dropping every format character also removed U+200B where these scripts use it as the word break, so a one-word edit in ZWSP-segmented Thai scored 0.00 instead of 0.40. U+200B next to a letter of those scripts is now a space; elsewhere it, the joiners, word joiner, BOM, soft hyphen and bidi controls are still dropped, so the zero-width evasion tests stay green. See docs/CLAIM-GROUNDING.md.
  • Markdown table escaping. media-storyboard put the owner’s --safe-zones source URL (and campaign ids) into its Markdown tables raw, so a |, backtick or newline could add cells or rows; these cells are now escaped. report --format markdown cells now also escape backslash, backtick, [ ] ( ) * _ (CommonMark backslash escapes) besides pipes and HTML, as defence in depth (report_format.markdown_cell).
  • media-check now rejects SRT or WebVTT subtitle files that still contain an unresolved placeholder or TODO.
  • reachweaver.__version__ said 0.1.0 while pyproject.toml said 0.2.0. Both now say 0.2.0, and the release gate fails on any mismatch.
  • The README five-minute setup scheduled a post for a future time and then ran publish --post-id, which refuses posts that are not yet due. The preview now runs before schedule.
  • No owner account has been connected and no live end-to-end send has been run. The live adapters (Bluesky, Mastodon, Telegram, LinkedIn member) are mock-tested only.
  • No field pilot has been run, so there are no pilot results (issue #27 stays open for the consented pilot and its report).