44import os
55import re
66import subprocess
7+ import time
78from datetime import datetime , timezone
89from pathlib import Path
910from typing import Callable
1617
1718Resolver = Callable [[str | None , str ], dict | None ]
1819
20+ EMOJI_RE = re .compile (
21+ "["
22+ "\U0001F300 -\U0001F5FF "
23+ "\U0001F600 -\U0001F64F "
24+ "\U0001F680 -\U0001F6FF "
25+ "\U0001F700 -\U0001F77F "
26+ "\U0001F780 -\U0001F7FF "
27+ "\U0001F800 -\U0001F8FF "
28+ "\U0001F900 -\U0001F9FF "
29+ "\U0001FA00 -\U0001FAFF "
30+ "\U00002700 -\U000027BF "
31+ "\U00002600 -\U000026FF "
32+ "]+" ,
33+ flags = re .UNICODE ,
34+ )
35+
1936
2037def load_json (path : Path ):
2138 if not path .exists ():
@@ -37,11 +54,20 @@ def load_credential_mapping() -> None:
3754 os .environ .update (mapping )
3855
3956
40- def run_json (cmd : list [str ]):
41- p = subprocess .run (cmd , capture_output = True , text = True )
42- if p .returncode != 0 :
57+ def run_json (cmd : list [str ], retries : int = 2 ):
58+ for attempt in range (retries + 1 ):
59+ p = subprocess .run (cmd , capture_output = True , text = True )
60+ if p .returncode == 0 :
61+ return json .loads (p .stdout )
62+
63+ reset_match = re .search (r"Resets at (\d+)" , p .stderr )
64+ if reset_match and attempt < retries :
65+ reset_ts = int (reset_match .group (1 ))
66+ sleep_for = max (1 , reset_ts - int (time .time ()) + 1 )
67+ time .sleep (min (sleep_for , 30 ))
68+ continue
69+
4370 raise RuntimeError (f"Command failed: { ' ' .join (cmd )} \n STDERR:\n { p .stderr } " )
44- return json .loads (p .stdout )
4571
4672
4773def normalize_text (text : str , limit : int = 100 ) -> str :
@@ -51,36 +77,44 @@ def normalize_text(text: str, limit: int = 100) -> str:
5177 return clean [: limit - 1 ].rstrip () + "…"
5278
5379
80+ def enforce_style (text : str ) -> str :
81+ s = text or ""
82+ s = s .replace ("!" , "" )
83+ s = s .replace ("—" , " " ).replace ("–" , " " )
84+ s = EMOJI_RE .sub ("" , s )
85+ s = re .sub (r"\s+" , " " , s ).strip ()
86+ return s [:278 ]
87+
88+
5489def build_root_text (account : str , role : str ) -> str :
5590 ideas = {
56- "founder" : "Building AI products is mostly distribution math + feedback loops. Own both, ship faster." ,
91+ "founder" : "Building AI products is mostly distribution math plus feedback loops. Own both and ship faster." ,
5792 "brand" : "Good AI products win when onboarding is instant, outcomes are measurable, and support feels human." ,
5893 "product-agent" : "Agent workflows improve when memory, orchestration, and evals are designed as one system." ,
5994 }
6095 base = ideas .get (role , ideas ["brand" ])
61- return f"{ base } #{ account } " [: 278 ]
96+ return enforce_style ( f"{ base } #{ account } " )
6297
6398
6499def safe_founder_reply (target_user : str , excerpt : str ) -> str :
65- return (
66- f"@{ target_user } Good signal here . I care less about hype and more about repeatable distribution + retention. "
100+ raw = (
101+ f"@{ target_user } Good signal. I care less about hype and more about repeatable distribution plus retention. "
67102 f"{ excerpt } "
68- )[:278 ]
103+ )
104+ return enforce_style (raw )
69105
70106
71107def build_reply_text (account : str , role : str , target_user : str , source_text : str ) -> str :
72108 excerpt = normalize_text (source_text , limit = 80 )
73109 if role == "founder" :
74110 return safe_founder_reply (target_user , excerpt )
75111 if role == "product-agent" :
76- return (
77- f"@{ target_user } Nice thread. Curious what your eval loop looks like once this is in production. "
78- f"{ excerpt } "
79- )[:278 ]
80- return (
81- f"@{ target_user } Solid point. We see the same pattern in shipping: clear UX + measurable outcomes compound. "
82- f"{ excerpt } "
83- )[:278 ]
112+ return enforce_style (
113+ f"@{ target_user } Useful thread. Curious what your eval loop looks like once this is in production. { excerpt } "
114+ )
115+ return enforce_style (
116+ f"@{ target_user } Solid point. We see the same pattern in shipping: clear UX plus measurable outcomes compound. { excerpt } "
117+ )
84118
85119
86120def contains_denylist (text : str , keywords : list [str ]) -> bool :
0 commit comments