@m13v/s4l 1.6.197-rc.7

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 +1314 -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 +497 -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,343 @@
1
+ #!/usr/bin/env python3
2
+ """Kill launchd-parented skill/*.sh processes that have been running too long.
3
+
4
+ Matches the hang pattern flagged in CLAUDE.md: a run-*.sh spawns `claude -p`
5
+ which blocks indefinitely (e.g. BSD grep on stale /tmp FIFOs), preventing
6
+ launchd from re-firing the job on its StartInterval.
7
+
8
+ For every kill, emits a synthetic log_run.py entry so the stuck run surfaces
9
+ as a failed job in the dashboard's job-history table (run_monitor.log), and
10
+ appends a line to skill/logs/watchdog.log for the kill trail.
11
+ """
12
+
13
+ import os
14
+ import subprocess
15
+ import time
16
+ from pathlib import Path
17
+
18
+ REPO = Path("/Users/matthewdi/social-autoposter")
19
+ LOG_RUN_PY = REPO / "scripts" / "log_run.py"
20
+ SKILL_PATH_MARKER = "/social-autoposter/skill/"
21
+ MAX_AGE_SEC = 45 * 60
22
+ # Per-script cap overrides for pipelines that legitimately run longer than
23
+ # the 45 min global (stats.py over ~4-5k posts + rate-limit sleeps).
24
+ # Key is (script_file, platform_or_None). Lookup order: (script, platform),
25
+ # (script, None), then global MAX_AGE_SEC. Raised 2026-04-24 after the global
26
+ # 45 min cap was killing stats.sh reddit at ~90% and github-engage every 2h.
27
+ PER_SCRIPT_CAP_SEC = {
28
+ ("github-engage.sh", None): 120 * 60,
29
+ ("stats.sh", "reddit"): 120 * 60,
30
+ # 2026-04-27: extend 120 min cap to remaining stats / audit / link-edit jobs.
31
+ # 45 min was killing audit-twitter mid-run and starving link-edit-* of time
32
+ # to actually post replies + verify SEO deploys.
33
+ # 2026-06-04: raised 120 -> 180 min. stats_twitter's total fxtwitter
34
+ # working set (posts + thread_top_replies + twitter replies + parent
35
+ # threads, ~1600-1900 polls @ ~1 req/s) crept past 7200s (~7400-7500s)
36
+ # as the post corpus grew, so every 6h run was SIGKILLed at the cap
37
+ # before stamping the final lanes -> the unstamped tail stayed stale ->
38
+ # the next run re-polled the same backlog and died again (death spiral).
39
+ # The job completes cleanly in <2.1h when not killed; 180 min gives
40
+ # durable headroom and the cron fires every 6h so there is no overlap.
41
+ ("stats.sh", "twitter"): 180 * 60,
42
+ ("stats.sh", "linkedin"): 120 * 60,
43
+ ("stats.sh", "moltbook"): 120 * 60,
44
+ ("audit.sh", None): 120 * 60,
45
+ ("audit-twitter.sh", None): 120 * 60,
46
+ ("audit-reddit.sh", None): 120 * 60,
47
+ ("audit-moltbook.sh", None): 120 * 60,
48
+ ("audit-linkedin.sh", None): 120 * 60,
49
+ ("audit-reddit-resurrect.sh", None): 120 * 60,
50
+ ("audit-dm-staleness.sh", None): 120 * 60,
51
+ # link-edit-twitter.sh retired 2026-05-07 (link embedded in primary reply
52
+ # by twitter_post_plan.py + suffix wrap; no separate sweep needed).
53
+ ("link-edit-reddit.sh", None): 120 * 60,
54
+ # link-edit-linkedin.sh retired 2026-05-29 (link embedded in original comment
55
+ # at composition by run-linkedin.sh + engage-linkedin.sh; no separate sweep).
56
+ ("link-edit-moltbook.sh", None): 120 * 60,
57
+ ("link-edit-github.sh", None): 120 * 60,
58
+ ("precompute-stats.sh", None): 120 * 60,
59
+ # 2026-05-10: bumped 60 min → 90 min → 120 min for all post-* runners.
60
+ # Rationale: a single cycle = discover claude (~5 min) + ripen sleep
61
+ # (30 min hardcoded) + draft claude (~9-10 min) + post phase (~12-15 min
62
+ # for 4 posts × 3-min inter-post sleep) + phase0/salvage overhead, with
63
+ # extra headroom for browser-lock contention with peer pipelines, slow
64
+ # CDP launches, and platform rate-limit retries. 120 min keeps premature
65
+ # kills from costing us drafted-but-unposted work; the lease-based
66
+ # reddit-browser lock + draft-aware salvage gate (both shipped 2026-05-10)
67
+ # mean a kill at the cap loses at most one cycle's posting work, never
68
+ # the drafts themselves. All post-runners share this cap so behavior is
69
+ # uniform across platforms.
70
+ ("run-reddit-search.sh", None): 120 * 60,
71
+ ("run-reddit-threads.sh", None): 120 * 60,
72
+ # 2026-05-19: raised 120 → 180 min after two consecutive cycles died at the
73
+ # 120 cap mid-Phase-2b-gen with `phase2b_silent:1`. Combined with
74
+ # twitter_gen_links.GEN_TIMEOUT_SEC drop (3000 → 900s) and
75
+ # MAX_AB_HITS_PER_CYCLE cap (4), gen phase now has a 60min worst-case
76
+ # ceiling, leaving 120min for scan + T1 sleep + prep + post.
77
+ ("run-twitter-cycle.sh", None): 180 * 60,
78
+ # 2026-05-28: launchd spawns the singleton WRAPPER, not the inner cycle, so
79
+ # the wrapper is the ppid==1 process main() matches; the inner cycle is its
80
+ # child (ppid != 1) and gets skipped. Without this entry the wrapper fell back
81
+ # to the 45 min global and killed healthy Variant-A cycles (20 min ripen sleep
82
+ # + scan + draft + gen + post routinely exceed 45 min) mid-Phase-2b, stamped
83
+ # phase2b_silent. Mirror the inner cycle's 180 min cap. The
84
+ # ("run-twitter-cycle.sh", None) entry above is now effectively unreachable but
85
+ # kept for clarity / in case the wrapper is ever bypassed.
86
+ ("run-twitter-cycle-singleton.sh", None): 180 * 60,
87
+ ("run-linkedin.sh", None): 120 * 60,
88
+ ("run-moltbook.sh", None): 120 * 60,
89
+ ("run-github.sh", None): 120 * 60,
90
+ }
91
+ WATCHDOG_LOG = REPO / "skill" / "logs" / "watchdog.log"
92
+ RUN_MONITOR_LOG = REPO / "skill" / "logs" / "run_monitor.log"
93
+ TRAP_GRACE_SEC = 5
94
+
95
+
96
+ def cap_for(script_file, platform):
97
+ return (
98
+ PER_SCRIPT_CAP_SEC.get((script_file, platform))
99
+ or PER_SCRIPT_CAP_SEC.get((script_file, None))
100
+ or MAX_AGE_SEC
101
+ )
102
+
103
+ # Map skill/*.sh filename -> script label used by the script's own log_run.py
104
+ # calls. Keeps dashboard job-history grouping consistent (e.g. a killed
105
+ # run-twitter-cycle.sh shows under the same "Post · Twitter" row as a normal
106
+ # post_twitter run). Unknown scripts fall through to a watchdog_killed_* label.
107
+ # Shared scripts (stats.sh, audit.sh, octolens.sh, engage.sh) dispatch on
108
+ # `--platform X`; the watchdog appends the platform to the label at kill time.
109
+ SHARED_SCRIPT_PREFIX = {
110
+ "stats.sh": "stats_",
111
+ "audit.sh": "audit-",
112
+ "octolens.sh": "octolens-",
113
+ "engage.sh": "engage_",
114
+ }
115
+
116
+ SCRIPT_LABELS = {
117
+ "run-twitter-cycle.sh": "post_twitter",
118
+ # 2026-05-28: wrapper is the launchd-parented process now; label its kills
119
+ # under the same post_twitter dashboard row as a normal cycle.
120
+ "run-twitter-cycle-singleton.sh": "post_twitter",
121
+ "run-linkedin.sh": "post_linkedin",
122
+ "run-moltbook.sh": "post_moltbook",
123
+ "run-reddit-threads.sh": "post_reddit",
124
+ "run-reddit-search.sh": "post_reddit",
125
+ "run-github.sh": "post_github",
126
+ "run-scan-moltbook-replies.sh": "scan_moltbook_replies",
127
+ "engage-reddit.sh": "engage_reddit",
128
+ "scan-twitter-followups.sh": "scan_twitter_followups",
129
+ "engage-twitter.sh": "engage_twitter",
130
+ "engage-linkedin.sh": "engage_linkedin",
131
+ "engage-moltbook.sh": "engage_moltbook",
132
+ "engage.sh": "engage_reddit",
133
+ "github-engage.sh": "engage_github",
134
+ "engage-dm-replies-twitter.sh": "dm_replies_twitter",
135
+ "engage-dm-replies-linkedin.sh": "dm_replies_linkedin",
136
+ "engage-dm-replies-reddit.sh": "dm_replies_reddit",
137
+ "engage-dm-replies.sh": "dm_replies_reddit",
138
+ "dm-outreach-twitter.sh": "dm_outreach_twitter",
139
+ "dm-outreach-linkedin.sh": "dm_outreach_linkedin",
140
+ "dm-outreach-reddit.sh": "dm_outreach_reddit",
141
+ # link-edit-twitter.sh retired 2026-05-07; link-edit-linkedin.sh retired 2026-05-29.
142
+ "link-edit-moltbook.sh": "link_edit_moltbook",
143
+ "link-edit-reddit.sh": "link_edit_reddit",
144
+ "link-edit-github.sh": "link_edit_github",
145
+ "audit-twitter.sh": "audit-twitter",
146
+ "audit-linkedin.sh": "audit-linkedin",
147
+ "audit-moltbook.sh": "audit-moltbook",
148
+ "audit-reddit.sh": "audit-reddit",
149
+ "audit-reddit-resurrect.sh": "audit-reddit-resurrect",
150
+ "audit-dm-staleness.sh": "audit-dm-staleness",
151
+ "octolens-twitter.sh": "octolens-twitter",
152
+ "octolens-linkedin.sh": "octolens-linkedin",
153
+ "octolens-reddit.sh": "octolens-reddit",
154
+ "stats-twitter.sh": "stats_twitter",
155
+ "stats-linkedin.sh": "stats_linkedin",
156
+ "stats-moltbook.sh": "stats_moltbook",
157
+ "stats-reddit.sh": "stats_reddit",
158
+ }
159
+
160
+
161
+ def watchdog_log(msg: str) -> None:
162
+ ts = time.strftime("%Y-%m-%dT%H:%M:%S")
163
+ line = f"{ts} | {msg}\n"
164
+ WATCHDOG_LOG.parent.mkdir(parents=True, exist_ok=True)
165
+ with open(WATCHDOG_LOG, "a") as f:
166
+ f.write(line)
167
+ print(line, end="")
168
+
169
+
170
+ def list_skill_shell_processes():
171
+ """Return [(pid, ppid, etimes_sec, script_filename, platform)] for skill/*.sh bash procs."""
172
+ res = subprocess.run(
173
+ ["ps", "-A", "-o", "pid=,ppid=,etime=,command="],
174
+ capture_output=True, text=True, check=True,
175
+ )
176
+ procs = []
177
+ for raw in res.stdout.splitlines():
178
+ parts = raw.strip().split(None, 3)
179
+ if len(parts) < 4:
180
+ continue
181
+ pid_s, ppid_s, etime_s, command = parts
182
+ try:
183
+ pid = int(pid_s)
184
+ ppid = int(ppid_s)
185
+ except ValueError:
186
+ continue
187
+ if SKILL_PATH_MARKER not in command:
188
+ continue
189
+ script_name = None
190
+ tokens = command.split()
191
+ for tok in tokens:
192
+ if tok.endswith(".sh") and SKILL_PATH_MARKER in tok:
193
+ script_name = os.path.basename(tok)
194
+ break
195
+ if not script_name:
196
+ continue
197
+ etimes = _parse_etime(etime_s)
198
+ if etimes is None:
199
+ continue
200
+ platform = None
201
+ if "--platform" in tokens:
202
+ idx = tokens.index("--platform")
203
+ if idx + 1 < len(tokens):
204
+ platform = tokens[idx + 1]
205
+ procs.append((pid, ppid, etimes, script_name, platform))
206
+ return procs
207
+
208
+
209
+ def _parse_etime(s: str):
210
+ """Parse ps etime format ([[DD-]HH:]MM:SS) into seconds."""
211
+ try:
212
+ days = 0
213
+ if "-" in s:
214
+ d, s = s.split("-", 1)
215
+ days = int(d)
216
+ parts = s.split(":")
217
+ parts = [int(p) for p in parts]
218
+ if len(parts) == 2:
219
+ h, m, sec = 0, parts[0], parts[1]
220
+ elif len(parts) == 3:
221
+ h, m, sec = parts
222
+ else:
223
+ return None
224
+ return days * 86400 + h * 3600 + m * 60 + sec
225
+ except Exception:
226
+ return None
227
+
228
+
229
+ def descendants(pid: int):
230
+ out = [pid]
231
+ i = 0
232
+ while i < len(out):
233
+ try:
234
+ r = subprocess.run(
235
+ ["pgrep", "-P", str(out[i])],
236
+ capture_output=True, text=True,
237
+ )
238
+ for tok in r.stdout.split():
239
+ if tok.isdigit():
240
+ out.append(int(tok))
241
+ except Exception:
242
+ pass
243
+ i += 1
244
+ return out
245
+
246
+
247
+ def kill_tree(root_pid: int) -> list:
248
+ pids = descendants(root_pid)
249
+ for p in reversed(pids):
250
+ try:
251
+ os.kill(p, 15)
252
+ except ProcessLookupError:
253
+ pass
254
+ except PermissionError:
255
+ pass
256
+ time.sleep(TRAP_GRACE_SEC)
257
+ for p in reversed(pids):
258
+ try:
259
+ os.kill(p, 9)
260
+ except ProcessLookupError:
261
+ pass
262
+ except PermissionError:
263
+ pass
264
+ return pids
265
+
266
+
267
+ def resolve_label(script_file, platform):
268
+ prefix = SHARED_SCRIPT_PREFIX.get(script_file)
269
+ if prefix and platform:
270
+ return prefix + platform
271
+ if script_file in SCRIPT_LABELS:
272
+ return SCRIPT_LABELS[script_file]
273
+ return "watchdog_killed_" + script_file.replace(".sh", "").replace("-", "_")
274
+
275
+
276
+ def recent_emit_exists(label, since_epoch):
277
+ """True if run_monitor.log has an entry for `label` at or after since_epoch.
278
+
279
+ The bash EXIT trap in scripts like run-twitter-cycle.sh runs log_run.py on
280
+ SIGTERM, so a fresh entry here means the watchdog's own emit would be a
281
+ duplicate.
282
+ """
283
+ try:
284
+ with open(RUN_MONITOR_LOG) as f:
285
+ tail = f.readlines()[-80:]
286
+ except FileNotFoundError:
287
+ return False
288
+ for raw in tail:
289
+ parts = raw.split("|", 2)
290
+ if len(parts) < 2:
291
+ continue
292
+ ts_str = parts[0].strip()
293
+ script = parts[1].strip()
294
+ if script != label:
295
+ continue
296
+ try:
297
+ ts = time.mktime(time.strptime(ts_str, "%Y-%m-%dT%H:%M:%S"))
298
+ except ValueError:
299
+ continue
300
+ if ts >= since_epoch:
301
+ return True
302
+ return False
303
+
304
+
305
+ def emit_job_log(label, elapsed_sec):
306
+ subprocess.run(
307
+ [
308
+ "python3", str(LOG_RUN_PY),
309
+ "--script", label,
310
+ "--posted", "0",
311
+ "--skipped", "0",
312
+ "--failed", "1",
313
+ "--cost", "0",
314
+ "--elapsed", str(elapsed_sec),
315
+ ],
316
+ check=False,
317
+ )
318
+
319
+
320
+ def main() -> None:
321
+ procs = list_skill_shell_processes()
322
+ for pid, ppid, etimes, script_file, platform in procs:
323
+ if ppid != 1:
324
+ continue
325
+ cap = cap_for(script_file, platform)
326
+ if etimes < cap:
327
+ continue
328
+ label = resolve_label(script_file, platform)
329
+ plat_tag = f" platform={platform}" if platform else ""
330
+ watchdog_log(
331
+ f"KILL {script_file}{plat_tag} pid={pid} elapsed={etimes}s cap={cap}s label={label}"
332
+ )
333
+ kill_started = time.time() - 1
334
+ killed = kill_tree(pid)
335
+ watchdog_log(f" killed pids: {killed}")
336
+ if recent_emit_exists(label, kill_started):
337
+ watchdog_log(f" script trap already logged {label} — skipping watchdog emit")
338
+ else:
339
+ emit_job_log(label, etimes)
340
+
341
+
342
+ if __name__ == "__main__":
343
+ main()
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ """CLI wrapper around generation_trace.build_trace / write_trace_tempfile.
3
+
4
+ Use case: bash pipelines (run-twitter-cycle.sh) that need to write a
5
+ generation_trace JSON file before invoking Claude. The bash script
6
+ gathers the context (TOP_REPORT, TOP_QUERIES_JSON, etc.) and pipes it
7
+ to this script as JSON on stdin; the script writes a tempfile and
8
+ prints the path on stdout. The bash script captures that path into a
9
+ variable, then forwards it via env var to the downstream post-phase
10
+ (twitter_post_plan.py), which appends --generation-trace to log_post.py.
11
+
12
+ Why this script exists: keeping the trace shape in one place
13
+ (scripts/generation_trace.py) is important; bash pipelines can't import
14
+ Python, so we shim through this one-liner. Don't expand it; if you need
15
+ more pipelines, just call it the same way.
16
+
17
+ Usage:
18
+ echo '{"platform":"twitter","project_name":"all","prompt_chars":1234,
19
+ "top_performers_text":"...","top_search_topics_text":"...",
20
+ "recent_comment_ids":[],"extras":{"top_queries":[],"supply":[]}}' \\
21
+ | python3 scripts/write_generation_trace.py --prefix twitter_gen_trace_
22
+
23
+ Prints the path on stdout; exits 0. On failure exits 1 with a JSON
24
+ error envelope on stderr. Callers should `|| true` if they want to
25
+ swallow failures (trace is nice-to-have).
26
+ """
27
+ import argparse
28
+ import json
29
+ import sys
30
+ import os
31
+
32
+ # scripts/ is on the path via the .py living there.
33
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
34
+ from generation_trace import build_trace, write_trace_tempfile
35
+
36
+
37
+ def main():
38
+ parser = argparse.ArgumentParser()
39
+ parser.add_argument("--prefix", default="gen_trace_",
40
+ help="Tempfile prefix (default gen_trace_).")
41
+ args = parser.parse_args()
42
+
43
+ try:
44
+ payload = json.load(sys.stdin)
45
+ except json.JSONDecodeError as e:
46
+ print(json.dumps({"error": "INVALID_STDIN_JSON", "message": str(e)}),
47
+ file=sys.stderr)
48
+ sys.exit(1)
49
+
50
+ # Forward every supported kwarg; unknown keys are dropped silently
51
+ # so the caller can over-send without breaking the schema contract.
52
+ trace = build_trace(
53
+ platform=payload.get("platform", ""),
54
+ project_name=payload.get("project_name", ""),
55
+ prompt_chars=int(payload.get("prompt_chars", 0) or 0),
56
+ top_performers_text=payload.get("top_performers_text", "") or "",
57
+ top_search_topics_text=payload.get("top_search_topics_text", "") or "",
58
+ recent_comment_ids=payload.get("recent_comment_ids") or [],
59
+ model=payload.get("model"),
60
+ min_score_floor=payload.get("min_score_floor"),
61
+ extras=payload.get("extras") or {},
62
+ )
63
+ path = write_trace_tempfile(trace, prefix=args.prefix)
64
+ if not path:
65
+ print(json.dumps({"error": "WRITE_FAILED"}), file=sys.stderr)
66
+ sys.exit(1)
67
+ # stdout is the path only — bash captures via $(...) and any extra
68
+ # noise would corrupt the env var downstream.
69
+ print(path)
70
+
71
+
72
+ if __name__ == "__main__":
73
+ main()