@m13v/s4l 1.6.197-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.
Files changed (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1336 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +513 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. package/skill/topics.sh +51 -0
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ # github-engage.sh — GitHub Issues engagement loop
3
+ # Scan our GitHub issue comments for replies, respond to substantive ones.
4
+ # Called by launchd every 6 hours.
5
+
6
+
7
+ set -euo pipefail
8
+
9
+ # GitHub engage lock: wait up to 60min for previous run to finish, then skip
10
+ source "$(dirname "$0")/lock.sh"
11
+ acquire_lock "github" 3600
12
+
13
+ # Load secrets
14
+ # shellcheck source=/dev/null
15
+ [ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
16
+
17
+ REPO_DIR="$HOME/social-autoposter"
18
+ SKILL_FILE="$REPO_DIR/SKILL.md"
19
+ LOG_DIR="$REPO_DIR/skill/logs"
20
+ mkdir -p "$LOG_DIR"
21
+ LOG_FILE="$LOG_DIR/github-engage-$(date +%Y-%m-%d_%H%M%S).log"
22
+
23
+ log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
24
+
25
+ # Per-cycle batch id stamped onto every claude_sessions row spawned by this
26
+ # engagement run (via SA_CYCLE_ID env -> log_claude_session.py). 2026-05-10
27
+ # cycle_id rollout.
28
+ BATCH_ID="engh-$(date +%Y%m%d-%H%M%S)-$$"
29
+ export SA_CYCLE_ID="$BATCH_ID"
30
+
31
+ RUN_START=$(date +%s)
32
+ log "=== GitHub Engagement Run: $(date) (cycle=$BATCH_ID) ==="
33
+
34
+ # HTTP-only lane (2026-06-01): all read-side counts route through the s4l.ai
35
+ # HTTP API via scripts/github_engage_helper.py. The direct-Postgres lane was
36
+ # removed; DATABASE_URL, if present in .env, is deliberately ignored. No psql,
37
+ # no DB fallback.
38
+ GH_HELPER="$REPO_DIR/scripts/github_engage_helper.py"
39
+
40
+ # ═══════════════════════════════════════════════════════
41
+ # PHASE A: Scan for replies to our GitHub comments
42
+ # ═══════════════════════════════════════════════════════
43
+ log "Phase A: Scanning GitHub issues for replies..."
44
+ python3 "$REPO_DIR/scripts/scan_github_replies.py" 2>&1 | tee -a "$LOG_FILE"
45
+
46
+ # ═══════════════════════════════════════════════════════
47
+ # PHASE A.5: Refresh engagement stats on our GitHub comments
48
+ # Reactions pulled via gh api; reply counts tallied from the replies
49
+ # table that Phase A just refreshed. Stored on posts.upvotes +
50
+ # posts.comments_count. Per-reply stats also refreshed (same call), and
51
+ # the count is forwarded to a stats_github row in the dashboard Jobs table.
52
+ # ═══════════════════════════════════════════════════════
53
+ log "Phase A.5: Updating github engagement stats (reactions + reply counts)..."
54
+ # Best-effort: stats failures (Postgres disconnects, gh rate limits) must not block
55
+ # Phase B reply handling. Subshell scopes the set-flags, `|| true` absorbs rc.
56
+ PHASE_A5_START=$(date +%s)
57
+ GH_REPLY_SUMMARY=$(mktemp -t fazm-gh-reply-summary.XXXXXX)
58
+ # Chain lock cleanup into our cleanup. A plain `trap '...' EXIT` here would
59
+ # REPLACE lock.sh's `trap _sa_release_locks EXIT INT TERM HUP`, orphaning
60
+ # /tmp/social-autoposter-github.lock across runs (root cause of the stale
61
+ # github-lock orphans seen 2026-04-29). All four signals must be covered so
62
+ # watchdog SIGTERM also frees the lock.
63
+ trap 'rm -f "$GH_REPLY_SUMMARY"; _sa_release_locks' EXIT INT TERM HUP
64
+ ( set +e +o pipefail
65
+ python3 "$REPO_DIR/scripts/stats.py" --github-only --reply-summary "$GH_REPLY_SUMMARY" 2>&1 | tee -a "$LOG_FILE"
66
+ ) || true
67
+ PHASE_A5_ELAPSED=$(( $(date +%s) - PHASE_A5_START ))
68
+
69
+ GH_REPLIES_REFRESHED=0
70
+ if [ -s "$GH_REPLY_SUMMARY" ]; then
71
+ GH_REPLIES_REFRESHED=$(python3 -c "import json; print(json.load(open('$GH_REPLY_SUMMARY')).get('github', 0))" 2>/dev/null || echo 0)
72
+ fi
73
+ GH_ACTIVE=$(python3 "$GH_HELPER" posts-active-count 2>/dev/null | tr -d '[:space:]')
74
+ [ -z "$GH_ACTIVE" ] && GH_ACTIVE=0
75
+ # Emit a stats_github row so the dashboard Jobs table shows the github stats run
76
+ # the same way it shows stats_reddit / stats_twitter.
77
+ python3 "$REPO_DIR/scripts/log_run.py" --script "stats_github" --posted "$GH_ACTIVE" --skipped 0 --failed 0 --replies-refreshed "$GH_REPLIES_REFRESHED" --cost 0 --elapsed "$PHASE_A5_ELAPSED" || true
78
+ log "Phase A.5: done (replies_refreshed=$GH_REPLIES_REFRESHED)"
79
+
80
+ # ═══════════════════════════════════════════════════════
81
+ # PHASE B: Respond to pending GitHub replies
82
+ # ═══════════════════════════════════════════════════════
83
+ PENDING_COUNT=$(python3 "$GH_HELPER" pending-count 2>/dev/null | tr -d '[:space:]')
84
+ [ -z "$PENDING_COUNT" ] && PENDING_COUNT=0
85
+
86
+ if [ "$PENDING_COUNT" -eq 0 ]; then
87
+ log "Phase B: No pending GitHub replies. Done!"
88
+ RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
89
+ # Pull scan-stage counters from Phase A so the empty-engage row still shows
90
+ # "scanned N / 0 new" instead of all-zeros. scan_github_replies.py prints:
91
+ # Scanning N GitHub issues for replies...
92
+ # GitHub scan complete: N new pending, N skipped, N errors
93
+ GH_SCAN_PROC_LINE=$(grep -m1 -E "^Scanning [0-9]+ GitHub issues" "$LOG_FILE" 2>/dev/null || true)
94
+ GH_SCAN_DONE_LINE=$(grep -m1 "^GitHub scan complete:" "$LOG_FILE" 2>/dev/null || true)
95
+ GH_SCAN_ARG=""
96
+ if [ -n "$GH_SCAN_PROC_LINE" ] || [ -n "$GH_SCAN_DONE_LINE" ]; then
97
+ gh_scanned=$(echo "$GH_SCAN_PROC_LINE" | grep -oE "[0-9]+" | head -1)
98
+ gh_new=$(echo "$GH_SCAN_DONE_LINE" | grep -oE "[0-9]+ new pending" | grep -oE "[0-9]+" | head -1)
99
+ gh_skip=$(echo "$GH_SCAN_DONE_LINE" | grep -oE "[0-9]+ skipped" | grep -oE "[0-9]+" | head -1)
100
+ gh_err=$(echo "$GH_SCAN_DONE_LINE" | grep -oE "[0-9]+ errors" | grep -oE "[0-9]+" | head -1)
101
+ parts=""
102
+ [ -n "$gh_scanned" ] && parts="${parts}scanned=${gh_scanned},"
103
+ [ -n "$gh_new" ] && parts="${parts}new=${gh_new},"
104
+ [ -n "$gh_skip" ] && [ "$gh_skip" -gt 0 ] && parts="${parts}backfill=${gh_skip},"
105
+ [ -n "$gh_err" ] && [ "$gh_err" -gt 0 ] && parts="${parts}unmatched=${gh_err},"
106
+ GH_SCAN_ARG="${parts%,}"
107
+ fi
108
+ if [ -n "$GH_SCAN_ARG" ]; then
109
+ python3 "$REPO_DIR/scripts/log_run.py" --script "engage_github" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed "$RUN_ELAPSED" --scan "$GH_SCAN_ARG"
110
+ else
111
+ python3 "$REPO_DIR/scripts/log_run.py" --script "engage_github" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed "$RUN_ELAPSED"
112
+ fi
113
+ find "$LOG_DIR" -name "github-engage-*.log" -mtime +7 -delete 2>/dev/null || true
114
+ exit 0
115
+ fi
116
+
117
+ log "Phase B: $PENDING_COUNT pending GitHub replies to process"
118
+
119
+ # One-at-a-time thread-aware orchestrator. Each reply gets its own Claude session
120
+ # with the full issue thread fetched via gh CLI, so Claude can see our prior
121
+ # comments and decide reply-or-skip with a JSON escape hatch. See
122
+ # scripts/engage_github.py for the prompt and skip-reason contract.
123
+ python3 "$REPO_DIR/scripts/engage_github.py" --timeout 3000 2>&1 | tee -a "$LOG_FILE"
124
+
125
+ # ═══════════════════════════════════════════════════════
126
+ # PHASE C: Summary
127
+ # ═══════════════════════════════════════════════════════
128
+ # engage_github.py prints a canonical LOG_RUN_SUMMARY line; we parse it and
129
+ # write ONE log_run.py row that also carries Phase A scan counters. Previously
130
+ # engage_github.py wrote its own row with no scan info and the shell wrote
131
+ # nothing on the has-work branch -- so productive cycles lost scan visibility
132
+ # and empty cycles wrote two rows (one with scan, one without).
133
+ RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
134
+ GH_SCAN_PROC_LINE=$(grep -m1 -E "^Scanning [0-9]+ GitHub issues" "$LOG_FILE" 2>/dev/null || true)
135
+ GH_SCAN_DONE_LINE=$(grep -m1 "^GitHub scan complete:" "$LOG_FILE" 2>/dev/null || true)
136
+ GH_SCAN_ARG=""
137
+ if [ -n "$GH_SCAN_PROC_LINE" ] || [ -n "$GH_SCAN_DONE_LINE" ]; then
138
+ gh_scanned=$(echo "$GH_SCAN_PROC_LINE" | grep -oE "[0-9]+" | head -1)
139
+ gh_new=$(echo "$GH_SCAN_DONE_LINE" | grep -oE "[0-9]+ new pending" | grep -oE "[0-9]+" | head -1)
140
+ gh_skip=$(echo "$GH_SCAN_DONE_LINE" | grep -oE "[0-9]+ skipped" | grep -oE "[0-9]+" | head -1)
141
+ gh_err=$(echo "$GH_SCAN_DONE_LINE" | grep -oE "[0-9]+ errors" | grep -oE "[0-9]+" | head -1)
142
+ parts=""
143
+ [ -n "$gh_scanned" ] && parts="${parts}scanned=${gh_scanned},"
144
+ [ -n "$gh_new" ] && parts="${parts}new=${gh_new},"
145
+ [ -n "$gh_skip" ] && [ "$gh_skip" -gt 0 ] && parts="${parts}backfill=${gh_skip},"
146
+ [ -n "$gh_err" ] && [ "$gh_err" -gt 0 ] && parts="${parts}unmatched=${gh_err},"
147
+ GH_SCAN_ARG="${parts%,}"
148
+ fi
149
+
150
+ GH_SUMMARY_LINE=$(grep -m1 "^\[engage_github\] LOG_RUN_SUMMARY" "$LOG_FILE" 2>/dev/null || true)
151
+ GH_POSTED=0; GH_SKIPPED=0; GH_FAILED=0; GH_COST="0.0000"
152
+ if [ -n "$GH_SUMMARY_LINE" ]; then
153
+ GH_POSTED=$(echo "$GH_SUMMARY_LINE" | grep -oE "posted=[0-9]+" | head -1 | cut -d= -f2)
154
+ GH_SKIPPED=$(echo "$GH_SUMMARY_LINE" | grep -oE "skipped=[0-9]+" | head -1 | cut -d= -f2)
155
+ GH_FAILED=$(echo "$GH_SUMMARY_LINE" | grep -oE "failed=[0-9]+" | head -1 | cut -d= -f2)
156
+ GH_COST=$(echo "$GH_SUMMARY_LINE" | grep -oE "cost=[0-9.]+" | head -1 | cut -d= -f2)
157
+ : "${GH_POSTED:=0}" "${GH_SKIPPED:=0}" "${GH_FAILED:=0}" "${GH_COST:=0.0000}"
158
+ fi
159
+
160
+ GH_LOG_RUN_ARGS=(--script "engage_github" --posted "$GH_POSTED" --skipped "$GH_SKIPPED" --failed "$GH_FAILED" --cost "$GH_COST" --elapsed "$RUN_ELAPSED")
161
+ [ -n "$GH_SCAN_ARG" ] && GH_LOG_RUN_ARGS+=(--scan "$GH_SCAN_ARG")
162
+ python3 "$REPO_DIR/scripts/log_run.py" "${GH_LOG_RUN_ARGS[@]}" || true
163
+
164
+ # Print cumulative status for visibility in the log file. One HTTP roundtrip
165
+ # for all three counts instead of three psql one-liners.
166
+ GH_COUNTS_JSON=$(python3 "$GH_HELPER" reply-counts 2>/dev/null || echo '{"pending":0,"replied":0,"skipped":0}')
167
+ TOTAL_PENDING=$(echo "$GH_COUNTS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('pending',0))" 2>/dev/null || echo "0")
168
+ TOTAL_REPLIED=$(echo "$GH_COUNTS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('replied',0))" 2>/dev/null || echo "0")
169
+ TOTAL_SKIPPED=$(echo "$GH_COUNTS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('skipped',0))" 2>/dev/null || echo "0")
170
+
171
+ log "GitHub replies cumulative: pending=$TOTAL_PENDING replied=$TOTAL_REPLIED skipped=$TOTAL_SKIPPED"
172
+
173
+ log "=== GitHub Engagement complete: $(date) ==="
174
+
175
+ # Clean up old logs
176
+ find "$LOG_DIR" -name "github-engage-*.log" -mtime +7 -delete 2>/dev/null || true
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # ingest-web-chat-replies.sh — Poll Gmail for [WEB-CHAT #N] replies and forward
3
+ # them to visitors via Resend. Called by launchd every 5 minutes.
4
+ #
5
+ # Mirror of ~/social-autoposter/skill/dm-replies-ingest pattern (the
6
+ # ingest_human_dm_replies.py launchd rail), specialised for the web-chat thread.
7
+
8
+ set -euo pipefail
9
+
10
+ source "$(dirname "$0")/lock.sh"
11
+ acquire_lock "ingest-web-chat-replies" 60
12
+
13
+ # DB access is HTTP-only via scripts/http_api.py -> s4l.ai /api/v1/web-chat/*.
14
+ # No DATABASE_URL needed here any more.
15
+
16
+ ANALYTICS_ENV="$HOME/analytics/.env.production.local"
17
+ if [ -f "$ANALYTICS_ENV" ]; then
18
+ export RESEND_API_KEY=$(grep '^RESEND_API_KEY=' "$ANALYTICS_ENV" | sed 's/^RESEND_API_KEY=//' | tr -d '"' | tr -d '\\n')
19
+ fi
20
+ export NODE_PATH="$HOME/analytics/node_modules"
21
+
22
+ REPO_DIR="$HOME/social-autoposter"
23
+ LOG_DIR="$REPO_DIR/skill/logs"
24
+ mkdir -p "$LOG_DIR"
25
+
26
+ PYTHON_BIN="${PYTHON_BIN:-/opt/homebrew/bin/python3.11}"
27
+ [ -x "$PYTHON_BIN" ] || PYTHON_BIN="/usr/bin/python3"
28
+
29
+ LOG_FILE="$LOG_DIR/web-chat-ingest.log"
30
+ echo "[$(date)] starting ingest" >> "$LOG_FILE"
31
+
32
+ "$PYTHON_BIN" "$REPO_DIR/scripts/ingest_web_chat_replies.py" >> "$LOG_FILE" 2>&1 || \
33
+ echo "[$(date)] ERROR: ingest_web_chat_replies.py failed" >> "$LOG_FILE"
34
+
35
+ # Trim log.
36
+ if [ -f "$LOG_FILE" ]; then
37
+ tail -2000 "$LOG_FILE" > "$LOG_FILE.tmp" 2>/dev/null && mv "$LOG_FILE.tmp" "$LOG_FILE" || true
38
+ fi
@@ -0,0 +1,100 @@
1
+ #!/bin/bash
2
+ # invent-supply-test.sh — supply-test a batch of drafted Twitter queries for the
3
+ # topic-invention job (scripts/invent_topics.py).
4
+ #
5
+ # Mirrors run-twitter-cycle.sh's Phase 1 lean scan loop, but stands alone so the
6
+ # hourly invent job can measure how much FRESH (6h) supply each freshly-drafted
7
+ # query returns BEFORE committing its parent topic. It:
8
+ #
9
+ # 1. Acquires the same "twitter-browser" mkdir-lock the cycle uses, so an
10
+ # invent run and a 15-min cycle never fight over the managed Chrome on
11
+ # port 9555. Short timeout (default 600s): if the cycle is mid-scan we'd
12
+ # rather skip this hour than block. acquire_lock exits 0 on timeout, which
13
+ # leaves SCAN_OUT empty and signals "untested" back to Python.
14
+ # 2. Runs ONE browser-harness -c invocation that loops twitter_scan.scan()
15
+ # over every query, writing one JSONL record per query to SCAN_OUT (via the
16
+ # SCAN_TWEETS_FILE env the scan module already honors). scan() writes a
17
+ # record even on a zero-tweet result, so a present-but-empty tweets array
18
+ # is a real "tested, 0 supply" signal, distinct from a missing record.
19
+ # 3. Releases the lock.
20
+ #
21
+ # Usage:
22
+ # invent-supply-test.sh <queries_json> <scan_out> [freshness_hours] [lock_timeout_s]
23
+ #
24
+ # queries_json : path to a JSON array of {project, query, search_topic}
25
+ # scan_out : path the per-query JSONL results are written to (truncated first)
26
+ # freshness_hours (default 6)
27
+ # lock_timeout_s (default 600)
28
+ #
29
+ # Exit status is always 0 on a clean run (tested or skipped); Python decides
30
+ # tested-vs-untested by whether SCAN_OUT has records. Mirrors the cycle's
31
+ # fail-open posture so a transient browser hiccup never crashes the invent job.
32
+ set -uo pipefail
33
+
34
+ REPO_DIR="$HOME/social-autoposter"
35
+ HARNESS_BIN="$HOME/.local/bin/browser-harness"
36
+
37
+ QUERIES_JSON="${1:?queries_json path required}"
38
+ SCAN_OUT="${2:?scan_out path required}"
39
+ FRESHNESS_HOURS="${3:-6}"
40
+ LOCK_TIMEOUT="${4:-600}"
41
+
42
+ if [ ! -s "$QUERIES_JSON" ]; then
43
+ echo "[invent-supply-test] queries json missing/empty: $QUERIES_JSON" >&2
44
+ exit 0
45
+ fi
46
+ if [ ! -x "$HARNESS_BIN" ]; then
47
+ echo "[invent-supply-test] browser-harness not found at $HARNESS_BIN" >&2
48
+ exit 0
49
+ fi
50
+
51
+ # Truncate the output so a stale file from a prior run can't masquerade as
52
+ # fresh results. Python treats an empty SCAN_OUT as "untested this run".
53
+ : > "$SCAN_OUT"
54
+
55
+ # Source the shared lock helpers (functions only; no lock acquired on source).
56
+ # shellcheck disable=SC1091
57
+ source "$REPO_DIR/skill/lock.sh"
58
+
59
+ # acquire_lock exits 0 if the lock can't be taken within LOCK_TIMEOUT, which
60
+ # unwinds this whole script — leaving SCAN_OUT empty. That's the intended
61
+ # "skip this hour" path: the cycle owns the browser right now.
62
+ echo "[invent-supply-test] acquiring twitter-browser lock (timeout=${LOCK_TIMEOUT}s)..." >&2
63
+ acquire_lock "twitter-browser" "$LOCK_TIMEOUT"
64
+ echo "[invent-supply-test] twitter-browser lock held (pid=$$)" >&2
65
+
66
+ # One harness invocation handles every query so we pay the CLI startup once.
67
+ # Each scan() call appends a JSONL record to SCAN_TWEETS_FILE=$SCAN_OUT.
68
+ # browser-harness upstream main reads the script from STDIN (the `-c` flag was
69
+ # removed). Feed the body via a quoted heredoc and pass $REPO_DIR / $QUERIES_JSON
70
+ # through the environment so the Python reads them from os.environ.
71
+ BU_NAME=twitter-harness BU_CDP_URL=http://127.0.0.1:9555 \
72
+ SCAN_TWEETS_FILE="$SCAN_OUT" \
73
+ BATCH_ID="${BATCH_ID:-}" \
74
+ FRESHNESS_HOURS_DISCOVER="$FRESHNESS_HOURS" \
75
+ REPO_DIR="$REPO_DIR" \
76
+ QUERIES_JSON="$QUERIES_JSON" \
77
+ "$HARNESS_BIN" <<'PY' 2>&1
78
+ import sys, json, os, time
79
+ sys.path.insert(0, os.environ['REPO_DIR'] + '/scripts')
80
+ from twitter_scan import scan
81
+ queries = json.load(open(os.environ['QUERIES_JSON']))
82
+ freshness = int(os.environ.get('FRESHNESS_HOURS_DISCOVER', '6'))
83
+ for q in queries:
84
+ project = q.get('project', '')
85
+ query = q.get('query', '')
86
+ topic = q.get('search_topic', '')
87
+ t0 = time.time()
88
+ try:
89
+ kept = scan(query=query, project=project, search_topic=topic,
90
+ freshness_hours=freshness)
91
+ dt = time.time() - t0
92
+ print(f' ok project={project!r} q={query[:50]!r} kept={len(kept)} in {dt:.1f}s', flush=True)
93
+ except Exception as e:
94
+ dt = time.time() - t0
95
+ print(f' err project={project!r} q={query[:50]!r} in {dt:.1f}s {type(e).__name__}: {e}', flush=True)
96
+ PY
97
+
98
+ release_lock "twitter-browser"
99
+ echo "[invent-supply-test] done; results in $SCAN_OUT" >&2
100
+ exit 0
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # invent-topics.sh — hourly topic invention job (replaces in-cycle EXPLORE_INVENT).
3
+ #
4
+ # Picks ONE project via pick_projects() (same inverse-recent-share weighting
5
+ # the post-comments cycle uses), then calls Claude to propose N new
6
+ # search_topic candidates given that project's ledger. Validates each
7
+ # proposal against the universe (exact-match + Jaccard similarity), commits
8
+ # survivors to project_search_topics with source='invented', status='active',
9
+ # and appends an audit row to state/invented_topics_audit.jsonl.
10
+ #
11
+ # Fires hourly via com.m13v.social-invent-topics.plist. Deliberately runs
12
+ # outside the 15-min cycle cadence because invention doesn't need realtime;
13
+ # new topics added at minute :00 vs :30 make no difference to engagement.
14
+
15
+ set -uo pipefail
16
+
17
+ # shellcheck source=/dev/null
18
+ [ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
19
+
20
+ REPO_DIR="$HOME/social-autoposter"
21
+ LOG_DIR="$REPO_DIR/skill/logs"
22
+ mkdir -p "$LOG_DIR"
23
+ LOG_FILE="$LOG_DIR/invent-topics-$(date +%Y-%m-%d_%H%M%S).log"
24
+
25
+ # Number of candidate topics to ask Claude for per attempt. One topic per loop
26
+ # matches the new supply-test rhythm: invent ONE topic, draft its queries,
27
+ # supply-test, gate on supply, decide whether to loop again. With the post-
28
+ # 2026-05-29 dupe-retry-doesn't-burn-attempts behavior, asking for more than
29
+ # one is wasteful (a dupe-only Claude call retries cost-free anyway).
30
+ # Override via INVENT_PROPOSALS_PER_RUN.
31
+ PROPOSALS="${INVENT_PROPOSALS_PER_RUN:-1}"
32
+
33
+ # Stop the run as soon as ONE topic clears the supply floor — the qualifying
34
+ # tweet count IS the real target, not "how many topics qualified." A single
35
+ # topic with supply >= SUPPLY_FLOOR fresh tweets is enough; no need to keep
36
+ # burning Claude calls on additional topics that hour. MAX_ATTEMPTS caps the
37
+ # loop only if the project is genuinely dry (no qualifier in N tries).
38
+ TARGET="${INVENT_TARGET:-1}"
39
+ MAX_ATTEMPTS="${INVENT_MAX_ATTEMPTS:-5}"
40
+
41
+ {
42
+ echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] invent-topics start (proposals=$PROPOSALS target=$TARGET max_attempts=$MAX_ATTEMPTS)"
43
+ /usr/bin/python3 "$REPO_DIR/scripts/invent_topics.py" \
44
+ --proposals "$PROPOSALS" \
45
+ --target "$TARGET" \
46
+ --max-attempts "$MAX_ATTEMPTS"
47
+ rc=$?
48
+ echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] invent-topics done rc=$rc"
49
+ exit $rc
50
+ } 2>&1 | tee -a "$LOG_FILE"