@m13v/s4l 1.7.4-rc.9 → 1.7.5-rc.1

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 (87) hide show
  1. package/README.md +134 -101
  2. package/bin/cli.js +23 -0
  3. package/mcp/dist/index.js +71 -5
  4. package/mcp/dist/panel.html +31 -16
  5. package/mcp/dist/product-link.html +1 -1
  6. package/mcp/dist/runtime.js +110 -0
  7. package/mcp/dist/version.js +163 -54
  8. package/mcp/dist/version.json +2 -2
  9. package/mcp/manifest.json +5 -1
  10. package/mcp/menubar/s4l_card.py +55 -19
  11. package/mcp/menubar/s4l_menubar.py +237 -56
  12. package/mcp/menubar/s4l_state.py +113 -1
  13. package/mcp/package.json +1 -1
  14. package/package.json +3 -1
  15. package/scripts/active_experiments.py +30 -6
  16. package/scripts/autopilot_stall_watch.py +185 -63
  17. package/scripts/cdp_ready_check.py +94 -0
  18. package/scripts/engage_twitter_helper.py +83 -17
  19. package/scripts/engagement_styles.py +221 -26
  20. package/scripts/enrich_reply_parents.py +68 -22
  21. package/scripts/feedback_digest.py +24 -1
  22. package/scripts/generate_daily_human_style.py +31 -7
  23. package/scripts/invent_styles.py +103 -26
  24. package/scripts/linkedin_cadence.py +234 -0
  25. package/scripts/linkedin_killswitch.py +43 -0
  26. package/scripts/memory_snapshot.py +57 -0
  27. package/scripts/patches/browser-harness-loopback-cdp-proxy.patch +118 -0
  28. package/scripts/recent_self_posts.py +6 -0
  29. package/scripts/reddit_browser.py +57 -3
  30. package/scripts/s4l_box_update.sh +20 -1
  31. package/scripts/s4l_mode.py +3 -4
  32. package/scripts/s4l_posting_mode.py +59 -0
  33. package/scripts/schedule_state.py +23 -0
  34. package/scripts/scheduled_task_selfheal.py +213 -16
  35. package/scripts/setup_twitter_auth.py +6 -1
  36. package/scripts/snapshot.py +179 -61
  37. package/scripts/stall_guard.py +70 -0
  38. package/scripts/test_scheduled_task_selfheal.py +121 -0
  39. package/scripts/twitter_browser.py +146 -8
  40. package/scripts/twitter_post_plan.py +10 -1
  41. package/scripts/twitter_scan.py +26 -0
  42. package/scripts/watchdog_hung_runs.py +117 -3
  43. package/skill/amplitude-24h-signups.sh +1 -1
  44. package/skill/audit-dm-staleness.sh +1 -1
  45. package/skill/audit-linkedin.sh +1 -1
  46. package/skill/audit-reddit-resurrect.sh +1 -1
  47. package/skill/audit.sh +5 -5
  48. package/skill/check-web-chats.sh +1 -1
  49. package/skill/dm-outreach-linkedin.sh +4 -4
  50. package/skill/dm-outreach-reddit.sh +1 -1
  51. package/skill/dm-outreach-twitter.sh +1 -1
  52. package/skill/engage-dm-replies.sh +16 -3
  53. package/skill/engage-linkedin.sh +2 -2
  54. package/skill/engage-moltbook.sh +1 -1
  55. package/skill/engage-twitter.sh +4 -4
  56. package/skill/github-engage.sh +1 -1
  57. package/skill/lib/browser-launch.sh +54 -0
  58. package/skill/lib/linkedin-backend.sh +22 -15
  59. package/skill/lib/reddit-backend.sh +17 -11
  60. package/skill/lib/twitter-backend.sh +137 -16
  61. package/skill/link-edit-github.sh +1 -1
  62. package/skill/link-edit-moltbook.sh +1 -1
  63. package/skill/link-edit-reddit.sh +1 -1
  64. package/skill/linkedin-cadence.sh +20 -0
  65. package/skill/linkedin-presence.sh +1 -1
  66. package/skill/lock.sh +30 -6
  67. package/skill/precompute-stats.sh +1 -1
  68. package/skill/refresh-instagram-tokens.sh +1 -1
  69. package/skill/refresh-twitter-following.sh +1 -1
  70. package/skill/run-generate-daily-style.sh +1 -1
  71. package/skill/run-instagram-daily.sh +1 -1
  72. package/skill/run-instagram-render.sh +1 -1
  73. package/skill/run-linkedin-launchd.sh +1 -1
  74. package/skill/run-linkedin-unipile.sh +1 -1
  75. package/skill/run-linkedin.sh +1 -1
  76. package/skill/run-reddit-search.sh +2 -2
  77. package/skill/run-reddit-threads.sh +1 -1
  78. package/skill/run-twitter-cycle.sh +162 -54
  79. package/skill/scan-instagram-replies.sh +1 -1
  80. package/skill/sentry-digest.sh +1 -1
  81. package/skill/stats-instagram.sh +1 -1
  82. package/skill/stats-linkedin.sh +2 -2
  83. package/skill/stats.sh +1 -1
  84. package/skill/sweep-link-clicks.sh +1 -1
  85. package/scripts/_lock_preempt_test.py +0 -60
  86. package/scripts/saps_activity.py +0 -18
  87. package/scripts/saps_mode.py +0 -18
package/README.md CHANGED
@@ -1,143 +1,176 @@
1
- # social-autoposter
1
+ # S4L
2
2
 
3
- Open-source repo behind **[S4L (s4lai)](https://s4l.ai)**: an automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Ships as a Claude Code skill plus a set of standalone Python helpers and macOS launchd jobs.
3
+ S4L is a desktop plugin for running a Reddit and X reply-drafting workflow from
4
+ your own machine.
4
5
 
5
- > The hosted managed version is **S4L** (written `s4lai`, domain `s4l.ai`): done-for-you Reddit and Twitter brand-awareness, $1/1K impressions, $50/1K site visits. See https://s4l.ai.
6
+ It watches the conversations your buyers already read, finds threads with real
7
+ momentum and fit, drafts contribution-first replies in your voice, and gives you
8
+ a review queue before anything goes live. The product is positioned as a
9
+ self-serve desktop plugin: subscribe, install, connect your accounts, review the
10
+ drafts, and track the results.
6
11
 
7
- State (posts, replies, candidates, stats) is read and written through the hosted S4L HTTP API (`AUTOPOSTER_API_BASE` + an install key in `~/social-autoposter/.env`); no database to provision. Each platform drives its own persistent Playwright MCP browser profile, so logins survive across runs.
12
+ - Website: https://s4l.ai
13
+ - Pricing: https://s4l.ai/pricing
14
+ - Source: https://github.com/m13v/social-autoposter
8
15
 
9
- ## Prerequisites
16
+ ## What S4L does
10
17
 
11
- A new machine needs all of these before the pipeline can run end to end:
18
+ Most social tools start after you already know what to post. S4L starts earlier:
19
+ it looks for conversations worth entering.
12
20
 
13
- - **macOS** (the launchd plists are mac-only; Linux users can crib the cron snippets from `setup/SKILL.md` Step 7)
14
- - **Node.js 16+** (for `npx`, the installer, and `@playwright/mcp` at runtime)
15
- - **Python 3.9+** with `pip3` (helper scripts; deps auto-installed by the installer)
16
- - **Claude Code CLI** on `PATH` (the cron scripts shell out to `claude -p` with a per-platform MCP config)
17
- - One Chromium install per platform (created on first run by `@playwright/mcp` against the persistent profile dirs)
21
+ - Finds high-traffic Reddit and X threads with enough context to join well.
22
+ - Ranks opportunities by momentum, intent, community norms, and product fit.
23
+ - Drafts replies that answer the thread first and mention your product only when
24
+ it belongs.
25
+ - Keeps project-specific voice, claim language, competitors, sensitive topics,
26
+ and hard lines in the drafting context.
27
+ - Presents drafts in a review workflow so you can approve, reject, or edit before
28
+ posting.
29
+ - Tracks views, upvotes, clicks, deletions, and misses so each run learns from
30
+ the previous one.
18
31
 
19
- Optional:
32
+ Nothing is posted automatically by default. Posting autopilot stays off until you
33
+ explicitly turn it on.
20
34
 
21
- - `MOLTBOOK_API_KEY` in `.env` for Moltbook posting and scanning
22
- - `RESEND_API_KEY` and `NOTIFICATION_EMAIL` in `.env` for DM-escalation emails
35
+ ## Who it is for
23
36
 
24
- ## Install
37
+ S4L is for founders and small teams who want an AI-assisted social workflow
38
+ without handing their accounts to an agency or living inside mention alerts all
39
+ day.
25
40
 
26
- ```bash
27
- npx social-autoposter init
28
- ```
41
+ You bring the accounts, product context, voice, and judgment. The plugin handles
42
+ thread discovery, draft generation, review cards, scheduling, and result memory.
29
43
 
30
- `bin/cli.js` does all of the wiring in one shot:
44
+ ## How users install it
31
45
 
32
- 1. Copies `scripts/`, `skill/`, `setup/`, `SKILL.md`, and `browser-agent-configs/` into `~/social-autoposter/`
33
- 2. Creates `config.json` from `config.example.json` and writes a blank `.env` template (fill in your S4L API key and optional `MOLTBOOK_API_KEY`)
34
- 3. Installs the Python helper deps via `pip3` if missing
35
- 4. Generates launchd plists in `~/social-autoposter/launchd/` with the user's actual `HOME` and `PATH`
36
- 5. Installs the Playwright MCP configs to `~/.claude/browser-agent-configs/` (twitter, reddit, linkedin) with `__HOME__` and `__NODE_BIN__` placeholders substituted. Existing files are left alone, so any window-position tweaks survive `npx social-autoposter update`.
37
- 6. Creates empty persistent browser profile dirs at `~/.claude/browser-profiles/{twitter,reddit,linkedin}`
38
- 7. Symlinks `~/.claude/skills/social-autoposter` and `~/.claude/skills/social-autoposter-setup` to the install dir
46
+ The production path is the desktop plugin download from S4L.
39
47
 
40
- To refresh code without touching user files (`config.json`, `.env`, `SKILL.md`, or any browser config you customized):
48
+ 1. Subscribe at https://s4l.ai/pricing.
49
+ 2. Download the plugin link emailed after checkout.
50
+ 3. Install the plugin in Claude Desktop.
51
+ 4. Start a new Claude chat and send:
41
52
 
42
- ```bash
43
- npx social-autoposter update
53
+ ```text
54
+ Set me up on S4L plugin end to end
44
55
  ```
45
56
 
46
- ## Configure
57
+ The setup flow repairs the local runtime, connects your X browser session,
58
+ discovers product and voice context, seeds search topics, schedules draft
59
+ generation, and verifies that draft cards appear without posting.
47
60
 
48
- Tell your Claude agent: **"set me up on social-autoposter plugin end to end"**. The
49
- setup skill treats that as a terminal goal:
61
+ ## What is in this repo
50
62
 
51
- 1. Inspect and repair the owned runtime.
52
- 2. Auto-detect the best browser profile and connect X/Twitter. macOS may require
53
- a Safe Storage approval; a logged-out account may require one manual sign-in.
54
- 3. Scan the X profile, discover and research the user's product, and infer a
55
- conservative project, ICP, voice, and search topics without an interview.
56
- 4. Save the project and seed its topics into the backend.
57
- 5. Run a draft-only cycle to verify the pipeline without posting.
63
+ This repository contains the open-source runtime behind the S4L plugin:
58
64
 
59
- The agent pauses only for an unavoidable login or when no product can be
60
- identified. Autopilot remains off until explicitly requested.
65
+ ```text
66
+ social-autoposter/
67
+ |-- mcp/ Desktop plugin / MCP server, panel UI, release bundle
68
+ |-- scripts/ Discovery, drafting, stats, telemetry, and queue helpers
69
+ |-- skill/ Shell entrypoints used by scheduled jobs
70
+ |-- setup/ End-to-end setup skill used by Claude
71
+ |-- browser-agent-configs/ Browser automation profile templates
72
+ |-- launchd/ macOS LaunchAgent templates
73
+ |-- mcp-servers/ Local MCP helpers used by the runtime
74
+ |-- config.example.json Example project/account configuration
75
+ `-- SKILL.md Legacy social-autoposter agent playbook
76
+ ```
61
77
 
62
- ## How the runtime is wired
78
+ The `mcp/` package is the plugin users interact with. The rest of the repo is the
79
+ pipeline it bundles, installs, and drives.
63
80
 
64
- ```
65
- launchd ──▶ skill/run-{platform}.sh ──▶ claude -p --strict-mcp-config --mcp-config ~/.claude/browser-agent-configs/{platform}-agent-mcp.json
66
- │ │
67
- │ └──▶ @playwright/mcp@latest
68
- │ │
69
- │ └──▶ ~/.claude/browser-profiles/{platform}/ (persistent userDataDir)
70
- │
71
- ├──▶ scripts/find_threads.py, top_twitter_queries.py (no browser, API dedup)
72
- ├──▶ scripts/pick_project.py (weighted project rotation)
73
- ├──▶ scripts/top_performers.py (feedback report from past stats)
74
- └──▶ S4L HTTP API (AUTOPOSTER_API_BASE in .env)
81
+ ## Architecture
82
+
83
+ ```text
84
+ Claude Desktop plugin
85
+ -> S4L MCP server
86
+ -> local runtime + menu bar review UI
87
+ -> scheduled queue worker
88
+ -> browser profiles you control
89
+ -> S4L API for install-scoped queues, stats, and configuration
75
90
  ```
76
91
 
77
- Each `skill/run-*.sh`:
92
+ Important properties:
78
93
 
79
- 1. Controlled by launchd (load/unload). Use the dashboard Pause All / Resume All button, or `launchctl unload/load` directly
80
- 2. Acquires a per-platform lock from `skill/lock.sh` (waits up to 60 min for any prior run)
81
- 3. Sources `~/social-autoposter/.env`
82
- 4. Picks a project, builds a feedback report, fetches `llms.txt` for product context
83
- 5. Calls `find_*.py` for API-side candidates already deduped against the DB
84
- 6. Spawns a child Claude process with `--strict-mcp-config` so it only sees the one platform's browser MCP
94
+ - Runs locally on macOS.
95
+ - Uses the user's own logged-in browser profiles.
96
+ - Stores install state locally and scopes API calls by install identity.
97
+ - Keeps secrets such as `.env`, `config.json`, browser profiles, logs, and local
98
+ databases out of Git.
99
+ - Uses approval-first drafting as the default operating mode.
85
100
 
86
- The launchd schedules generated by `bin/cli.js` on install:
101
+ ## Develop from source
87
102
 
88
- | Job | Cadence |
89
- |-----|---------|
90
- | `com.m13v.social-stats` (`stats.sh`) | every 21600 s (6 h) |
91
- | `com.m13v.social-engage` (`engage.sh`) | every 21600 s (6 h) |
103
+ For normal users, use the plugin download from S4L. These steps are for working
104
+ on the repo itself.
92
105
 
93
- All per-platform plists live in `launchd/` (reddit-search, reddit-threads, twitter-cycle, linkedin, moltbook, github, octolens, audit, dm-replies-*, link-edit-*, scan-reddit-replies, scan-moltbook-replies, etc.) and use either `StartInterval` or `StartCalendarInterval` for fixed wall-clock times. Activate any of them with:
106
+ Prerequisites:
107
+
108
+ - macOS
109
+ - Node.js 16+
110
+ - Python 3.9+
111
+ - Claude Desktop or Claude Code for MCP testing
112
+
113
+ Install dependencies:
94
114
 
95
115
  ```bash
96
- ln -sf ~/social-autoposter/launchd/com.m13v.social-twitter-cycle.plist ~/Library/LaunchAgents/
97
- launchctl load ~/Library/LaunchAgents/com.m13v.social-twitter-cycle.plist
116
+ npm install
117
+ cd mcp
118
+ npm install
98
119
  ```
99
120
 
100
- ## Skill commands
101
-
102
- | Command | What it does |
103
- |---------|-------------|
104
- | `/social-autoposter` | Comment run: find threads, draft, post, log (cron-safe) |
105
- | `/social-autoposter post` | Create an original post or thread (manual only) |
106
- | `/social-autoposter stats` | Update engagement stats via API |
107
- | `/social-autoposter engage` | Scan and reply to responses on our posts |
108
- | `/social-autoposter audit` | Full browser audit of all posts |
121
+ Build the plugin server and panel:
109
122
 
110
- View live stats at `https://s4l.ai/stats/<your-handle>` once posts start landing.
123
+ ```bash
124
+ cd mcp
125
+ npm run build
126
+ ```
111
127
 
112
- ## Repo layout
128
+ Register this checkout with Claude Desktop and Claude Code for local testing:
113
129
 
114
- ```
115
- social-autoposter/
116
- ├── SKILL.md the playbook (locked, immutable)
117
- ├── bin/cli.js installer + dashboard launcher
118
- ├── browser-agent-configs/ Playwright MCP templates (twitter/reddit/linkedin)
119
- ├── config.example.json config template
120
- ├── setup/SKILL.md autonomous end-to-end setup skill (locked)
121
- ├── scripts/ Python and JS helpers (no browser, no LLM)
122
- ├── skill/ shell wrappers invoked by launchd
123
- └── launchd/ generated macOS LaunchAgent plists
130
+ ```bash
131
+ cd mcp
132
+ node install.mjs
124
133
  ```
125
134
 
126
- ## For other AI agents
135
+ Then fully quit and reopen Claude so MCP servers reload.
127
136
 
128
- The skill works with any agent that has shell access, browser automation, and an LLM. The Python and JS helpers in `scripts/` handle thread discovery, reply scanning, and stats updates without needing a browser. `SKILL.md` is the playbook; any agent can read it and execute the workflows with its own tools.
137
+ Run the test suite:
129
138
 
130
- ## Pause and resume
139
+ ```bash
140
+ npm test
141
+ ```
131
142
 
132
- Use the dashboard at `localhost:3141` (Pause All / Resume All button), or manually:
143
+ Build a local `.mcpb` artifact without publishing:
133
144
 
134
145
  ```bash
135
- # Pause: unload all jobs + kill running processes
136
- for plist in ~/Library/LaunchAgents/com.m13v.social-*.plist; do launchctl unload "$plist"; done
137
-
138
- # Resume: reload all jobs
139
- for plist in ~/social-autoposter/launchd/com.m13v.social-*.plist; do
140
- ln -sf "$plist" ~/Library/LaunchAgents/
141
- launchctl load ~/Library/LaunchAgents/$(basename "$plist")
142
- done
146
+ bash scripts/release-mcpb.sh --no-bump --no-npm --no-release
143
147
  ```
148
+
149
+ That command packs the current pipeline into `mcp/dist/pipeline.tgz`, builds the
150
+ plugin, creates `mcp/social-autoposter.mcpb`, and runs the release checks without
151
+ touching npm or GitHub releases.
152
+
153
+ ## Runtime commands
154
+
155
+ The plugin exposes MCP tools for the user-facing workflow:
156
+
157
+ - `project_config` configures projects, products, voice, topics, and X auth.
158
+ - `engagement_mode` chooses personal-brand and product-promotion lanes.
159
+ - `dashboard` opens the review dashboard.
160
+ - `post_drafts` posts only the drafts the user selected.
161
+ - `get_stats` reads X/Twitter stats.
162
+ - `pause_s4l` pauses or resumes scheduled S4L jobs.
163
+ - `runtime` installs, updates, and diagnoses the local runtime.
164
+ - `report_diagnosis` sends a support report to the S4L team.
165
+
166
+ The legacy `/social-autoposter` skill and npm package remain in the repo because
167
+ the plugin bundles and reuses the same pipeline scripts.
168
+
169
+ ## Public-repo hygiene
170
+
171
+ This repo is public. Do not commit local customer data, browser state, generated
172
+ media, private automation experiments, `.mcpb` bundles, `.env` files, databases,
173
+ or logs. The root `.gitignore` intentionally keeps those out of source control.
174
+
175
+ If a workflow needs private scratch space, keep it outside the repo or under an
176
+ ignored directory.
package/bin/cli.js CHANGED
@@ -427,6 +427,29 @@ function installBrowserHarness() {
427
427
  if (reset.status !== 0) {
428
428
  console.warn(' WARNING: could not reset browser-harness clone to pinned commit; using existing checkout.');
429
429
  }
430
+ // Vendored fix on top of the pin (keep in sync with runtime.ts): loopback
431
+ // CDP requests must never route through a proxy — macOS system proxy
432
+ // settings leak into urllib's default opener and a box-wide forwarder
433
+ // 403s every 127.0.0.1 probe (2026-07-13). Applied at install time since
434
+ // we don't control the upstream repo. If it stops applying, upstream has
435
+ // either merged the fix (detected; fine) or refactored (warn); never fail
436
+ // the install over it.
437
+ const harnessPatch = path.join(PKG_ROOT, 'scripts', 'patches', 'browser-harness-loopback-cdp-proxy.patch');
438
+ if (fs.existsSync(harnessPatch)) {
439
+ const chk = spawnSync('git', ['-C', harnessDir, 'apply', '--check', harnessPatch], { stdio: 'pipe' });
440
+ if (chk.status === 0) {
441
+ spawnSync('git', ['-C', harnessDir, 'apply', harnessPatch], { stdio: 'inherit' });
442
+ console.log(' applied loopback-proxy patch to browser-harness');
443
+ } else {
444
+ let fixedUpstream = false;
445
+ try {
446
+ fixedUpstream = fs.readFileSync(path.join(harnessDir, 'src', 'browser_harness', 'admin.py'), 'utf-8').includes('cdp_urlopen');
447
+ } catch { /* file moved — fall through to the warning */ }
448
+ if (!fixedUpstream) {
449
+ console.warn(' WARNING: browser-harness loopback-proxy patch no longer applies and the fix is absent upstream; CDP probes may 403 behind a system proxy.');
450
+ }
451
+ }
452
+ }
430
453
  };
431
454
  if (!fs.existsSync(harnessDir)) {
432
455
  fs.mkdirSync(path.dirname(harnessDir), { recursive: true });
package/mcp/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import fs from "node:fs";
22
22
  import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
23
23
  import { applySetup, resolveProject, hasReadyProject, personaReady, listManagedProjectStatus, listProjectSettings, ensureShortLinksDefault, ensurePersonaProject, findPersonaProject, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, normalizeStringList, } from "./setup.js";
24
24
  import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
25
- import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, retryProvisionIfStalled, } from "./runtime.js";
25
+ import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, ensureHarnessPatched, retryProvisionIfStalled, } from "./runtime.js";
26
26
  import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
27
27
  import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
28
28
  import { initSentry, sendHeartbeat, sendStateSnapshot, captureError, captureMessage, flushSentry, startLogStreaming, flushLogs, logLine, checkVersionChange } from "./telemetry.js";
@@ -163,6 +163,9 @@ function plistXml(opts) {
163
163
  const schedule = opts.keepAlive
164
164
  ? `\t<key>KeepAlive</key>\n\t<true/>`
165
165
  : `\t<key>StartInterval</key>\n\t<integer>${opts.intervalSecs}</integer>`;
166
+ const abandon = opts.abandonProcessGroup
167
+ ? `\n\t<key>AbandonProcessGroup</key>\n\t<true/>`
168
+ : "";
166
169
  // Background (cron/autopilot) runs get the same Chrome the interactive cycle
167
170
  // uses, so a no-sudo ~/Applications install (which the shell's own resolver
168
171
  // doesn't scan) is still found off-screen. Omitted when Chrome resolves via
@@ -188,7 +191,7 @@ function plistXml(opts) {
188
191
  \t<array>
189
192
  ${args}
190
193
  \t</array>
191
- ${schedule}
194
+ ${schedule}${abandon}
192
195
  \t<key>StandardOutPath</key>
193
196
  \t<string>${opts.stdoutLog}</string>
194
197
  \t<key>StandardErrorPath</key>
@@ -2673,6 +2676,53 @@ tool("restart_menubar", {
2673
2676
  menubar_running: running,
2674
2677
  });
2675
2678
  });
2679
+ // ---- posting_volume: per-install posting-volume mode (virality bar) --------
2680
+ // Server-side throttle (2026-07-13): installations.posting_mode maps to a
2681
+ // virality-bar percentile on the API (high~0.90, medium~0.97, low~0.995) and
2682
+ // OVERRIDES the cycle driver's hardcoded percentile, so a change applies on
2683
+ // the next cycle with no client update. 'get' also returns per-mode estimated
2684
+ // posts/day replayed from THIS install's trailing-7d candidate pool.
2685
+ tool("posting_volume", {
2686
+ title: "Read or set posting volume (high / medium / low)",
2687
+ description: "Read or set this install's posting-volume mode, the quality bar that decides how many drafts " +
2688
+ "per day the twitter cycle produces. Three modes: high (~100+ posts/day), medium (~30/day), " +
2689
+ "low (~5/day, only the very best candidates); 'default' clears the override so the cycle's " +
2690
+ "built-in setting applies. action:'get' returns the current mode plus per-mode estimated " +
2691
+ "posts/day computed from this install's own recent candidate pool (show those numbers when " +
2692
+ "the user is choosing). Use when the user asks to post more, post less, slow down, raise the " +
2693
+ "quality bar, or change posting volume. Takes effect on the next cycle; in draft-review mode " +
2694
+ "it equally paces how many review cards appear.",
2695
+ inputSchema: {
2696
+ action: z.enum(["get", "set"]).default("get").describe("get = read mode + rates; set = change it"),
2697
+ mode: z
2698
+ .enum(["high", "medium", "low", "default"])
2699
+ .optional()
2700
+ .describe("Required for action:'set'. 'default' clears the override."),
2701
+ },
2702
+ }, async (args) => {
2703
+ const action = args.action || "get";
2704
+ if (action === "set") {
2705
+ if (!args.mode) {
2706
+ return jsonContent({ error: "mode is required for action:'set' (high|medium|low|default)" });
2707
+ }
2708
+ const r = await runPython("scripts/s4l_posting_mode.py", ["set", String(args.mode)], {
2709
+ timeoutMs: 30_000,
2710
+ });
2711
+ try {
2712
+ return jsonContent(JSON.parse((r.stdout || "").trim()));
2713
+ }
2714
+ catch {
2715
+ return jsonContent({ error: `posting-mode set failed: ${(r.stderr || r.stdout || "").slice(0, 300)}` });
2716
+ }
2717
+ }
2718
+ const r = await runPython("scripts/s4l_posting_mode.py", ["get"], { timeoutMs: 30_000 });
2719
+ try {
2720
+ return jsonContent(JSON.parse((r.stdout || "").trim()));
2721
+ }
2722
+ catch {
2723
+ return jsonContent({ error: `posting-mode get failed: ${(r.stderr || r.stdout || "").slice(0, 300)}` });
2724
+ }
2725
+ });
2676
2726
  // ---- pause_s4l: temporarily stop drafting/posting, reversibly --------------
2677
2727
  // The lighter alternative to Quit: unloads only the launchd jobs that scan,
2678
2728
  // draft, and post (plus their support daemons), leaving Claude Desktop, the
@@ -2985,9 +3035,11 @@ function queueDir() {
2985
3035
  // routine is draining the queue — the worker would claim within a minute if it
2986
3036
  // were firing. This is the liveness signal that survives a Claude account switch
2987
3037
  // (which orphans the routines while their global SKILL.md files stay put, so the
2988
- // SKILL.md-presence check in autopilotLoaded() reads a FALSE green). Mirrors the
2989
- // menu bar's AUTOPILOT_STALL_SECONDS in mcp/menubar/s4l_menubar.py — keep in sync.
2990
- const AUTOPILOT_STALL_MS = 180_000;
3038
+ // SKILL.md-presence check in autopilotLoaded() reads a FALSE green). Mirrors
3039
+ // HEALTHY_DRAIN_MAX_SECONDS in scripts/schedule_state.py, the single source of
3040
+ // truth every Python stall detector derives from; this TS constant can't import
3041
+ // it, so keep it in sync by hand when retuning there.
3042
+ const AUTOPILOT_STALL_MS = 1_200_000;
2991
3043
  // True when no scheduled-task routine is draining the draft queue. Two signals,
2992
3044
  // OR'd (keep in lockstep with mcp/menubar/s4l_menubar.py::_autopilot_stalled and
2993
3045
  // scripts/autopilot_stall_watch.py):
@@ -3500,6 +3552,9 @@ async function ensureQueueKickerInstalled() {
3500
3552
  stdoutLog: path.join(logDir, "launchd-twitter-cycle-stdout.log"),
3501
3553
  stderrLog: path.join(logDir, "launchd-twitter-cycle-stderr.log"),
3502
3554
  extraEnv: kickerEnv(),
3555
+ // Don't let launchd reap the harness Chrome the cycle launches when the
3556
+ // kicker shell exits (2026-07-12 foreground-steal loop).
3557
+ abandonProcessGroup: true,
3503
3558
  });
3504
3559
  // Content-aware install: an existing box has the OLD kicker plist pointing at
3505
3560
  // run-twitter-cycle.sh (no merge step). ensurePlist won't overwrite, so detect
@@ -5101,6 +5156,17 @@ async function main() {
5101
5156
  if (ensureRuntimeProvisioned()) {
5102
5157
  console.error("[social-autoposter-mcp] owned runtime not ready; provisioning on boot");
5103
5158
  }
5159
+ // Vendored harness patch for ALREADY-ready runtimes: provision() is skipped
5160
+ // above when the runtime is ready, so a patch shipped in an rc would never
5161
+ // reach an existing install's harness checkout without this. Idempotent
5162
+ // (marker check) and best-effort; never blocks boot.
5163
+ void ensureHarnessPatched()
5164
+ .then((r) => {
5165
+ if (!(r.ok && r.detail === "already patched")) {
5166
+ console.error(`[social-autoposter-mcp] harness patch: ${r.ok ? "ok" : "skip"} (${r.detail})`);
5167
+ }
5168
+ })
5169
+ .catch(() => { });
5104
5170
  // Queue-backed drafting (2026-06-23): keep the two worker-task prompts current,
5105
5171
  // pre-approve their tools EAGERLY (before onboarding even creates the tasks, so
5106
5172
  // the first unattended fire can't stall), and (re)install the launchd kicker