@m13v/s4l 1.7.4-rc.9 → 1.7.4

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 (84) hide show
  1. package/README.md +134 -101
  2. package/bin/cli.js +23 -0
  3. package/mcp/dist/index.js +24 -5
  4. package/mcp/dist/runtime.js +110 -0
  5. package/mcp/dist/version.js +163 -54
  6. package/mcp/dist/version.json +2 -2
  7. package/mcp/manifest.json +1 -1
  8. package/mcp/menubar/s4l_card.py +55 -19
  9. package/mcp/menubar/s4l_menubar.py +179 -56
  10. package/mcp/menubar/s4l_state.py +41 -1
  11. package/mcp/package.json +1 -1
  12. package/package.json +3 -1
  13. package/scripts/active_experiments.py +30 -6
  14. package/scripts/autopilot_stall_watch.py +185 -63
  15. package/scripts/cdp_ready_check.py +94 -0
  16. package/scripts/engage_twitter_helper.py +83 -17
  17. package/scripts/engagement_styles.py +221 -26
  18. package/scripts/enrich_reply_parents.py +68 -22
  19. package/scripts/feedback_digest.py +24 -1
  20. package/scripts/generate_daily_human_style.py +31 -7
  21. package/scripts/invent_styles.py +103 -26
  22. package/scripts/linkedin_cadence.py +234 -0
  23. package/scripts/linkedin_killswitch.py +43 -0
  24. package/scripts/memory_snapshot.py +57 -0
  25. package/scripts/patches/browser-harness-loopback-cdp-proxy.patch +118 -0
  26. package/scripts/recent_self_posts.py +6 -0
  27. package/scripts/reddit_browser.py +57 -3
  28. package/scripts/s4l_box_update.sh +20 -1
  29. package/scripts/s4l_mode.py +3 -4
  30. package/scripts/schedule_state.py +23 -0
  31. package/scripts/scheduled_task_selfheal.py +213 -16
  32. package/scripts/setup_twitter_auth.py +6 -1
  33. package/scripts/snapshot.py +179 -61
  34. package/scripts/stall_guard.py +70 -0
  35. package/scripts/test_scheduled_task_selfheal.py +121 -0
  36. package/scripts/twitter_browser.py +146 -8
  37. package/scripts/twitter_post_plan.py +10 -1
  38. package/scripts/twitter_scan.py +26 -0
  39. package/scripts/watchdog_hung_runs.py +117 -3
  40. package/skill/amplitude-24h-signups.sh +1 -1
  41. package/skill/audit-dm-staleness.sh +1 -1
  42. package/skill/audit-linkedin.sh +1 -1
  43. package/skill/audit-reddit-resurrect.sh +1 -1
  44. package/skill/audit.sh +5 -5
  45. package/skill/check-web-chats.sh +1 -1
  46. package/skill/dm-outreach-linkedin.sh +4 -4
  47. package/skill/dm-outreach-reddit.sh +1 -1
  48. package/skill/dm-outreach-twitter.sh +1 -1
  49. package/skill/engage-dm-replies.sh +16 -3
  50. package/skill/engage-linkedin.sh +2 -2
  51. package/skill/engage-moltbook.sh +1 -1
  52. package/skill/engage-twitter.sh +4 -4
  53. package/skill/github-engage.sh +1 -1
  54. package/skill/lib/browser-launch.sh +54 -0
  55. package/skill/lib/linkedin-backend.sh +22 -15
  56. package/skill/lib/reddit-backend.sh +17 -11
  57. package/skill/lib/twitter-backend.sh +137 -16
  58. package/skill/link-edit-github.sh +1 -1
  59. package/skill/link-edit-moltbook.sh +1 -1
  60. package/skill/link-edit-reddit.sh +1 -1
  61. package/skill/linkedin-cadence.sh +20 -0
  62. package/skill/linkedin-presence.sh +1 -1
  63. package/skill/lock.sh +30 -6
  64. package/skill/precompute-stats.sh +1 -1
  65. package/skill/refresh-instagram-tokens.sh +1 -1
  66. package/skill/refresh-twitter-following.sh +1 -1
  67. package/skill/run-generate-daily-style.sh +1 -1
  68. package/skill/run-instagram-daily.sh +1 -1
  69. package/skill/run-instagram-render.sh +1 -1
  70. package/skill/run-linkedin-launchd.sh +1 -1
  71. package/skill/run-linkedin-unipile.sh +1 -1
  72. package/skill/run-linkedin.sh +1 -1
  73. package/skill/run-reddit-search.sh +2 -2
  74. package/skill/run-reddit-threads.sh +1 -1
  75. package/skill/run-twitter-cycle.sh +162 -54
  76. package/skill/scan-instagram-replies.sh +1 -1
  77. package/skill/sentry-digest.sh +1 -1
  78. package/skill/stats-instagram.sh +1 -1
  79. package/skill/stats-linkedin.sh +2 -2
  80. package/skill/stats.sh +1 -1
  81. package/skill/sweep-link-clicks.sh +1 -1
  82. package/scripts/_lock_preempt_test.py +0 -60
  83. package/scripts/saps_activity.py +0 -18
  84. 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>
@@ -2985,9 +2988,11 @@ function queueDir() {
2985
2988
  // routine is draining the queue — the worker would claim within a minute if it
2986
2989
  // were firing. This is the liveness signal that survives a Claude account switch
2987
2990
  // (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;
2991
+ // SKILL.md-presence check in autopilotLoaded() reads a FALSE green). Mirrors
2992
+ // HEALTHY_DRAIN_MAX_SECONDS in scripts/schedule_state.py, the single source of
2993
+ // truth every Python stall detector derives from; this TS constant can't import
2994
+ // it, so keep it in sync by hand when retuning there.
2995
+ const AUTOPILOT_STALL_MS = 1_200_000;
2991
2996
  // True when no scheduled-task routine is draining the draft queue. Two signals,
2992
2997
  // OR'd (keep in lockstep with mcp/menubar/s4l_menubar.py::_autopilot_stalled and
2993
2998
  // scripts/autopilot_stall_watch.py):
@@ -3500,6 +3505,9 @@ async function ensureQueueKickerInstalled() {
3500
3505
  stdoutLog: path.join(logDir, "launchd-twitter-cycle-stdout.log"),
3501
3506
  stderrLog: path.join(logDir, "launchd-twitter-cycle-stderr.log"),
3502
3507
  extraEnv: kickerEnv(),
3508
+ // Don't let launchd reap the harness Chrome the cycle launches when the
3509
+ // kicker shell exits (2026-07-12 foreground-steal loop).
3510
+ abandonProcessGroup: true,
3503
3511
  });
3504
3512
  // Content-aware install: an existing box has the OLD kicker plist pointing at
3505
3513
  // run-twitter-cycle.sh (no merge step). ensurePlist won't overwrite, so detect
@@ -5101,6 +5109,17 @@ async function main() {
5101
5109
  if (ensureRuntimeProvisioned()) {
5102
5110
  console.error("[social-autoposter-mcp] owned runtime not ready; provisioning on boot");
5103
5111
  }
5112
+ // Vendored harness patch for ALREADY-ready runtimes: provision() is skipped
5113
+ // above when the runtime is ready, so a patch shipped in an rc would never
5114
+ // reach an existing install's harness checkout without this. Idempotent
5115
+ // (marker check) and best-effort; never blocks boot.
5116
+ void ensureHarnessPatched()
5117
+ .then((r) => {
5118
+ if (!(r.ok && r.detail === "already patched")) {
5119
+ console.error(`[social-autoposter-mcp] harness patch: ${r.ok ? "ok" : "skip"} (${r.detail})`);
5120
+ }
5121
+ })
5122
+ .catch(() => { });
5104
5123
  // Queue-backed drafting (2026-06-23): keep the two worker-task prompts current,
5105
5124
  // pre-approve their tools EAGERLY (before onboarding even creates the tasks, so
5106
5125
  // the first unattended fire can't stall), and (re)install the launchd kicker
@@ -239,6 +239,30 @@ function bundledVersion() {
239
239
  return null;
240
240
  }
241
241
  }
242
+ // `ensurePipelineCurrent()` extracts updates over the existing materialized
243
+ // repo so customer config and logs survive. That also means deleting a file
244
+ // from the npm tarball does not delete an older copy already on disk. Keep
245
+ // explicit tombstones for retired helpers long enough for the fleet to pass
246
+ // through this release; new installs never receive these files.
247
+ const RETIRED_MATERIALIZED_FILES = [
248
+ "scripts/saps_mode.py",
249
+ "scripts/saps_activity.py",
250
+ "scripts/_lock_preempt_test.py",
251
+ ];
252
+ function removeRetiredMaterializedFiles() {
253
+ for (const rel of RETIRED_MATERIALIZED_FILES) {
254
+ const target = path.join(MATERIALIZED_REPO, rel);
255
+ try {
256
+ if (!fs.existsSync(target))
257
+ continue;
258
+ fs.rmSync(target, { force: true });
259
+ console.error(`[runtime] removed retired pipeline file ${rel}`);
260
+ }
261
+ catch (e) {
262
+ console.error(`[runtime] could not remove retired pipeline file ${rel}: ${e?.message || e}`);
263
+ }
264
+ }
265
+ }
242
266
  // Re-materialize the pipeline source when a plugin UPDATE shipped a newer
243
267
  // pipeline.tgz than what's on disk. The .mcpb update refreshes dist/ (this
244
268
  // server) but does NOT re-extract the embedded tarball, so without this the box
@@ -269,6 +293,9 @@ export function ensurePipelineCurrent() {
269
293
  const bundled = bundledVersion();
270
294
  if (!bundled)
271
295
  return; // dev build, no stamp — leave the materialized repo alone.
296
+ // Run before the version early-return too: a prior package build may have
297
+ // accidentally included a retired file under this same version.
298
+ removeRetiredMaterializedFiles();
272
299
  const rt = readRuntime();
273
300
  if (rt?.pipeline_version === bundled)
274
301
  return; // already current.
@@ -283,6 +310,9 @@ export function ensurePipelineCurrent() {
283
310
  console.error(`[runtime] pipeline re-materialize failed (exit ${r.status}); keeping existing pipeline`);
284
311
  return;
285
312
  }
313
+ // Defense in depth: a stale/locally-built tarball cannot reintroduce a
314
+ // retired helper even if its package allowlist drifts.
315
+ removeRetiredMaterializedFiles();
286
316
  // Record the new version so we don't re-extract on every boot.
287
317
  const next = rt ?? readRuntime();
288
318
  if (next) {
@@ -589,6 +619,55 @@ export function ensureRuntimeProvisioned() {
589
619
  return false; // best-effort; a boot provision must never break startup
590
620
  }
591
621
  }
622
+ // Boot hook closing the provisioning gap for ALREADY-ready runtimes: provision()
623
+ // skips when runtimeReady(), so a vendored-patch update shipped in an rc never
624
+ // reaches an existing install's harness checkout (proved on the QA box: rc.26
625
+ // delivered scripts/patches/ but the checkout stayed unpatched, 2026-07-13).
626
+ // This runs on every MCP boot: if the fix marker is absent and the patch
627
+ // applies, apply it and reload the harness daemon so the running process picks
628
+ // up the new source (the uv tool install is EDITABLE, so patched files are
629
+ // live without a reinstall). Best-effort; never blocks startup.
630
+ export async function ensureHarnessPatched() {
631
+ try {
632
+ // Version marker of the CURRENT vendored patch: a symbol that exists only
633
+ // in its newest revision (v2 added the daemon.py websocket-proxy
634
+ // neutralization after v1's cdp_urlopen alone proved insufficient — the
635
+ // WS dial still routed through the macOS system proxy and died with
636
+ // "proxy rejected connection: HTTP 503"). Bump this string whenever the
637
+ // patch gains a new hunk, or upgraded installs will silently keep the
638
+ // previous revision.
639
+ const daemonPy = path.join(HARNESS_DIR, "src", "browser_harness", "daemon.py");
640
+ if (!fs.existsSync(daemonPy))
641
+ return { ok: false, detail: "harness not installed yet" };
642
+ if (fs.readFileSync(daemonPy, "utf-8").includes("s4l_no_proxy_ws")) {
643
+ return { ok: true, detail: "already patched" };
644
+ }
645
+ const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
646
+ if (!fs.existsSync(harnessPatch))
647
+ return { ok: false, detail: "patch file missing from package" };
648
+ // Discard any PRIOR vendored-patch revision (e.g. v1) so the cumulative
649
+ // patch applies onto pristine pinned sources. Only src/ is touched; the
650
+ // checkout is a managed artifact, never a place for local edits.
651
+ await sh("git", ["-C", HARNESS_DIR, "checkout", "--", "src/browser_harness"], {
652
+ timeoutMs: 30000,
653
+ });
654
+ const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
655
+ timeoutMs: 30000,
656
+ });
657
+ if (chk.code !== 0) {
658
+ return { ok: false, detail: "patch does not apply (dirty or diverged checkout)" };
659
+ }
660
+ const app = await sh("git", ["-C", HARNESS_DIR, "apply", harnessPatch], { timeoutMs: 30000 });
661
+ if (app.code !== 0)
662
+ return { ok: false, detail: `git apply failed (exit ${app.code})` };
663
+ // Reload the daemon so the long-lived process re-imports the patched source.
664
+ await sh(HARNESS_BIN, ["--reload"], { timeoutMs: 30000 });
665
+ return { ok: true, detail: "patched + daemon reloaded" };
666
+ }
667
+ catch (e) {
668
+ return { ok: false, detail: String(e?.message || e) };
669
+ }
670
+ }
592
671
  async function provision(progress) {
593
672
  const setStep = (id, status, detail) => {
594
673
  const st = progress.steps.find((s) => s.id === id);
@@ -828,6 +907,37 @@ async function provision(progress) {
828
907
  timeoutMs: 120000,
829
908
  });
830
909
  await sh("git", ["-C", HARNESS_DIR, "reset", "--hard", "FETCH_HEAD"], { timeoutMs: 60000 });
910
+ // Vendored fix on top of the pin: loopback CDP requests must never route
911
+ // through a proxy. macOS system proxy settings leak into urllib's default
912
+ // opener, and a box-wide forwarder 403s every 127.0.0.1 probe, so Chrome
913
+ // reads as "wedged"/logged-out while it is actually fine (2026-07-13).
914
+ // Upstream doesn't carry the fix and we don't control that repo, so apply
915
+ // our patch at install time, on every machine. If it stops applying
916
+ // cleanly, upstream has either merged the fix (detected below; fine) or
917
+ // refactored the files (warn); never fail the install over it.
918
+ const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
919
+ if (fs.existsSync(harnessPatch)) {
920
+ const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
921
+ timeoutMs: 30000,
922
+ });
923
+ if (chk.code === 0) {
924
+ await sh("git", ["-C", HARNESS_DIR, "apply", harnessPatch], { timeoutMs: 30000 });
925
+ }
926
+ else {
927
+ let fixedUpstream = false;
928
+ try {
929
+ fixedUpstream = fs
930
+ .readFileSync(path.join(HARNESS_DIR, "src", "browser_harness", "admin.py"), "utf-8")
931
+ .includes("cdp_urlopen");
932
+ }
933
+ catch {
934
+ /* file moved — fall through to the warning */
935
+ }
936
+ if (!fixedUpstream) {
937
+ console.error("[runtime] browser-harness loopback-proxy patch no longer applies and the fix is absent upstream; CDP probes may 403 behind a system proxy");
938
+ }
939
+ }
940
+ }
831
941
  // Install the CLI via uv tool (lands at ~/.local/bin/browser-harness).
832
942
  // --force so a refreshed source / changed entry point is reinstalled.
833
943
  const inst = await sh(uv, ["tool", "install", "--force", "-e", HARNESS_DIR], {