@m13v/s4l 1.7.6-rc.2 → 1.7.6-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/dist/index.js +256 -92
- package/mcp/dist/telemetry.js +2 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_browser_foreground.py +7 -1
- package/mcp/menubar/s4l_card.py +144 -18
- package/mcp/menubar/s4l_card_canvas.py +135 -14
- package/mcp/menubar/s4l_menubar.py +216 -73
- package/mcp/menubar/s4l_state.py +45 -130
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/account_resolver.py +35 -1
- package/scripts/autopilot_stall_watch.py +8 -1
- package/scripts/browser_lifecycle.py +90 -0
- package/scripts/cdp_ready_check.py +81 -50
- package/scripts/draft_prompt_core.py +100 -17
- package/scripts/engagement_styles.py +135 -98
- package/scripts/merge_review_queue.py +192 -129
- package/scripts/platform_strike_events.py +54 -5
- package/scripts/post_reddit.py +10 -0
- package/scripts/preview_reddit_card.py +38 -0
- package/scripts/reap_stale_claude_sessions.py +7 -6
- package/scripts/reddit_ban_check.py +220 -0
- package/scripts/reddit_browser.py +24 -0
- package/scripts/release-mcpb.sh +38 -7
- package/scripts/setup_twitter_auth.py +20 -96
- package/scripts/store_patch.py +111 -0
- package/scripts/strike_alert.py +161 -12
- package/scripts/test_no_silent_fallbacks.py +11 -0
- package/scripts/test_stall_watch_batch_progression.py +99 -0
- package/scripts/top_performers.py +24 -0
- package/scripts/twitter_browser.py +22 -52
- package/skill/archive-old-logs.sh +24 -0
- package/skill/lib/reddit-backend.sh +1 -0
- package/skill/run-twitter-cycle.sh +146 -66
|
@@ -1675,6 +1675,66 @@ if [ -z "$CANDIDATES" ]; then
|
|
|
1675
1675
|
exit 0
|
|
1676
1676
|
fi
|
|
1677
1677
|
|
|
1678
|
+
# --- ROLLING VIRALITY BAR fetch + candidate-level PREFILTER (2026-07-17) -----
|
|
1679
|
+
# The p0.99 bar (see the PRE-DRAFT VIRALITY GATE below for history) is fetched
|
|
1680
|
+
# HERE, immediately after the candidate fetch, and applied as a deterministic
|
|
1681
|
+
# per-candidate filter BEFORE the prompt is ever built: when the bar is active,
|
|
1682
|
+
# below-bar candidates are removed from $CANDIDATES so the drafting session
|
|
1683
|
+
# only sees candidates that can actually enter the plan (plan-parse keeps the
|
|
1684
|
+
# top-1 on-brand pick ONLY if it clears this same bar). Measured 2026-07-17,
|
|
1685
|
+
# before this prefilter: 125 below-bar draft-reuse offers across 50 prompts
|
|
1686
|
+
# converted into 0 of 24 plan entries, so drafting below-bar candidates bought
|
|
1687
|
+
# nothing. Filtered rows are NOT expired or rejected: they stay 'pending', are
|
|
1688
|
+
# salvaged by later cycles, and re-enter the prompt automatically if their
|
|
1689
|
+
# thread's virality rises above the bar within the freshness window. Placement
|
|
1690
|
+
# AFTER the empty-batch expire check above is deliberate: a batch that is
|
|
1691
|
+
# non-empty but fully below-bar must NOT be expired, only skipped this cycle.
|
|
1692
|
+
# Fail-open: bar OFF (cold start / thin pool / fetch failure / sandbox) means
|
|
1693
|
+
# no filtering and drafting behaves exactly as before.
|
|
1694
|
+
if [ -n "${S4L_SANDBOX_CANDIDATES_FILE:-}" ]; then
|
|
1695
|
+
# The fetch below reads /api/v1/twitter-candidates/virality-threshold under
|
|
1696
|
+
# THIS machine's own identity (http_api.py's X-Installation), so it would
|
|
1697
|
+
# reflect the OPERATOR's trailing-24h candidate pool -- the wrong baseline
|
|
1698
|
+
# entirely when replaying another install's (e.g. Karol's, Nhat's)
|
|
1699
|
+
# candidates. A sandbox test also wants every model-approved draft
|
|
1700
|
+
# visible, not gated to only the top-1 that would clear a live posting
|
|
1701
|
+
# cadence bar which, again, doesn't apply since nothing is ever posted.
|
|
1702
|
+
VIRALITY_THRESHOLD=""
|
|
1703
|
+
log "Virality bar OFF for sandbox mode (would reflect the operator's own pool, not the replayed install's; every model-approved draft stays visible)."
|
|
1704
|
+
else
|
|
1705
|
+
VIRALITY_THRESHOLD=$(S4L_VPCTILE="0.99" \
|
|
1706
|
+
S4L_VMIN="${S4L_TWITTER_VIRALITY_MIN_SAMPLE:-200}" \
|
|
1707
|
+
S4L_SCRIPTS_DIR="$REPO_DIR/scripts" \
|
|
1708
|
+
python3 -c "
|
|
1709
|
+
import os, sys
|
|
1710
|
+
_repo = os.path.expanduser(os.environ.get('S4L_REPO_DIR') or os.environ.get('REPO_DIR') or '~/social-autoposter')
|
|
1711
|
+
sys.path.insert(0, os.environ.get('S4L_SCRIPTS_DIR') or os.path.join(_repo, 'scripts'))
|
|
1712
|
+
from http_api import api_get
|
|
1713
|
+
try:
|
|
1714
|
+
r = api_get('/api/v1/twitter-candidates/virality-threshold',
|
|
1715
|
+
{'pctile': os.environ['S4L_VPCTILE'], 'hours': 24})
|
|
1716
|
+
d = (r or {}).get('data') or {}
|
|
1717
|
+
thr = d.get('threshold')
|
|
1718
|
+
n = int(d.get('sample_count') or 0)
|
|
1719
|
+
mn = int(os.environ['S4L_VMIN'])
|
|
1720
|
+
if thr is not None and n >= mn:
|
|
1721
|
+
print(f'{float(thr):.4f}')
|
|
1722
|
+
except BaseException as e:
|
|
1723
|
+
sys.stderr.write(f'virality-bar fetch failed (bar OFF this cycle): {e}\n')
|
|
1724
|
+
" 2>>"$LOG_FILE" || echo "")
|
|
1725
|
+
if [ -n "$VIRALITY_THRESHOLD" ]; then
|
|
1726
|
+
log "Virality bar ACTIVE: p0.99 = $VIRALITY_THRESHOLD (this install, trailing 24h); top-1 kept only if it clears the bar."
|
|
1727
|
+
else
|
|
1728
|
+
log "Virality bar OFF this cycle (cold-start/thin pool or fetch failed); top-1 kept ungated."
|
|
1729
|
+
fi
|
|
1730
|
+
fi
|
|
1731
|
+
if [ -n "$VIRALITY_THRESHOLD" ] && [ -n "$CANDIDATES" ]; then
|
|
1732
|
+
_VF_TOTAL=$(printf '%s\n' "$CANDIDATES" | grep -c '^[0-9]' || true)
|
|
1733
|
+
CANDIDATES=$(printf '%s\n' "$CANDIDATES" | awk -F'|' -v b="$VIRALITY_THRESHOLD" '$1 ~ /^[0-9]+$/ && NF>=5 && $5+0 >= b+0')
|
|
1734
|
+
_VF_KEPT=$(printf '%s\n' "$CANDIDATES" | grep -c '^[0-9]' || true)
|
|
1735
|
+
log "[virality_prefilter] kept ${_VF_KEPT:-0} of ${_VF_TOTAL:-0} candidate(s) at/above bar=$VIRALITY_THRESHOLD; below-bar rows stay pending and are never sent to the drafter"
|
|
1736
|
+
fi
|
|
1737
|
+
|
|
1678
1738
|
# --- SCAN_ONLY gate: stop after scoring, emit candidates, skip drafting -------
|
|
1679
1739
|
# When SCAN_ONLY=1 the cycle runs scan -> score -> top-N select, writes the chosen
|
|
1680
1740
|
# candidates as JSON, and STOPS before the claude drafting step. The MCP
|
|
@@ -1784,13 +1844,43 @@ done <<< "$CANDIDATES"
|
|
|
1784
1844
|
# (honoring the same S4L_SANDBOX_CONFIG_DIR override and lane whitelist/ops
|
|
1785
1845
|
# denylist). See the render-twitter call below.
|
|
1786
1846
|
|
|
1847
|
+
# --- Draft-prompt A/B arm (v4, voice-first) ----------------------------------
|
|
1848
|
+
# Owned by scripts/draft_prompt_core.py (single source of truth, shared with
|
|
1849
|
+
# the Reddit draft lane): the full experiment history, both arm texts, the
|
|
1850
|
+
# Draft-B divergence note, the persona-lane directive, and the pick
|
|
1851
|
+
# precedence (pre-set env for sandbox forcing -> server-side per-install pin
|
|
1852
|
+
# -> 50/50 coin flip via TWITTER_DRAFT_PROMPT_AB_RATE) ALL live there. The
|
|
1853
|
+
# arm is stamped onto every post this cycle via S4L_DRAFT_PROMPT_VARIANT
|
|
1854
|
+
# (active_experiments.collect -> plan candidates -> twitter_post_plan.py ->
|
|
1855
|
+
# posts.draft_prompt_variant), and engagement_styles.get_assigned_style_prompt
|
|
1856
|
+
# reads the SAME env var at render time in this process.
|
|
1857
|
+
#
|
|
1858
|
+
# MOVED HERE, before the style picker, on 2026-07-17: treatment_v4 assigns no
|
|
1859
|
+
# engagement style at all (see the style-picker block below), so the arm must
|
|
1860
|
+
# be known BEFORE deciding whether to run the picker. Do not move this back
|
|
1861
|
+
# below the style picker.
|
|
1862
|
+
source "$REPO_DIR/skill/styles.sh"
|
|
1863
|
+
S4L_DRAFT_PROMPT_VARIANT=$(python3 "$REPO_DIR/scripts/draft_prompt_core.py" pick-arm 2>>"$LOG_FILE" || echo treatment_v4)
|
|
1864
|
+
export S4L_DRAFT_PROMPT_VARIANT
|
|
1865
|
+
log "Draft-prompt A/B arm: $S4L_DRAFT_PROMPT_VARIANT (draft_prompt_core pick-arm: env>pin>coin)"
|
|
1866
|
+
|
|
1787
1867
|
# Engagement-style picker (2026-05-19): pick ONE assigned style for this
|
|
1788
1868
|
# cycle. The picked style flows two places: (1) --style filter for
|
|
1789
1869
|
# top_performers.py so the per-style exemplars section shows only posts
|
|
1790
1870
|
# matching the assigned style, (2) s4l_render_style_block (below) so the
|
|
1791
1871
|
# prompt block embeds the same assignment. On invent mode picked_style is
|
|
1792
1872
|
# empty and top_performers stays unfiltered (model sees full landscape).
|
|
1793
|
-
|
|
1873
|
+
#
|
|
1874
|
+
# Called UNCONDITIONALLY regardless of arm (2026-07-17 consolidation): under
|
|
1875
|
+
# treatment_v4, pick_style_for_post()/pick_exploration_style() themselves
|
|
1876
|
+
# (scripts/engagement_styles.py) refuse to assign a real style and return
|
|
1877
|
+
# the "voice_first" sentinel directly -- no network round-trip, and no arm
|
|
1878
|
+
# check needed here in the shell. This used to be a Twitter-only branch
|
|
1879
|
+
# here that skipped the picker and blanked PICKED_STYLE by hand; that logic
|
|
1880
|
+
# moved into the picker functions themselves so every caller (this bash
|
|
1881
|
+
# driver AND scripts/post_reddit.py, which calls the same functions
|
|
1882
|
+
# directly and was never covered by the old Twitter-only branch) gets the
|
|
1883
|
+
# same behavior automatically. Do not reintroduce an arm check in this file.
|
|
1794
1884
|
STYLE_ASSIGN_FILE=$(mktemp -t s4l_twitter_assign_XXXXXX.json)
|
|
1795
1885
|
s4l_pick_style twitter posting "$STYLE_ASSIGN_FILE" >/dev/null 2>&1 || true
|
|
1796
1886
|
PICKED_STYLE=$(python3 -c "
|
|
@@ -1821,7 +1911,7 @@ log "Engagement style assigned: mode=$PICKED_MODE style=${PICKED_STYLE:-(invent)
|
|
|
1821
1911
|
# are actually different (a same-name pair defeats the whole point); INVENT
|
|
1822
1912
|
# mode on either side is accepted immediately since an invented name is
|
|
1823
1913
|
# definitionally distinct from a pinned one. This is orthogonal to the
|
|
1824
|
-
# treatment_v4/control_v4 draft-prompt A/B
|
|
1914
|
+
# treatment_v4/control_v4 draft-prompt A/B above (that varies WORDING of the
|
|
1825
1915
|
# directive for the whole cycle; this varies STYLE per draft slot), so neither
|
|
1826
1916
|
# experiment disturbs the other.
|
|
1827
1917
|
STYLE_ASSIGN_FILE_B=$(mktemp -t s4l_twitter_assign_b_XXXXXX.json)
|
|
@@ -1889,20 +1979,6 @@ fi
|
|
|
1889
1979
|
export S4L_EXP_DRAFT_B_SOURCE="$DRAFT_B_SOURCE"
|
|
1890
1980
|
log "Engagement style B assigned: mode=$PICKED_MODE_B style=${PICKED_STYLE_B:-(invent)} source=$DRAFT_B_SOURCE"
|
|
1891
1981
|
|
|
1892
|
-
# --- Draft-prompt A/B arm (v4, voice-first) ----------------------------------
|
|
1893
|
-
# Owned by scripts/draft_prompt_core.py (single source of truth, shared with
|
|
1894
|
-
# the Reddit draft lane): the full experiment history, both arm texts, the
|
|
1895
|
-
# Draft-B divergence note, the persona-lane directive, and the pick
|
|
1896
|
-
# precedence (pre-set env for sandbox forcing -> server-side per-install pin
|
|
1897
|
-
# -> 50/50 coin flip via TWITTER_DRAFT_PROMPT_AB_RATE) ALL live there. The
|
|
1898
|
-
# arm is stamped onto every post this cycle via S4L_DRAFT_PROMPT_VARIANT
|
|
1899
|
-
# (active_experiments.collect -> plan candidates -> twitter_post_plan.py ->
|
|
1900
|
-
# posts.draft_prompt_variant), and engagement_styles.get_assigned_style_prompt
|
|
1901
|
-
# reads the SAME env var at render time in this process.
|
|
1902
|
-
S4L_DRAFT_PROMPT_VARIANT=$(python3 "$REPO_DIR/scripts/draft_prompt_core.py" pick-arm 2>>"$LOG_FILE" || echo treatment_v4)
|
|
1903
|
-
export S4L_DRAFT_PROMPT_VARIANT
|
|
1904
|
-
log "Draft-prompt A/B arm: $S4L_DRAFT_PROMPT_VARIANT (draft_prompt_core pick-arm: env>pin>coin)"
|
|
1905
|
-
|
|
1906
1982
|
# 2026-07-10 anti-sameness: --no-project-sections strips the multi-project
|
|
1907
1983
|
# winner corpus (~400 lines of "Top Posts by Project" + summary table) that
|
|
1908
1984
|
# used to ride along in EVERY draft prompt and homogenize drafts. The prep
|
|
@@ -2031,6 +2107,28 @@ SKIP_FILE="/tmp/twitter_cycle_skips_${BATCH_ID}.json"
|
|
|
2031
2107
|
if [ -z "${S4L_SANDBOX_CANDIDATES_FILE:-}" ]; then
|
|
2032
2108
|
python3 "$REPO_DIR/scripts/twitter_batch_phase.py" advance "$BATCH_ID" --phase phase2b-prep 2>&1 | tee -a "$LOG_FILE" || true
|
|
2033
2109
|
fi
|
|
2110
|
+
|
|
2111
|
+
# --- PRE-DRAFT VIRALITY GATE (2026-07-15; prefilter degenerate case 2026-07-17) ---
|
|
2112
|
+
# The bar is now fetched and applied as a candidate-level PREFILTER right after
|
|
2113
|
+
# the candidate fetch (see [virality_prefilter] above), so the drafting session
|
|
2114
|
+
# never sees a candidate that cannot enter the plan. This gate is the leftover
|
|
2115
|
+
# degenerate case: NOTHING survived the prefilter => skip media capture and the
|
|
2116
|
+
# whole Claude drafting session and synthesize an empty prep envelope through
|
|
2117
|
+
# the existing plan_count=0 path. Candidates stay status='pending' (never
|
|
2118
|
+
# expired or rejected here); Phase 0 salvages and re-judges them next cycle.
|
|
2119
|
+
# With the bar OFF the prefilter is a no-op and this gate can only fire on an
|
|
2120
|
+
# empty candidate list, which the expire check above already handles.
|
|
2121
|
+
PREGATE_SKIP=0
|
|
2122
|
+
_PG_N=$(printf '%s\n' "$CANDIDATES" | grep -c '^[0-9]' || true)
|
|
2123
|
+
if [ "${_PG_N:-0}" = "0" ]; then
|
|
2124
|
+
PREGATE_SKIP=1
|
|
2125
|
+
if [ -n "$VIRALITY_THRESHOLD" ]; then
|
|
2126
|
+
log "[virality_pregate] SKIP drafting: no candidate at/above bar=$VIRALITY_THRESHOLD survived the prefilter; rows stay pending for next-cycle salvage"
|
|
2127
|
+
else
|
|
2128
|
+
log "[virality_pregate] SKIP drafting: candidate list is empty"
|
|
2129
|
+
fi
|
|
2130
|
+
fi
|
|
2131
|
+
|
|
2034
2132
|
# Thread-media capture (2026-06-03, gated by S4L_TWITTER_CAPTURE_MEDIA, default
|
|
2035
2133
|
# OFF). When enabled, does ONE cheap deterministic pass over every candidate
|
|
2036
2134
|
# thread to pull its media (images/videos/GIFs/link-cards), persist each into
|
|
@@ -2047,7 +2145,11 @@ fi
|
|
|
2047
2145
|
# capture off). Found live: a sandbox test run waited ~6 minutes in the FIFO
|
|
2048
2146
|
# lock queue behind a real cycle for a lock it never ended up needing.
|
|
2049
2147
|
MEDIA_BLOCK=""
|
|
2050
|
-
if [ "$
|
|
2148
|
+
if [ "$PREGATE_SKIP" = "1" ]; then
|
|
2149
|
+
# Virality pre-gate: no draft will be produced this cycle, so thread-media
|
|
2150
|
+
# capture would be pure waste; skip it along with the drafting session.
|
|
2151
|
+
rm -f "$MEDIA_URLS_FILE" 2>/dev/null || true
|
|
2152
|
+
elif [ "${S4L_TWITTER_CAPTURE_MEDIA:-0}" = "1" ] || [ "${S4L_TWITTER_CAPTURE_MEDIA:-}" = "true" ]; then
|
|
2051
2153
|
log "Re-acquiring twitter-browser lock for Phase 2b-prep (read+draft only)..."
|
|
2052
2154
|
acquire_lock "twitter-browser" 3600 2>>"$LOG_FILE"
|
|
2053
2155
|
log "twitter-browser lock held (pid=$$) Phase 2b-prep"
|
|
@@ -2166,20 +2268,29 @@ fi
|
|
|
2166
2268
|
# only the exemplar context, so this is the ONLY durable full-prompt record.
|
|
2167
2269
|
# Local-only, newest 50 kept (file cleanup, not candidate-row retention; the
|
|
2168
2270
|
# no-retention rule covers DB *_candidates rows). Never blocks the run.
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2271
|
+
if [ "$PREGATE_SKIP" = "1" ]; then
|
|
2272
|
+
# Virality pre-gate fired: skip the prompt snapshot (nothing is sent) and
|
|
2273
|
+
# the whole Claude drafting session, and synthesize the empty envelope the
|
|
2274
|
+
# plan parser already handles (identical outcome to "drafted, all picks
|
|
2275
|
+
# dropped below bar": plan_count=0, candidates stay pending for salvage).
|
|
2276
|
+
log "[virality_pregate] drafting session skipped; synthesizing empty prep envelope"
|
|
2277
|
+
PREP_OUTPUT='{"type":"result","subtype":"success","is_error":false,"structured_output":{"candidates":[],"rejected":[]}}'
|
|
2278
|
+
else
|
|
2279
|
+
PREP_PROMPT_DIR="${S4L_STATE_DIR:-$HOME/.social-autoposter-mcp}/prep-prompts"
|
|
2280
|
+
if mkdir -p "$PREP_PROMPT_DIR" 2>/dev/null; then
|
|
2281
|
+
_PP_FILE="$PREP_PROMPT_DIR/prep-prompt-$BATCH_ID.md"
|
|
2282
|
+
if printf '%s' "$PREP_PROMPT" > "$_PP_FILE" 2>/dev/null; then
|
|
2283
|
+
ls -t "$PREP_PROMPT_DIR"/prep-prompt-*.md 2>/dev/null | tail -n +51 | while IFS= read -r _pp_old; do
|
|
2284
|
+
rm -f "$_pp_old"
|
|
2285
|
+
done
|
|
2286
|
+
log "[prep_prompt_snapshot] batch=$BATCH_ID bytes=$(wc -c < "$_PP_FILE" | tr -d ' ') path=$_PP_FILE"
|
|
2287
|
+
else
|
|
2288
|
+
log "WARN: prep-prompt snapshot write failed for batch=$BATCH_ID (non-fatal)"
|
|
2289
|
+
fi
|
|
2179
2290
|
fi
|
|
2180
|
-
fi
|
|
2181
2291
|
|
|
2182
|
-
PREP_OUTPUT=$(printf '%s' "$PREP_PROMPT" | "$REPO_DIR/scripts/run_claude.sh" "run-twitter-cycle-prep" --strict-mcp-config --mcp-config "$TW_MCP_CONFIG" --allowedTools WebSearch,WebFetch -p --output-format json --json-schema "$PREP_SCHEMA" 2>&1)
|
|
2292
|
+
PREP_OUTPUT=$(printf '%s' "$PREP_PROMPT" | "$REPO_DIR/scripts/run_claude.sh" "run-twitter-cycle-prep" --strict-mcp-config --mcp-config "$TW_MCP_CONFIG" --allowedTools WebSearch,WebFetch -p --output-format json --json-schema "$PREP_SCHEMA" 2>&1)
|
|
2293
|
+
fi
|
|
2183
2294
|
|
|
2184
2295
|
echo "$PREP_OUTPUT" >> "$LOG_FILE"
|
|
2185
2296
|
|
|
@@ -2227,43 +2338,12 @@ fi
|
|
|
2227
2338
|
# reach-recovery: quality over volume): single source of truth, no env
|
|
2228
2339
|
# var, no fallback, one path (every install behaves identically regardless of how
|
|
2229
2340
|
# its plist was generated). Sample floor S4L_TWITTER_VIRALITY_MIN_SAMPLE default 200.
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
# visible, not gated to only the top-1 that would clear a live posting
|
|
2237
|
-
# cadence bar which, again, doesn't apply since nothing is ever posted.
|
|
2238
|
-
VIRALITY_THRESHOLD=""
|
|
2239
|
-
log "Virality bar OFF for sandbox mode (would reflect the operator's own pool, not the replayed install's; every model-approved draft stays visible)."
|
|
2240
|
-
else
|
|
2241
|
-
VIRALITY_THRESHOLD=$(S4L_VPCTILE="0.99" \
|
|
2242
|
-
S4L_VMIN="${S4L_TWITTER_VIRALITY_MIN_SAMPLE:-200}" \
|
|
2243
|
-
S4L_SCRIPTS_DIR="$REPO_DIR/scripts" \
|
|
2244
|
-
python3 -c "
|
|
2245
|
-
import os, sys
|
|
2246
|
-
_repo = os.path.expanduser(os.environ.get('S4L_REPO_DIR') or os.environ.get('REPO_DIR') or '~/social-autoposter')
|
|
2247
|
-
sys.path.insert(0, os.environ.get('S4L_SCRIPTS_DIR') or os.path.join(_repo, 'scripts'))
|
|
2248
|
-
from http_api import api_get
|
|
2249
|
-
try:
|
|
2250
|
-
r = api_get('/api/v1/twitter-candidates/virality-threshold',
|
|
2251
|
-
{'pctile': os.environ['S4L_VPCTILE'], 'hours': 24})
|
|
2252
|
-
d = (r or {}).get('data') or {}
|
|
2253
|
-
thr = d.get('threshold')
|
|
2254
|
-
n = int(d.get('sample_count') or 0)
|
|
2255
|
-
mn = int(os.environ['S4L_VMIN'])
|
|
2256
|
-
if thr is not None and n >= mn:
|
|
2257
|
-
print(f'{float(thr):.4f}')
|
|
2258
|
-
except BaseException as e:
|
|
2259
|
-
sys.stderr.write(f'virality-bar fetch failed (bar OFF this cycle): {e}\n')
|
|
2260
|
-
" 2>>"$LOG_FILE" || echo "")
|
|
2261
|
-
if [ -n "$VIRALITY_THRESHOLD" ]; then
|
|
2262
|
-
log "Virality bar ACTIVE: p0.99 = $VIRALITY_THRESHOLD (this install, trailing 24h); top-1 kept only if it clears the bar."
|
|
2263
|
-
else
|
|
2264
|
-
log "Virality bar OFF this cycle (cold-start/thin pool or fetch failed); top-1 kept ungated."
|
|
2265
|
-
fi
|
|
2266
|
-
fi
|
|
2341
|
+
# VIRALITY_THRESHOLD is fetched ONCE per cycle by the candidate-level
|
|
2342
|
+
# PREFILTER right after the candidate fetch (2026-07-17; before that by the
|
|
2343
|
+
# 2026-07-15 pre-draft gate; originally fetched here, post-draft). The parse
|
|
2344
|
+
# below reuses the value; with the prefilter active its below-bar drop should
|
|
2345
|
+
# never fire and remains as belt-and-suspenders. Sandbox runs carry an empty
|
|
2346
|
+
# threshold (bar OFF) from the same block.
|
|
2267
2347
|
|
|
2268
2348
|
# Parse the prep envelope and write the plan to \$PLAN_FILE; also extract the
|
|
2269
2349
|
# 'rejected' array into \$SKIP_FILE so log_twitter_skips.py can persist a
|