@m13v/s4l 1.7.5 → 1.7.6-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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.5",
3
- "installedAt": "2026-07-17T00:02:11.441Z"
2
+ "version": "1.7.6-rc.1",
3
+ "installedAt": "2026-07-17T00:30:46.607Z"
4
4
  }
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.5",
5
+ "version": "1.7.6-rc.1",
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.5",
3
+ "version": "1.7.6-rc.1",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.5",
3
+ "version": "1.7.6-rc.1",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js",
@@ -0,0 +1,58 @@
1
+ import os, re, json, subprocess, glob
2
+ REPO = os.path.expanduser("~/social-autoposter"); os.chdir(REPO)
3
+ all_py = {os.path.basename(p) for p in glob.glob("scripts/*.py")}
4
+
5
+ entry_surfaces = glob.glob("skill/*.sh") + glob.glob("skill/lib/*.sh")
6
+ entry_surfaces += ["SKILL.md", "setup/SKILL.md", "bin/cli.js"]
7
+ entry_surfaces += glob.glob("mcp/dist/*.js") + glob.glob("mcp/*.mjs")
8
+
9
+ ref_re = re.compile(r"scripts/([A-Za-z0-9_]+)\.py")
10
+ def refs_in_text(txt):
11
+ return {m+".py" for m in ref_re.findall(txt) if (m+".py") in all_py}
12
+
13
+ entries, surface_hits = set(), {}
14
+ for s in entry_surfaces:
15
+ if not os.path.exists(s): continue
16
+ txt = open(s, encoding="utf-8", errors="ignore").read()
17
+ # strip // comments for js, # comments for md is harder; keep simple: count all refs but mark cli.js comment lines
18
+ for fn in refs_in_text(txt):
19
+ entries.add(fn); surface_hits.setdefault(fn,set()).add(s)
20
+
21
+ imp_res = [re.compile(r"^\s*import\s+([A-Za-z0-9_]+)", re.M),
22
+ re.compile(r"^\s*from\s+([A-Za-z0-9_]+)\s+import", re.M),
23
+ re.compile(r"^\s*from\s+scripts\s+import\s+([A-Za-z0-9_,\s]+)", re.M),
24
+ re.compile(r"^\s*from\s+scripts\.([A-Za-z0-9_]+)\s+import", re.M),
25
+ re.compile(r"^\s*import\s+scripts\.([A-Za-z0-9_]+)", re.M)]
26
+ def expand(fn):
27
+ path=os.path.join("scripts",fn)
28
+ if not os.path.exists(path): return set()
29
+ txt=open(path,encoding="utf-8",errors="ignore").read()
30
+ found=set()
31
+ for rx in imp_res:
32
+ for g in rx.findall(txt):
33
+ for name in re.split(r"[,\s]+",g):
34
+ name=name.strip()
35
+ if name and (name+".py") in all_py: found.add(name+".py")
36
+ found |= refs_in_text(txt) # NEW: intra-python subprocess scripts/X.py refs
37
+ # follow symlink targets too (update_stats.py -> stats.py)
38
+ if os.path.islink(path):
39
+ tgt=os.path.basename(os.readlink(path))
40
+ if tgt in all_py: found.add(tgt)
41
+ return found
42
+
43
+ closure=set(entries); stack=list(entries)
44
+ while stack:
45
+ for d in expand(stack.pop()):
46
+ if d not in closure: closure.add(d); stack.append(d)
47
+
48
+ out=subprocess.run(["npm","pack","--dry-run","--json"],capture_output=True,text=True)
49
+ shipped=sorted(os.path.basename(f["path"]) for f in json.loads(out.stdout)[0]["files"] if f["path"].startswith("scripts/") and f["path"].endswith(".py"))
50
+ drop=sorted(set(shipped)-closure); keep=sorted(set(shipped)&closure)
51
+ print("entries:",len(entries),"| closure:",len(closure),"| shipped:",len(shipped))
52
+ print(f"\n=== KEEP (shipped & needed): {len(keep)}")
53
+ print(f"=== DROP (shipped but unreferenced anywhere consumer): {len(drop)}")
54
+ for d in drop:
55
+ if d=="_compute_allowlist.py": continue
56
+ print(" -",d)
57
+ open("/tmp/keep.txt","w").write("\n".join(k for k in keep if k!="_compute_allowlist.py"))
58
+ open("/tmp/drop.txt","w").write("\n".join(d for d in drop if d!="_compute_allowlist.py"))
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Scratch driver: read JSON list of {post_id, session, text} from argv[1] and
3
+ run link_edit_helper mark-edited + dm_short_links backfill-post for each.
4
+ Gitignored scratch helper for the reddit link-edit run."""
5
+ import json, subprocess, sys, os
6
+
7
+ HERE = os.path.dirname(os.path.abspath(__file__))
8
+ items = json.load(open(sys.argv[1]))
9
+ for it in items:
10
+ pid = str(it["post_id"]); sess = it["session"]; text = it["text"]
11
+ src = it.get("source", "plain_url_ab_skip")
12
+ r1 = subprocess.run([sys.executable, os.path.join(HERE, "link_edit_helper.py"),
13
+ "mark-edited", "--post-id", pid, "--content", text, "--source", src],
14
+ capture_output=True, text=True)
15
+ r2 = subprocess.run([sys.executable, os.path.join(HERE, "dm_short_links.py"),
16
+ "backfill-post", "--minted-session", sess, "--post-id", pid],
17
+ capture_output=True, text=True)
18
+ bf = (r2.stdout or "").strip().splitlines()[-1:] or [""]
19
+ print(f"post {pid}: mark_edited_rc={r1.returncode} backfill={bf[0]}"
20
+ + (f" ERR1={r1.stderr.strip()}" if r1.returncode else ""))
@@ -0,0 +1,9 @@
1
+ import sys,json
2
+ d=json.load(sys.stdin)
3
+ print("result_count:",d.get("result_count"),"error:",d.get("error"))
4
+ for r in d.get("results",[])[:5]:
5
+ name=r.get("author_name")
6
+ hl=(r.get("author_headline") or "")[:75]
7
+ t=(r.get("post_text") or "")[:170].replace("\n"," ")
8
+ print(" - %s | %s | age=%sh rx=%s c=%s vs=%s" % (name,hl,r.get("age_hours"),r.get("reactions"),r.get("comments"),r.get("velocity_score")))
9
+ print(" "+t)
@@ -0,0 +1,76 @@
1
+ import json, re
2
+
3
+ items = json.load(open("/tmp/li_actionable.json"))
4
+ cids = set(l.strip() for l in open("/tmp/li_cids.txt") if l.strip())
5
+ pairs = set(l.strip() for l in open("/tmp/li_pairs.txt") if l.strip())
6
+ posts = []
7
+ for l in open("/tmp/li_posts.txt"):
8
+ l=l.strip()
9
+ if not l: continue
10
+ pid, _, url = l.partition("|")
11
+ posts.append((pid, url))
12
+
13
+ EXCLUDED = {"louis030195","louis3195"}
14
+ OWN = {"matthew diakonov","m13v"}
15
+
16
+ def parse_urn(urn):
17
+ # urn:li:comment:(NS:PARENT,COMMENT)
18
+ m = re.match(r"urn:li:comment:\((\w+):(\d+),(\d+)\)", urn or "")
19
+ if not m: return (None,None,None)
20
+ return m.group(1), m.group(2), m.group(3)
21
+
22
+ def find_post_id(parent_id):
23
+ for pid, url in posts:
24
+ if parent_id and parent_id in url:
25
+ return pid
26
+ return None
27
+
28
+ def author_engaged(author, parent_id):
29
+ # any engaged pair with same author AND url containing parent_id
30
+ al = author.strip().lower()
31
+ for p in pairs:
32
+ a, _, url = p.partition("|||")
33
+ if a.strip().lower()==al and parent_id and parent_id in url:
34
+ return True
35
+ return False
36
+
37
+ seen_batch = set()
38
+ plan = []
39
+ counts = dict(new=0, already=0, engaged=0, excluded=0, own=0, nourn=0, dup_batch=0)
40
+
41
+ for it in items:
42
+ urn = it.get("comment_urn")
43
+ author = (it.get("author") or "").strip()
44
+ ns, parent_id, comment_id = parse_urn(urn)
45
+ rec = dict(it, ns=ns, parent_id=parent_id, comment_id=comment_id, decision=None, post_id=None)
46
+
47
+ if not urn or not parent_id:
48
+ rec["decision"]="skip:no_comment_urn"; counts["nourn"]+=1; plan.append(rec); continue
49
+ if urn in seen_batch:
50
+ rec["decision"]="skip:dup_in_batch"; counts["dup_batch"]+=1; plan.append(rec); continue
51
+ seen_batch.add(urn)
52
+ if urn in cids:
53
+ rec["decision"]="skip:already_tracked"; counts["already"]+=1; plan.append(rec); continue
54
+ al = author.lower()
55
+ if al in OWN or any(al==e for e in EXCLUDED):
56
+ rec["decision"]="skip:own_or_excluded"
57
+ if al in OWN: counts["own"]+=1
58
+ else: counts["excluded"]+=1
59
+ plan.append(rec); continue
60
+ if author_engaged(author, parent_id):
61
+ rec["decision"]="skip:author_already_engaged"; counts["engaged"]+=1; plan.append(rec); continue
62
+ pid = find_post_id(parent_id)
63
+ rec["post_id"]=pid
64
+ rec["decision"]="insert" if pid else "create_post+insert"
65
+ counts["new"]+=1
66
+ plan.append(rec)
67
+
68
+ json.dump(plan, open("/tmp/li_plan.json","w"), indent=2)
69
+ print("COUNTS:", counts)
70
+ print("TOTAL inspected:", len(items))
71
+ print()
72
+ for r in plan:
73
+ if r["decision"].startswith("skip"): continue
74
+ print(f"[{r['decision']}] {r['author']} | ns={r['ns']} parent={r['parent_id']} post_id={r['post_id']}")
75
+ print(f" urn: {r['comment_urn']}")
76
+ print(f" snip: {r['snippet'][:120]}")
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ import json, re, subprocess, os, sys
3
+
4
+ REPO = os.path.expanduser("~/social-autoposter")
5
+ LID = os.path.join(REPO, "scripts", "li_discovery.py")
6
+
7
+ EXCLUDED_AUTHORS = {"louis030195", "louis3195"}
8
+ OWN = {"matthew diakonov", "m13v"}
9
+
10
+ def run(args):
11
+ r = subprocess.run([sys.executable, LID] + args, capture_output=True, text=True)
12
+ return (r.stdout or "").strip(), (r.stderr or "").strip(), r.returncode
13
+
14
+ # one context dump
15
+ ctx_out, ctx_err, rc = run(["context"])
16
+ ctx = json.loads(ctx_out) if ctx_out else {}
17
+ existing = set(ctx.get("existing_comment_ids") or [])
18
+ engaged_pairs = ctx.get("engaged_pairs") or []
19
+ posts = ctx.get("posts") or []
20
+
21
+ PARENT_RE = re.compile(r"urn:li:(?:activity|ugcPost|share):(\d+)")
22
+
23
+ # build parent_id -> post_id map
24
+ parent_to_post = {}
25
+ for p in posts:
26
+ u = p.get("our_url") or ""
27
+ m = PARENT_RE.search(u)
28
+ if m:
29
+ parent_to_post.setdefault(m.group(1), p["id"])
30
+
31
+ # build engaged set (author_lower, parent_id)
32
+ engaged_set = set()
33
+ for pair in engaged_pairs:
34
+ if "|||" not in pair:
35
+ continue
36
+ author, url = pair.split("|||", 1)
37
+ m = PARENT_RE.search(url)
38
+ if m:
39
+ engaged_set.add((author.strip().lower(), m.group(1)))
40
+
41
+ CU_RE = re.compile(r"\((?:activity|ugcPost|share):(\d+),(\d+)\)")
42
+
43
+ data = json.load(open("/tmp/li_notifs.json"))
44
+
45
+ counts = dict(scanned=len(data), new=0, already=0, engaged=0, excluded=0, own=0, no_urn=0)
46
+ new_items = []
47
+
48
+ def proj_for(snippet):
49
+ s = (snippet or "").lower()
50
+ # our niche is claude code / ai agents -> fazm flagship
51
+ if any(k in s for k in ["claude code", "claude.md", "agent", "context window", "mcp", "subagent", "harness", "codex", "anthropic", "llm", "ai "]):
52
+ return "fazm"
53
+ return "general"
54
+
55
+ for it in data:
56
+ cu = it.get("comment_urn")
57
+ author = (it.get("author") or "").strip()
58
+ if not cu:
59
+ counts["no_urn"] += 1
60
+ continue
61
+ m = CU_RE.search(cu)
62
+ if not m:
63
+ counts["no_urn"] += 1
64
+ continue
65
+ parent_id = m.group(1)
66
+ al = author.lower()
67
+ # exclusion
68
+ if al in OWN or author in ("unknown",):
69
+ counts["own"] += 1
70
+ continue
71
+ if al in EXCLUDED_AUTHORS or any(x in al for x in EXCLUDED_AUTHORS):
72
+ counts["excluded"] += 1
73
+ continue
74
+ if cu in existing:
75
+ counts["already"] += 1
76
+ continue
77
+ if (al, parent_id) in engaged_set:
78
+ counts["engaged"] += 1
79
+ continue
80
+ # find or create post
81
+ post_id = parent_to_post.get(parent_id)
82
+ if not post_id:
83
+ proj = proj_for(it.get("snippet"))
84
+ out, err, rc = run(["create-post", "--activity-id", parent_id, "--project", proj, "--author", author])
85
+ post_id = out.strip().splitlines()[-1] if out.strip() else ""
86
+ if not post_id:
87
+ print(f" [create-post FAILED parent={parent_id}] err={err}", file=sys.stderr)
88
+ continue
89
+ parent_to_post[parent_id] = post_id
90
+ # insert reply
91
+ out, err, rc = run([
92
+ "insert-reply", "--post-id", str(post_id),
93
+ "--comment-urn", cu, "--author", author,
94
+ "--content", (it.get("snippet") or "")[:3000],
95
+ "--href", it.get("href") or "",
96
+ ])
97
+ res = out.strip().splitlines()[-1] if out.strip() else ""
98
+ if res == "duplicate":
99
+ counts["already"] += 1
100
+ elif res.startswith("gated"):
101
+ counts["engaged"] += 1 # gated by blocklist/velocity, not actionable
102
+ print(f" [gated] {author} parent={parent_id} -> {res}", file=sys.stderr)
103
+ elif res:
104
+ counts["new"] += 1
105
+ new_items.append((res, author, parent_id, cu))
106
+ # mark as existing to dedup within this run
107
+ existing.add(cu)
108
+ engaged_set.add((al, parent_id))
109
+ else:
110
+ print(f" [insert FAILED] {author} parent={parent_id} err={err}", file=sys.stderr)
111
+
112
+ print("\n=== NEW REPLIES INSERTED ===")
113
+ for rid, author, parent, cu in new_items:
114
+ print(f" reply_id={rid} author={author} parent={parent}")
115
+
116
+ print("\n=== SUMMARY ===")
117
+ print(f"New replies discovered: {counts['new']}")
118
+ print(f"Already tracked: {counts['already']}")
119
+ print(f"Author already engaged thread: {counts['engaged']}")
120
+ print(f"Excluded: {counts['excluded']}")
121
+ print(f"Own account: {counts['own']}")
122
+ print(f"No comment URN: {counts['no_urn']}")
123
+ print(f"Total scanned: {counts['scanned']}")
124
+
125
+ excl_total = counts["excluded"] + counts["own"]
126
+ print(f"\nLINKEDIN_SCAN_SUMMARY: scanned={counts['scanned']} new={counts['new']} already={counts['already']} excluded={excl_total} unmatched={counts['no_urn']}")
@@ -0,0 +1,91 @@
1
+ import json, re, urllib.parse
2
+
3
+ NOTIFS = "/tmp/li_notifs.json"
4
+ CID = "/Users/matthewdi/.claude/projects/-/f0a4ef0d-11f2-4618-85e2-bf62ad39cea2/tool-results/brzcccx3h.txt"
5
+ PAIRS = "/Users/matthewdi/.claude/projects/-/f0a4ef0d-11f2-4618-85e2-bf62ad39cea2/tool-results/bu0y0bphg.txt"
6
+ POSTS = "/Users/matthewdi/.claude/projects/-/f0a4ef0d-11f2-4618-85e2-bf62ad39cea2/tool-results/bnb5ginhc.txt"
7
+
8
+ def load_lines(p):
9
+ out=[]
10
+ for ln in open(p, encoding="utf-8", errors="replace"):
11
+ ln=ln.rstrip("\n")
12
+ # skip persisted-output wrapper lines
13
+ if not ln or ln.startswith("<") or ln.startswith("Output too large") or ln.startswith("Preview") or ln.startswith("..."):
14
+ continue
15
+ out.append(ln)
16
+ return out
17
+
18
+ comment_ids = set(load_lines(CID))
19
+ pairs = set(load_lines(PAIRS))
20
+ posts = load_lines(POSTS)
21
+
22
+ # activity number -> post id
23
+ post_by_act = {}
24
+ for ln in posts:
25
+ if "|" not in ln: continue
26
+ pid, url = ln.split("|",1)
27
+ for m in re.findall(r"urn:li:(?:activity|share|ugcPost):(\d+)", url):
28
+ post_by_act[m]=pid
29
+
30
+ # config projects search_topics
31
+ cfg = json.load(open("/Users/matthewdi/social-autoposter/config.json"))
32
+ projtopics = []
33
+ for pr in cfg.get("projects",[]):
34
+ projtopics.append((pr["name"], [t.lower() for t in pr.get("search_topics",[])]))
35
+
36
+ EXCLUDED = {"louis030195","louis3195"}
37
+ OWN = {"matthew diakonov","m13v"}
38
+
39
+ def match_project(snippet):
40
+ s = snippet.lower()
41
+ best=None; bestscore=0
42
+ for name, topics in projtopics:
43
+ score=0
44
+ for t in topics:
45
+ if t and t in s:
46
+ score+=1
47
+ if score>bestscore:
48
+ bestscore=score; best=name
49
+ return best or "S4L"
50
+
51
+ data = json.load(open(NOTIFS))
52
+ decisions=[]
53
+ for d in data:
54
+ href = d["href"]
55
+ dec = urllib.parse.unquote(href)
56
+ m = re.search(r"urn:li:activity:(\d+)", dec)
57
+ activity_id = m.group(1) if m else None
58
+ comment_urn = d["comment_urn"]
59
+ author = d["author"]
60
+ a_low = author.lower().strip()
61
+ rec = dict(author=author, activity_id=activity_id, comment_urn=comment_urn,
62
+ snippet=d["snippet"], href=href, type=d["type"])
63
+ if not comment_urn or not activity_id:
64
+ rec["decision"]="no_comment_urn"; decisions.append(rec); continue
65
+ if a_low in OWN:
66
+ rec["decision"]="own_account"; decisions.append(rec); continue
67
+ if a_low in EXCLUDED or any(x in a_low for x in EXCLUDED):
68
+ rec["decision"]="excluded_author"; decisions.append(rec); continue
69
+ if comment_urn in comment_ids:
70
+ rec["decision"]="already_tracked"; decisions.append(rec); continue
71
+ our_url = "https://www.linkedin.com/feed/update/urn:li:activity:%s/" % activity_id
72
+ apk = author + "|||" + our_url
73
+ if apk in pairs:
74
+ rec["decision"]="author_already_engaged"; decisions.append(rec); continue
75
+ # match post
76
+ pid = post_by_act.get(activity_id)
77
+ rec["post_id"]=pid
78
+ rec["project"]=match_project(d["snippet"])
79
+ rec["decision"]="INSERT"
80
+ decisions.append(rec)
81
+
82
+ json.dump(decisions, open("/tmp/li_decisions.json","w"))
83
+ # summary
84
+ from collections import Counter
85
+ c=Counter(r["decision"] for r in decisions)
86
+ print("TOTAL", len(decisions))
87
+ for k,v in c.items(): print(k,v)
88
+ print("---INSERTS---")
89
+ for i,r in enumerate(decisions):
90
+ if r["decision"]=="INSERT":
91
+ print(i, r["author"], "| act", r["activity_id"], "| post", r.get("post_id"), "| proj", r["project"])
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python3
2
+ """Throwaway helper: run dm_conversation.py set-icp-precheck for every project.
3
+
4
+ Usage:
5
+ python3 _run_icp_precheck.py --dm-id 4583 --default-label icp_miss \
6
+ --default-notes "crypto researcher, not target vertical" \
7
+ --override 'fazm=icp_miss=engaged on agentic token cost but no claude-code-wrapper signal' \
8
+ --override 'Agora=icp_miss=crypto researcher not a protocol governance buyer'
9
+
10
+ Any project NOT overridden gets the default label/notes.
11
+ """
12
+ import argparse, subprocess, sys
13
+
14
+ PROJECTS = [
15
+ "fazm", "Terminator", "macOS MCP", "Vipassana", "S4L", "AI Browser Profile",
16
+ "WhatsApp MCP", "macOS Session Replay", "Cyrano", "Assrt", "PieLine", "Clone",
17
+ "mk0r", "fde10x", "claude-meter", "c0nsl", "tenxats", "paperback-expert",
18
+ "studyly", "Mediar", "NightOwl", "Runner", "Agora", "Podlog", "ccmd",
19
+ ]
20
+ SCRIPT = "/Users/matthewdi/social-autoposter/scripts/dm_conversation.py"
21
+
22
+ def main():
23
+ ap = argparse.ArgumentParser()
24
+ ap.add_argument("--dm-id", required=True)
25
+ ap.add_argument("--default-label", default="icp_miss")
26
+ ap.add_argument("--default-notes", default="not target vertical")
27
+ ap.add_argument("--override", action="append", default=[],
28
+ help="PROJECT=LABEL=NOTES")
29
+ args = ap.parse_args()
30
+
31
+ overrides = {}
32
+ for o in args.override:
33
+ parts = o.split("=", 2)
34
+ if len(parts) != 3:
35
+ print("bad override:", o); sys.exit(2)
36
+ overrides[parts[0]] = (parts[1], parts[2])
37
+
38
+ # validate override keys
39
+ for k in overrides:
40
+ if k not in PROJECTS:
41
+ print("UNKNOWN project in override:", k); sys.exit(2)
42
+
43
+ fails = 0
44
+ for p in PROJECTS:
45
+ label, notes = overrides.get(p, (args.default_label, args.default_notes))
46
+ r = subprocess.run(
47
+ ["python3", SCRIPT, "set-icp-precheck", "--dm-id", args.dm_id,
48
+ "--project", p, "--label", label, "--notes", notes],
49
+ capture_output=True, text=True)
50
+ tag = "ok" if r.returncode == 0 else "FAIL"
51
+ if r.returncode != 0:
52
+ fails += 1
53
+ print(f"{tag} {p}={label}", (r.stderr or r.stdout or "").strip()[:120])
54
+ print(f"DONE dm={args.dm_id} fails={fails}")
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -125,15 +125,20 @@ TYPE_TO_WORKER_NOTES = {
125
125
  ),
126
126
  "reddit-draft": (
127
127
  "WORKER EXECUTION NOTES (queue metadata; follow while executing the "
128
- "prompt below): every Reddit thread's content is already inlined in "
129
- "the prompt do NOT fetch anything, do NOT run searches, no tools "
130
- "are needed. Apply the prompt's SELECTION GATE to each candidate, "
131
- "draft comments for the ones that pass, and submit ONE result object "
132
- "matching the schema: {\"posts\": [...], \"rejects\": [...]}. A "
133
- "candidate that fails the gate is simply absent from posts (add a "
134
- "rejects entry only for the structural false-positive cases the "
135
- "prompt describes). Batches are small; draft them in one pass and "
136
- "submit promptly."
128
+ "prompt below): this unattended session is terminated ~90 seconds "
129
+ "after your LAST tool call. Every Reddit thread's content is already "
130
+ "inlined in the prompt never fetch or open a reddit.com URL; "
131
+ "WebSearch/WebFetch are for EXTERNAL fact-checking only, per the "
132
+ "prompt's THREAD CONTENT rules. Apply the prompt's SELECTION GATE to "
133
+ "each candidate and work ONE thread at a time: draft both texts, "
134
+ "then IMMEDIATELY run that thread's log_draft.py persist command "
135
+ "exactly as the prompt's PERSIST step specifies (a quick Bash call), "
136
+ "THEN move to the next. Those per-thread Bash calls keep the session "
137
+ "alive. Only after EVERY candidate is handled do you assemble and "
138
+ "submit ONE result object matching the schema: {\"posts\": [...], "
139
+ "\"rejects\": [...]}. A candidate that fails the gate is simply "
140
+ "absent from posts (add a rejects entry only for the structural "
141
+ "false-positive cases the prompt describes)."
137
142
  ),
138
143
  }
139
144