@m13v/s4l 1.7.5-rc.30 → 1.7.5-rc.32

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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.5-rc.30",
3
- "installedAt": "2026-07-16T00:37:45.238Z"
2
+ "version": "1.7.5-rc.32",
3
+ "installedAt": "2026-07-16T01:09:13.414Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "S4L",
5
- "version": "1.7.5-rc.30",
5
+ "version": "1.7.5-rc.32",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
8
8
  "author": {
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.5-rc.30",
3
+ "version": "1.7.5-rc.32",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.5-rc.30",
3
+ "version": "1.7.5-rc.32",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js",
@@ -107,6 +107,17 @@ def _atomic_write(path: str, obj) -> None:
107
107
 
108
108
  def _dedup_key(c: dict) -> str:
109
109
  """Match submit_drafts: dedup by the thread/candidate URL, else candidate_id."""
110
+ # Sandbox cards (experiments.sandbox=true) replay real historical threads
111
+ # ON PURPOSE, including ones already dealt with for real -- that's the
112
+ # whole point of testing against past data. Namespace their key so they
113
+ # never collide with (and get silently swallowed by) a real card for the
114
+ # same URL, and keep the reply text IN the key so two different prompt
115
+ # variants drafting the same thread both surface as distinct cards
116
+ # instead of the second dropping as "already seen" (identical reruns
117
+ # still naturally dedupe against each other, which is fine).
118
+ if (c.get("experiments") or {}).get("sandbox"):
119
+ url = c.get("candidate_url") or c.get("tweet_url") or c.get("thread_url") or ""
120
+ return f"sandbox:{url}:{(c.get('reply_text') or '')[:80]}"
110
121
  for k in ("candidate_url", "tweet_url", "thread_url", "candidate_id"):
111
122
  v = c.get(k)
112
123
  if v:
@@ -2,6 +2,15 @@
2
2
  # harness-common.sh - the shared engine behind every per-platform browser
3
3
  # backend (twitter-backend.sh, reddit-backend.sh, linkedin-backend.sh).
4
4
  #
5
+ # LOCAL OVERRIDES: ~/.social-autoposter-mcp/harness-local.env is sourced (when
6
+ # present) before any launch decision. It is machine-local (never shipped),
7
+ # for per-box experiments without a release: e.g. pointing one operator box's
8
+ # harness at Chrome Beta (BH_CHROME_BIN=...) or adding launch flags
9
+ # (HC_EXTRA_FLAGS="--no-startup-window") while every other install keeps
10
+ # stock behavior. Values here are plain shell assignments; keep it tiny.
11
+ # shellcheck disable=SC1090
12
+ [ -f "$HOME/.social-autoposter-mcp/harness-local.env" ] && . "$HOME/.social-autoposter-mcp/harness-local.env" 2>/dev/null || true
13
+ #
5
14
  # Born 2026-07-14 from the three near-duplicate backends. Every hard-won fix
6
15
  # used to land in ONE backend and drift: the CDP wedge detector, the two-strike
7
16
  # tolerance, the focus-safe launcher, the occlusion flags, and the repo-relative
@@ -287,6 +296,15 @@ hc_ensure_browser() {
287
296
  # 2026-07-14/15 EXC_BREAKPOINT (deliberate internal abort) crash dumps
288
297
  # arriving ~once per cycle with zero jetsam/OOM evidence. Playwright
289
298
  # and Puppeteer both launch with it for exactly this reason.
299
+ # With --no-startup-window (focus-steal prevention: Chrome creates NO
300
+ # window at launch, so macOS has nothing to activate; the first
301
+ # pipeline op mints its tab via the background Target.createTarget
302
+ # path) a positional URL argument would force a window and defeat the
303
+ # flag — drop the launch URL in that mode.
304
+ local _launch_url="${HC_LAUNCH_URL:-about:blank}"
305
+ case " ${HC_EXTRA_FLAGS:-} " in
306
+ *" --no-startup-window "*) _launch_url="" ;;
307
+ esac
290
308
  launch_harness_chrome "$_chrome_bin" "$_prof" \
291
309
  --remote-debugging-port="$_port" \
292
310
  --user-data-dir="$_prof" \
@@ -296,7 +314,7 @@ hc_ensure_browser() {
296
314
  --disable-backgrounding-occluded-windows \
297
315
  ${HC_EXTRA_FLAGS:-} \
298
316
  ${_os_extra} \
299
- "${HC_LAUNCH_URL:-about:blank}"
317
+ ${_launch_url}
300
318
  for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
301
319
  curl -sf --max-time 2 -o /dev/null "${_def_url}/json/version" 2>/dev/null && break
302
320
  sleep 1
@@ -284,8 +284,23 @@ python3 "$REPO_DIR/scripts/reddit_browser_lock.py" acquire --timeout 60 --ttl 30
284
284
  # old ensure_browser_healthy "reddit" ps-scan-for-MCP-Chrome path; the harness is
285
285
  # the single browser the whole Reddit pipeline now rides (REDDIT_CDP_URL points
286
286
  # every child Python proc at it). Falls back to ensure_browser_healthy on failure.
287
- if ! ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE"; then
288
- log "WARNING: reddit-harness bootstrap failed; falling back to ensure_browser_healthy reddit"
287
+ ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE" || true
288
+ _ensure_rc="${PIPESTATUS[0]}"
289
+ if [ "$_ensure_rc" = "78" ]; then
290
+ # Reserved skip code (harness-common.sh:182-185): the pre-launch hook
291
+ # deliberately deferred because a poster is mid-drain on the ONE shared
292
+ # tab. This is NOT a bootstrap failure — falling back to
293
+ # ensure_browser_healthy here (as this used to, treating ANY nonzero rc
294
+ # as broken) is wrong on two counts: it misreports a clean defer as a
295
+ # warning, and ensure_browser_healthy checks the pre-harness
296
+ # browser-profiles/reddit path (dead since the 2026-05-29 migration), so
297
+ # it is pure wasted work every time cycles overlap around an active post
298
+ # (routine — see the "double-fork wrapper" note above BATCH_ID).
299
+ log "reddit-harness bootstrap deferred (rc=78, poster active elsewhere); skipping this cycle so the poster keeps the tab."
300
+ python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
301
+ exit 0
302
+ elif [ "$_ensure_rc" != "0" ]; then
303
+ log "WARNING: reddit-harness bootstrap failed (rc=$_ensure_rc); falling back to ensure_browser_healthy reddit"
289
304
  ensure_browser_healthy "reddit"
290
305
  fi
291
306
  python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
@@ -256,8 +256,22 @@ export CLAUDE_SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')
256
256
  log "Acquiring reddit-browser lease (TTL 90s, MCP-proxy heartbeated)..."
257
257
  python3 "$REPO_DIR/scripts/reddit_browser_lock.py" acquire --timeout 600 --ttl 90 2>&1 | tee -a "$LOG_FILE" || \
258
258
  log "WARNING: reddit_browser_lock.py acquire failed; proceeding without lease (peer pipelines may collide)."
259
- if ! ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE"; then
260
- log "WARNING: reddit-harness bootstrap failed; falling back to ensure_browser_healthy reddit"
259
+ ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE" || true
260
+ _ensure_rc="${PIPESTATUS[0]}"
261
+ if [ "$_ensure_rc" = "78" ]; then
262
+ # Reserved skip code (harness-common.sh:182-185): the pre-launch hook
263
+ # deliberately deferred because a poster is mid-drain on the ONE shared
264
+ # tab. This is NOT a bootstrap failure — falling back to the legacy
265
+ # ensure_browser_healthy here (as this used to, treating ANY nonzero rc
266
+ # as broken) is wrong on two counts: it misreports a clean defer as a
267
+ # warning, and ensure_browser_healthy checks the pre-harness
268
+ # browser-profiles/reddit path (dead since the 2026-05-29 migration), so
269
+ # it is pure wasted work every time cycles overlap around an active post
270
+ # (routine, see the "double-fork wrapper" note in run-reddit-search.sh).
271
+ log "reddit-harness bootstrap deferred (rc=78, poster active elsewhere); skipping this run so the poster keeps the tab."
272
+ exit 0
273
+ elif [ "$_ensure_rc" != "0" ]; then
274
+ log "WARNING: reddit-harness bootstrap failed (rc=$_ensure_rc); falling back to ensure_browser_healthy reddit"
261
275
  ensure_browser_healthy "reddit"
262
276
  fi
263
277
 
@@ -2205,23 +2205,29 @@ SKIP_FILE="/tmp/twitter_cycle_skips_${BATCH_ID}.json"
2205
2205
  if [ -z "${S4L_SANDBOX_CANDIDATES_FILE:-}" ]; then
2206
2206
  python3 "$REPO_DIR/scripts/twitter_batch_phase.py" advance "$BATCH_ID" --phase phase2b-prep 2>&1 | tee -a "$LOG_FILE" || true
2207
2207
  fi
2208
- log "Re-acquiring twitter-browser lock for Phase 2b-prep (read+draft only)..."
2209
- acquire_lock "twitter-browser" 3600 2>>"$LOG_FILE"
2210
- log "twitter-browser lock held (pid=$$) Phase 2b-prep"
2211
- # Drop stale singleton locks (see clean_stale_singleton.sh, also called in Phase 1).
2212
- ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
2213
-
2214
2208
  # Thread-media capture (2026-06-03, gated by S4L_TWITTER_CAPTURE_MEDIA, default
2215
- # OFF). Now that the browser lock is held and the harness Chrome is up, do ONE
2216
- # cheap deterministic pass over every candidate thread to pull its media
2217
- # (images/videos/GIFs/link-cards), persist each into
2209
+ # OFF). When enabled, does ONE cheap deterministic pass over every candidate
2210
+ # thread to pull its media (images/videos/GIFs/link-cards), persist each into
2218
2211
  # twitter_candidates.thread_media, and build a MEDIA CONTEXT block injected into
2219
2212
  # the prep prompt so the reply-writer can react to what the tweet visually shows
2220
2213
  # instead of replying text-blind. Must be deterministic (Python pre-fetch) because
2221
2214
  # the prep prompt forbids the model from calling twitter_browser.py. Entirely
2222
2215
  # best-effort: any failure leaves MEDIA_BLOCK empty and the cycle proceeds.
2216
+ #
2217
+ # The twitter-browser lock exists SOLELY to guard this capture step, so it is
2218
+ # now gated by the same flag (2026-07-16): acquiring it unconditionally meant
2219
+ # every cycle -- sandbox or live -- contended for the browser lock even when
2220
+ # nothing in this phase touches the browser, which is the default state (media
2221
+ # capture off). Found live: a sandbox test run waited ~6 minutes in the FIFO
2222
+ # lock queue behind a real cycle for a lock it never ended up needing.
2223
2223
  MEDIA_BLOCK=""
2224
2224
  if [ "${S4L_TWITTER_CAPTURE_MEDIA:-0}" = "1" ] || [ "${S4L_TWITTER_CAPTURE_MEDIA:-}" = "true" ]; then
2225
+ log "Re-acquiring twitter-browser lock for Phase 2b-prep (read+draft only)..."
2226
+ acquire_lock "twitter-browser" 3600 2>>"$LOG_FILE"
2227
+ log "twitter-browser lock held (pid=$$) Phase 2b-prep"
2228
+ # Drop stale singleton locks (see clean_stale_singleton.sh, also called in Phase 1).
2229
+ ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
2230
+
2225
2231
  if [ -s "$MEDIA_URLS_FILE" ]; then
2226
2232
  log "Phase 2b-prep: capturing thread media for $(wc -l < "$MEDIA_URLS_FILE" | tr -d ' ') candidate(s)..."
2227
2233
  MEDIA_BLOCK=$(python3 "$REPO_DIR/scripts/capture_thread_media.py" --urls-file "$MEDIA_URLS_FILE" --scroll 1 2>>"$LOG_FILE" || true)
@@ -2231,28 +2237,29 @@ if [ "${S4L_TWITTER_CAPTURE_MEDIA:-0}" = "1" ] || [ "${S4L_TWITTER_CAPTURE_MEDIA
2231
2237
  log "Phase 2b-prep: no media captured (none found or capture skipped)."
2232
2238
  fi
2233
2239
  fi
2240
+ rm -f "$MEDIA_URLS_FILE" 2>/dev/null || true
2241
+
2242
+ # Release the twitter-browser lock now. Thread-media capture above was the
2243
+ # ONLY browser-touching step in Phase 2b-prep; the Claude drafting call below
2244
+ # is architecturally browser-free (--strict-mcp-config omits the
2245
+ # twitter-harness MCP, so the model can never reach the CDP Chrome even if it
2246
+ # tried) and, since the 2026-06-23 queue migration, "run-twitter-cycle-prep"
2247
+ # routes through claude_job.py to an independent worker process that can
2248
+ # block for up to S4L_CLAUDE_QUEUE_TIMEOUT (1800s default). Holding the
2249
+ # browser lock across that wait made this process a preemption target for
2250
+ # any post that needed the browser: the post-vs-scan hijack fix SIGKILLs
2251
+ # whoever holds the lock (see docs/twitter_browser_lock.md), which killed
2252
+ # this process mid-wait while the worker kept drafting in the background,
2253
+ # stranding the finished result until the salvage reconciler's 35-min window
2254
+ # (scripts/salvage_orphaned_prep_results.py) picked it up. Releasing here
2255
+ # removes this whole phase from being a SIGKILL target for the rest of the
2256
+ # drafting wait. Phase 2b-post re-acquires unconditionally below.
2257
+ log "Releasing twitter-browser lock before Claude drafting (drafting never touches the browser)..."
2258
+ release_lock "twitter-browser" 2>>"$LOG_FILE"
2234
2259
  else
2235
- log "Phase 2b-prep: thread-media capture disabled (S4L_TWITTER_CAPTURE_MEDIA not set)."
2260
+ log "Phase 2b-prep: thread-media capture disabled (S4L_TWITTER_CAPTURE_MEDIA not set); skipping the twitter-browser lock entirely."
2261
+ rm -f "$MEDIA_URLS_FILE" 2>/dev/null || true
2236
2262
  fi
2237
- rm -f "$MEDIA_URLS_FILE" 2>/dev/null || true
2238
-
2239
- # Release the twitter-browser lock now. Thread-media capture above was the
2240
- # ONLY browser-touching step in Phase 2b-prep; the Claude drafting call below
2241
- # is architecturally browser-free (--strict-mcp-config omits the
2242
- # twitter-harness MCP, so the model can never reach the CDP Chrome even if it
2243
- # tried) and, since the 2026-06-23 queue migration, "run-twitter-cycle-prep"
2244
- # routes through claude_job.py to an independent worker process that can
2245
- # block for up to S4L_CLAUDE_QUEUE_TIMEOUT (1800s default). Holding the
2246
- # browser lock across that wait made this process a preemption target for
2247
- # any post that needed the browser: the post-vs-scan hijack fix SIGKILLs
2248
- # whoever holds the lock (see docs/twitter_browser_lock.md), which killed
2249
- # this process mid-wait while the worker kept drafting in the background,
2250
- # stranding the finished result until the salvage reconciler's 35-min window
2251
- # (scripts/salvage_orphaned_prep_results.py) picked it up. Releasing here
2252
- # removes this whole phase from being a SIGKILL target for the rest of the
2253
- # drafting wait. Phase 2b-post re-acquires unconditionally below.
2254
- log "Releasing twitter-browser lock before Claude drafting (drafting never touches the browser)..."
2255
- release_lock "twitter-browser" 2>>"$LOG_FILE"
2256
2263
 
2257
2264
  # --- ACCOUNT VOICE CORPUS injection (both lanes, 2026-07-15) -----------------
2258
2265
  # build_persona.py apply / voice_exemplars.py write a raw first-hand corpus
@@ -2824,6 +2831,17 @@ if [ "${DRAFT_ONLY:-0}" = "1" ]; then
2824
2831
  --tweets-pulled "${TWEETS_PULLED:-0}" --candidates "${BATCH_COUNT:-0}" --above-floor "${HIGH_DELTA_COUNT:-0}" \
2825
2832
  --cost "$_COST" --elapsed $(( $(date +%s) - RUN_START )) 2>/dev/null || true
2826
2833
  _SA_RUN_SUMMARY_EMITTED=1
2834
+ # Sandbox runs have no MCP wrapper watching stdout for this marker to hand
2835
+ # the plan off to merge_review_queue.py (that handoff normally happens
2836
+ # outside this script), so a sandbox plan would otherwise just rot on
2837
+ # disk -- found live 2026-07-15, had to hand-reconstruct one after the
2838
+ # fact. $PLAN_FILE already carries the fully-transformed candidates
2839
+ # (reply_text/drafts/experiments.sandbox stamp, same shape any real draft
2840
+ # gets), so this is a straight, no-transform call into the same merge
2841
+ # step everything else uses.
2842
+ if [ -n "${S4L_SANDBOX_CANDIDATES_FILE:-}" ]; then
2843
+ python3 "$REPO_DIR/scripts/merge_review_queue.py" --plan "$PLAN_FILE" 2>&1 | tee -a "$LOG_FILE"
2844
+ fi
2827
2845
  # IMPORTANT: do NOT rm -f "$PLAN_FILE" here; the reviewer needs it. Print a
2828
2846
  # machine-readable marker so the MCP wrapper can locate the plan deterministically.
2829
2847
  echo "DRAFT_ONLY_PLAN=$PLAN_FILE"