@m13v/s4l 1.7.4-rc.4 → 1.7.4
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/README.md +134 -101
- package/bin/cli.js +23 -0
- package/mcp/dist/index.js +24 -5
- package/mcp/dist/runtime.js +110 -0
- package/mcp/dist/version.js +163 -54
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_card.py +55 -19
- package/mcp/menubar/s4l_menubar.py +179 -56
- package/mcp/menubar/s4l_state.py +41 -1
- package/mcp/package.json +1 -1
- package/package.json +4 -2
- package/scripts/active_experiments.py +30 -6
- package/scripts/autopilot_stall_watch.py +185 -63
- package/scripts/cdp_ready_check.py +94 -0
- package/scripts/engage_twitter_helper.py +83 -17
- package/scripts/engagement_styles.py +221 -26
- package/scripts/enrich_reply_parents.py +68 -22
- package/scripts/feedback_digest.py +24 -1
- package/scripts/generate_daily_human_style.py +31 -7
- package/scripts/invent_styles.py +103 -26
- package/scripts/linkedin_cadence.py +234 -0
- package/scripts/linkedin_killswitch.py +43 -0
- package/scripts/memory_snapshot.py +57 -0
- package/scripts/patches/browser-harness-loopback-cdp-proxy.patch +118 -0
- package/scripts/recent_self_posts.py +6 -0
- package/scripts/reddit_browser.py +57 -3
- package/scripts/s4l_box_update.sh +20 -1
- package/scripts/s4l_mode.py +3 -4
- package/scripts/scan_x_profile.py +25 -7
- package/scripts/schedule_state.py +23 -0
- package/scripts/scheduled_task_selfheal.py +213 -16
- package/scripts/setup_twitter_auth.py +6 -1
- package/scripts/snapshot.py +179 -61
- package/scripts/stall_guard.py +70 -0
- package/scripts/test_scheduled_task_selfheal.py +121 -0
- package/scripts/top_performers.py +34 -0
- package/scripts/twitter_browser.py +146 -8
- package/scripts/twitter_post_plan.py +10 -1
- package/scripts/twitter_scan.py +26 -0
- package/scripts/watchdog_hung_runs.py +117 -3
- package/skill/amplitude-24h-signups.sh +1 -1
- package/skill/audit-dm-staleness.sh +1 -1
- package/skill/audit-linkedin.sh +1 -1
- package/skill/audit-reddit-resurrect.sh +1 -1
- package/skill/audit.sh +5 -5
- package/skill/check-web-chats.sh +1 -1
- package/skill/dm-outreach-linkedin.sh +4 -4
- package/skill/dm-outreach-reddit.sh +1 -1
- package/skill/dm-outreach-twitter.sh +1 -1
- package/skill/engage-dm-replies.sh +16 -3
- package/skill/engage-linkedin.sh +2 -2
- package/skill/engage-moltbook.sh +1 -1
- package/skill/engage-twitter.sh +4 -4
- package/skill/github-engage.sh +1 -1
- package/skill/lib/browser-launch.sh +54 -0
- package/skill/lib/linkedin-backend.sh +22 -15
- package/skill/lib/reddit-backend.sh +17 -11
- package/skill/lib/twitter-backend.sh +137 -16
- package/skill/link-edit-github.sh +1 -1
- package/skill/link-edit-moltbook.sh +1 -1
- package/skill/link-edit-reddit.sh +1 -1
- package/skill/linkedin-cadence.sh +20 -0
- package/skill/linkedin-presence.sh +1 -1
- package/skill/lock.sh +30 -6
- package/skill/precompute-stats.sh +1 -1
- package/skill/refresh-instagram-tokens.sh +1 -1
- package/skill/refresh-twitter-following.sh +1 -1
- package/skill/run-generate-daily-style.sh +1 -1
- package/skill/run-instagram-daily.sh +1 -1
- package/skill/run-instagram-render.sh +1 -1
- package/skill/run-linkedin-launchd.sh +1 -1
- package/skill/run-linkedin-unipile.sh +1 -1
- package/skill/run-linkedin.sh +1 -1
- package/skill/run-reddit-search.sh +2 -2
- package/skill/run-reddit-threads.sh +1 -1
- package/skill/run-twitter-cycle.sh +190 -55
- package/skill/scan-instagram-replies.sh +1 -1
- package/skill/sentry-digest.sh +1 -1
- package/skill/stats-instagram.sh +1 -1
- package/skill/stats-linkedin.sh +2 -2
- package/skill/stats.sh +1 -1
- package/skill/sweep-link-clicks.sh +1 -1
- package/scripts/_lock_preempt_test.py +0 -60
- package/scripts/saps_activity.py +0 -18
- package/scripts/saps_mode.py +0 -18
package/skill/engage-linkedin.sh
CHANGED
|
@@ -17,7 +17,7 @@ set -euo pipefail
|
|
|
17
17
|
# State: ~/.claude/social-autoposter/linkedin.killswitch
|
|
18
18
|
# Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
|
|
19
19
|
if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
|
|
20
|
-
echo "[$(date +%H:%M:%
|
|
20
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
|
|
21
21
|
echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
|
|
22
22
|
exit 0
|
|
23
23
|
fi
|
|
@@ -84,7 +84,7 @@ LOG_FILE="$LOG_DIR/engage-linkedin-$(date +%Y-%m-%d_%H%M%S).log"
|
|
|
84
84
|
BATCH_ID="enli-$(date +%Y%m%d_%H%M%S)-$$"
|
|
85
85
|
export SA_CYCLE_ID="$BATCH_ID"
|
|
86
86
|
|
|
87
|
-
log() { echo "[$(date +%H:%M:%
|
|
87
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
88
88
|
|
|
89
89
|
RUN_START=$(date +%s)
|
|
90
90
|
log "=== LinkedIn Engagement Run: $(date) ==="
|
package/skill/engage-moltbook.sh
CHANGED
|
@@ -23,7 +23,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
|
|
|
23
23
|
mkdir -p "$LOG_DIR"
|
|
24
24
|
LOG_FILE="$LOG_DIR/engage-moltbook-$(date +%Y-%m-%d_%H%M%S).log"
|
|
25
25
|
|
|
26
|
-
log() { echo "[$(date +%H:%M:%
|
|
26
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
27
27
|
|
|
28
28
|
RUN_START=$(date +%s)
|
|
29
29
|
log "=== MoltBook Engage Run: $(date) ==="
|
package/skill/engage-twitter.sh
CHANGED
|
@@ -25,12 +25,12 @@ source "$(dirname "$0")/lock.sh"
|
|
|
25
25
|
# Sets MCP_CONFIG_FILE, BROWSER_INSTRUCTIONS, exports TWITTER_CDP_URL=9555.
|
|
26
26
|
source "$(dirname "$0")/lib/twitter-backend.sh"
|
|
27
27
|
|
|
28
|
-
echo "[$(date +%H:%M:%
|
|
28
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Acquiring twitter-browser lock (pid=$$)..." | tee -a "$LOG_FILE"
|
|
29
29
|
acquire_lock "twitter-browser" 3600 2>>"$LOG_FILE"
|
|
30
|
-
echo "[$(date +%H:%M:%
|
|
30
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] twitter-browser lock held (pid=$$)" | tee -a "$LOG_FILE"
|
|
31
31
|
# Probe + launch harness Chrome on port 9555 if needed, then sweep leftover tabs.
|
|
32
32
|
ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
|
|
33
|
-
echo "[$(date +%H:%M:%
|
|
33
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Acquiring twitter (pipeline) lock (pid=$$)..." | tee -a "$LOG_FILE"
|
|
34
34
|
acquire_lock "twitter" 3600 2>>"$LOG_FILE"
|
|
35
35
|
|
|
36
36
|
# Load secrets
|
|
@@ -47,7 +47,7 @@ BATCH_SIZE=500
|
|
|
47
47
|
ENGAGE_TWITTER_HELPER="$REPO_DIR/scripts/engage_twitter_helper.py"
|
|
48
48
|
# (LOG_DIR/LOG_FILE bootstrapped at top of script.)
|
|
49
49
|
|
|
50
|
-
log() { echo "[$(date +%H:%M:%
|
|
50
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
51
51
|
|
|
52
52
|
RUN_START=$(date +%s)
|
|
53
53
|
log "=== Twitter Engagement Run: $(date) ==="
|
package/skill/github-engage.sh
CHANGED
|
@@ -20,7 +20,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
|
|
|
20
20
|
mkdir -p "$LOG_DIR"
|
|
21
21
|
LOG_FILE="$LOG_DIR/github-engage-$(date +%Y-%m-%d_%H%M%S).log"
|
|
22
22
|
|
|
23
|
-
log() { echo "[$(date +%H:%M:%
|
|
23
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
24
24
|
|
|
25
25
|
# Per-cycle batch id stamped onto every claude_sessions row spawned by this
|
|
26
26
|
# engagement run (via SA_CYCLE_ID env -> log_claude_session.py). 2026-05-10
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# browser-launch.sh — the ONE way any backend spawns a harness Chrome.
|
|
3
|
+
#
|
|
4
|
+
# Consolidates what twitter-backend.sh and linkedin-backend.sh grew as
|
|
5
|
+
# hand-rolled near-duplicates on 2026-07-13 (and reddit-backend.sh never got
|
|
6
|
+
# at all). Source this from a backend lib and call:
|
|
7
|
+
#
|
|
8
|
+
# launch_harness_chrome "$_chrome_bin" "$_prof_dir" <chrome arg>...
|
|
9
|
+
#
|
|
10
|
+
# What every launch gets, uniformly:
|
|
11
|
+
# 1. CLEAN-EXIT STAMP: mark the profile's last exit as clean first, so a
|
|
12
|
+
# SIGKILLed Chrome (wedge heal, stall abort) doesn't session-restore a
|
|
13
|
+
# crashed "Aw, Snap" corpse tab on relaunch.
|
|
14
|
+
# 2. NO FOCUS STEAL (macOS + .app bundle): `open -n -g` launches a NEW
|
|
15
|
+
# instance (-n: the user's personal Chrome may already be running;
|
|
16
|
+
# without it LaunchServices pokes that instance and drops our --args)
|
|
17
|
+
# without activation (-g). A directly-exec'd Chrome always activates
|
|
18
|
+
# itself and planted a window over the user's work on every relaunch.
|
|
19
|
+
# LaunchServices also parents the process outside the caller's launchd
|
|
20
|
+
# job process group — independent cover for the pgroup-reaping bug.
|
|
21
|
+
# (Tested alternatives that DON'T work: `-j` — Chrome unhides and
|
|
22
|
+
# activates itself anyway; `--no-startup-window` — the activation just
|
|
23
|
+
# moves to first-tab creation.)
|
|
24
|
+
# 3. DETACHED FALLBACK (Linux / bare chromium binaries): direct exec in a
|
|
25
|
+
# NEW SESSION via os.setsid, so a transient launchd job's exit can't
|
|
26
|
+
# SIGKILL Chrome with the job's process group (2026-07-12 root cause of
|
|
27
|
+
# the kill-relaunch-foreground loop).
|
|
28
|
+
launch_harness_chrome() {
|
|
29
|
+
local _bl_chrome_bin="$1"; shift
|
|
30
|
+
local _bl_prof_dir="$1"; shift
|
|
31
|
+
"${S4L_PYTHON:-python3}" -c 'import json, os, sys
|
|
32
|
+
p = os.path.join(sys.argv[1], "Default", "Preferences")
|
|
33
|
+
try:
|
|
34
|
+
d = json.load(open(p))
|
|
35
|
+
except Exception:
|
|
36
|
+
raise SystemExit(0)
|
|
37
|
+
prof = d.setdefault("profile", {})
|
|
38
|
+
prof["exit_type"] = "Normal"
|
|
39
|
+
prof["exited_cleanly"] = True
|
|
40
|
+
json.dump(d, open(p, "w"))' "$_bl_prof_dir" 2>/dev/null || true
|
|
41
|
+
local _bl_app_bundle=""
|
|
42
|
+
case "$_bl_chrome_bin" in
|
|
43
|
+
*.app/Contents/MacOS/*) _bl_app_bundle="${_bl_chrome_bin%%/Contents/MacOS/*}" ;;
|
|
44
|
+
esac
|
|
45
|
+
if [ "$(uname -s)" = "Darwin" ] && [ -n "$_bl_app_bundle" ] && [ -d "$_bl_app_bundle" ]; then
|
|
46
|
+
open -n -g -a "$_bl_app_bundle" --args "$@" >/dev/null 2>&1 || true
|
|
47
|
+
else
|
|
48
|
+
"${S4L_PYTHON:-python3}" -c 'import os,sys
|
|
49
|
+
os.setsid()
|
|
50
|
+
os.execv(sys.argv[1], sys.argv[1:])' \
|
|
51
|
+
"$_bl_chrome_bin" "$@" >/dev/null 2>&1 &
|
|
52
|
+
disown
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
@@ -154,7 +154,7 @@ cleanup_harness_tabs() {
|
|
|
154
154
|
if ! $_probe 2>/dev/null; then
|
|
155
155
|
sleep 1
|
|
156
156
|
if ! $_probe 2>/dev/null; then
|
|
157
|
-
echo "[$(date +%H:%M:%
|
|
157
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (linkedin-harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
158
158
|
return 0
|
|
159
159
|
fi
|
|
160
160
|
fi
|
|
@@ -221,7 +221,7 @@ _acquire_linkedin_pipeline_lock() {
|
|
|
221
221
|
echo "$$" > "$_LI_PIPELINE_LOCK_DIR/pid"
|
|
222
222
|
echo "$_who" > "$_LI_PIPELINE_LOCK_DIR/holder"
|
|
223
223
|
export _LI_PIPELINE_LOCK_HELD=1
|
|
224
|
-
echo "[$(date +%H:%M:%
|
|
224
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock ACQUIRED by $_who (pid $$)" >&2
|
|
225
225
|
return 0
|
|
226
226
|
fi
|
|
227
227
|
local _h_pid _h_who
|
|
@@ -236,11 +236,11 @@ _acquire_linkedin_pipeline_lock() {
|
|
|
236
236
|
return 0
|
|
237
237
|
fi
|
|
238
238
|
if [ -z "$_h_pid" ] || ! kill -0 "$_h_pid" 2>/dev/null; then
|
|
239
|
-
echo "[$(date +%H:%M:%
|
|
239
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock: reclaiming stale lock (dead holder ${_h_who} pid ${_h_pid:-unknown})" >&2
|
|
240
240
|
rm -rf "$_LI_PIPELINE_LOCK_DIR"
|
|
241
241
|
continue
|
|
242
242
|
fi
|
|
243
|
-
echo "[$(date +%H:%M:%
|
|
243
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock: held by ${_h_who} (pid ${_h_pid}); ${_who} skipping this fire (rc=78) to avoid two drivers on the 9556 Chrome" >&2
|
|
244
244
|
return 78
|
|
245
245
|
done
|
|
246
246
|
}
|
|
@@ -271,10 +271,10 @@ ensure_linkedin_browser_for_backend() {
|
|
|
271
271
|
if [ "${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" != "$_BH_LINKEDIN_DEFAULT_URL" ]; then
|
|
272
272
|
local _ext_url="${LINKEDIN_CDP_URL}"
|
|
273
273
|
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
274
|
-
echo "[$(date +%H:%M:%
|
|
274
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
275
275
|
return 0
|
|
276
276
|
fi
|
|
277
|
-
echo "[$(date +%H:%M:%
|
|
277
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: LINKEDIN_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
|
|
278
278
|
return 1
|
|
279
279
|
fi
|
|
280
280
|
# Cross-pipeline whole-run lock: only one LinkedIn browser pipeline drives
|
|
@@ -288,11 +288,14 @@ ensure_linkedin_browser_for_backend() {
|
|
|
288
288
|
_acquire_linkedin_pipeline_lock || return $?
|
|
289
289
|
# Probe + launch harness Chrome on port 9556 if needed.
|
|
290
290
|
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
291
|
-
echo "[$(date +%H:%M:%
|
|
291
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LinkedIn harness Chrome down on port 9556, launching..." >&2
|
|
292
|
+
# Dated relaunch stamp for central observability (see twitter-backend.sh).
|
|
293
|
+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) port=9556" \
|
|
294
|
+
>> "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/logs/chrome-relaunch-events.log" 2>/dev/null || true
|
|
292
295
|
local _chrome_bin
|
|
293
296
|
_chrome_bin=$(_resolve_chrome_bin)
|
|
294
297
|
if [ -z "$_chrome_bin" ]; then
|
|
295
|
-
echo "[$(date +%H:%M:%
|
|
298
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
296
299
|
return 1
|
|
297
300
|
fi
|
|
298
301
|
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
@@ -320,7 +323,7 @@ ensure_linkedin_browser_for_backend() {
|
|
|
320
323
|
local _stale_pids
|
|
321
324
|
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
322
325
|
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
323
|
-
echo "[$(date +%H:%M:%
|
|
326
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
324
327
|
kill $_stale_pids 2>/dev/null || true
|
|
325
328
|
sleep 2
|
|
326
329
|
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
@@ -330,7 +333,12 @@ ensure_linkedin_browser_for_backend() {
|
|
|
330
333
|
# The occlusion/backgrounding flags matter: the window sits offscreen,
|
|
331
334
|
# and without them Chrome stops laying out SPA-rendered content, so
|
|
332
335
|
# every element measures 0x0 and clicks become impossible (2026-07-03).
|
|
333
|
-
|
|
336
|
+
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh) — see
|
|
337
|
+
# that file for the clean-exit stamp + no-focus-steal + detach
|
|
338
|
+
# rationale. Do NOT hand-roll launch logic per backend.
|
|
339
|
+
# shellcheck disable=SC1091
|
|
340
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
341
|
+
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
334
342
|
--remote-debugging-port=9556 \
|
|
335
343
|
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness-linkedin" \
|
|
336
344
|
--no-first-run --no-default-browser-check \
|
|
@@ -339,17 +347,16 @@ ensure_linkedin_browser_for_backend() {
|
|
|
339
347
|
--disable-renderer-backgrounding \
|
|
340
348
|
--disable-background-timer-throttling \
|
|
341
349
|
"${_extra[@]}" \
|
|
342
|
-
about:blank
|
|
343
|
-
disown
|
|
350
|
+
about:blank
|
|
344
351
|
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
345
352
|
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null && break
|
|
346
353
|
sleep 1
|
|
347
354
|
done
|
|
348
355
|
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
349
|
-
echo "[$(date +%H:%M:%
|
|
356
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: LinkedIn harness Chrome failed to start within 12s" >&2
|
|
350
357
|
return 1
|
|
351
358
|
fi
|
|
352
|
-
echo "[$(date +%H:%M:%
|
|
359
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LinkedIn harness Chrome up on port 9556" >&2
|
|
353
360
|
fi
|
|
354
361
|
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
355
362
|
# "linkedin-browser" serialization.
|
|
@@ -384,7 +391,7 @@ _linkedin_session_detect_gate() {
|
|
|
384
391
|
"$_py" "$HOME/social-autoposter/scripts/linkedin_killswitch.py" detect-gate \
|
|
385
392
|
--cdp-url "${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" >&2 || _rc=$?
|
|
386
393
|
if [ "$_rc" = "2" ]; then
|
|
387
|
-
echo "[$(date +%H:%M:%
|
|
394
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] detect-gate tripped the LinkedIn killswitch; aborting this fire" >&2
|
|
388
395
|
return 1
|
|
389
396
|
fi
|
|
390
397
|
return 0
|
|
@@ -123,7 +123,7 @@ cleanup_harness_tabs() {
|
|
|
123
123
|
if ! $_probe 2>/dev/null; then
|
|
124
124
|
sleep 1
|
|
125
125
|
if ! $_probe 2>/dev/null; then
|
|
126
|
-
echo "[$(date +%H:%M:%
|
|
126
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (reddit-harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
127
127
|
return 0
|
|
128
128
|
fi
|
|
129
129
|
fi
|
|
@@ -156,19 +156,19 @@ ensure_reddit_browser_for_backend() {
|
|
|
156
156
|
if [ "${REDDIT_CDP_URL:-$_BH_REDDIT_DEFAULT_URL}" != "$_BH_REDDIT_DEFAULT_URL" ]; then
|
|
157
157
|
local _ext_url="${REDDIT_CDP_URL}"
|
|
158
158
|
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
159
|
-
echo "[$(date +%H:%M:%
|
|
159
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
160
160
|
return 0
|
|
161
161
|
fi
|
|
162
|
-
echo "[$(date +%H:%M:%
|
|
162
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: REDDIT_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
|
|
163
163
|
return 1
|
|
164
164
|
fi
|
|
165
165
|
# Probe + launch harness Chrome on port 9557 if needed.
|
|
166
166
|
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
167
|
-
echo "[$(date +%H:%M:%
|
|
167
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Reddit harness Chrome down on port 9557, launching..." >&2
|
|
168
168
|
local _chrome_bin
|
|
169
169
|
_chrome_bin=$(_resolve_chrome_bin)
|
|
170
170
|
if [ -z "$_chrome_bin" ]; then
|
|
171
|
-
echo "[$(date +%H:%M:%
|
|
171
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
172
172
|
return 1
|
|
173
173
|
fi
|
|
174
174
|
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
@@ -196,30 +196,36 @@ ensure_reddit_browser_for_backend() {
|
|
|
196
196
|
local _stale_pids
|
|
197
197
|
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
198
198
|
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
199
|
-
echo "[$(date +%H:%M:%
|
|
199
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
200
200
|
kill $_stale_pids 2>/dev/null || true
|
|
201
201
|
sleep 2
|
|
202
202
|
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
203
203
|
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
204
204
|
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
205
205
|
fi
|
|
206
|
-
|
|
206
|
+
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
|
|
207
|
+
# exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
|
|
208
|
+
# steal, out of this job's pgroup), setsid exec fallback elsewhere.
|
|
209
|
+
# This backend previously used a bare exec and had NONE of those
|
|
210
|
+
# protections (2026-07-13 consolidation).
|
|
211
|
+
# shellcheck disable=SC1091
|
|
212
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
213
|
+
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
207
214
|
--remote-debugging-port=9557 \
|
|
208
215
|
--user-data-dir="$HOME/.claude/browser-profiles/reddit-harness" \
|
|
209
216
|
--no-first-run --no-default-browser-check \
|
|
210
217
|
--disable-features=ChromeWhatsNewUI \
|
|
211
218
|
"${_extra[@]}" \
|
|
212
|
-
about:blank
|
|
213
|
-
disown
|
|
219
|
+
about:blank
|
|
214
220
|
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
215
221
|
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null && break
|
|
216
222
|
sleep 1
|
|
217
223
|
done
|
|
218
224
|
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
219
|
-
echo "[$(date +%H:%M:%
|
|
225
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: Reddit harness Chrome failed to start within 12s" >&2
|
|
220
226
|
return 1
|
|
221
227
|
fi
|
|
222
|
-
echo "[$(date +%H:%M:%
|
|
228
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Reddit harness Chrome up on port 9557" >&2
|
|
223
229
|
fi
|
|
224
230
|
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
225
231
|
# "reddit-browser" serialization.
|
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
|
|
32
32
|
MCP_CONFIG_FILE="$HOME/.claude/browser-agent-configs/twitter-harness-mcp.json"
|
|
33
33
|
|
|
34
|
+
# Repo root for the helper scripts this file shells out to, resolved from this
|
|
35
|
+
# file's own location (skill/lib/ -> two up), honoring S4L_REPO_DIR when the
|
|
36
|
+
# caller sets it. The old $HOME/social-autoposter hardcodes ran code OUTSIDE
|
|
37
|
+
# the managed package on customer boxes (unreachable by auto-update) and
|
|
38
|
+
# silently no-op'd where that directory didn't exist, so session restore and
|
|
39
|
+
# tab cleanup never actually ran on such installs (S4L-4H triage 2026-07-12).
|
|
40
|
+
_BH_REPO_DIR="${S4L_REPO_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
41
|
+
|
|
34
42
|
# Per-host env override (written by bin/cli.js when installing on an AppMaker
|
|
35
43
|
# VM, where the canonical browser is Chromium on port 9222 behind the SOAX
|
|
36
44
|
# residential proxy at 127.0.0.1:3003, NOT the harness Chrome on 9555). On a
|
|
@@ -130,11 +138,34 @@ cleanup_harness_tabs() {
|
|
|
130
138
|
if ! $_probe 2>/dev/null; then
|
|
131
139
|
sleep 1
|
|
132
140
|
if ! $_probe 2>/dev/null; then
|
|
133
|
-
echo "[$(date +%H:%M:%
|
|
141
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
134
142
|
return 0
|
|
135
143
|
fi
|
|
136
144
|
fi
|
|
137
|
-
python3 "$
|
|
145
|
+
python3 "$_BH_REPO_DIR/scripts/cleanup_harness_tabs.py" 2>/dev/null || true
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_bh_cdp_ready() {
|
|
149
|
+
# Real CDP readiness: complete an actual connect_over_cdp handshake against
|
|
150
|
+
# the harness. /json/version alone is a liveness probe that a WEDGED Chrome
|
|
151
|
+
# still passes (S4L-4H); see scripts/cdp_ready_check.py. Prints the probe's
|
|
152
|
+
# one-line JSON verdict on stdout; exit status is the verdict.
|
|
153
|
+
"${S4L_PYTHON:-python3}" "$_BH_REPO_DIR/scripts/cdp_ready_check.py" \
|
|
154
|
+
"${1:-$_BH_DEFAULT_URL}" 8000 2>/dev/null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_bh_record_cdp_health() {
|
|
158
|
+
# Persist the latest readiness verdict where memory_snapshot.py picks it up
|
|
159
|
+
# (cdp_health block on the per-minute heartbeat sample), so a wedge and its
|
|
160
|
+
# restart are visible centrally in installation_resource_samples without
|
|
161
|
+
# SSHing the box. $1 = action tag, $2 = the probe's JSON verdict (may be
|
|
162
|
+
# empty). Best effort, never fails the caller.
|
|
163
|
+
printf '{"ts":"%s","url":"%s","action":"%s","verdict":%s}\n' \
|
|
164
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
165
|
+
"${TWITTER_CDP_URL:-$_BH_DEFAULT_URL}" \
|
|
166
|
+
"$1" \
|
|
167
|
+
"${2:-null}" \
|
|
168
|
+
> "$_BH_REPO_DIR/skill/logs/cdp-health.json" 2>/dev/null || true
|
|
138
169
|
}
|
|
139
170
|
|
|
140
171
|
_resolve_chrome_bin() {
|
|
@@ -165,25 +196,96 @@ ensure_twitter_browser_for_backend() {
|
|
|
165
196
|
if [ "${TWITTER_CDP_URL:-$_BH_DEFAULT_URL}" != "$_BH_DEFAULT_URL" ]; then
|
|
166
197
|
local _ext_url="${TWITTER_CDP_URL}"
|
|
167
198
|
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
168
|
-
|
|
199
|
+
# HTTP answers; verify the CDP handshake actually completes before
|
|
200
|
+
# handing this browser to the pipeline. We do NOT restart an
|
|
201
|
+
# externally-managed Chrome; fail fast so the cycle doesn't burn
|
|
202
|
+
# 180s per downstream attach against a wedged browser.
|
|
203
|
+
local _ext_verdict
|
|
204
|
+
if ! _ext_verdict=$(_bh_cdp_ready "$_ext_url"); then
|
|
205
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: external Chrome at ${_ext_url} is WEDGED (/json/version answers but the CDP handshake never completes). Host must restart it (AppMaker /opt/startup.sh, etc)." >&2
|
|
206
|
+
echo "twitter_cdp_wedge: detected url=${_ext_url} action=none-external" >&2
|
|
207
|
+
_bh_record_cdp_health external_wedged "$_ext_verdict"
|
|
208
|
+
return 1
|
|
209
|
+
fi
|
|
210
|
+
_bh_record_cdp_health ok-external "$_ext_verdict"
|
|
211
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
169
212
|
# Restore the Twitter login if the sandbox was substituted. AppMaker
|
|
170
213
|
# Hobby-tier sandboxes have a 1h TTL; on substitution /root is reseeded
|
|
171
214
|
# from /etc/skel-root and the harness profile (cookies) is wiped. This
|
|
172
215
|
# re-injects the stored session from social_accounts via the HTTP API.
|
|
173
216
|
# No-op when already logged in. Never blocks the cycle on failure.
|
|
174
|
-
python3 "$
|
|
217
|
+
python3 "$_BH_REPO_DIR/scripts/restore_twitter_session.py" 2>&1 | sed 's/^/[restore] /' >&2 || true
|
|
175
218
|
return 0
|
|
176
219
|
fi
|
|
177
|
-
echo "[$(date +%H:%M:%
|
|
220
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: TWITTER_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host (AppMaker /opt/startup.sh, etc)." >&2
|
|
178
221
|
return 1
|
|
179
222
|
fi
|
|
180
|
-
# Probe + launch harness Chrome on port 9555 if needed.
|
|
223
|
+
# Probe + launch harness Chrome on port 9555 if needed. Two-stage probe:
|
|
224
|
+
# /json/version (liveness) then a real CDP handshake (readiness). A wedged
|
|
225
|
+
# Chrome passes the first and fails the second; handing it downstream made
|
|
226
|
+
# every attach eat Playwright's 180s default while holding the browser lock
|
|
227
|
+
# (S4L-4H), so it gets killed and relaunched here instead.
|
|
228
|
+
local _need_launch=0 _launch_reason=""
|
|
229
|
+
local _bh_prof_dir="$HOME/.claude/browser-profiles/browser-harness"
|
|
181
230
|
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
|
|
182
|
-
|
|
231
|
+
_need_launch=1; _launch_reason="http_down"
|
|
232
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Harness Chrome down on port 9555, launching..." >&2
|
|
233
|
+
else
|
|
234
|
+
local _ready_verdict
|
|
235
|
+
# TWO-STRIKE gate (2026-07-13): one failed handshake is NOT proof of a
|
|
236
|
+
# wedge. Under heavy system load (observed at loadavg ~18) a perfectly
|
|
237
|
+
# healthy Chrome blows the 8s Playwright attach budget, and a
|
|
238
|
+
# single-failure kill produced 3 false wedge-kills in one day on the
|
|
239
|
+
# operator Mac — each one a relaunch flash plus an "Aw, Snap" corpse
|
|
240
|
+
# tab from the SIGKILLed renderer. First failure: stamp a strike file
|
|
241
|
+
# and proceed WITHOUT killing (a truly wedged Chrome makes the cycle's
|
|
242
|
+
# own pre-flight fail loudly right after, so nothing silently hangs).
|
|
243
|
+
# Second consecutive failure within 30 minutes: genuinely wedged, kill
|
|
244
|
+
# + relaunch. A passing check clears any stale strike.
|
|
245
|
+
local _strike="/tmp/s4l_cdp_wedge_strike_9555"
|
|
246
|
+
if ! _ready_verdict=$(_bh_cdp_ready "$_BH_DEFAULT_URL"); then
|
|
247
|
+
if [ -z "$(find "$_strike" -mmin -30 2>/dev/null)" ]; then
|
|
248
|
+
touch "$_strike"
|
|
249
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Harness Chrome failed the CDP handshake check (${_ready_verdict:-no verdict}); first strike — NOT killing (loaded-machine tolerance). Second consecutive failure within 30m triggers the wedge heal." >&2
|
|
250
|
+
echo "twitter_cdp_wedge: detected url=$_BH_DEFAULT_URL action=first_strike" >&2
|
|
251
|
+
_bh_record_cdp_health wedge_first_strike "$_ready_verdict"
|
|
252
|
+
# Fall through WITHOUT killing: session restore + tab cleanup
|
|
253
|
+
# below still run.
|
|
254
|
+
else
|
|
255
|
+
rm -f "$_strike" 2>/dev/null || true
|
|
256
|
+
_need_launch=1; _launch_reason="cdp_wedge"
|
|
257
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Harness Chrome WEDGED on port 9555 (second consecutive handshake failure: ${_ready_verdict:-no verdict}); killing and relaunching..." >&2
|
|
258
|
+
# Machine-greppable marker (same stderr-marker convention as
|
|
259
|
+
# twitter_access_gate; bin/server.js parses these).
|
|
260
|
+
echo "twitter_cdp_wedge: detected url=$_BH_DEFAULT_URL action=restart" >&2
|
|
261
|
+
_bh_record_cdp_health wedge_restart "$_ready_verdict"
|
|
262
|
+
local _wedge_pids
|
|
263
|
+
_wedge_pids=$(pgrep -f -- "--user-data-dir=$_bh_prof_dir " 2>/dev/null || true)
|
|
264
|
+
if [ -n "$_wedge_pids" ]; then
|
|
265
|
+
kill $_wedge_pids 2>/dev/null || true
|
|
266
|
+
sleep 2
|
|
267
|
+
_wedge_pids=$(pgrep -f -- "--user-data-dir=$_bh_prof_dir " 2>/dev/null || true)
|
|
268
|
+
[ -n "$_wedge_pids" ] && { kill -9 $_wedge_pids 2>/dev/null || true; sleep 1; }
|
|
269
|
+
rm -f "$_bh_prof_dir/SingletonLock" "$_bh_prof_dir/SingletonSocket" "$_bh_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
else
|
|
273
|
+
# Healthy handshake — clear any earlier strike so isolated blips
|
|
274
|
+
# never accumulate into a kill.
|
|
275
|
+
rm -f "$_strike" 2>/dev/null || true
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
if [ "$_need_launch" = 1 ]; then
|
|
279
|
+
# Dated relaunch stamp for central observability: memory_snapshot.py
|
|
280
|
+
# counts these (chrome_relaunches block) so a kill-respawn loop like
|
|
281
|
+
# 2026-07-12's launchd pgroup reaping is visible per-install in Cloud
|
|
282
|
+
# Logging without depending on the menubar foreground observer.
|
|
283
|
+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) port=9555 reason=${_launch_reason}" \
|
|
284
|
+
>> "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/logs/chrome-relaunch-events.log" 2>/dev/null || true
|
|
183
285
|
local _chrome_bin
|
|
184
286
|
_chrome_bin=$(_resolve_chrome_bin)
|
|
185
287
|
if [ -z "$_chrome_bin" ]; then
|
|
186
|
-
echo "[$(date +%H:%M:%
|
|
288
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
187
289
|
return 1
|
|
188
290
|
fi
|
|
189
291
|
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
@@ -225,14 +327,20 @@ ensure_twitter_browser_for_backend() {
|
|
|
225
327
|
local _stale_pids
|
|
226
328
|
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
227
329
|
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
|
|
228
|
-
echo "[$(date +%H:%M:%
|
|
330
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
229
331
|
kill $_stale_pids 2>/dev/null || true
|
|
230
332
|
sleep 2
|
|
231
333
|
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
232
334
|
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
233
335
|
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
234
336
|
fi
|
|
235
|
-
|
|
337
|
+
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
|
|
338
|
+
# exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
|
|
339
|
+
# steal, out of this job's pgroup), setsid exec fallback elsewhere.
|
|
340
|
+
# Do NOT hand-roll launch logic per backend.
|
|
341
|
+
# shellcheck disable=SC1091
|
|
342
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
343
|
+
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
236
344
|
--remote-debugging-port=9555 \
|
|
237
345
|
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness" \
|
|
238
346
|
--no-first-run --no-default-browser-check \
|
|
@@ -240,17 +348,30 @@ ensure_twitter_browser_for_backend() {
|
|
|
240
348
|
--disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion \
|
|
241
349
|
--disable-backgrounding-occluded-windows \
|
|
242
350
|
"${_extra[@]}" \
|
|
243
|
-
"${BH_LAUNCH_URL:-https://x.com}"
|
|
244
|
-
disown
|
|
351
|
+
"${BH_LAUNCH_URL:-https://x.com}"
|
|
245
352
|
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
246
353
|
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null && break
|
|
247
354
|
sleep 1
|
|
248
355
|
done
|
|
249
356
|
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
|
|
250
|
-
echo "[$(date +%H:%M:%
|
|
357
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: harness Chrome failed to start within 12s" >&2
|
|
358
|
+
_bh_record_cdp_health launch_failed null
|
|
359
|
+
return 1
|
|
360
|
+
fi
|
|
361
|
+
# Verify the fresh Chrome actually completes a CDP handshake before
|
|
362
|
+
# declaring victory; a relaunch that comes up wedged again should fail
|
|
363
|
+
# the cycle loudly, not feed 180s hangs downstream.
|
|
364
|
+
local _post_verdict
|
|
365
|
+
if ! _post_verdict=$(_bh_cdp_ready "$_BH_DEFAULT_URL"); then
|
|
366
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: harness Chrome answers HTTP after relaunch but the CDP handshake is STILL failing: ${_post_verdict:-no verdict}" >&2
|
|
367
|
+
echo "twitter_cdp_wedge: detected url=$_BH_DEFAULT_URL action=relaunch_failed" >&2
|
|
368
|
+
_bh_record_cdp_health relaunch_failed "$_post_verdict"
|
|
251
369
|
return 1
|
|
252
370
|
fi
|
|
253
|
-
|
|
371
|
+
_bh_record_cdp_health relaunched "$_post_verdict"
|
|
372
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Harness Chrome up on port 9555" >&2
|
|
373
|
+
else
|
|
374
|
+
_bh_record_cdp_health ok "${_ready_verdict:-null}"
|
|
254
375
|
fi
|
|
255
376
|
# Re-inject the stored X session if the harness Chrome is logged out — e.g. a
|
|
256
377
|
# keychain re-lock wiped Chrome's encrypted Cookies SQLite on this launch
|
|
@@ -259,7 +380,7 @@ ensure_twitter_browser_for_backend() {
|
|
|
259
380
|
# No-op when already logged in; never blocks the cycle on failure. Runs on
|
|
260
381
|
# both the freshly-launched and already-up paths so a mid-life logout heals.
|
|
261
382
|
TWITTER_CDP_URL="http://127.0.0.1:9555" \
|
|
262
|
-
python3 "$
|
|
383
|
+
python3 "$_BH_REPO_DIR/scripts/restore_twitter_session.py" 2>&1 \
|
|
263
384
|
| sed 's/^/[restore] /' >&2 || true
|
|
264
385
|
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
265
386
|
# "twitter-browser" serialization (every caller of this function holds
|
|
@@ -290,7 +411,7 @@ defer_if_foreign_for_backend() {
|
|
|
290
411
|
_sa_harness_log() {
|
|
291
412
|
# Use the caller's log() FUNCTION when present; `declare -F` matches only a
|
|
292
413
|
# shell function, never the macOS /usr/bin/log binary (command -v would).
|
|
293
|
-
if declare -F log >/dev/null 2>&1; then log "$*"; else echo "[$(date +%H:%M:%
|
|
414
|
+
if declare -F log >/dev/null 2>&1; then log "$*"; else echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >&2; fi
|
|
294
415
|
}
|
|
295
416
|
_sa_resolve_uv() {
|
|
296
417
|
local c
|
|
@@ -30,7 +30,7 @@ LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
|
|
|
30
30
|
mkdir -p "$LOG_DIR"
|
|
31
31
|
LOG_FILE="$LOG_DIR/link-edit-github-$(date +%Y-%m-%d_%H%M%S).log"
|
|
32
32
|
|
|
33
|
-
log() { echo "[$(date +%H:%M:%
|
|
33
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
34
34
|
|
|
35
35
|
RUN_START=$(date +%s)
|
|
36
36
|
log "=== GitHub Link Edit Run: $(date) ==="
|
|
@@ -29,7 +29,7 @@ LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
|
|
|
29
29
|
mkdir -p "$LOG_DIR"
|
|
30
30
|
LOG_FILE="$LOG_DIR/link-edit-moltbook-$(date +%Y-%m-%d_%H%M%S).log"
|
|
31
31
|
|
|
32
|
-
log() { echo "[$(date +%H:%M:%
|
|
32
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
33
33
|
|
|
34
34
|
RUN_START=$(date +%s)
|
|
35
35
|
log "=== Moltbook Link Edit Run: $(date) ==="
|
|
@@ -38,7 +38,7 @@ LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
|
|
|
38
38
|
mkdir -p "$LOG_DIR"
|
|
39
39
|
LOG_FILE="$LOG_DIR/link-edit-reddit-$(date +%Y-%m-%d_%H%M%S).log"
|
|
40
40
|
|
|
41
|
-
log() { echo "[$(date +%H:%M:%
|
|
41
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
42
42
|
|
|
43
43
|
RUN_START=$(date +%s)
|
|
44
44
|
log "=== Reddit Link Edit Run: $(date) ==="
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# linkedin-cadence.sh — enforce the 4-active-day / 2-day-break LinkedIn
|
|
3
|
+
# posting schedule (user instruction, 2026-07-11). Fired every 15 minutes by
|
|
4
|
+
# launchd (com.m13v.social-linkedin-cadence). All logic lives in
|
|
5
|
+
# scripts/linkedin_cadence.py; this wrapper just logs the tick.
|
|
6
|
+
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
export PATH="/opt/homebrew/bin:$PATH"
|
|
9
|
+
|
|
10
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
11
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
12
|
+
mkdir -p "$LOG_DIR"
|
|
13
|
+
LOG="$LOG_DIR/linkedin-cadence.log"
|
|
14
|
+
|
|
15
|
+
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG" >&2; }
|
|
16
|
+
|
|
17
|
+
PY="/opt/homebrew/bin/python3"
|
|
18
|
+
[ -x "$PY" ] || PY="/usr/bin/python3"
|
|
19
|
+
|
|
20
|
+
"$PY" "$REPO_DIR/scripts/linkedin_cadence.py" enforce >>"$LOG" 2>&1
|
|
@@ -25,7 +25,7 @@ BATCH_ID="lipres-$(date +%Y%m%d_%H%M%S)-$$"
|
|
|
25
25
|
export SA_CYCLE_ID="$BATCH_ID"
|
|
26
26
|
export S4L_PIPELINE_NAME="linkedin-presence"
|
|
27
27
|
|
|
28
|
-
log() { echo "[$(date +%H:%M:%
|
|
28
|
+
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
|
|
29
29
|
|
|
30
30
|
DRY_RUN="${LINKEDIN_PRESENCE_DRY_RUN:-0}"
|
|
31
31
|
|