@m13v/s4l 1.7.6-rc.2 → 1.7.6-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/dist/index.js +256 -92
- package/mcp/dist/telemetry.js +2 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_browser_foreground.py +7 -1
- package/mcp/menubar/s4l_card.py +144 -18
- package/mcp/menubar/s4l_card_canvas.py +135 -14
- package/mcp/menubar/s4l_menubar.py +216 -73
- package/mcp/menubar/s4l_state.py +45 -130
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/account_resolver.py +35 -1
- package/scripts/autopilot_stall_watch.py +8 -1
- package/scripts/browser_lifecycle.py +90 -0
- package/scripts/cdp_ready_check.py +81 -50
- package/scripts/draft_prompt_core.py +100 -17
- package/scripts/engagement_styles.py +135 -98
- package/scripts/merge_review_queue.py +192 -129
- package/scripts/platform_strike_events.py +54 -5
- package/scripts/post_reddit.py +10 -0
- package/scripts/preview_reddit_card.py +38 -0
- package/scripts/reap_stale_claude_sessions.py +7 -6
- package/scripts/reddit_ban_check.py +220 -0
- package/scripts/reddit_browser.py +24 -0
- package/scripts/release-mcpb.sh +38 -7
- package/scripts/setup_twitter_auth.py +20 -96
- package/scripts/store_patch.py +111 -0
- package/scripts/strike_alert.py +161 -12
- package/scripts/test_no_silent_fallbacks.py +11 -0
- package/scripts/test_stall_watch_batch_progression.py +99 -0
- package/scripts/top_performers.py +24 -0
- package/scripts/twitter_browser.py +22 -52
- package/skill/archive-old-logs.sh +24 -0
- package/skill/lib/reddit-backend.sh +1 -0
- package/skill/run-twitter-cycle.sh +146 -66
|
@@ -76,6 +76,12 @@ def _config_path():
|
|
|
76
76
|
ARM_TREATMENT = "treatment_v4"
|
|
77
77
|
ARM_CONTROL = "control_v4"
|
|
78
78
|
|
|
79
|
+
# The "treatment_v4 assigns no real style" decision (and its sentinel value)
|
|
80
|
+
# lives ONLY in engagement_styles.py now (STYLE_SENTINEL_TREATMENT,
|
|
81
|
+
# pick_style_for_post, pick_exploration_style, 2026-07-17 consolidation):
|
|
82
|
+
# every platform's picker routes through those two functions, so this
|
|
83
|
+
# module never needs its own copy of the sentinel or the arm check for it.
|
|
84
|
+
|
|
79
85
|
|
|
80
86
|
def pick_draft_prompt_arm(env=None, export=True):
|
|
81
87
|
"""Return (variant, source). source in {env, pin, coin, fallback}.
|
|
@@ -118,17 +124,83 @@ def pick_draft_prompt_arm(env=None, export=True):
|
|
|
118
124
|
# Text transcribed verbatim from run-twitter-cycle.sh (2026-07-16).
|
|
119
125
|
# --------------------------------------------------------------------------
|
|
120
126
|
|
|
121
|
-
|
|
127
|
+
# --------------------------------------------------------------------------
|
|
128
|
+
# Shared treatment_v4 core (2026-07-17). Written ONCE, used by BOTH lanes.
|
|
129
|
+
#
|
|
130
|
+
# Why this exists: promotion and persona used to carry independent copies of
|
|
131
|
+
# nearly-identical priority-order/skeleton-ban/vary-entry-point text. They
|
|
132
|
+
# drifted -- persona's copy was missing the "vary the entry point across
|
|
133
|
+
# replies" sentence promotion had, which a real batch comparison traced to
|
|
134
|
+
# the "nobody ___s it" tell appearing 2x as often in treatment as control:
|
|
135
|
+
# a batch shares ONE assigned style, and without an explicit
|
|
136
|
+
# vary-across-the-batch instruction the model reused that style's defining
|
|
137
|
+
# vocabulary verbatim across every unrelated candidate. Do NOT fork this
|
|
138
|
+
# text again for a lane-specific tweak; parameterize _treatment_core()
|
|
139
|
+
# instead, or add a lane-specific sentence OUTSIDE it.
|
|
140
|
+
# --------------------------------------------------------------------------
|
|
122
141
|
|
|
123
|
-
|
|
142
|
+
def _treatment_core(voice_examples_ref):
|
|
143
|
+
"""Shared PRIORITY ORDER + skeleton ban + entry-point-variation text.
|
|
144
|
+
|
|
145
|
+
voice_examples_ref: the lane-specific noun phrase for priority item (2)
|
|
146
|
+
(promotion scopes to "the matched project's voice.examples"; persona
|
|
147
|
+
scopes to "voice.examples above" since it's already narrowed to the one
|
|
148
|
+
persona project). No mention of engagement style anywhere in here on
|
|
149
|
+
purpose: treatment_v4 assigns none (see engagement_styles.py's
|
|
150
|
+
STYLE_SENTINEL_TREATMENT / pick_style_for_post).
|
|
151
|
+
"""
|
|
152
|
+
return (
|
|
153
|
+
"PRIORITY ORDER for how you write this, highest first: (1) "
|
|
154
|
+
"learned_preferences.draft_style_notes and edit_examples are the "
|
|
155
|
+
"strongest signal available, real human corrections to this "
|
|
156
|
+
"account's own past drafts, and are MANDATORY, not advisory. (2) "
|
|
157
|
+
f"The ACCOUNT VOICE CORPUS block and {voice_examples_ref} are "
|
|
158
|
+
"VERBATIM GROUND TRUTH for how this account actually writes: "
|
|
159
|
+
"capitalization, punctuation, contractions, sentence length, "
|
|
160
|
+
"terseness. Match those mechanics exactly. (3) voice.tone is a "
|
|
161
|
+
"supporting description only, never ground truth: if it ever "
|
|
162
|
+
"conflicts with what the corpus/examples actually show, follow the "
|
|
163
|
+
"examples, not the tone description. Do NOT use the "
|
|
164
|
+
"concede-then-reverse skeleton in ANY form. Banned openings "
|
|
165
|
+
"include: 'X is the easy part/half/win, the hard part is Y'; 'X "
|
|
166
|
+
"was never the [thing], it's Y'; 'X isn't the [problem], it's Y'; "
|
|
167
|
+
"'the real/actual/harder part is Y'; 'what actually "
|
|
168
|
+
"breaks/ships/matters is Y'; 'the part nobody says/shows is Y'; 'X "
|
|
169
|
+
"is solved, Y is what breaks'. Lead with substance from ONE entry "
|
|
170
|
+
"point and vary the entry point AND the rhetorical move across "
|
|
171
|
+
"every reply you draft this batch, even when the same topic or "
|
|
172
|
+
"style would naturally suggest reusing the same construction: a "
|
|
173
|
+
"concrete first-hand specific or number; a direct answer to the "
|
|
174
|
+
"exact question asked; one sharp opinion with no hedge; a genuine "
|
|
175
|
+
"question that moves the thread forward; or a relevant pointer. No "
|
|
176
|
+
"warm-up framing sentence before the substance."
|
|
177
|
+
)
|
|
124
178
|
|
|
125
|
-
_DIVERGENCE_NOTE_TREATMENT = " Beyond following different style templates: draft_a_text and draft_b_text must be genuinely distinct takes on this thread -- think of them as variant 1a and variant 1b: different entry point, different specific detail seized on, different rhetorical move -- not two phrasings of the same underlying observation dressed in different style language. If 1a and 1b would say essentially the same thing, pick a different angle entirely for 1b rather than just rewording 1a."
|
|
126
179
|
|
|
127
|
-
|
|
180
|
+
_TREATMENT_LENGTH_NOTE = (
|
|
181
|
+
"Length: match how long the account's own real examples above actually "
|
|
182
|
+
"run; when the corpus doesn't make it obvious, default to one or two "
|
|
183
|
+
"sentences, well under Twitter's 250-character practical limit. NEVER "
|
|
184
|
+
"em dashes."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
_DIRECTIVE_TREATMENT = (
|
|
188
|
+
"Otherwise: draft a direct, natural reply that stands on its own as a "
|
|
189
|
+
"useful contribution to the thread. Mention the matched project only "
|
|
190
|
+
"when it is genuinely the most relevant thing to say, and state it "
|
|
191
|
+
"plainly in one clause; most replies will not need it. "
|
|
192
|
+
+ _treatment_core("the matched project's voice.examples") +
|
|
193
|
+
" " + _TREATMENT_LENGTH_NOTE +
|
|
194
|
+
" Never violate voice.never. Treat learned_preferences.audience_avoid "
|
|
195
|
+
"/ thread_avoid matches as strong reasons to skip the candidate. Never "
|
|
196
|
+
"violate content_guardrails.do_not."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
_DIRECTIVE_CONTROL = "Otherwise: draft a reply using the best engagement style. Length is governed ENTIRELY by the per-style LENGTH LIMIT in the style block above; obey that target and ceiling, do not apply any other length rule here. NEVER em dashes. Apply the matched project's `voice` block from ALL_PROJECTS_JSON: follow voice.tone, never violate voice.never, mirror voice.examples / voice.examples_good when present. The global learned_preferences block under PROJECT ROUTING is distilled human review feedback and is MANDATORY, not advisory: follow every learned_preferences.draft_style_notes entry when writing (it overrides the engagement style's structural template on conflict), and treat learned_preferences.audience_avoid / thread_avoid matches as strong reasons to skip the candidate. Never violate content_guardrails.do_not."
|
|
128
200
|
|
|
129
|
-
|
|
201
|
+
_DIVERGENCE_NOTE_TREATMENT = " draft_a_text and draft_b_text must be genuinely distinct takes on this thread regardless of style -- think of them as variant 1a and variant 1b: different entry point, different specific detail seized on, different rhetorical move -- not two phrasings of the same underlying observation. If 1a and 1b would say essentially the same thing, pick a different angle entirely for 1b rather than just rewording 1a."
|
|
130
202
|
|
|
131
|
-
_PERSONA_DIRECTIVE_TEMPLATE = "Otherwise: draft a reply that stands on its own as a genuinely useful contribution to THIS thread. Ground it in the persona's real, first-hand experience from the ACCOUNT VOICE CORPUS block below (specific projects, real numbers, sharp opinions, actual failures) and in the persona's `voice` block from ALL_PROJECTS_JSON. Add exactly ONE of: a concrete specific from that lived experience, a sharp non-obvious opinion, a useful pointer, or a question that genuinely moves the thread forward. NEVER generic agreement ('makes sense', 'this is spot on', 'great point', 'the nuance here is').@
|
|
203
|
+
_PERSONA_DIRECTIVE_TEMPLATE = "Otherwise: draft a reply that stands on its own as a genuinely useful contribution to THIS thread. Ground it in the persona's real, first-hand experience from the ACCOUNT VOICE CORPUS block below (specific projects, real numbers, sharp opinions, actual failures) and in the persona's `voice` block from ALL_PROJECTS_JSON. Add exactly ONE of: a concrete specific from that lived experience, a sharp non-obvious opinion, a useful pointer, or a question that genuinely moves the thread forward. NEVER generic agreement ('makes sense', 'this is spot on', 'great point', 'the nuance here is').@TREATMENT_CORE@ This is a personal account, not a brand: sound like a real person in the thread. If web search is available and the thread hinges on a current fact, verify it before drafting rather than guessing.@LENGTH_NOTE@ Follow voice.tone, never violate voice.never, mirror voice.examples / voice.examples_good when present. The global learned_preferences block under PROJECT ROUTING is distilled human review feedback and is MANDATORY, not advisory: follow every learned_preferences.draft_style_notes entry when writing @PREFS_RELATION@, and treat learned_preferences.audience_avoid / thread_avoid matches as strong reasons to skip the candidate. Never violate content_guardrails.do_not."
|
|
132
204
|
|
|
133
205
|
|
|
134
206
|
def draft_directive(arm=None, lane=None):
|
|
@@ -137,17 +209,17 @@ def draft_directive(arm=None, lane=None):
|
|
|
137
209
|
lane = lane if lane is not None else os.environ.get("S4L_ACTIVE_LANE", "")
|
|
138
210
|
if lane == "personal_brand":
|
|
139
211
|
if arm == ARM_TREATMENT:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
rel = "(per the PRIORITY ORDER above: learned_preferences and the account's real voice
|
|
212
|
+
core = " " + _treatment_core("voice.examples above")
|
|
213
|
+
length_note = " " + _TREATMENT_LENGTH_NOTE
|
|
214
|
+
rel = "(per the PRIORITY ORDER above: learned_preferences and the account's real voice are what drive this reply)"
|
|
143
215
|
else:
|
|
144
|
-
|
|
145
|
-
|
|
216
|
+
core = ""
|
|
217
|
+
length_note = " Length is governed by the per-style LENGTH LIMIT in the style block above. NEVER em dashes."
|
|
146
218
|
rel = "(it overrides the engagement style's structural template on conflict)"
|
|
147
219
|
return (
|
|
148
220
|
_PERSONA_DIRECTIVE_TEMPLATE
|
|
149
|
-
.replace("@
|
|
150
|
-
.replace("@
|
|
221
|
+
.replace("@TREATMENT_CORE@", core)
|
|
222
|
+
.replace("@LENGTH_NOTE@", length_note)
|
|
151
223
|
.replace("@PREFS_RELATION@", rel)
|
|
152
224
|
)
|
|
153
225
|
if arm == ARM_TREATMENT:
|
|
@@ -354,7 +426,7 @@ For each chosen candidate:
|
|
|
354
426
|
- If the candidate block shows an EXISTING DRAFT line AND draft age < 30 minutes, REUSE the draft text verbatim as draft_a_text/draft_a_style (set is_reused_draft=true, draft_b_text=null, draft_b_style=null). Do NOT call log_draft.py; do NOT redraft; do NOT write a second variant, prior cycle already paid the LLM cost for the one draft you have.
|
|
355
427
|
- Otherwise (fresh candidate, is_reused_draft=false): write TWO independent drafts. Do NOT judge, rank, or pick a favorite between them, both are shown to the reviewer, who decides.
|
|
356
428
|
- draft_a_text: follow the DRAFT A style block above (its own description/example/note/length limit).
|
|
357
|
-
- draft_b_text: follow the DRAFT B style block above, written INDEPENDENTLY from scratch as if draft_a_text did not exist. Do NOT lightly reword draft_a_text into draft_b_text, they must diverge in length and rhetorical move
|
|
429
|
+
- draft_b_text: follow the DRAFT B style block above, written INDEPENDENTLY from scratch as if draft_a_text did not exist. Do NOT lightly reword draft_a_text into draft_b_text, they must diverge in length and rhetorical move, not just differ in phrasing. If you notice draft_b_text ending up as a paraphrase of draft_a_text, stop and rewrite it from Style B's own example instead.@DRAFT_B_DIVERGENCE_NOTE@
|
|
358
430
|
- @DRAFT_DIRECTIVE@ (applies to both drafts on fresh candidates; each still obeys its OWN style's length limit, not a shared one).
|
|
359
431
|
3a. PERSIST DRAFT A (skip entirely for reused drafts):
|
|
360
432
|
python3 @REPO_DIR@/scripts/log_draft.py --candidate-id CANDIDATE_ID --text 'DRAFT_A_TEXT' --style DRAFT_A_STYLE --assigned-style '@PICKED_STYLE@' --assigned-mode '@PICKED_MODE@'
|
|
@@ -439,7 +511,18 @@ def render_twitter_prompt(ing):
|
|
|
439
511
|
"""
|
|
440
512
|
arm = ing.get("arm") or os.environ.get("S4L_DRAFT_PROMPT_VARIANT") or ""
|
|
441
513
|
lane = ing.get("lane") if ing.get("lane") is not None else os.environ.get("S4L_ACTIVE_LANE", "")
|
|
514
|
+
# picked_style/picked_style_b already carry the sentinel for treatment_v4
|
|
515
|
+
# (engagement_styles.pick_style_for_post/pick_exploration_style refuse to
|
|
516
|
+
# assign a real style under that arm, 2026-07-17 consolidation), no
|
|
517
|
+
# override needed here -- this used to re-derive the sentinel from `arm`
|
|
518
|
+
# independently of the picker, which is exactly the kind of duplicated
|
|
519
|
+
# decision that let a second, unfixed copy (Reddit's post_reddit.py,
|
|
520
|
+
# which calls the picker directly and never went through this function)
|
|
521
|
+
# silently keep assigning real styles under treatment_v4.
|
|
442
522
|
picked_style = ing.get("picked_style") or ""
|
|
523
|
+
picked_style_b = ing.get("picked_style_b") or ""
|
|
524
|
+
picked_mode = ing.get("picked_mode") or "use"
|
|
525
|
+
picked_mode_b = ing.get("picked_mode_b") or "use"
|
|
443
526
|
return (
|
|
444
527
|
_TW_TEMPLATE
|
|
445
528
|
.replace("@PREFIX@", ing.get("prefix") or "")
|
|
@@ -459,10 +542,10 @@ def render_twitter_prompt(ing):
|
|
|
459
542
|
.replace("@DRAFT_B_DIVERGENCE_NOTE@", draft_b_divergence_note(arm))
|
|
460
543
|
.replace("@DRAFT_DIRECTIVE@", draft_directive(arm, lane))
|
|
461
544
|
.replace("@PICKED_STYLE_OR_INVENT@", picked_style or "(invent)")
|
|
462
|
-
.replace("@PICKED_STYLE_B@",
|
|
463
|
-
.replace("@PICKED_MODE_B@",
|
|
545
|
+
.replace("@PICKED_STYLE_B@", picked_style_b)
|
|
546
|
+
.replace("@PICKED_MODE_B@", picked_mode_b)
|
|
464
547
|
.replace("@PICKED_STYLE@", picked_style)
|
|
465
|
-
.replace("@PICKED_MODE@",
|
|
548
|
+
.replace("@PICKED_MODE@", picked_mode)
|
|
466
549
|
)
|
|
467
550
|
|
|
468
551
|
|
|
@@ -1048,6 +1048,51 @@ def compute_target_distribution(platform, context="posting"):
|
|
|
1048
1048
|
INVENT_RATE = 0.0 # retired inline roll (was 0.05, ~1 in 20 posts)
|
|
1049
1049
|
CURATED_TOP_N = 5 # size of the invent-mode reference list (top 5 by score)
|
|
1050
1050
|
|
|
1051
|
+
# Draft-prompt treatment_v4 assigns NO real engagement style, on ANY platform
|
|
1052
|
+
# that participates in the S4L_DRAFT_PROMPT_VARIANT experiment (currently
|
|
1053
|
+
# Twitter and Reddit, both of which export it via
|
|
1054
|
+
# draft_prompt_core.pick_draft_prompt_arm() before picking a style). This is
|
|
1055
|
+
# the ONE place that decision is made: pick_style_for_post() and
|
|
1056
|
+
# pick_exploration_style() below both short-circuit on it, so every caller
|
|
1057
|
+
# (Twitter's s4l_pick_style bash wrapper, Reddit's direct Python call, and
|
|
1058
|
+
# any future platform driver) gets the same behavior for free, with nothing
|
|
1059
|
+
# to remember to wire up per-platform. A caller that reads the env var
|
|
1060
|
+
# directly instead of routing through these functions is the only way to
|
|
1061
|
+
# miss this. Must match draft_prompt_core.py's ARM_TREATMENT = "treatment_v4".
|
|
1062
|
+
# 2026-07-17: consolidated here after a real bug -- the same "treatment_v4 =
|
|
1063
|
+
# no style" check used to live only in run-twitter-cycle.sh (Twitter's bash
|
|
1064
|
+
# driver), one layer above this shared function, so Reddit (which calls
|
|
1065
|
+
# pick_style_for_post() directly, bypassing that bash script entirely) never
|
|
1066
|
+
# got it: Reddit kept assigning a real style, the model still correctly
|
|
1067
|
+
# wrote "voice_first" per get_assigned_style_prompt()'s render-side check
|
|
1068
|
+
# below, and validate_or_register()'s USE-mode drift protection silently
|
|
1069
|
+
# coerced "voice_first" back to the real assigned style at post time.
|
|
1070
|
+
STYLE_SENTINEL_TREATMENT = "voice_first"
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _is_treatment_v4():
|
|
1074
|
+
return (os.environ.get("S4L_DRAFT_PROMPT_VARIANT") or "").strip() == "treatment_v4"
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _treatment_v4_assignment():
|
|
1078
|
+
"""The single no-style assignment returned by both pickers below when
|
|
1079
|
+
_is_treatment_v4(). style is the literal sentinel, not None/empty, on
|
|
1080
|
+
purpose: this makes the assignment identical to what the model is told
|
|
1081
|
+
to output (see get_assigned_style_prompt), so validate_or_register's
|
|
1082
|
+
first, simplest check (style == assigned_style -> valid) accepts it
|
|
1083
|
+
with no special-casing anywhere downstream, on either platform."""
|
|
1084
|
+
return {
|
|
1085
|
+
"mode": "use",
|
|
1086
|
+
"style": STYLE_SENTINEL_TREATMENT,
|
|
1087
|
+
"description": None,
|
|
1088
|
+
"example": None,
|
|
1089
|
+
"note": None,
|
|
1090
|
+
"target_chars": None,
|
|
1091
|
+
"reference_styles": [],
|
|
1092
|
+
"distribution_snapshot": [],
|
|
1093
|
+
"picked_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1051
1096
|
# Fallback target comment length (chars) for any style that lacks an explicit
|
|
1052
1097
|
# target_chars (legacy DB rows, cold-start before the registry is reachable).
|
|
1053
1098
|
# Set just above the top-human-reply median (~74) so the long tail of styles
|
|
@@ -1207,6 +1252,9 @@ def pick_style_for_post(platform, context="posting",
|
|
|
1207
1252
|
"picked_at": ISO-8601 UTC,
|
|
1208
1253
|
}
|
|
1209
1254
|
"""
|
|
1255
|
+
if _is_treatment_v4():
|
|
1256
|
+
return _treatment_v4_assignment()
|
|
1257
|
+
|
|
1210
1258
|
rnd = rng or random
|
|
1211
1259
|
|
|
1212
1260
|
# Human-derived branch (2026-05-22, second pass): on every platform,
|
|
@@ -1377,6 +1425,12 @@ def pick_exploration_style(platform, context="posting", exclude=None,
|
|
|
1377
1425
|
posted draft's engagement write the style's first real score, and
|
|
1378
1426
|
winners graduate into the Draft-A pool through the normal sampler.
|
|
1379
1427
|
"""
|
|
1428
|
+
if _is_treatment_v4():
|
|
1429
|
+
# No exploration under treatment_v4 (see STYLE_SENTINEL_TREATMENT
|
|
1430
|
+
# above): return None so the caller's existing "pool empty, fall
|
|
1431
|
+
# back to the scored picker" path fires, which routes through
|
|
1432
|
+
# pick_style_for_post() and converges on the same sentinel.
|
|
1433
|
+
return None
|
|
1380
1434
|
rnd = rng or random
|
|
1381
1435
|
try:
|
|
1382
1436
|
never = set(PLATFORM_POLICY.get(platform, {}).get("never", []))
|
|
@@ -1463,112 +1517,95 @@ def get_assigned_style_prompt(platform, assignment, context="posting"):
|
|
|
1463
1517
|
policy = PLATFORM_POLICY.get(platform, PLATFORM_POLICY["reddit"])
|
|
1464
1518
|
lines = []
|
|
1465
1519
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1520
|
+
# Draft-prompt A/B v4 (voice-first, 2026-07-15 RESET of v3; style
|
|
1521
|
+
# assignment dropped entirely 2026-07-17): v3's treatment rendered the
|
|
1522
|
+
# style as the BINDING FORM of the draft. v4 first demoted it to "an
|
|
1523
|
+
# optional idea for angle/length", but a real 21-pair batch comparison
|
|
1524
|
+
# showed the demoted style block was STILL polluting context: the
|
|
1525
|
+
# picker's one canonical example for a batch's assigned style bled its
|
|
1526
|
+
# own vocabulary into every candidate that batch regardless of topic
|
|
1527
|
+
# (e.g. a batch drawing 'attention_is_the_input' produced 6/6 drafts
|
|
1528
|
+
# pivoting on "who's paying attention", across unrelated threads). Style
|
|
1529
|
+
# is now not shown to the model AT ALL for treatment_v4: the account's
|
|
1530
|
+
# real voice corpus + learned_preferences are the sole drafting signal.
|
|
1531
|
+
# The arm is read from the env HERE because this block is rendered
|
|
1532
|
+
# inside the cycle process where run-twitter-cycle.sh assigns and
|
|
1533
|
+
# exports S4L_DRAFT_PROMPT_VARIANT (stamp-at-source: that same process
|
|
1534
|
+
# also stamps the arm onto every plan candidate via active_experiments).
|
|
1535
|
+
# Unset or control_v4 (and every non-twitter caller, which never has the
|
|
1536
|
+
# env) renders the legacy block below unchanged — that block IS the v4
|
|
1537
|
+
# control arm, same as it was the v3 control arm.
|
|
1538
|
+
#
|
|
1539
|
+
# pick_style_for_post()/pick_exploration_style() (above, in this same
|
|
1540
|
+
# module) already refuse to assign a real style under treatment_v4 and
|
|
1541
|
+
# return STYLE_SENTINEL_TREATMENT itself as `assignment["style"]`, so
|
|
1542
|
+
# this render-side check and that pick-side check agree by construction
|
|
1543
|
+
# instead of by convention -- no separate literal to keep in sync here.
|
|
1544
|
+
if _is_treatment_v4():
|
|
1545
|
+
lines.append(
|
|
1546
|
+
"## Engagement style: not used for this draft (voice-first mode)"
|
|
1547
|
+
)
|
|
1548
|
+
lines.append("")
|
|
1549
|
+
lines.append(
|
|
1550
|
+
"This account's real voice (the ACCOUNT VOICE CORPUS block and "
|
|
1551
|
+
"voice.examples) and learned_preferences drive this reply, not a "
|
|
1552
|
+
"named engagement style — no style name, description, example, "
|
|
1553
|
+
"or length target is assigned this draft. "
|
|
1554
|
+
f'In your output JSON, set "engagement_style" to exactly '
|
|
1555
|
+
f'"{STYLE_SENTINEL_TREATMENT}" and leave "new_style" as null, for BOTH '
|
|
1556
|
+
"Draft A and Draft B. Do not invent, reuse, or reference any "
|
|
1557
|
+
"other style name for this draft."
|
|
1558
|
+
)
|
|
1559
|
+
elif assignment["mode"] == "use":
|
|
1560
|
+
# This IS the v4 control arm (unchanged since v3 control / v2's control
|
|
1561
|
+
# text). treatment_v4 never reaches here; see the sentinel branch above.
|
|
1562
|
+
lines.append(f"## Your assigned engagement style: **{assignment['style']}**")
|
|
1563
|
+
lines.append("")
|
|
1564
|
+
lines.append(
|
|
1565
|
+
f"This style was selected by the picker (weighted by live "
|
|
1566
|
+
f"click-driven performance across {platform}). Use it. Do not "
|
|
1567
|
+
f"swap it for a different listed style."
|
|
1568
|
+
)
|
|
1569
|
+
lines.append("")
|
|
1570
|
+
lines.append(f"Platform tone: {policy.get('note', '')}")
|
|
1571
|
+
lines.append("")
|
|
1572
|
+
lines.append(f"**{assignment['style']}**: {assignment.get('description', '')}")
|
|
1573
|
+
if assignment.get("example"):
|
|
1574
|
+
lines.append(f' Example: "{assignment["example"]}"')
|
|
1575
|
+
if assignment.get("note"):
|
|
1576
|
+
lines.append(f" Note: {assignment['note']}")
|
|
1577
|
+
# LENGTH A/B CONCLUDED 2026-06-04: control won, so the prompt always
|
|
1578
|
+
# uses the legacy generic length guidance. The treatment's per-style
|
|
1579
|
+
# target prompt remains preserved only in the shipped experiment card.
|
|
1580
|
+
#
|
|
1581
|
+
# EXCEPTION (2026-07-11, Draft-B explore slot): exploration
|
|
1582
|
+
# assignments (pick_exploration_style, marked by `source`) honor the
|
|
1583
|
+
# style's own target_chars so the A/B pair diverges on the length
|
|
1584
|
+
# axis too. The uniform clamp flattened every draft to the same
|
|
1585
|
+
# 2-sentence shape (user: "the older drafts looked all very similar
|
|
1586
|
+
# in terms of the length"). The 06-04 conclusion still governs the
|
|
1587
|
+
# scored path, which never carries `source`.
|
|
1588
|
+
_explore_tc = assignment.get("target_chars")
|
|
1589
|
+
if (assignment.get("source") in ("human_derived", "model_invented")
|
|
1590
|
+
and _explore_tc):
|
|
1512
1591
|
lines.append("")
|
|
1513
1592
|
lines.append(
|
|
1514
|
-
f"**LENGTH: aim for about {int(
|
|
1593
|
+
f"**LENGTH: aim for about {int(_explore_tc)} characters** "
|
|
1515
1594
|
"(this style's own winning length; within about 30% either "
|
|
1516
|
-
"way is fine, never above 250)
|
|
1517
|
-
"
|
|
1518
|
-
"
|
|
1519
|
-
"
|
|
1595
|
+
"way is fine, never above 250). Let the target set the form: "
|
|
1596
|
+
"a very short target means one clipped line, a long one can "
|
|
1597
|
+
"breathe. Do NOT default to the usual two-sentence shape. "
|
|
1598
|
+
"This applies to the comment text only; any link/CTA the "
|
|
1599
|
+
"system appends afterward is separate."
|
|
1520
1600
|
)
|
|
1521
1601
|
else:
|
|
1522
|
-
lines.append(f"## Your assigned engagement style: **{assignment['style']}**")
|
|
1523
1602
|
lines.append("")
|
|
1524
1603
|
lines.append(
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1604
|
+
"**LENGTH: keep it tight.** One or two sentences, well under the "
|
|
1605
|
+
"250-character Twitter limit. A short, sharp reply almost always "
|
|
1606
|
+
"beats a paragraph. This applies to the comment text only; any "
|
|
1607
|
+
"link/CTA the system appends afterward is separate."
|
|
1528
1608
|
)
|
|
1529
|
-
lines.append("")
|
|
1530
|
-
lines.append(f"Platform tone: {policy.get('note', '')}")
|
|
1531
|
-
lines.append("")
|
|
1532
|
-
lines.append(f"**{assignment['style']}**: {assignment.get('description', '')}")
|
|
1533
|
-
if assignment.get("example"):
|
|
1534
|
-
lines.append(f' Example: "{assignment["example"]}"')
|
|
1535
|
-
if assignment.get("note"):
|
|
1536
|
-
lines.append(f" Note: {assignment['note']}")
|
|
1537
|
-
# LENGTH A/B CONCLUDED 2026-06-04: control won, so the prompt always
|
|
1538
|
-
# uses the legacy generic length guidance. The treatment's per-style
|
|
1539
|
-
# target prompt remains preserved only in the shipped experiment card.
|
|
1540
|
-
#
|
|
1541
|
-
# EXCEPTION (2026-07-11, Draft-B explore slot): exploration
|
|
1542
|
-
# assignments (pick_exploration_style, marked by `source`) honor the
|
|
1543
|
-
# style's own target_chars so the A/B pair diverges on the length
|
|
1544
|
-
# axis too. The uniform clamp flattened every draft to the same
|
|
1545
|
-
# 2-sentence shape (user: "the older drafts looked all very similar
|
|
1546
|
-
# in terms of the length"). The 06-04 conclusion still governs the
|
|
1547
|
-
# scored path, which never carries `source`.
|
|
1548
|
-
#
|
|
1549
|
-
# (The draft-prompt v3 treatment arm above supersedes this clamp
|
|
1550
|
-
# for its cycles; this legacy branch IS the v3 control arm.)
|
|
1551
|
-
_explore_tc = assignment.get("target_chars")
|
|
1552
|
-
if (assignment.get("source") in ("human_derived", "model_invented")
|
|
1553
|
-
and _explore_tc):
|
|
1554
|
-
lines.append("")
|
|
1555
|
-
lines.append(
|
|
1556
|
-
f"**LENGTH: aim for about {int(_explore_tc)} characters** "
|
|
1557
|
-
"(this style's own winning length; within about 30% either "
|
|
1558
|
-
"way is fine, never above 250). Let the target set the form: "
|
|
1559
|
-
"a very short target means one clipped line, a long one can "
|
|
1560
|
-
"breathe. Do NOT default to the usual two-sentence shape. "
|
|
1561
|
-
"This applies to the comment text only; any link/CTA the "
|
|
1562
|
-
"system appends afterward is separate."
|
|
1563
|
-
)
|
|
1564
|
-
else:
|
|
1565
|
-
lines.append("")
|
|
1566
|
-
lines.append(
|
|
1567
|
-
"**LENGTH: keep it tight.** One or two sentences, well under the "
|
|
1568
|
-
"250-character Twitter limit. A short, sharp reply almost always "
|
|
1569
|
-
"beats a paragraph. This applies to the comment text only; any "
|
|
1570
|
-
"link/CTA the system appends afterward is separate."
|
|
1571
|
-
)
|
|
1572
1609
|
lines.append("")
|
|
1573
1610
|
lines.append(
|
|
1574
1611
|
'In your output JSON, set "engagement_style" to exactly '
|