@m13v/s4l 1.7.2 → 1.7.4-rc.1
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 +93 -8
- package/mcp/dist/screencast.js +27 -0
- package/mcp/dist/setup.js +8 -2
- package/mcp/dist/twitterAuth.js +3 -2
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_browser_foreground.py +188 -0
- package/mcp/menubar/s4l_card.py +138 -37
- package/mcp/menubar/s4l_log_relay.py +32 -0
- package/mcp/menubar/s4l_menubar.py +20 -7
- package/mcp/menubar/s4l_state.py +37 -4
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/build_persona.py +8 -3
- package/scripts/engagement_styles.py +12 -1
- package/scripts/invent_styles.py +245 -0
- package/scripts/merge_review_queue.py +24 -4
- package/scripts/pick_project.py +12 -3
- package/scripts/recent_self_posts.py +146 -0
- package/scripts/release-mcpb.sh +15 -1
- package/scripts/s4l_mode.py +9 -8
- package/scripts/scan_x_profile.py +143 -11
- package/scripts/tlh_pick_248.py +92 -0
- package/scripts/top_performers.py +55 -9
- package/scripts/twitter_post_plan.py +67 -0
- package/scripts/voice_exemplars.py +424 -0
- package/skill/run-draft-and-publish.sh +4 -3
- package/skill/run-twitter-cycle.sh +24 -4
package/mcp/menubar/s4l_card.py
CHANGED
|
@@ -13,9 +13,11 @@ The panel is NONACTIVATING and auto-presented cards never take keyboard focus
|
|
|
13
13
|
field becomes editable on first click, and only a user-initiated open
|
|
14
14
|
(present_review(focus=True), the menu bar's "Review N pending drafts")
|
|
15
15
|
activates the app and seats the caret immediately. A title-bar "Snooze 1h"
|
|
16
|
-
button sits
|
|
17
|
-
panel
|
|
18
|
-
|
|
16
|
+
button sits top-left where the traffic lights would be (they're hidden: the
|
|
17
|
+
panel can't minimize or zoom, and the cross duplicated Snooze); it just
|
|
18
|
+
closes the panel, and the menu bar interprets any close with undecided
|
|
19
|
+
drafts (including Cmd-W) as a snooze (drafts stay pending, re-present after
|
|
20
|
+
REVIEW_SNOOZE_SECONDS).
|
|
19
21
|
|
|
20
22
|
Decision shape: {"n": int, "approved": bool, "loved": bool, "text": str,
|
|
21
23
|
"edited": bool, "drop_link": bool, "reject_category": str|None,
|
|
@@ -134,14 +136,30 @@ try:
|
|
|
134
136
|
from AppKit import NSWindowStyleMaskNonactivatingPanel
|
|
135
137
|
except Exception:
|
|
136
138
|
NSWindowStyleMaskNonactivatingPanel = 0
|
|
137
|
-
# Title-bar "Snooze 1h" button (
|
|
138
|
-
#
|
|
139
|
-
#
|
|
139
|
+
# Title-bar "Snooze 1h" button (top-left, where the traffic lights would be;
|
|
140
|
+
# those are hidden because minimize/zoom are disabled no-ops on this panel and
|
|
141
|
+
# the close cross just duplicated Snooze). Missing on very old AppKit; the
|
|
142
|
+
# card then degrades to the stock cross (closing still snoozes, the labeled
|
|
143
|
+
# button is just absent) because the traffic lights are only hidden when the
|
|
144
|
+
# accessory actually mounted.
|
|
140
145
|
try:
|
|
141
|
-
from AppKit import
|
|
146
|
+
from AppKit import (
|
|
147
|
+
NSTitlebarAccessoryViewController,
|
|
148
|
+
NSLayoutAttributeLeft,
|
|
149
|
+
NSLayoutAttributeRight,
|
|
150
|
+
)
|
|
142
151
|
except Exception:
|
|
143
152
|
NSTitlebarAccessoryViewController = None
|
|
153
|
+
NSLayoutAttributeLeft = None
|
|
144
154
|
NSLayoutAttributeRight = None
|
|
155
|
+
try:
|
|
156
|
+
from AppKit import (
|
|
157
|
+
NSWindowCloseButton,
|
|
158
|
+
NSWindowMiniaturizeButton,
|
|
159
|
+
NSWindowZoomButton,
|
|
160
|
+
)
|
|
161
|
+
except Exception:
|
|
162
|
+
NSWindowCloseButton = NSWindowMiniaturizeButton = NSWindowZoomButton = None
|
|
145
163
|
try:
|
|
146
164
|
from AppKit import NSBezelStyleInline
|
|
147
165
|
except Exception:
|
|
@@ -889,41 +907,95 @@ class _ReviewController(NSObject):
|
|
|
889
907
|
|
|
890
908
|
@objc.python_method
|
|
891
909
|
def _add_snooze_accessory(self, panel):
|
|
892
|
-
"""
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
910
|
+
"""Title-bar controls, replacing the traffic lights: "Snooze 1h" at
|
|
911
|
+
the top-left (minimize/zoom were disabled no-op dots on this panel,
|
|
912
|
+
and the close cross meant snooze anyway, so one labeled button says
|
|
913
|
+
what the only dismissal actually does) and "Discard all…" at the
|
|
914
|
+
top-right (moved here from the menu bar dropdown 2026-07-10; the
|
|
915
|
+
ellipsis is honest, the handler opens a confirmation alert first).
|
|
916
|
+
Snooze just closes the panel; the menu bar treats any close with
|
|
917
|
+
undecided drafts as a snooze. The traffic lights are hidden ONLY once
|
|
918
|
+
the snooze accessory mounts, so a failure on old AppKit leaves the
|
|
919
|
+
stock cross as the fallback dismissal. Cmd-W keeps working either way
|
|
920
|
+
(the Closable mask stays on)."""
|
|
921
|
+
if NSTitlebarAccessoryViewController is None or NSLayoutAttributeLeft is None:
|
|
897
922
|
return
|
|
898
923
|
try:
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
"card comes back later (or sooner from the S4L menu)."
|
|
924
|
+
self._add_titlebar_button(
|
|
925
|
+
panel,
|
|
926
|
+
title=_snooze_title(),
|
|
927
|
+
action="snoozeClicked:",
|
|
928
|
+
tooltip=(
|
|
929
|
+
"Put these drafts away for now. They stay pending and the "
|
|
930
|
+
"card comes back later (or sooner from the S4L menu)."
|
|
931
|
+
),
|
|
932
|
+
layout=NSLayoutAttributeLeft,
|
|
909
933
|
)
|
|
910
|
-
|
|
911
|
-
btn.setAction_("snoozeClicked:")
|
|
912
|
-
btn.sizeToFit()
|
|
913
|
-
bf = btn.frame()
|
|
914
|
-
holder = NSView.alloc().initWithFrame_(
|
|
915
|
-
NSMakeRect(0, 0, bf.size.width + 8, max(19, bf.size.height + 2))
|
|
916
|
-
)
|
|
917
|
-
btn.setFrameOrigin_(
|
|
918
|
-
(4, max(0, (holder.frame().size.height - bf.size.height) / 2.0))
|
|
919
|
-
)
|
|
920
|
-
holder.addSubview_(btn)
|
|
921
|
-
acc = NSTitlebarAccessoryViewController.alloc().init()
|
|
922
|
-
acc.setView_(holder)
|
|
923
|
-
acc.setLayoutAttribute_(NSLayoutAttributeRight)
|
|
924
|
-
panel.addTitlebarAccessoryViewController_(acc)
|
|
934
|
+
self._hide_traffic_lights(panel)
|
|
925
935
|
except Exception as e:
|
|
926
936
|
_log(f"snooze accessory unavailable: {e}")
|
|
937
|
+
if _discard_all_handler is not None and NSLayoutAttributeRight is not None:
|
|
938
|
+
try:
|
|
939
|
+
self._add_titlebar_button(
|
|
940
|
+
panel,
|
|
941
|
+
title="Discard all…",
|
|
942
|
+
action="discardAllClicked:",
|
|
943
|
+
tooltip=(
|
|
944
|
+
"Throw away every pending draft (asks first). Nothing "
|
|
945
|
+
"posts, and unlike a per-card reject this sends no "
|
|
946
|
+
"feedback signal to the AI."
|
|
947
|
+
),
|
|
948
|
+
layout=NSLayoutAttributeRight,
|
|
949
|
+
)
|
|
950
|
+
except Exception as e:
|
|
951
|
+
_log(f"discard-all accessory unavailable: {e}")
|
|
952
|
+
|
|
953
|
+
@objc.python_method
|
|
954
|
+
def _add_titlebar_button(self, panel, title, action, tooltip, layout):
|
|
955
|
+
btn = NSButton.alloc().initWithFrame_(NSMakeRect(0, 0, 80, 17))
|
|
956
|
+
btn.setTitle_(title)
|
|
957
|
+
if NSBezelStyleInline is not None:
|
|
958
|
+
btn.setBezelStyle_(NSBezelStyleInline)
|
|
959
|
+
else:
|
|
960
|
+
btn.setBezelStyle_(NSBezelStyleRounded)
|
|
961
|
+
btn.setFont_(NSFont.systemFontOfSize_(10.0))
|
|
962
|
+
btn.setToolTip_(tooltip)
|
|
963
|
+
btn.setTarget_(self)
|
|
964
|
+
btn.setAction_(action)
|
|
965
|
+
btn.sizeToFit()
|
|
966
|
+
bf = btn.frame()
|
|
967
|
+
holder = NSView.alloc().initWithFrame_(
|
|
968
|
+
NSMakeRect(0, 0, bf.size.width + 8, max(19, bf.size.height + 2))
|
|
969
|
+
)
|
|
970
|
+
btn.setFrameOrigin_(
|
|
971
|
+
(4, max(0, (holder.frame().size.height - bf.size.height) / 2.0))
|
|
972
|
+
)
|
|
973
|
+
holder.addSubview_(btn)
|
|
974
|
+
acc = NSTitlebarAccessoryViewController.alloc().init()
|
|
975
|
+
acc.setView_(holder)
|
|
976
|
+
acc.setLayoutAttribute_(layout)
|
|
977
|
+
panel.addTitlebarAccessoryViewController_(acc)
|
|
978
|
+
|
|
979
|
+
@objc.python_method
|
|
980
|
+
def _hide_traffic_lights(self, panel):
|
|
981
|
+
"""Hide close/minimize/zoom once the Snooze accessory is up: minimize
|
|
982
|
+
and zoom are disabled dots (no Miniaturizable/Resizable in the mask)
|
|
983
|
+
and the cross duplicated Snooze. Hiding, not removing: performClose_
|
|
984
|
+
(Cmd-W and snoozeClicked_) still routes through the hidden close
|
|
985
|
+
button's machinery, and windowShouldClose_ still fires."""
|
|
986
|
+
if NSWindowCloseButton is None:
|
|
987
|
+
return
|
|
988
|
+
for which in (
|
|
989
|
+
NSWindowCloseButton,
|
|
990
|
+
NSWindowMiniaturizeButton,
|
|
991
|
+
NSWindowZoomButton,
|
|
992
|
+
):
|
|
993
|
+
try:
|
|
994
|
+
b = panel.standardWindowButton_(which)
|
|
995
|
+
if b is not None:
|
|
996
|
+
b.setHidden_(True)
|
|
997
|
+
except Exception:
|
|
998
|
+
pass
|
|
927
999
|
|
|
928
1000
|
def snoozeClicked_(self, sender):
|
|
929
1001
|
self._track("snooze")
|
|
@@ -933,6 +1005,20 @@ class _ReviewController(NSObject):
|
|
|
933
1005
|
except Exception:
|
|
934
1006
|
self._finish()
|
|
935
1007
|
|
|
1008
|
+
def discardAllClicked_(self, sender):
|
|
1009
|
+
"""Hand off to the menu bar's bulk-discard handler (registered via
|
|
1010
|
+
set_discard_all_handler); it confirms with the user, flips the store,
|
|
1011
|
+
and dismisses this panel via dismiss_active(), so nothing more happens
|
|
1012
|
+
here. On cancel the card simply stays up."""
|
|
1013
|
+
self._track("discard_all")
|
|
1014
|
+
cb = _discard_all_handler
|
|
1015
|
+
if cb is None:
|
|
1016
|
+
return
|
|
1017
|
+
try:
|
|
1018
|
+
cb()
|
|
1019
|
+
except Exception as e:
|
|
1020
|
+
_log(f"discard-all handler failed: {e}")
|
|
1021
|
+
|
|
936
1022
|
@objc.python_method
|
|
937
1023
|
def _eye_button(self, frame, kind):
|
|
938
1024
|
"""Borderless SF-Symbol eye whose hover/click opens a popover. A plain
|
|
@@ -2122,6 +2208,21 @@ def set_feedback_handler(cb):
|
|
|
2122
2208
|
_feedback_handler = cb
|
|
2123
2209
|
|
|
2124
2210
|
|
|
2211
|
+
# Bulk-discard hook for the title bar's "Discard all…" button, same
|
|
2212
|
+
# registration pattern as _feedback_handler above. The menu bar registers
|
|
2213
|
+
# _discard_all_pending here (it owns the confirmation alert, the store flip,
|
|
2214
|
+
# and dismissing the panel). Cards built while this is None simply don't get
|
|
2215
|
+
# the button.
|
|
2216
|
+
_discard_all_handler = None
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
def set_discard_all_handler(cb):
|
|
2220
|
+
"""Register the title-bar "Discard all…" action. The menu bar calls this
|
|
2221
|
+
before presenting cards with its bulk-discard handler."""
|
|
2222
|
+
global _discard_all_handler
|
|
2223
|
+
_discard_all_handler = cb
|
|
2224
|
+
|
|
2225
|
+
|
|
2125
2226
|
def _feedback_frame():
|
|
2126
2227
|
"""Just below the open review card when there is one (so it never covers
|
|
2127
2228
|
the card being reviewed), else the top-right corner of the pointer's
|
|
@@ -165,6 +165,38 @@ def _loop():
|
|
|
165
165
|
pass
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def emit(line, context="menubar"):
|
|
169
|
+
"""Buffer ONE structured line for the relay under a caller-chosen context
|
|
170
|
+
(e.g. "browser-foreground"), and mirror it to the real stderr so the local
|
|
171
|
+
menubar.err.log keeps a copy. Bypasses the _Tee so the line is not ALSO
|
|
172
|
+
buffered under the default "menubar" context. Best-effort; never raises."""
|
|
173
|
+
try:
|
|
174
|
+
t = str(line).strip()
|
|
175
|
+
if not t or _BASE64_BLOB_RE.match(t):
|
|
176
|
+
return
|
|
177
|
+
real = sys.stderr._real if isinstance(sys.stderr, _Tee) else sys.stderr
|
|
178
|
+
try:
|
|
179
|
+
real.write(t + "\n")
|
|
180
|
+
real.flush()
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
if not ENABLED:
|
|
184
|
+
return
|
|
185
|
+
with _lock:
|
|
186
|
+
_buf.append(
|
|
187
|
+
{
|
|
188
|
+
"ts": _now_iso(),
|
|
189
|
+
"stream": "stdout",
|
|
190
|
+
"line": t[:MAX_LINE_LEN],
|
|
191
|
+
"context": str(context or "menubar")[:200],
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
if len(_buf) > MAX_BUFFER:
|
|
195
|
+
del _buf[: len(_buf) - MAX_BUFFER]
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
168
200
|
def install():
|
|
169
201
|
"""Wrap sys.stderr with the relay tee and start the background flusher.
|
|
170
202
|
Call once at menubar boot, after the S4L_REPO_DIR sys.path insertion."""
|
|
@@ -117,6 +117,19 @@ try:
|
|
|
117
117
|
except Exception:
|
|
118
118
|
pass
|
|
119
119
|
|
|
120
|
+
# Ground-truth browser-foreground telemetry: whenever a MANAGED harness Chrome
|
|
121
|
+
# becomes the frontmost app (fresh-launch activation, screencast bringToFront,
|
|
122
|
+
# macOS clamping the off-screen window back on-screen, anything), ship one
|
|
123
|
+
# structured line with context "browser-foreground" through the relay above.
|
|
124
|
+
# Cause-agnostic OS-level observation; the causing sites (screencast.ts) log
|
|
125
|
+
# their own attribution lines under the same context. Best-effort.
|
|
126
|
+
try:
|
|
127
|
+
import s4l_browser_foreground # noqa: E402
|
|
128
|
+
|
|
129
|
+
s4l_browser_foreground.install()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
120
133
|
|
|
121
134
|
def _capture(err, **tags):
|
|
122
135
|
"""Report a handled menu-bar error to Sentry (component=menubar) without ever
|
|
@@ -2800,8 +2813,11 @@ class S4LMenuBar(rumps.App):
|
|
|
2800
2813
|
|
|
2801
2814
|
# present_feedback (the menu bar's feedback item) falls back to
|
|
2802
2815
|
# the module-level default handler; register ours before any
|
|
2803
|
-
# card shows.
|
|
2816
|
+
# card shows. Same pattern for the title bar's "Discard all…"
|
|
2817
|
+
# button (moved out of the dropdown 2026-07-10), which reuses the
|
|
2818
|
+
# bulk-discard handler wholesale, confirmation alert included.
|
|
2804
2819
|
s4l_card.set_feedback_handler(self._on_feedback_text)
|
|
2820
|
+
s4l_card.set_discard_all_handler(self._discard_all_pending)
|
|
2805
2821
|
s4l_card.present_review(
|
|
2806
2822
|
drafts,
|
|
2807
2823
|
on_decision=lambda d: self._on_card_decision(batch, d),
|
|
@@ -3531,12 +3547,9 @@ class S4LMenuBar(rumps.App):
|
|
|
3531
3547
|
)
|
|
3532
3548
|
)
|
|
3533
3549
|
)
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
callback=self._discard_all_pending,
|
|
3538
|
-
)
|
|
3539
|
-
)
|
|
3550
|
+
# No bulk-discard item here anymore: "Discard all…" lives in the
|
|
3551
|
+
# review card's title bar (2026-07-10). Reaching it with no card open
|
|
3552
|
+
# is "Review N pending drafts" -> Discard all, two clicks.
|
|
3540
3553
|
return items
|
|
3541
3554
|
|
|
3542
3555
|
|
package/mcp/menubar/s4l_state.py
CHANGED
|
@@ -736,6 +736,37 @@ def _match_candidate(data, n, candidate_id):
|
|
|
736
736
|
return None
|
|
737
737
|
|
|
738
738
|
|
|
739
|
+
def candidate_state(c):
|
|
740
|
+
"""Canonical lifecycle state of ONE review-queue candidate. Use this instead
|
|
741
|
+
of ad hoc flag checks — "not posted" is NOT "awaiting review".
|
|
742
|
+
|
|
743
|
+
The review queue (review-queue.json) is an APPEND-FOREVER LEDGER: handled
|
|
744
|
+
candidates are never removed, they are flag-stamped in place, so most rows
|
|
745
|
+
in an old queue are retired. States, in precedence order:
|
|
746
|
+
|
|
747
|
+
posted — approved and successfully posted (our_url set).
|
|
748
|
+
terminal — retired without a post; terminal_reason says why
|
|
749
|
+
(rejected, human_rejected, human_discarded_all,
|
|
750
|
+
duplicate_thread_pre_post, ...; None on older rows).
|
|
751
|
+
post_failed — approved but the post attempt errored (post_error says
|
|
752
|
+
why). Settled from the cards' point of view; the resume
|
|
753
|
+
path retries only after a fresh approval clears it.
|
|
754
|
+
approved — human approved, post not yet attempted/confirmed.
|
|
755
|
+
awaiting_review — none of the above. The ONLY state cards present, and
|
|
756
|
+
the only honest "pending" count (review-request.json's
|
|
757
|
+
.count mirrors it at merge time).
|
|
758
|
+
"""
|
|
759
|
+
if c.get("posted") is True:
|
|
760
|
+
return "posted"
|
|
761
|
+
if c.get("terminal") is True:
|
|
762
|
+
return "terminal"
|
|
763
|
+
if c.get("post_failed"):
|
|
764
|
+
return "post_failed"
|
|
765
|
+
if c.get("approved") is True:
|
|
766
|
+
return "approved"
|
|
767
|
+
return "awaiting_review"
|
|
768
|
+
|
|
769
|
+
|
|
739
770
|
def store_stamp_decision(batch, decision):
|
|
740
771
|
"""Write a card decision INTO the store the instant the user clicks. This is
|
|
741
772
|
the durable record (the old approved-queue.json ledger is no longer
|
|
@@ -784,7 +815,7 @@ def discard_all_pending(drafts):
|
|
|
784
815
|
done = 0
|
|
785
816
|
for d in drafts:
|
|
786
817
|
c = _match_candidate(data, d.get("n"), d.get("candidate_id"))
|
|
787
|
-
if c is None or c
|
|
818
|
+
if c is None or candidate_state(c) in ("posted", "terminal"):
|
|
788
819
|
continue
|
|
789
820
|
c["terminal"] = True
|
|
790
821
|
c["terminal_reason"] = "human_discarded_all"
|
|
@@ -865,7 +896,7 @@ def store_pending_posts(batch="review-queue"):
|
|
|
865
896
|
plan = read_plan(store_path())
|
|
866
897
|
out = []
|
|
867
898
|
for i, c in enumerate((plan or {}).get("candidates") or []):
|
|
868
|
-
if
|
|
899
|
+
if candidate_state(c) != "approved":
|
|
869
900
|
continue
|
|
870
901
|
d = c.get("decision") or {}
|
|
871
902
|
out.append(
|
|
@@ -932,7 +963,7 @@ def review_queue_posted_count():
|
|
|
932
963
|
cands = (plan or {}).get("candidates")
|
|
933
964
|
if not cands:
|
|
934
965
|
return None
|
|
935
|
-
return sum(1 for c in cands if c
|
|
966
|
+
return sum(1 for c in cands if candidate_state(c) == "posted")
|
|
936
967
|
|
|
937
968
|
|
|
938
969
|
def _plan_generation(batch):
|
|
@@ -1008,7 +1039,9 @@ def review_drafts(plan, batch="review-queue"):
|
|
|
1008
1039
|
settled_ns = {it.get("n") for it in items if it.get("candidate_id") is None}
|
|
1009
1040
|
out = []
|
|
1010
1041
|
for i, c in enumerate(((plan or {}).get("candidates") or [])):
|
|
1011
|
-
|
|
1042
|
+
# Only awaiting_review rows become cards. This also skips post_failed
|
|
1043
|
+
# rows (decided-but-failed is settled per the docstring above).
|
|
1044
|
+
if candidate_state(c) != "awaiting_review":
|
|
1012
1045
|
continue
|
|
1013
1046
|
cid = c.get("candidate_id")
|
|
1014
1047
|
if cid is not None and cid in settled_ids:
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4-rc.1",
|
|
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
package/scripts/build_persona.py
CHANGED
|
@@ -90,9 +90,14 @@ GROUNDING_INSTRUCTIONS = (
|
|
|
90
90
|
" description 2-3 sentences: who this person is as a builder/voice.\n"
|
|
91
91
|
" content_angle one paragraph of concrete, first-hand experience the "
|
|
92
92
|
"persona can speak from (real projects, real numbers, real pain).\n"
|
|
93
|
-
" voice {tone, never[]}: how they actually write (read
|
|
94
|
-
"posts/replies in the x source). Keep the organic rules: first
|
|
95
|
-
"specific, no links, no feature lists, no sales, no em dashes
|
|
93
|
+
" voice {tone, never[], examples[]}: how they actually write (read "
|
|
94
|
+
"their own posts/replies in the x source). Keep the organic rules: first "
|
|
95
|
+
"person, specific, no links, no feature lists, no sales, no em dashes. For "
|
|
96
|
+
"examples[], the x source pre-ranks their best real replies/posts by "
|
|
97
|
+
"engagement (top_replies / top_posts, each with stats, the parent tweet, and "
|
|
98
|
+
"thread continuations): keep up to 5 verbatim, or run "
|
|
99
|
+
"`voice_exemplars.py apply --scan <x-source.json>` after apply to write "
|
|
100
|
+
"voice.examples + the persona_corpus.txt exemplar section deterministically.\n"
|
|
96
101
|
" search_topics ~15 topics they have genuine experience with.\n"
|
|
97
102
|
" content_corpus (OPTIONAL but STRONGLY encouraged) the RAW gathered "
|
|
98
103
|
"corpus as one plain-text block: the persona's actual posts, replies, repo "
|
|
@@ -1034,7 +1034,18 @@ def compute_target_distribution(platform, context="posting"):
|
|
|
1034
1034
|
# mode="invent" and the prompt hands the model the top N as reference
|
|
1035
1035
|
# material to derive a new style from.
|
|
1036
1036
|
|
|
1037
|
-
|
|
1037
|
+
# 2026-07-10: inline invention retired (rate 0). It ran inside the drafting
|
|
1038
|
+
# prep session with the top-performers leaderboard + winner exemplars in
|
|
1039
|
+
# context, so every "new" style was a renamed clone of the winning
|
|
1040
|
+
# agree-then-relocate move; combined with name-only dedup the registry hit
|
|
1041
|
+
# ~938 styles that were mostly one structure. Invention now lives in the
|
|
1042
|
+
# standalone scripts/invent_styles.py job (operator-only launchd
|
|
1043
|
+
# com.m13v.s4l-invent-styles: the registry is global across installs, so a
|
|
1044
|
+
# central daily run replaces per-post rolls). The invent-mode plumbing in
|
|
1045
|
+
# pickers/posters is kept intact: register_style() is what the standalone
|
|
1046
|
+
# job calls, and a nonzero rate here re-enables the inline path if ever
|
|
1047
|
+
# wanted.
|
|
1048
|
+
INVENT_RATE = 0.0 # retired inline roll (was 0.05, ~1 in 20 posts)
|
|
1038
1049
|
CURATED_TOP_N = 5 # size of the invent-mode reference list (top 5 by score)
|
|
1039
1050
|
|
|
1040
1051
|
# Fallback target comment length (chars) for any style that lacks an explicit
|