@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.
- package/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1314 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +497 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- 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()
|