@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
|
@@ -83,9 +83,10 @@ sys.stderr = _TimestampedStderr(sys.stderr)
|
|
|
83
83
|
# best-effort: a missing repo path or SDK degrades to a silent no-op.
|
|
84
84
|
_sentry = None
|
|
85
85
|
try:
|
|
86
|
-
# Read the repo dir from the plist env
|
|
87
|
-
#
|
|
88
|
-
|
|
86
|
+
# Read the repo dir from the plist env. This runs BEFORE scripts/ is on
|
|
87
|
+
# sys.path, so the s4l_env mirror cannot help yet; the old key is accepted
|
|
88
|
+
# only at this bootstrap seam so pre-rename plists can find the mirror.
|
|
89
|
+
_repo = os.environ.get("S4L_REPO_DIR") or os.environ.get("SAPS_REPO_DIR")
|
|
89
90
|
if _repo:
|
|
90
91
|
_scripts = os.path.join(_repo, "scripts")
|
|
91
92
|
if _scripts not in sys.path:
|
|
@@ -337,9 +338,9 @@ UPDATE_PROMPT = "Update the S4L plugin to the latest version."
|
|
|
337
338
|
# Re-arm goes through the HOST create_scheduled_task path (the same one onboarding
|
|
338
339
|
# uses) — it registers the routines under whatever account is logged in and shows
|
|
339
340
|
# up in Routines. The host tool only runs inside an agent chat, so the menu bar
|
|
340
|
-
# hands Claude this prompt (auto-typed, clipboard+paste fallback).
|
|
341
|
-
#
|
|
342
|
-
#
|
|
341
|
+
# hands Claude this prompt (auto-typed, clipboard+paste fallback). The automatic
|
|
342
|
+
# fix can now direct-write after resolving lastActiveOrg; keep this host path for
|
|
343
|
+
# Keychain denial, missing/stale cookie, and other resolution failures.
|
|
343
344
|
REARM_PROMPT = (
|
|
344
345
|
"Set up the S4L draft autopilot schedule for this Claude account. "
|
|
345
346
|
"If queue_setup is available, call it; then for s4l-worker call the host tool "
|
|
@@ -398,32 +399,33 @@ DIAGNOSE_PROMPT_TEMPLATE = (
|
|
|
398
399
|
"<that file>` so the report reaches the S4L developers. Keep replies short."
|
|
399
400
|
)
|
|
400
401
|
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
#
|
|
411
|
-
#
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
402
|
+
# Stall thresholds live in the single source of truth, scripts/schedule_state.py
|
|
403
|
+
# (HEALTHY_DRAIN_MAX_SECONDS and the names derived from it) — retune there, not
|
|
404
|
+
# here. Same module that owns FIRING_WINDOW; _schedule_state already delegates to
|
|
405
|
+
# it. The three names cover three distinct failure modes:
|
|
406
|
+
# AUTOPILOT_STALL_SECONDS pending job unclaimed → no routine draining
|
|
407
|
+
# (fix: Re-arm). Idle queue has no pending job,
|
|
408
|
+
# so a quiet pipeline never trips it.
|
|
409
|
+
# AUTOPILOT_RUNNING_STALL_SECONDS job claimed but wedged in running/ → worker
|
|
410
|
+
# died mid-run (fix: Diagnose).
|
|
411
|
+
# DRAFT_STUCK_SECONDS the producer's "draft Nm" label alive this
|
|
412
|
+
# long → end-to-end clock; the only timer that
|
|
413
|
+
# spans claim→die→requeue churn loops, which
|
|
414
|
+
# reset the two per-state timers on every
|
|
415
|
+
# transition while nothing ever completes.
|
|
416
|
+
# The import is best-effort because the S4L_REPO_DIR sys.path insertion near the
|
|
417
|
+
# top of this file is best-effort; the fallback literal mirrors
|
|
418
|
+
# HEALTHY_DRAIN_MAX_SECONDS.
|
|
419
|
+
try:
|
|
420
|
+
from schedule_state import ( # noqa: E402
|
|
421
|
+
STALL_SECONDS as AUTOPILOT_STALL_SECONDS,
|
|
422
|
+
RUNNING_STALL_SECONDS as AUTOPILOT_RUNNING_STALL_SECONDS,
|
|
423
|
+
DRAFT_STUCK_SECONDS,
|
|
424
|
+
)
|
|
425
|
+
except Exception:
|
|
426
|
+
AUTOPILOT_STALL_SECONDS = 1200
|
|
427
|
+
AUTOPILOT_RUNNING_STALL_SECONDS = 1200
|
|
428
|
+
DRAFT_STUCK_SECONDS = 1200
|
|
427
429
|
|
|
428
430
|
# A poll can read a transient "not stuck" moment (e.g. the activity file
|
|
429
431
|
# between one dying worker attempt and the next claim on the same aged job)
|
|
@@ -463,6 +465,14 @@ REVIEW_UNATTENDED_SENTRY_SECONDS = 3600.0
|
|
|
463
465
|
REVIEW_SNOOZE_SECONDS = float(os.environ.get("S4L_REVIEW_SNOOZE_S", "3600"))
|
|
464
466
|
SNOOZE_FILE = os.path.join(st.state_dir(), "review-snooze.json")
|
|
465
467
|
|
|
468
|
+
# Reveal cadence (2026-07-13): how often FRESH draft cards may pop, independent
|
|
469
|
+
# of how often the pipeline drafts (mode.json `reveal_cadence_secs`, default 1h,
|
|
470
|
+
# 0 = as soon as ready). Only fresh presentations are paced: an already-open
|
|
471
|
+
# card still grows live via extend_active, snooze still parks everything, and
|
|
472
|
+
# the menu's "Review N pending drafts" always bypasses. The last-presented
|
|
473
|
+
# epoch is persisted so a menubar restart mid-interval doesn't re-pop early.
|
|
474
|
+
REVEAL_STAMP_FILE = os.path.join(st.state_dir(), "review-last-presented.json")
|
|
475
|
+
|
|
466
476
|
|
|
467
477
|
def _label_elapsed_secs(label):
|
|
468
478
|
"""Parse the trailing duration the producer encodes in a drafting activity
|
|
@@ -547,6 +557,9 @@ class S4LMenuBar(rumps.App):
|
|
|
547
557
|
# (0 = not snoozed). Loaded from disk so a menubar restart mid-snooze
|
|
548
558
|
# doesn't re-pop the card the user just put away.
|
|
549
559
|
self._review_snooze_until = self._read_snooze_until()
|
|
560
|
+
# Reveal cadence: when the last fresh card presentation happened (epoch,
|
|
561
|
+
# 0 = never). Loaded from disk so a restart mid-interval doesn't re-pop.
|
|
562
|
+
self._last_presented_at = self._read_last_presented()
|
|
550
563
|
# Unattended-review watchdog state (_maybe_heal_review).
|
|
551
564
|
self._review_heal_at = 0.0
|
|
552
565
|
self._review_unattended_notified = False
|
|
@@ -786,8 +799,8 @@ class S4LMenuBar(rumps.App):
|
|
|
786
799
|
bridge is panel-only), so the reliable path here is: copy the prompt to the
|
|
787
800
|
clipboard, open Claude, and tell the user to paste it. (The dashboard
|
|
788
801
|
widget's button does this in one click via app.sendMessage — no paste.) We
|
|
789
|
-
do NOT auto-type (focus/timing flaky)
|
|
790
|
-
|
|
802
|
+
do NOT auto-type (focus/timing flaky). This remains the fallback when the
|
|
803
|
+
automatic direct-write path cannot resolve/decrypt the active org cookie.
|
|
791
804
|
|
|
792
805
|
The click itself is captured (mirroring _diagnose_fix) — previously this
|
|
793
806
|
button had NO telemetry at all, so a past incident (Karol, 2026-07-07)
|
|
@@ -810,15 +823,15 @@ class S4LMenuBar(rumps.App):
|
|
|
810
823
|
|
|
811
824
|
def _finish_schedule_setup(self, _=None):
|
|
812
825
|
"""One-click fix for schedule_state == 'missing' when
|
|
813
|
-
scheduled_task_selfheal.can_create_for_active_account() confirms
|
|
814
|
-
session directory
|
|
815
|
-
quit Claude, create the registration
|
|
816
|
-
same heal() the update flow uses — see
|
|
817
|
-
this is equivalent to what
|
|
818
|
-
relaunch. Primary action
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
826
|
+
scheduled_task_selfheal.can_create_for_active_account() confirms an
|
|
827
|
+
existing session directory OR the active-org cookie needed to make
|
|
828
|
+
the exact account/org directory: quit Claude, create the registration
|
|
829
|
+
via a direct file write (the same heal() the update flow uses — see
|
|
830
|
+
its module docstring for why this is equivalent to what
|
|
831
|
+
create_scheduled_task would produce), then relaunch. Primary action
|
|
832
|
+
for this case now instead of re-arm: no clipboard paste or chat turn.
|
|
833
|
+
Re-arm remains the fallback when the cookie is unavailable or macOS
|
|
834
|
+
denies its Safe Storage Keychain read.
|
|
822
835
|
|
|
823
836
|
No confirm dialog: the restart is already disclosed in the menu item
|
|
824
837
|
label itself ("...(restarts Claude)", _build_menu) — a modal repeating
|
|
@@ -898,7 +911,7 @@ class S4LMenuBar(rumps.App):
|
|
|
898
911
|
to see between cycles) -> the ⚠ stays on continuously instead of
|
|
899
912
|
flickering off. This is the durable signal.
|
|
900
913
|
(2) FAST: a draft job has sat unclaimed in pending/ past
|
|
901
|
-
AUTOPILOT_STALL_SECONDS -> catches a fresh stall ~
|
|
914
|
+
AUTOPILOT_STALL_SECONDS -> catches a fresh stall ~20 min in, before
|
|
902
915
|
the first full producer timeout has even latched (1).
|
|
903
916
|
(3) IN-FLIGHT: a draft job was claimed (moved to running/) but never
|
|
904
917
|
finished within AUTOPILOT_RUNNING_STALL_SECONDS -> the worker picked
|
|
@@ -1127,6 +1140,40 @@ class S4LMenuBar(rumps.App):
|
|
|
1127
1140
|
"""Menu callback shim: rumps passes the clicked MenuItem last."""
|
|
1128
1141
|
self._set_split(share)
|
|
1129
1142
|
|
|
1143
|
+
# Reveal-cadence presets (seconds between fresh card pop-ups; 0 = as soon
|
|
1144
|
+
# as drafts are ready). Manual mode.json edits can set any value; the
|
|
1145
|
+
# checkmark then lands on an exact match or none, same as the lane split.
|
|
1146
|
+
REVEAL_CADENCE_PRESETS = (
|
|
1147
|
+
(0, "Immediately"),
|
|
1148
|
+
(1800, "Every 30 minutes"),
|
|
1149
|
+
(3600, "Every hour"),
|
|
1150
|
+
(14400, "Every 4 hours"),
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
def _cadence_label(self, secs=None):
|
|
1154
|
+
"""Human label for a cadence value, for menu copy."""
|
|
1155
|
+
if secs is None:
|
|
1156
|
+
secs = st.read_reveal_cadence()
|
|
1157
|
+
for preset, label in self.REVEAL_CADENCE_PRESETS:
|
|
1158
|
+
if round(secs) == preset:
|
|
1159
|
+
return label
|
|
1160
|
+
m = round(secs / 60)
|
|
1161
|
+
return f"Every {m} min" if m else "Immediately"
|
|
1162
|
+
|
|
1163
|
+
def _on_cadence_preset(self, secs, _sender=None):
|
|
1164
|
+
"""Menu callback shim: rumps passes the clicked MenuItem last."""
|
|
1165
|
+
written = st.write_reveal_cadence(secs)
|
|
1166
|
+
self._notify(
|
|
1167
|
+
"S4L draft cards",
|
|
1168
|
+
f"New draft cards will show: {self._cadence_label(written).lower()}",
|
|
1169
|
+
)
|
|
1170
|
+
self._sig = None
|
|
1171
|
+
try:
|
|
1172
|
+
self._tick(None)
|
|
1173
|
+
except Exception as e:
|
|
1174
|
+
sys.stderr.write(f"[s4l-menubar] cadence rebuild failed: {e}\n")
|
|
1175
|
+
sys.stderr.flush()
|
|
1176
|
+
|
|
1130
1177
|
def _set_split(self, share):
|
|
1131
1178
|
"""Set the personal-brand share (pure local mode.json write, same
|
|
1132
1179
|
contract as _toggle_lane) and rebuild so the checkmark moves instantly."""
|
|
@@ -2400,11 +2447,12 @@ class S4LMenuBar(rumps.App):
|
|
|
2400
2447
|
self._stall_reason_info = ("", "")
|
|
2401
2448
|
# Draft worker stuck/killed: the producer narrates "drafting replies (Nm)"
|
|
2402
2449
|
# the whole time it blocks waiting for a worker to return a result, with NO
|
|
2403
|
-
# idea the worker died. A healthy drain
|
|
2404
|
-
# has been "drafting" past
|
|
2405
|
-
# killed mid-run (or never
|
|
2406
|
-
#
|
|
2407
|
-
#
|
|
2450
|
+
# idea the worker died. A healthy drain can run 8-10 min end to end (slot
|
|
2451
|
+
# wait + the drafting turn); once that label has been "drafting" past
|
|
2452
|
+
# DRAFT_STUCK_SECONDS the worker keeps getting killed mid-run (or never
|
|
2453
|
+
# claims) and nothing is draining — flip to ⚠ instead of leaving the
|
|
2454
|
+
# reassuring "drafting (25m)" spinner up. Skip when a more specific cause
|
|
2455
|
+
# (rate limit) already owns the reason.
|
|
2408
2456
|
#
|
|
2409
2457
|
# Gated on schedule_state in ("ok", "stalled"), NOT missing/disabled: when
|
|
2410
2458
|
# the schedule is missing/disabled (e.g. orphaned by an account switch),
|
|
@@ -2625,6 +2673,11 @@ class S4LMenuBar(rumps.App):
|
|
|
2625
2673
|
int(self._review_snooze_until)
|
|
2626
2674
|
if time.time() < self._review_snooze_until
|
|
2627
2675
|
else 0,
|
|
2676
|
+
# Reveal cadence: rebuilds when the preset changes (checkmark +
|
|
2677
|
+
# submenu title) and when a hold starts or lapses (the "Next cards
|
|
2678
|
+
# around HH:MM" line in _state_c).
|
|
2679
|
+
round(st.read_reveal_cadence()),
|
|
2680
|
+
int(self._reveal_hold_until(pending_count)),
|
|
2628
2681
|
)
|
|
2629
2682
|
if sig != self._sig:
|
|
2630
2683
|
self._sig = sig
|
|
@@ -2723,6 +2776,37 @@ class S4LMenuBar(rumps.App):
|
|
|
2723
2776
|
except Exception:
|
|
2724
2777
|
pass
|
|
2725
2778
|
|
|
2779
|
+
def _read_last_presented(self):
|
|
2780
|
+
try:
|
|
2781
|
+
with open(REVEAL_STAMP_FILE) as f:
|
|
2782
|
+
return float((json.load(f) or {}).get("at") or 0.0)
|
|
2783
|
+
except Exception:
|
|
2784
|
+
return 0.0
|
|
2785
|
+
|
|
2786
|
+
def _stamp_presented(self):
|
|
2787
|
+
"""Record a fresh card presentation, mirrored to disk (restart-safe).
|
|
2788
|
+
Starts the next reveal-cadence interval."""
|
|
2789
|
+
self._last_presented_at = time.time()
|
|
2790
|
+
try:
|
|
2791
|
+
with open(REVEAL_STAMP_FILE, "w") as f:
|
|
2792
|
+
json.dump({"at": self._last_presented_at}, f)
|
|
2793
|
+
except Exception:
|
|
2794
|
+
pass
|
|
2795
|
+
|
|
2796
|
+
def _reveal_hold_until(self, pending_count):
|
|
2797
|
+
"""Epoch until which the reveal cadence is actually holding fresh cards
|
|
2798
|
+
back (0.0 = no hold). A hold only exists while there is something to
|
|
2799
|
+
hold: pending drafts, no review in flight, and not snoozed (the snooze
|
|
2800
|
+
label and gate take precedence)."""
|
|
2801
|
+
if pending_count <= 0 or self._review_active:
|
|
2802
|
+
return 0.0
|
|
2803
|
+
now = time.time()
|
|
2804
|
+
if now < self._review_snooze_until:
|
|
2805
|
+
return 0.0
|
|
2806
|
+
cadence = st.read_reveal_cadence()
|
|
2807
|
+
until = self._last_presented_at + cadence
|
|
2808
|
+
return until if (cadence > 0 and now < until) else 0.0
|
|
2809
|
+
|
|
2726
2810
|
def _review_now(self, _=None):
|
|
2727
2811
|
"""Menu: bring the pending draft cards up right now. Clears any snooze;
|
|
2728
2812
|
this is the one presentation path that may take keyboard focus (the
|
|
@@ -2804,6 +2888,17 @@ class S4LMenuBar(rumps.App):
|
|
|
2804
2888
|
sys.stderr.flush()
|
|
2805
2889
|
self._last_review_sig = sig
|
|
2806
2890
|
return
|
|
2891
|
+
# Reveal cadence: hold FRESH pop-ups until the configured interval since
|
|
2892
|
+
# the last presentation has passed (0 = show immediately). Drafting is
|
|
2893
|
+
# untouched; the backlog keeps accumulating and is presented whole once
|
|
2894
|
+
# due. Deliberately after the extend-open-card path above (an on-screen
|
|
2895
|
+
# card keeps growing live) and skipped for focus=True (the menu's
|
|
2896
|
+
# "Review N pending drafts" is an explicit ask). _last_review_sig is NOT
|
|
2897
|
+
# stamped on a hold, so the same set still presents fresh when due.
|
|
2898
|
+
if not focus:
|
|
2899
|
+
cadence = st.read_reveal_cadence()
|
|
2900
|
+
if cadence > 0 and time.time() - self._last_presented_at < cadence:
|
|
2901
|
+
return
|
|
2807
2902
|
with self._review_lock:
|
|
2808
2903
|
self._reset_posting_progress_locked()
|
|
2809
2904
|
self._review_active = True
|
|
@@ -2827,6 +2922,7 @@ class S4LMenuBar(rumps.App):
|
|
|
2827
2922
|
# Record as shown only AFTER the cards are actually up, so a transient
|
|
2828
2923
|
# card-UI failure never permanently suppresses this pending set.
|
|
2829
2924
|
self._last_review_sig = sig
|
|
2925
|
+
self._stamp_presented()
|
|
2830
2926
|
# No macOS notification for fresh drafts, per explicit user
|
|
2831
2927
|
# request (2026-07-03): the card itself is the surface. A missed
|
|
2832
2928
|
# card is the unattended watchdog's job (it heals the window and
|
|
@@ -3331,13 +3427,12 @@ class S4LMenuBar(rumps.App):
|
|
|
3331
3427
|
items.append(rumps.MenuItem("Set up draft schedule for this account", callback=self._rearm))
|
|
3332
3428
|
else:
|
|
3333
3429
|
items.append(self._label("⚠ Draft tasks aren’t scheduled on this account"))
|
|
3334
|
-
# Prefer the automatic fix
|
|
3335
|
-
#
|
|
3336
|
-
#
|
|
3337
|
-
#
|
|
3338
|
-
#
|
|
3339
|
-
#
|
|
3340
|
-
# only the live create_scheduled_task host tool can help.
|
|
3430
|
+
# Prefer the automatic fix: an existing session directory OR
|
|
3431
|
+
# lastActiveOrg cookie lets _finish_schedule_setup target the
|
|
3432
|
+
# active account exactly (heal() fix 5), with no clipboard
|
|
3433
|
+
# paste. Fall back to the live create_scheduled_task host tool
|
|
3434
|
+
# when neither source is available; decryption denial after a
|
|
3435
|
+
# click is also caught by the post-restart verification.
|
|
3341
3436
|
can_selfheal = False
|
|
3342
3437
|
try:
|
|
3343
3438
|
import scheduled_task_selfheal
|
|
@@ -3416,6 +3511,22 @@ class S4LMenuBar(rumps.App):
|
|
|
3416
3511
|
split_menu.add(it)
|
|
3417
3512
|
items.append(split_menu)
|
|
3418
3513
|
|
|
3514
|
+
# Reveal cadence: how often fresh draft cards may pop (the drafting
|
|
3515
|
+
# pipeline is unchanged; this only paces the pop-up). Always offered:
|
|
3516
|
+
# it is the "don't interrupt me every few minutes" control.
|
|
3517
|
+
cadence = st.read_reveal_cadence()
|
|
3518
|
+
cadence_menu = rumps.MenuItem(
|
|
3519
|
+
f"Show draft cards: {self._cadence_label(cadence).lower()}"
|
|
3520
|
+
)
|
|
3521
|
+
for preset, label in self.REVEAL_CADENCE_PRESETS:
|
|
3522
|
+
it = rumps.MenuItem(
|
|
3523
|
+
label,
|
|
3524
|
+
callback=functools.partial(self._on_cadence_preset, preset),
|
|
3525
|
+
)
|
|
3526
|
+
it.state = 1 if round(cadence) == preset else 0
|
|
3527
|
+
cadence_menu.add(it)
|
|
3528
|
+
items.append(cadence_menu)
|
|
3529
|
+
|
|
3419
3530
|
items.append(rumps.separator)
|
|
3420
3531
|
items.append(rumps.MenuItem("Open dashboard", callback=self._open_dashboard))
|
|
3421
3532
|
# The one entry point for overall feedback (the review card no longer
|
|
@@ -3554,6 +3665,18 @@ class S4LMenuBar(rumps.App):
|
|
|
3554
3665
|
)
|
|
3555
3666
|
)
|
|
3556
3667
|
)
|
|
3668
|
+
else:
|
|
3669
|
+
# Reveal-cadence hold: the backlog exists but the pop-up is being
|
|
3670
|
+
# paced. Same courtesy line as the snooze; "Review N pending
|
|
3671
|
+
# drafts" above shows them right now regardless.
|
|
3672
|
+
hold_until = self._reveal_hold_until(pending_count)
|
|
3673
|
+
if hold_until:
|
|
3674
|
+
items.append(
|
|
3675
|
+
self._label(
|
|
3676
|
+
"Next cards around "
|
|
3677
|
+
+ time.strftime("%H:%M", time.localtime(hold_until))
|
|
3678
|
+
)
|
|
3679
|
+
)
|
|
3557
3680
|
# No bulk-discard item here anymore: "Discard all…" lives in the
|
|
3558
3681
|
# review card's title bar (2026-07-10). Reaching it with no card open
|
|
3559
3682
|
# is "Review N pending drafts" -> Discard all, two clicks.
|
package/mcp/menubar/s4l_state.py
CHANGED
|
@@ -31,7 +31,7 @@ from pathlib import Path
|
|
|
31
31
|
# The repo dir must be read tolerantly INLINE (old name included) because the
|
|
32
32
|
# mirror module itself lives in $REPO/scripts and isn't importable before the
|
|
33
33
|
# sys.path insertion below. Best-effort: failure degrades to defaults.
|
|
34
|
-
_repo_for_env = os.environ.get("S4L_REPO_DIR")
|
|
34
|
+
_repo_for_env = os.environ.get("S4L_REPO_DIR") or os.environ.get("SAPS_REPO_DIR")
|
|
35
35
|
if _repo_for_env:
|
|
36
36
|
_scripts_for_env = os.path.join(_repo_for_env, "scripts")
|
|
37
37
|
if _scripts_for_env not in sys.path:
|
|
@@ -1251,6 +1251,46 @@ def write_split(share):
|
|
|
1251
1251
|
return share
|
|
1252
1252
|
|
|
1253
1253
|
|
|
1254
|
+
# Draft-card reveal cadence: minimum seconds between FRESH card pop-ups
|
|
1255
|
+
# (0 = present as soon as drafts are ready). Drafting itself is untouched;
|
|
1256
|
+
# drafts keep accumulating in the review store and the menubar just paces the
|
|
1257
|
+
# pop-up. Lives in mode.json (every writer preserves unknown keys). Presets
|
|
1258
|
+
# are the menubar's REVEAL_CADENCE_PRESETS; any non-negative value is valid.
|
|
1259
|
+
REVEAL_CADENCE_DEFAULT = 3600.0
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def read_reveal_cadence():
|
|
1263
|
+
"""Seconds between draft-card reveals (0 = immediately, default 1 hour)."""
|
|
1264
|
+
d = read_json(MODE_FILE)
|
|
1265
|
+
try:
|
|
1266
|
+
secs = float(d.get("reveal_cadence_secs"))
|
|
1267
|
+
except (TypeError, ValueError, AttributeError):
|
|
1268
|
+
return REVEAL_CADENCE_DEFAULT
|
|
1269
|
+
return max(0.0, secs)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def write_reveal_cadence(secs):
|
|
1273
|
+
"""Persist the reveal cadence, preserving every other mode.json key.
|
|
1274
|
+
Returns the written value. Never raises (a menu click must not crash)."""
|
|
1275
|
+
try:
|
|
1276
|
+
secs = max(0.0, float(secs))
|
|
1277
|
+
except (TypeError, ValueError):
|
|
1278
|
+
return read_reveal_cadence()
|
|
1279
|
+
try:
|
|
1280
|
+
payload = read_json(MODE_FILE)
|
|
1281
|
+
if not isinstance(payload, dict):
|
|
1282
|
+
payload = {}
|
|
1283
|
+
payload["reveal_cadence_secs"] = secs
|
|
1284
|
+
p = Path(state_dir()) / MODE_FILE
|
|
1285
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
1286
|
+
tmp = p.with_suffix(".json.tmp")
|
|
1287
|
+
tmp.write_text(json.dumps(payload))
|
|
1288
|
+
os.replace(str(tmp), str(p))
|
|
1289
|
+
except Exception:
|
|
1290
|
+
pass
|
|
1291
|
+
return secs
|
|
1292
|
+
|
|
1293
|
+
|
|
1254
1294
|
def write_mode(mode):
|
|
1255
1295
|
"""Legacy single-mode setter: named lane ON, the other OFF (compat)."""
|
|
1256
1296
|
if mode not in _VALID_MODES:
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.4
|
|
3
|
+
"version": "1.7.4",
|
|
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.4
|
|
3
|
+
"version": "1.7.4",
|
|
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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"!bin/server.js",
|
|
18
18
|
"!bin/auth.js",
|
|
19
19
|
"scripts/*.py",
|
|
20
|
+
"scripts/patches/",
|
|
20
21
|
"!scripts/db_direct.py",
|
|
21
22
|
"!scripts/sync_blocked_authors.py",
|
|
22
23
|
"!scripts/backfill_real_clicks.py",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"!scripts/_serp_report.py",
|
|
57
58
|
"!scripts/_serp_vs_gsc_report.py",
|
|
58
59
|
"!scripts/_test_since_hook.py",
|
|
60
|
+
"!scripts/_lock_preempt_test.py",
|
|
59
61
|
"!scripts/add_deploy_metadata.py",
|
|
60
62
|
"!scripts/add_nightowl_to_config.py",
|
|
61
63
|
"!scripts/amplitude_user_lookup.py",
|
|
@@ -160,4 +162,4 @@
|
|
|
160
162
|
"pg": "^8.20.0",
|
|
161
163
|
"ws": "^8.0.0"
|
|
162
164
|
}
|
|
163
|
-
}
|
|
165
|
+
}
|
|
@@ -40,16 +40,39 @@ ENV_PREFIX = "S4L_EXP_"
|
|
|
40
40
|
# Keep draft_prompt entries in sync with bin/server.js
|
|
41
41
|
# DRAFT_PROMPT_VARIANT_DEFS and the arm strings in run-twitter-cycle.sh.
|
|
42
42
|
DESCRIPTIONS = {
|
|
43
|
+
"draft_b_source": {
|
|
44
|
+
"human_derived": (
|
|
45
|
+
"Draft B explore slot: style distilled from real top-performing "
|
|
46
|
+
"human replies (daily synthesizer), least-used first"
|
|
47
|
+
),
|
|
48
|
+
"model_invented": (
|
|
49
|
+
"Draft B explore slot: freshly invented style from the "
|
|
50
|
+
"standalone invention job (post 2026-07-10), least-used first"
|
|
51
|
+
),
|
|
52
|
+
"scored_fallback": (
|
|
53
|
+
"Draft B explore pool was empty; fell back to a second scored "
|
|
54
|
+
"pick from the proven-style pool"
|
|
55
|
+
),
|
|
56
|
+
},
|
|
43
57
|
"draft_prompt": {
|
|
58
|
+
"treatment_v3": (
|
|
59
|
+
"style-as-form: the assigned style is the binding FORM (defining "
|
|
60
|
+
"move + per-style length + self-check), learned preferences apply "
|
|
61
|
+
"inside it; keeps the v2 skeleton ban"
|
|
62
|
+
),
|
|
63
|
+
"control_v3": (
|
|
64
|
+
"plain draft directive, uniform length clamp, no structure ban"
|
|
65
|
+
),
|
|
66
|
+
# v2 arms (skeleton-ban) retired 2026-07-10; v1 arms
|
|
67
|
+
# (decoupled-product-pivot) retired 2026-07-06; kept so any
|
|
68
|
+
# straggler card from an old plan still explains itself.
|
|
44
69
|
"treatment_v2": (
|
|
45
|
-
"skeleton ban
|
|
70
|
+
"v2, retired: skeleton ban, forbids the concede-then-reverse "
|
|
46
71
|
'"easy X / hard Y" structure and forces varied entry points'
|
|
47
72
|
),
|
|
48
73
|
"control_v2": (
|
|
49
|
-
"
|
|
74
|
+
"v2, retired: draft directive (style + product pivot), no structure ban"
|
|
50
75
|
),
|
|
51
|
-
# v1 arms (decoupled-product-pivot) retired 2026-07-06; kept so any
|
|
52
|
-
# straggler card from an old plan still explains itself.
|
|
53
76
|
"treatment": "v1, retired: product pivot decoupled from the reply",
|
|
54
77
|
"control": "v1, retired: original draft directive",
|
|
55
78
|
},
|
|
@@ -99,8 +122,9 @@ def collect(env=None):
|
|
|
99
122
|
if var.startswith(ENV_PREFIX) and (v or "").strip():
|
|
100
123
|
out[var[len(ENV_PREFIX):].lower()] = v.strip()
|
|
101
124
|
# 2026-07-06: the personal_brand persona directive is now ARM-AWARE in
|
|
102
|
-
# run-twitter-cycle.sh (
|
|
103
|
-
#
|
|
125
|
+
# run-twitter-cycle.sh (treatment_v3 adds the skeleton ban + two-layer
|
|
126
|
+
# style/preferences contract, control_v3 does not), so the assigned
|
|
127
|
+
# draft_prompt arm DOES touch persona
|
|
104
128
|
# drafts. Keep it stamped so the arm surfaces on persona cards and the per-arm
|
|
105
129
|
# readout covers both lanes. (Previously dropped here because the persona
|
|
106
130
|
# directive overrode both arms wholesale; that is no longer the case.)
|