@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.
Files changed (35) hide show
  1. package/mcp/dist/index.js +256 -92
  2. package/mcp/dist/telemetry.js +2 -1
  3. package/mcp/dist/version.json +2 -2
  4. package/mcp/manifest.json +1 -1
  5. package/mcp/menubar/s4l_browser_foreground.py +7 -1
  6. package/mcp/menubar/s4l_card.py +144 -18
  7. package/mcp/menubar/s4l_card_canvas.py +135 -14
  8. package/mcp/menubar/s4l_menubar.py +216 -73
  9. package/mcp/menubar/s4l_state.py +45 -130
  10. package/mcp/package.json +1 -1
  11. package/package.json +1 -1
  12. package/scripts/account_resolver.py +35 -1
  13. package/scripts/autopilot_stall_watch.py +8 -1
  14. package/scripts/browser_lifecycle.py +90 -0
  15. package/scripts/cdp_ready_check.py +81 -50
  16. package/scripts/draft_prompt_core.py +100 -17
  17. package/scripts/engagement_styles.py +135 -98
  18. package/scripts/merge_review_queue.py +192 -129
  19. package/scripts/platform_strike_events.py +54 -5
  20. package/scripts/post_reddit.py +10 -0
  21. package/scripts/preview_reddit_card.py +38 -0
  22. package/scripts/reap_stale_claude_sessions.py +7 -6
  23. package/scripts/reddit_ban_check.py +220 -0
  24. package/scripts/reddit_browser.py +24 -0
  25. package/scripts/release-mcpb.sh +38 -7
  26. package/scripts/setup_twitter_auth.py +20 -96
  27. package/scripts/store_patch.py +111 -0
  28. package/scripts/strike_alert.py +161 -12
  29. package/scripts/test_no_silent_fallbacks.py +11 -0
  30. package/scripts/test_stall_watch_batch_progression.py +99 -0
  31. package/scripts/top_performers.py +24 -0
  32. package/scripts/twitter_browser.py +22 -52
  33. package/skill/archive-old-logs.sh +24 -0
  34. package/skill/lib/reddit-backend.sh +1 -0
  35. 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
- source "$REPO_DIR/skill/styles.sh"
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 below (that varies WORDING of the
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 [ "${S4L_TWITTER_CAPTURE_MEDIA:-0}" = "1" ] || [ "${S4L_TWITTER_CAPTURE_MEDIA:-}" = "true" ]; then
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
- PREP_PROMPT_DIR="${S4L_STATE_DIR:-$HOME/.social-autoposter-mcp}/prep-prompts"
2170
- if mkdir -p "$PREP_PROMPT_DIR" 2>/dev/null; then
2171
- _PP_FILE="$PREP_PROMPT_DIR/prep-prompt-$BATCH_ID.md"
2172
- if printf '%s' "$PREP_PROMPT" > "$_PP_FILE" 2>/dev/null; then
2173
- ls -t "$PREP_PROMPT_DIR"/prep-prompt-*.md 2>/dev/null | tail -n +51 | while IFS= read -r _pp_old; do
2174
- rm -f "$_pp_old"
2175
- done
2176
- log "[prep_prompt_snapshot] batch=$BATCH_ID bytes=$(wc -c < "$_PP_FILE" | tr -d ' ') path=$_PP_FILE"
2177
- else
2178
- log "WARN: prep-prompt snapshot write failed for batch=$BATCH_ID (non-fatal)"
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
- if [ -n "${S4L_SANDBOX_CANDIDATES_FILE:-}" ]; then
2231
- # The fetch below reads /api/v1/twitter-candidates/virality-threshold under
2232
- # THIS machine's own identity (http_api.py's X-Installation), so it would
2233
- # reflect the OPERATOR's trailing-24h candidate pool -- the wrong baseline
2234
- # entirely when replaying another install's (e.g. Karol's, Nhat's)
2235
- # candidates. A sandbox test also wants every model-approved draft
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