@m13v/s4l 1.7.6-rc.34 → 1.7.6-rc.35
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/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/draft_prompt_core.py +8 -4
- package/scripts/link_tail.py +1 -1
- package/scripts/merge_review_queue.py +2 -2
- package/scripts/post_reddit.py +64 -7
- package/scripts/twitter_gen_links.py +3 -3
- package/scripts/twitter_post_plan.py +4 -4
- package/setup/SKILL.md +1 -1
- package/skill/run-reddit-search.sh +10 -1
- package/skill/run-twitter-cycle.sh +1 -1
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.7.6-rc.
|
|
5
|
+
"version": "1.7.6-rc.35",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts.",
|
|
7
7
|
"long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
|
|
8
8
|
"author": {
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.6-rc.
|
|
3
|
+
"version": "1.7.6-rc.35",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -688,10 +688,14 @@ def render_reddit_prompt(ing):
|
|
|
688
688
|
recent_self_block, arm (optional), lane (optional, promotion assumed).
|
|
689
689
|
"""
|
|
690
690
|
arm = ing.get("arm") or os.environ.get("S4L_DRAFT_PROMPT_VARIANT") or ""
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
#
|
|
694
|
-
#
|
|
691
|
+
# Lane comes ONLY from the ingredients (post_reddit.py stamps it onto the
|
|
692
|
+
# discover plan at the cycle's assignment point and threads it through
|
|
693
|
+
# build_draft_prompt). Deliberately NOT read from S4L_ACTIVE_LANE here:
|
|
694
|
+
# salvage plans drafted in a later cycle must keep their own (promotion)
|
|
695
|
+
# directive even when THIS cycle's coin flip landed personal_brand.
|
|
696
|
+
# lane='personal_brand' renders the persona directive + the persona-
|
|
697
|
+
# whitelisted projects_json; the RD template carries every block the
|
|
698
|
+
# persona directive references (@CORPUS_BLOCK@ = ACCOUNT VOICE CORPUS).
|
|
695
699
|
lane = ing.get("lane") or ""
|
|
696
700
|
|
|
697
701
|
from engagement_styles import get_content_rules, get_voice_relationship_rule
|
package/scripts/link_tail.py
CHANGED
|
@@ -557,7 +557,7 @@ def main() -> int:
|
|
|
557
557
|
print(json.dumps(out), flush=True)
|
|
558
558
|
return 0
|
|
559
559
|
|
|
560
|
-
# Plugin (MCP
|
|
560
|
+
# Plugin (MCP approve_drafts) flow sets S4L_SKIP_LINK_TAIL=1. The bridge only
|
|
561
561
|
# rewords prose around the URL — the minted short link is produced by a
|
|
562
562
|
# separate deterministic wrap step in twitter_post_plan.py — so the Claude
|
|
563
563
|
# call buys nothing there, and on .mcpb customer boxes (no `claude` binary)
|
|
@@ -235,7 +235,7 @@ def _sync_with_backend(cands: list) -> tuple[int, int]:
|
|
|
235
235
|
skill/run-twitter-cycle.sh) and mark it terminal here too, same as a
|
|
236
236
|
human "discard all pending" would. Without this, a card can sit in
|
|
237
237
|
the review queue as an approvable draft long after the backend has
|
|
238
|
-
moved on; approving it later silently no-ops (
|
|
238
|
+
moved on; approving it later silently no-ops (approve_drafts returns
|
|
239
239
|
posted:0, no browser ever launches, no post-*.log — see the
|
|
240
240
|
2026-07-09 "approved 3 cards, nothing posted" investigation).
|
|
241
241
|
|
|
@@ -246,7 +246,7 @@ def _sync_with_backend(cands: list) -> tuple[int, int]:
|
|
|
246
246
|
|
|
247
247
|
APPROVED cards are exempt from the prune: an approval is a settled human
|
|
248
248
|
decision awaiting its serialized post, and stamping terminal on it makes
|
|
249
|
-
|
|
249
|
+
approve_drafts refuse it as already-decided (posted:0). That exact race ate
|
|
250
250
|
2 of 3 approvals on 2026-07-10 (approved 04:30:02Z, merge stamped
|
|
251
251
|
backend_status_expired 04:32:33Z, poster refused both). The freshness
|
|
252
252
|
gate exists to stop stale UNDECIDED drafts from burning review attention;
|
package/scripts/post_reddit.py
CHANGED
|
@@ -442,8 +442,21 @@ def _db_pick_salvage_candidates(batch_id, limit=1):
|
|
|
442
442
|
data = (resp or {}).get("data") or {}
|
|
443
443
|
if not data.get("decisions"):
|
|
444
444
|
return None
|
|
445
|
+
# Lane for salvage rows is derived from CONFIG (persona:true on the
|
|
446
|
+
# row's project), not from this cycle's env: a persona row that
|
|
447
|
+
# failed transiently must re-draft under the persona directive even
|
|
448
|
+
# when the retry cycle's coin flip landed promotion, and vice versa.
|
|
449
|
+
_sv_proj = (data.get("project_name") or "").lower()
|
|
450
|
+
try:
|
|
451
|
+
_sv_lane = "personal_brand" if any(
|
|
452
|
+
p.get("persona") is True and (p.get("name") or "").lower() == _sv_proj
|
|
453
|
+
for p in (load_config().get("projects") or [])
|
|
454
|
+
) else "promotion"
|
|
455
|
+
except Exception:
|
|
456
|
+
_sv_lane = "promotion"
|
|
445
457
|
return {
|
|
446
458
|
"project_name": data.get("project_name") or "general",
|
|
459
|
+
"lane": _sv_lane,
|
|
447
460
|
# batch_id must ride the plan file into --phase draft: the
|
|
448
461
|
# snapshot filename, the prompt's top_performers --invoked-by,
|
|
449
462
|
# and the proposed_excludes 2-distinct-batch activation gate all
|
|
@@ -1204,7 +1217,7 @@ def _prefetch_thread_digests(candidates, reddit_username=None):
|
|
|
1204
1217
|
|
|
1205
1218
|
def build_draft_prompt(project, config, candidates, top_report, recent_comments,
|
|
1206
1219
|
style_assignment_a=None, style_assignment_b=None, digests=None,
|
|
1207
|
-
batch_id=None, recent_self=""):
|
|
1220
|
+
batch_id=None, recent_self="", lane=""):
|
|
1208
1221
|
"""DRAFT phase: assemble reddit ingredients, render via draft_prompt_core.
|
|
1209
1222
|
|
|
1210
1223
|
The prompt TEXT lives in scripts/draft_prompt_core.py, the single source
|
|
@@ -1275,6 +1288,11 @@ def build_draft_prompt(project, config, candidates, top_report, recent_comments,
|
|
|
1275
1288
|
"style_a_name": (style_assignment_a or {}).get("style") or "style_name",
|
|
1276
1289
|
"style_b_name": (style_assignment_b or {}).get("style") or "style_name",
|
|
1277
1290
|
"recent_self_block": recent_self or "",
|
|
1291
|
+
# personal_brand renders the persona directive + persona-whitelisted
|
|
1292
|
+
# project JSON in draft_prompt_core; comes from the plan stamp (see
|
|
1293
|
+
# _discover_iteration), NEVER from env; salvage plans predate this
|
|
1294
|
+
# cycle's lane and must keep the promotion directive.
|
|
1295
|
+
"lane": lane or "",
|
|
1278
1296
|
}
|
|
1279
1297
|
return _dpc.render_reddit_prompt(ing)
|
|
1280
1298
|
|
|
@@ -1992,15 +2010,24 @@ def _discover_iteration(args, config, reddit_username, already_picked):
|
|
|
1992
2010
|
builds the query bank and runs reddit_tools.cmd_search directly; no Claude
|
|
1993
2011
|
session). Uses `decisions` key for downstream-phase compatibility.
|
|
1994
2012
|
"""
|
|
1995
|
-
|
|
2013
|
+
# Force-inject (2026-07-17): --project (manual MCP runs) or S4L_FORCE_PROJECT
|
|
2014
|
+
# (the personal_brand lane's env, set by s4l_mode.py via the wrapper's eval)
|
|
2015
|
+
# bypasses pick_project entirely. This is the ONLY way a persona:true entry
|
|
2016
|
+
# can be selected; pick_project structurally excludes personas. Mirrors
|
|
2017
|
+
# run-twitter-cycle.sh's S4L_FORCE_PROJECT handling.
|
|
2018
|
+
force_project = args.project or (os.environ.get("S4L_FORCE_PROJECT") or "").strip()
|
|
2019
|
+
if force_project:
|
|
1996
2020
|
project = None
|
|
1997
2021
|
for p in config.get("projects", []):
|
|
1998
|
-
if p["name"].lower() ==
|
|
2022
|
+
if p["name"].lower() == force_project.lower():
|
|
1999
2023
|
project = p
|
|
2000
2024
|
break
|
|
2001
2025
|
if not project:
|
|
2002
|
-
print(f"[post_reddit] ERROR: project '{
|
|
2026
|
+
print(f"[post_reddit] ERROR: project '{force_project}' not found")
|
|
2003
2027
|
return None
|
|
2028
|
+
if not args.project:
|
|
2029
|
+
print(f"[post_reddit] force-inject: S4L_FORCE_PROJECT={force_project} "
|
|
2030
|
+
f"(lane={os.environ.get('S4L_ACTIVE_LANE') or 'promotion'})")
|
|
2004
2031
|
else:
|
|
2005
2032
|
project = pick_project("reddit", exclude=already_picked)
|
|
2006
2033
|
if not project:
|
|
@@ -2259,8 +2286,20 @@ def _discover_iteration(args, config, reddit_username, already_picked):
|
|
|
2259
2286
|
# batch_id rides the plan file into --phase draft (snapshot filename,
|
|
2260
2287
|
# top_performers --invoked-by, proposed_excludes activation gate). Same
|
|
2261
2288
|
# reason as the salvage plan's batch_id above.
|
|
2289
|
+
#
|
|
2290
|
+
# lane (2026-07-17): stamped ONCE here at plan-write time and read from
|
|
2291
|
+
# the PLAN by every downstream phase (_draft_iteration →
|
|
2292
|
+
# render_reddit_prompt), never from env; salvage plans from earlier
|
|
2293
|
+
# cycles and the env-less MCP approval poster must not inherit this
|
|
2294
|
+
# cycle's lane. Same stamp-at-source convention as active_experiments
|
|
2295
|
+
# (see CLAUDE.md). Derived from CONFIG (persona:true), not from
|
|
2296
|
+
# S4L_ACTIVE_LANE, so an env-less manual `--project <persona>` run also
|
|
2297
|
+
# drafts under the persona directive; the personal_brand lane can only
|
|
2298
|
+
# ever force-inject the persona project, so the two always agree.
|
|
2262
2299
|
return {"project_name": project_name, "decisions": selected,
|
|
2263
2300
|
"batch_id": queue_batch,
|
|
2301
|
+
"lane": ("personal_brand" if project.get("persona") is True
|
|
2302
|
+
else "promotion"),
|
|
2264
2303
|
"cost": 0.0, "session_id": None,
|
|
2265
2304
|
"phase": "discover"}
|
|
2266
2305
|
|
|
@@ -2434,7 +2473,8 @@ def _draft_iteration(plan, config, reddit_username):
|
|
|
2434
2473
|
style_assignment_a=style_assignment_a,
|
|
2435
2474
|
style_assignment_b=style_assignment_b, digests=digests,
|
|
2436
2475
|
batch_id=plan.get("batch_id"),
|
|
2437
|
-
recent_self=_recent_self
|
|
2476
|
+
recent_self=_recent_self,
|
|
2477
|
+
lane=plan.get("lane") or "")
|
|
2438
2478
|
# Durable full-prompt record per batch (same prep-prompts dir the X
|
|
2439
2479
|
# cycle writes; reddit-draft-prompt-<batch>.md, newest 50 kept), so
|
|
2440
2480
|
# prompt-block presence is verifiable after any release.
|
|
@@ -2580,7 +2620,7 @@ def _draft_iteration(plan, config, reddit_username):
|
|
|
2580
2620
|
merged_d["thread_title"] = drafted_d.get("thread_title") or c.get("thread_title")
|
|
2581
2621
|
merged_d["engagement_style"] = _style_a or c.get("engagement_style")
|
|
2582
2622
|
# Top-level assigned_style/assigned_mode default to Draft A (the
|
|
2583
|
-
# legacy-mirror draft); the MCP
|
|
2623
|
+
# legacy-mirror draft); the MCP approve_drafts edit path overwrites
|
|
2584
2624
|
# these on the card when a human switches to Draft B (mirrors
|
|
2585
2625
|
# twitter_post_plan.py's per-candidate override, see
|
|
2586
2626
|
# _post_iteration below and index.ts's reddit approval branch).
|
|
@@ -2654,6 +2694,23 @@ def _post_iteration(plan, reddit_username):
|
|
|
2654
2694
|
os.environ["CLAUDE_SESSION_ID"] = plan_session_id
|
|
2655
2695
|
|
|
2656
2696
|
active_campaigns = load_active_reddit_campaigns()
|
|
2697
|
+
# Persona posts never carry a product campaign suffix (2026-07-17): the
|
|
2698
|
+
# reddit analog of the X lane's TWITTER_TAIL_LINK_RATE=0. Resolved from
|
|
2699
|
+
# config (persona:true on the plan's project), NOT from lane env; the
|
|
2700
|
+
# MCP approval poster runs env-less, and config is the durable truth.
|
|
2701
|
+
if active_campaigns:
|
|
2702
|
+
_plan_proj = (plan.get("project_name") or "").lower()
|
|
2703
|
+
try:
|
|
2704
|
+
_is_persona = any(
|
|
2705
|
+
p.get("persona") is True and (p.get("name") or "").lower() == _plan_proj
|
|
2706
|
+
for p in (load_config().get("projects") or [])
|
|
2707
|
+
)
|
|
2708
|
+
except Exception:
|
|
2709
|
+
_is_persona = False
|
|
2710
|
+
if _is_persona:
|
|
2711
|
+
print(f"[post_reddit] persona project {_plan_proj!r}: campaign "
|
|
2712
|
+
f"suffixes disabled for this plan")
|
|
2713
|
+
active_campaigns = []
|
|
2657
2714
|
if active_campaigns:
|
|
2658
2715
|
for c in active_campaigns:
|
|
2659
2716
|
print(f"[post_reddit] active campaign id={c['id']} "
|
|
@@ -2681,7 +2738,7 @@ def _post_iteration(plan, reddit_username):
|
|
|
2681
2738
|
# Two-draft cards (2026-07-15): each decision may carry its OWN
|
|
2682
2739
|
# (assigned_style, assigned_mode) reflecting whichever draft is
|
|
2683
2740
|
# actually posting — either Draft A (stamped at plan-write time) or
|
|
2684
|
-
# Draft B (stamped by the MCP
|
|
2741
|
+
# Draft B (stamped by the MCP approve_drafts edit path when a human
|
|
2685
2742
|
# switched drafts on the review card). Without this, every card
|
|
2686
2743
|
# would coerce back to the single batch-wide Draft A assignment even
|
|
2687
2744
|
# when it posted under Draft B's style, silently corrupting the
|
|
@@ -148,7 +148,7 @@ def _tail_link_rate() -> float:
|
|
|
148
148
|
# that calls this script). Force rate=1.0 there so a hand-approved draft
|
|
149
149
|
# never drops the link the user already saw baked into the card text —
|
|
150
150
|
# ported from the old post-time override in mcp/src/index.ts
|
|
151
|
-
# (`TWITTER_TAIL_LINK_RATE: "1.0"` on the
|
|
151
|
+
# (`TWITTER_TAIL_LINK_RATE: "1.0"` on the approve_drafts path), which no
|
|
152
152
|
# longer fires now that the decision happens here, before the card is
|
|
153
153
|
# ever shown. The autonomous DRAFT_ONLY=0 lane keeps running the real A/B
|
|
154
154
|
# experiment at the configured rate.
|
|
@@ -169,7 +169,7 @@ def apply_tail_link(candidate: dict, link_url: str) -> None:
|
|
|
169
169
|
gate (both the review-card path and the autonomous-post path pass through
|
|
170
170
|
twitter_gen_links.py first) and already budgets long waits (SEO page-gen
|
|
171
171
|
alone can take 10-40 min). twitter_post_plan.py's post-time call was a bad
|
|
172
|
-
fit for the queue-backed pipeline:
|
|
172
|
+
fit for the queue-backed pipeline: approve_drafts is a synchronous MCP call
|
|
173
173
|
the user is actively waiting on after clicking Approve, while the
|
|
174
174
|
s4l-worker scheduled task claims one job per minute and doesn't overlap a
|
|
175
175
|
multi-minute drafting turn — so a queue-routed call there could stall an
|
|
@@ -313,7 +313,7 @@ def resolve_link(candidate: dict, projects: dict, page_gen_rate: float) -> tuple
|
|
|
313
313
|
# `website`/`url` a persona project happens to carry (some installs got the
|
|
314
314
|
# user's own X profile written there) must NEVER become a tail link. Enforce
|
|
315
315
|
# it here at the single source so no downstream surface (review card, manual
|
|
316
|
-
#
|
|
316
|
+
# approve_drafts) has to strip a link that should never have been generated.
|
|
317
317
|
if proj.get("persona"):
|
|
318
318
|
return ("", "persona_no_link")
|
|
319
319
|
plain_url = proj.get("website") or proj.get("url") or ""
|
|
@@ -80,7 +80,7 @@ def print(*args, **kwargs): # noqa: A001 -- deliberate builtins.print shadow
|
|
|
80
80
|
_neuter_stream(sys.stderr)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
# Graceful SIGTERM: the MCP
|
|
83
|
+
# Graceful SIGTERM: the MCP approve_drafts timeout (and anything else that asks
|
|
84
84
|
# nicely before SIGKILL) sends SIGTERM. Dying instantly reopens the same
|
|
85
85
|
# posted-but-unrecorded window as the broken pipe, so instead flag the loop to
|
|
86
86
|
# stop at the next candidate boundary: current candidate finishes its
|
|
@@ -617,7 +617,7 @@ def post_one(c: dict, picker_assignment: dict | None = None) -> tuple[str, str]:
|
|
|
617
617
|
# BEFORE the DRAFT_ONLY gate for every candidate, so both the review-card
|
|
618
618
|
# path and the autonomous-post path already carry a stamped
|
|
619
619
|
# tail_link_variant + finalized reply_text by the time they reach here.
|
|
620
|
-
# Post time is a bad fit for the queue-backed Claude call:
|
|
620
|
+
# Post time is a bad fit for the queue-backed Claude call: approve_drafts is
|
|
621
621
|
# a synchronous MCP call the user is actively waiting on after clicking
|
|
622
622
|
# Approve, while the s4l-worker scheduled task claims one job per minute
|
|
623
623
|
# and doesn't overlap a multi-minute drafting turn, so a queue-routed call
|
|
@@ -1127,7 +1127,7 @@ def main() -> int:
|
|
|
1127
1127
|
|
|
1128
1128
|
posted = skipped = failed = 0
|
|
1129
1129
|
# Per-candidate outcome rows for the summary JSON. The MCP layer
|
|
1130
|
-
# (mcp/src/index.ts
|
|
1130
|
+
# (mcp/src/index.ts approve_drafts) prefers summary.candidate_results over
|
|
1131
1131
|
# regex-parsing our stdout; this is the authoritative channel that marks
|
|
1132
1132
|
# review-queue cards posted/terminal so a card is never re-fed to a poster
|
|
1133
1133
|
# after its reply already landed (2026-07-06 duplicate-retry incident).
|
|
@@ -1262,7 +1262,7 @@ def main() -> int:
|
|
|
1262
1262
|
# (assigned_style, assigned_mode) reflecting whichever draft is
|
|
1263
1263
|
# actually posting, either the recommended one (stamped at
|
|
1264
1264
|
# plan-write time) or the other one (stamped by the MCP
|
|
1265
|
-
#
|
|
1265
|
+
# approve_drafts edits path when a human switched drafts on the
|
|
1266
1266
|
# review card). Without this, every card would coerce back to
|
|
1267
1267
|
# ONE cycle-wide style even when it posted under the other
|
|
1268
1268
|
# style, silently corrupting the engagement_style label the
|
package/setup/SKILL.md
CHANGED
|
@@ -71,7 +71,7 @@ Do not report setup complete until all of these are true:
|
|
|
71
71
|
## Choose the path
|
|
72
72
|
|
|
73
73
|
- If the social-autoposter MCP tools are connected (`project_config`, `runtime`,
|
|
74
|
-
`queue_setup`, `
|
|
74
|
+
`queue_setup`, `approve_drafts`, `get_stats`, `dashboard`), use the MCP path.
|
|
75
75
|
Do not hand-edit `config.json`.
|
|
76
76
|
- If only the CLI/skill is installed, use the CLI fallback.
|
|
77
77
|
|
|
@@ -210,12 +210,21 @@ trap '_sa_emit_run_summary_oneshot; _sa_release_lease_oneshot; _sa_release_locks
|
|
|
210
210
|
BATCH_ID="rdcycle-$(date +%Y%m%d-%H%M%S)"
|
|
211
211
|
log "Cycle batch_id=$BATCH_ID"
|
|
212
212
|
|
|
213
|
+
# --- Lane env (2026-07-17, mirrors run-draft-and-publish.sh) ----------------
|
|
214
|
+
# personal_brand lane force-injects the persona project into the discover
|
|
215
|
+
# phase (S4L_FORCE_PROJECT + S4L_ACTIVE_LANE=personal_brand); promotion-only
|
|
216
|
+
# exports S4L_CYCLE_LANE=promotion and nothing else (normal weighted pick).
|
|
217
|
+
# Both lanes on = weighted per-cycle coin flip inside s4l_mode.py, same as X.
|
|
218
|
+
# Discover STAMPS the lane onto the plan JSON (post_reddit.py), so the draft
|
|
219
|
+
# phase and the env-less MCP approval poster read the plan, never this env.
|
|
220
|
+
eval "$(python3 "$REPO_DIR/scripts/s4l_mode.py" env 2>/dev/null || true)"
|
|
221
|
+
|
|
213
222
|
# --- Draft-only mode (2026-07-14, mirrors run-draft-and-publish.sh) ---------
|
|
214
223
|
# The ONE mode flag (scripts/s4l_mode.py draft-only, stdout 1/0) now governs
|
|
215
224
|
# Reddit exactly like the X cycle: when ON, both lanes stop after the draft
|
|
216
225
|
# phase and merge their drafted decisions into the menu-bar review cards
|
|
217
226
|
# (merge_review_queue.py --reddit-plan); posting then happens one approved
|
|
218
|
-
# card at a time via the plugin's
|
|
227
|
+
# card at a time via the plugin's approve_drafts tool, which reconstructs a
|
|
219
228
|
# one-decision plan and reuses `post_reddit.py --phase post` unchanged. When
|
|
220
229
|
# OFF (operator opt-out), the lanes post autonomously below as always.
|
|
221
230
|
# Fail-safe default is 1 (draft-only) — same posture as the X wrapper.
|
|
@@ -1741,7 +1741,7 @@ fi
|
|
|
1741
1741
|
# scan_candidates tool reads this so a Claude Desktop scheduled-task session can do
|
|
1742
1742
|
# the drafting ITSELF (on the user's plan, no `claude -p`) and hand the drafts back
|
|
1743
1743
|
# via submit_drafts. Candidates stay 'pending' (drafted+posted via submit_drafts ->
|
|
1744
|
-
#
|
|
1744
|
+
# approve_drafts, or salvaged by a later cycle). The browser lock was already released
|
|
1745
1745
|
# at the Phase 1 handoff, so this exits clean via the EXIT trap. NO current caller
|
|
1746
1746
|
# sets SCAN_ONLY, so the autopilot/draft_cycle paths are byte-for-byte unchanged.
|
|
1747
1747
|
if [ "${SCAN_ONLY:-0}" = "1" ]; then
|