@m13v/s4l 1.7.3 → 1.7.4-rc.10

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.
@@ -1824,17 +1824,30 @@ if [ "${S4L_ACTIVE_LANE:-}" = "personal_brand" ]; then
1824
1824
  DRAFT_DIRECTIVE="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 PERSONA 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').${PERSONA_SKELETON_BAN} 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 is governed ENTIRELY by the per-style LENGTH LIMIT in the style block above; obey that target and ceiling. NEVER em dashes. Follow voice.tone, never violate voice.never, mirror voice.examples / voice.examples_good when present. The persona's learned_preferences block in ALL_PROJECTS_JSON 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."
1825
1825
  fi
1826
1826
 
1827
+ # 2026-07-10 anti-sameness: --no-project-sections strips the multi-project
1828
+ # winner corpus (~400 lines of "Top Posts by Project" + summary table) that
1829
+ # used to ride along in EVERY draft prompt and homogenize drafts. The prep
1830
+ # session now queries per-project winners on demand AFTER routing a
1831
+ # candidate (see PROJECT TOP PERFORMERS section in PREP_PROMPT below).
1827
1832
  if [ -n "$PICKED_STYLE" ]; then
1828
- TOP_REPORT=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter --style "$PICKED_STYLE" 2>/dev/null || echo "(top performers report unavailable)")
1833
+ TOP_REPORT=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter --style "$PICKED_STYLE" --no-project-sections 2>/dev/null || echo "(top performers report unavailable)")
1829
1834
  else
1830
- TOP_REPORT=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter 2>/dev/null || echo "(top performers report unavailable)")
1835
+ TOP_REPORT=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter --no-project-sections 2>/dev/null || echo "(top performers report unavailable)")
1831
1836
  fi
1832
1837
  if [ -n "$PICKED_STYLE_B" ]; then
1833
- TOP_REPORT_B=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter --style "$PICKED_STYLE_B" 2>/dev/null || echo "(top performers report unavailable)")
1838
+ TOP_REPORT_B=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter --style "$PICKED_STYLE_B" --no-project-sections 2>/dev/null || echo "(top performers report unavailable)")
1834
1839
  else
1835
- TOP_REPORT_B=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter 2>/dev/null || echo "(top performers report unavailable)")
1840
+ TOP_REPORT_B=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter --no-project-sections 2>/dev/null || echo "(top performers report unavailable)")
1836
1841
  fi
1837
1842
 
1843
+ # --- Cross-cycle self-memory (anti-repetition) ------------------------------
1844
+ # 2026-07-10: the prep session never used to see this account's own recent
1845
+ # replies across threads (author_history_block is per-author only), so it
1846
+ # recycled the same openers and sentence skeletons cycle after cycle. This
1847
+ # block is NEGATIVE context, rendered with explicit do-not-imitate rules by
1848
+ # recent_self_posts.py. Empty string on any failure; never blocks the cycle.
1849
+ RECENT_SELF_BLOCK=$(python3 "$REPO_DIR/scripts/recent_self_posts.py" --platform twitter --limit 20 2>/dev/null || echo "")
1850
+
1838
1851
  # --- Generation trace -------------------------------------------------------
1839
1852
  # Snapshot the few-shot context this cycle will feed to Claude — top_performers
1840
1853
  # report, top_queries from Phase 1, supply signal, dud queries — and write to a
@@ -2032,6 +2045,13 @@ $CORPUS_BLOCK
2032
2045
  Each candidate has a 'Project match' field. Use that project unless the thread content clearly better fits another project.
2033
2046
  All project configs: $ALL_PROJECTS_JSON
2034
2047
 
2048
+ ## PROJECT TOP PERFORMERS (query on demand, do NOT skip routing first)
2049
+ The feedback reports below carry a per-style exemplar only; project winners are no longer bulk-injected. AFTER you have decided which project a candidate's draft is for, you MAY pull that project's own recent winners (last 30 days, ranked by real click rate) when you are unsure how this product converts in replies:
2050
+ python3 $REPO_DIR/scripts/top_performers.py --platform twitter --project 'PROJECT_NAME' --top 3 --brief --invoked-by '$BATCH_ID'
2051
+ (PROJECT_NAME exactly as it appears in the candidate's 'Project match' / config.json.) Treat the results as evidence of which CLAIMS and ANGLES landed for that product, never as structural templates: do not copy their sentence shape, opener, or pivot wording. One call per project at most; skip the call entirely for projects you already queried this session.
2052
+
2053
+ $RECENT_SELF_BLOCK
2054
+
2035
2055
  ## DRAFT A: assigned style + feedback from past performance
2036
2056
  $TOP_REPORT
2037
2057
 
@@ -2344,6 +2364,33 @@ if [ "$PREP_PARSE_EXIT" -eq 0 ] && [ -f "$PLAN_FILE" ]; then
2344
2364
  fi
2345
2365
  log "Phase 2b-prep complete. plan_count=$PLAN_COUNT"
2346
2366
 
2367
+ # On-demand project-winners usage marker (2026-07-10). top_performers.py
2368
+ # appends a JSON line to the state-dir ledger on every --project call (the
2369
+ # draft prompt tells the model to pass --invoked-by "$BATCH_ID"). Count this
2370
+ # batch's lines and log a greppable marker so "did the drafting session
2371
+ # actually use the per-project query" is answerable from the cycle log alone.
2372
+ # Stderr-marker convention: format is load-bearing elsewhere; keep it stable.
2373
+ TP_ONDEMAND=$(python3 -c "
2374
+ import json, os, sys
2375
+ path = os.path.join(os.environ.get('S4L_STATE_DIR', os.path.expanduser('~/.social-autoposter-mcp')), 'top-performers-invocations.jsonl')
2376
+ n, projects = 0, []
2377
+ try:
2378
+ for line in open(path):
2379
+ try:
2380
+ r = json.loads(line)
2381
+ except Exception:
2382
+ continue
2383
+ if r.get('invoked_by') == '$BATCH_ID':
2384
+ n += 1
2385
+ p = r.get('project')
2386
+ if p and p not in projects:
2387
+ projects.append(p)
2388
+ except OSError:
2389
+ pass
2390
+ print(f'{n} projects={projects}')
2391
+ " 2>/dev/null || echo "0 projects=[]")
2392
+ log "[project_top_performers] batch=$BATCH_ID on_demand_invocations=$TP_ONDEMAND"
2393
+
2347
2394
  # twitter-browser lock was already released right after thread-media capture
2348
2395
  # (before the Claude drafting call above), since nothing from there through
2349
2396
  # Phase 2b-gen touches the browser. Phase 2b-post re-acquires unconditionally