@m13v/s4l 1.7.4-rc.30 → 1.7.4-rc.32

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 (50) hide show
  1. package/mcp/dist/index.js +4 -2
  2. package/mcp/dist/runtime.js +16 -3
  3. package/mcp/dist/version.json +2 -2
  4. package/mcp/manifest.json +1 -1
  5. package/mcp/menubar/s4l_menubar.py +33 -31
  6. package/mcp/package.json +1 -1
  7. package/package.json +1 -1
  8. package/scripts/autopilot_stall_watch.py +6 -9
  9. package/scripts/patches/browser-harness-loopback-cdp-proxy.patch +20 -28
  10. package/scripts/schedule_state.py +23 -0
  11. package/skill/amplitude-24h-signups.sh +1 -1
  12. package/skill/audit-dm-staleness.sh +1 -1
  13. package/skill/audit-linkedin.sh +1 -1
  14. package/skill/audit-reddit-resurrect.sh +1 -1
  15. package/skill/audit.sh +5 -5
  16. package/skill/check-web-chats.sh +1 -1
  17. package/skill/dm-outreach-linkedin.sh +4 -4
  18. package/skill/dm-outreach-reddit.sh +1 -1
  19. package/skill/dm-outreach-twitter.sh +1 -1
  20. package/skill/engage-dm-replies.sh +2 -2
  21. package/skill/engage-linkedin.sh +2 -2
  22. package/skill/engage-moltbook.sh +1 -1
  23. package/skill/engage-twitter.sh +4 -4
  24. package/skill/github-engage.sh +1 -1
  25. package/skill/lib/linkedin-backend.sh +12 -12
  26. package/skill/lib/reddit-backend.sh +8 -8
  27. package/skill/lib/twitter-backend.sh +12 -12
  28. package/skill/link-edit-github.sh +1 -1
  29. package/skill/link-edit-moltbook.sh +1 -1
  30. package/skill/link-edit-reddit.sh +1 -1
  31. package/skill/linkedin-presence.sh +1 -1
  32. package/skill/lock.sh +6 -6
  33. package/skill/precompute-stats.sh +1 -1
  34. package/skill/refresh-instagram-tokens.sh +1 -1
  35. package/skill/refresh-twitter-following.sh +1 -1
  36. package/skill/run-generate-daily-style.sh +1 -1
  37. package/skill/run-instagram-daily.sh +1 -1
  38. package/skill/run-instagram-render.sh +1 -1
  39. package/skill/run-linkedin-launchd.sh +1 -1
  40. package/skill/run-linkedin-unipile.sh +1 -1
  41. package/skill/run-linkedin.sh +1 -1
  42. package/skill/run-reddit-search.sh +2 -2
  43. package/skill/run-reddit-threads.sh +1 -1
  44. package/skill/run-twitter-cycle.sh +1 -1
  45. package/skill/scan-instagram-replies.sh +1 -1
  46. package/skill/sentry-digest.sh +1 -1
  47. package/skill/stats-instagram.sh +1 -1
  48. package/skill/stats-linkedin.sh +2 -2
  49. package/skill/stats.sh +1 -1
  50. package/skill/sweep-link-clicks.sh +1 -1
package/mcp/dist/index.js CHANGED
@@ -2988,8 +2988,10 @@ function queueDir() {
2988
2988
  // routine is draining the queue — the worker would claim within a minute if it
2989
2989
  // were firing. This is the liveness signal that survives a Claude account switch
2990
2990
  // (which orphans the routines while their global SKILL.md files stay put, so the
2991
- // SKILL.md-presence check in autopilotLoaded() reads a FALSE green). Mirrors the
2992
- // menu bar's AUTOPILOT_STALL_SECONDS in mcp/menubar/s4l_menubar.py — keep in sync.
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.
2993
2995
  const AUTOPILOT_STALL_MS = 1_200_000;
2994
2996
  // True when no scheduled-task routine is draining the draft queue. Two signals,
2995
2997
  // OR'd (keep in lockstep with mcp/menubar/s4l_menubar.py::_autopilot_stalled and
@@ -599,15 +599,28 @@ export function ensureRuntimeProvisioned() {
599
599
  // live without a reinstall). Best-effort; never blocks startup.
600
600
  export async function ensureHarnessPatched() {
601
601
  try {
602
- const adminPy = path.join(HARNESS_DIR, "src", "browser_harness", "admin.py");
603
- if (!fs.existsSync(adminPy))
602
+ // Version marker of the CURRENT vendored patch: a symbol that exists only
603
+ // in its newest revision (v2 added the daemon.py websocket-proxy
604
+ // neutralization after v1's cdp_urlopen alone proved insufficient — the
605
+ // WS dial still routed through the macOS system proxy and died with
606
+ // "proxy rejected connection: HTTP 503"). Bump this string whenever the
607
+ // patch gains a new hunk, or upgraded installs will silently keep the
608
+ // previous revision.
609
+ const daemonPy = path.join(HARNESS_DIR, "src", "browser_harness", "daemon.py");
610
+ if (!fs.existsSync(daemonPy))
604
611
  return { ok: false, detail: "harness not installed yet" };
605
- if (fs.readFileSync(adminPy, "utf-8").includes("cdp_urlopen")) {
612
+ if (fs.readFileSync(daemonPy, "utf-8").includes("s4l_no_proxy_ws")) {
606
613
  return { ok: true, detail: "already patched" };
607
614
  }
608
615
  const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
609
616
  if (!fs.existsSync(harnessPatch))
610
617
  return { ok: false, detail: "patch file missing from package" };
618
+ // Discard any PRIOR vendored-patch revision (e.g. v1) so the cumulative
619
+ // patch applies onto pristine pinned sources. Only src/ is touched; the
620
+ // checkout is a managed artifact, never a place for local edits.
621
+ await sh("git", ["-C", HARNESS_DIR, "checkout", "--", "src/browser_harness"], {
622
+ timeoutMs: 30000,
623
+ });
611
624
  const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
612
625
  timeoutMs: 30000,
613
626
  });
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.4-rc.30",
3
- "installedAt": "2026-07-13T22:13:17.873Z"
2
+ "version": "1.7.4-rc.32",
3
+ "installedAt": "2026-07-13T22:36:08.709Z"
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.4-rc.30",
5
+ "version": "1.7.4-rc.32",
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": {
@@ -398,32 +398,33 @@ DIAGNOSE_PROMPT_TEMPLATE = (
398
398
  "<that file>` so the report reaches the S4L developers. Keep replies short."
399
399
  )
400
400
 
401
- # A pending draft job older than this (seconds) with nothing claiming it means no
402
- # routine is draining the queue — the worker would claim within a minute if it
403
- # were firing. False-positive-free: an idle queue has no pending job at all, so a
404
- # quiet pipeline (no candidates) never trips this. Comfortably above the host
405
- # scheduler's per-minute cadence + a slow claim.
406
- AUTOPILOT_STALL_SECONDS = 1200
407
-
408
- # A job CLAIMED but never finished (sits in running/ this long) means a worker
409
- # picked it up and then wedged mid-run (the claude -p drafting child died / never
410
- # spawned). Generous enough that the longest real drafting turn never trips it.
411
- # Keep in sync with RUNNING_STALL_SECONDS (scripts/autopilot_stall_watch.py).
412
- AUTOPILOT_RUNNING_STALL_SECONDS = 1200
413
-
414
- # The "firing" window (how fresh lastRunAt must be) lives in the single source of
415
- # truth, scripts/schedule_state.py (FIRING_WINDOW there). _schedule_state delegates
416
- # to it, so it is intentionally NOT redefined here.
417
-
418
- # How long the producer can sit narrating "draft Nm" before we treat
419
- # the draft as STUCK rather than healthy. The producer writes that label the whole
420
- # time it blocks waiting for a worker to return a result (up to its 30-min queue
421
- # timeout). A healthy drain clears in ~1-2 min; if the label has been "drafting"
422
- # this long, the worker keeps dying mid-run (host inactivity-kill) or never claims,
423
- # and nothing is draining — so we flip the menu bar from a reassuring spinner to
424
- # ⚠ instead of letting the stale "drafting (8m)" lie persist. Well above any
425
- # healthy single drain (the worker itself dies at ~2 min today).
426
- DRAFT_STUCK_SECONDS = 300
401
+ # Stall thresholds live in the single source of truth, scripts/schedule_state.py
402
+ # (HEALTHY_DRAIN_MAX_SECONDS and the names derived from it) — retune there, not
403
+ # here. Same module that owns FIRING_WINDOW; _schedule_state already delegates to
404
+ # it. The three names cover three distinct failure modes:
405
+ # AUTOPILOT_STALL_SECONDS pending job unclaimed → no routine draining
406
+ # (fix: Re-arm). Idle queue has no pending job,
407
+ # so a quiet pipeline never trips it.
408
+ # AUTOPILOT_RUNNING_STALL_SECONDS job claimed but wedged in running/ → worker
409
+ # died mid-run (fix: Diagnose).
410
+ # DRAFT_STUCK_SECONDS the producer's "draft Nm" label alive this
411
+ # long → end-to-end clock; the only timer that
412
+ # spans claim→die→requeue churn loops, which
413
+ # reset the two per-state timers on every
414
+ # transition while nothing ever completes.
415
+ # The import is best-effort because the S4L_REPO_DIR sys.path insertion near the
416
+ # top of this file is best-effort; the fallback literal mirrors
417
+ # HEALTHY_DRAIN_MAX_SECONDS.
418
+ try:
419
+ from schedule_state import ( # noqa: E402
420
+ STALL_SECONDS as AUTOPILOT_STALL_SECONDS,
421
+ RUNNING_STALL_SECONDS as AUTOPILOT_RUNNING_STALL_SECONDS,
422
+ DRAFT_STUCK_SECONDS,
423
+ )
424
+ except Exception:
425
+ AUTOPILOT_STALL_SECONDS = 1200
426
+ AUTOPILOT_RUNNING_STALL_SECONDS = 1200
427
+ DRAFT_STUCK_SECONDS = 1200
427
428
 
428
429
  # A poll can read a transient "not stuck" moment (e.g. the activity file
429
430
  # between one dying worker attempt and the next claim on the same aged job)
@@ -2445,11 +2446,12 @@ class S4LMenuBar(rumps.App):
2445
2446
  self._stall_reason_info = ("", "")
2446
2447
  # Draft worker stuck/killed: the producer narrates "drafting replies (Nm)"
2447
2448
  # the whole time it blocks waiting for a worker to return a result, with NO
2448
- # idea the worker died. A healthy drain clears in ~1-2 min; once that label
2449
- # has been "drafting" past DRAFT_STUCK_SECONDS the worker keeps getting
2450
- # killed mid-run (or never claims) and nothing is draining — flip to ⚠
2451
- # instead of leaving the reassuring "drafting (8m)" spinner up. Skip when a
2452
- # more specific cause (rate limit) already owns the reason.
2449
+ # idea the worker died. A healthy drain can run 8-10 min end to end (slot
2450
+ # wait + the drafting turn); once that label has been "drafting" past
2451
+ # DRAFT_STUCK_SECONDS the worker keeps getting killed mid-run (or never
2452
+ # claims) and nothing is draining — flip to ⚠ instead of leaving the
2453
+ # reassuring "drafting (25m)" spinner up. Skip when a more specific cause
2454
+ # (rate limit) already owns the reason.
2453
2455
  #
2454
2456
  # Gated on schedule_state in ("ok", "stalled"), NOT missing/disabled: when
2455
2457
  # the schedule is missing/disabled (e.g. orphaned by an account switch),
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.4-rc.30",
3
+ "version": "1.7.4-rc.32",
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.4-rc.30",
3
+ "version": "1.7.4-rc.32",
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",
@@ -14,7 +14,8 @@ autopilot" item). This watcher is the part the user can't see: a fleet-side aler
14
14
  so a sustained stall pages us even when nobody is looking at the menu bar.
15
15
 
16
16
  Design mirrors the stall signal in mcp/menubar/s4l_menubar.py (_autopilot_stalled)
17
- and mcp/src/index.ts (autopilotStalled) — keep the threshold in sync:
17
+ and mcp/src/index.ts (autopilotStalled); the thresholds are shared via
18
+ schedule_state.py (index.ts mirrors by hand):
18
19
  stalled = the autopilot is configured (a complete worker set's SKILL.md
19
20
  files present — see WORKER_TASK_SETS)
20
21
  AND a draft job has sat unclaimed in pending/ past STALL_SECONDS.
@@ -46,14 +47,10 @@ import identity # noqa: E402 (lives next to this file in scripts/)
46
47
 
47
48
  s4l_env.mirror()
48
49
 
49
- # Keep in sync with AUTOPILOT_STALL_SECONDS (menubar) / AUTOPILOT_STALL_MS (index.ts).
50
- STALL_SECONDS = 1200
51
- # A job CLAIMED but never finished (sits in running/ this long) means a worker
52
- # picked it up and then died mid-run — the claude -p drafting child never came up
53
- # or crashed. Must be generous enough to clear the longest real drafting turn so a
54
- # healthy run never trips it. Keep in sync with AUTOPILOT_RUNNING_STALL_SECONDS
55
- # (menubar). See _oldest_running_age.
56
- RUNNING_STALL_SECONDS = 1200
50
+ # Stall thresholds live in schedule_state.py (HEALTHY_DRAIN_MAX_SECONDS and the
51
+ # names derived from it) — retune there, not here. STALL_SECONDS = pending job
52
+ # unclaimed; RUNNING_STALL_SECONDS = claimed but wedged (see _oldest_running_age).
53
+ from schedule_state import STALL_SECONDS, RUNNING_STALL_SECONDS # noqa: E402
57
54
  # Require the stall to persist this many consecutive checks before paging, so a
58
55
  # transient slow claim (e.g. right after a Claude restart) doesn't false-alarm.
59
56
  # At StartInterval 120 that is ~6 min of continuous stall.
@@ -1,23 +1,3 @@
1
- From a97b7cdae960813f13af39ef99ce878426008dcc Mon Sep 17 00:00:00 2001
2
- From: m13v <i@m13v.com>
3
- Date: Mon, 13 Jul 2026 08:48:50 -0700
4
- Subject: [PATCH] Never route loopback CDP requests through a proxy
5
-
6
- urllib's default opener honors proxy env vars and macOS system proxy
7
- settings (scutil --proxy). On a machine with a system-wide proxy (e.g. a
8
- local gost/ISP forwarder), every 127.0.0.1 CDP probe is sent to the proxy
9
- and returns 403 Forbidden, so Chrome looks unreachable while curl (env
10
- proxies only) and Playwright (own websocket stack) work fine. Route all
11
- loopback CDP HTTP calls through a proxy-free opener; non-loopback CDP
12
- URLs keep default proxy behavior.
13
-
14
- Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15
- ---
16
- src/browser_harness/admin.py | 24 +++++++++++++++++++++++-
17
- src/browser_harness/daemon.py | 7 ++++---
18
- src/browser_harness/run.py | 3 ++-
19
- 3 files changed, 29 insertions(+), 5 deletions(-)
20
-
21
1
  diff --git a/src/browser_harness/admin.py b/src/browser_harness/admin.py
22
2
  index c72a8fb..64addee 100644
23
3
  --- a/src/browser_harness/admin.py
@@ -62,18 +42,33 @@ index c72a8fb..64addee 100644
62
42
 
63
43
  def _has_local_gui():
64
44
  diff --git a/src/browser_harness/daemon.py b/src/browser_harness/daemon.py
65
- index 0f0f255..ceb08b9 100644
45
+ index 0f0f255..de51934 100644
66
46
  --- a/src/browser_harness/daemon.py
67
47
  +++ b/src/browser_harness/daemon.py
68
- @@ -5,6 +5,7 @@ from collections import deque
48
+ @@ -5,8 +5,22 @@ from collections import deque
69
49
  from pathlib import Path
70
50
 
71
51
  from . import _ipc as ipc
72
52
  +from .admin import cdp_urlopen
73
53
  from cdp_use.client import CDPClient
74
54
 
55
+ +# s4l_no_proxy_ws: the daemon's CDP traffic is loopback; never let ANY client
56
+ +# in this process route it through a proxy. websockets>=14 autodetects proxies
57
+ +# at dial time via urllib.request.proxy_bypass()/getproxies() (which on macOS
58
+ +# include SYSTEM proxy settings from scutil --proxy), so a box-wide forwarder
59
+ +# turns the CDP WS dial into a proxied CONNECT that fails ("proxy rejected
60
+ +# connection: HTTP 503") — same failure class as the urllib 403 that
61
+ +# cdp_urlopen fixes for the HTTP lookup. Neutralize resolution process-wide:
62
+ +# the daemon's only non-loopback traffic (browser-use cloud API) works direct.
63
+ +# Both lookups happen at call time via module attributes, so this holds
64
+ +# regardless of import order.
65
+ +urllib.request.getproxies = lambda: {} # type: ignore[assignment]
66
+ +urllib.request.proxy_bypass = lambda host: 1 # type: ignore[assignment]
67
+ +
75
68
 
76
- @@ -113,7 +114,7 @@ def get_ws_url():
69
+ def _load_env():
70
+ repo_root = Path(__file__).resolve().parents[2]
71
+ @@ -113,7 +127,7 @@ def get_ws_url():
77
72
  base_url = url.rstrip("/")
78
73
  while time.time() < deadline:
79
74
  try:
@@ -82,7 +77,7 @@ index 0f0f255..ceb08b9 100644
82
77
  except urllib.error.HTTPError as e:
83
78
  last_err = e
84
79
  if e.code == 404 and (ws := _ws_from_devtools_active_port(url)):
85
- @@ -139,7 +140,7 @@ def get_ws_url():
80
+ @@ -139,7 +153,7 @@ def get_ws_url():
86
81
  deadline = time.time() + 30
87
82
  while time.time() < deadline:
88
83
  try:
@@ -91,7 +86,7 @@ index 0f0f255..ceb08b9 100644
91
86
  except urllib.error.HTTPError as e:
92
87
  # Chrome 147+ disables /json/* HTTP discovery on the default user-data-dir;
93
88
  # the ws path Chrome wrote to DevToolsActivePort still works.
94
- @@ -153,7 +154,7 @@ def get_ws_url():
89
+ @@ -153,7 +167,7 @@ def get_ws_url():
95
90
  )
96
91
  for probe_port in (9222, 9223):
97
92
  try:
@@ -121,6 +116,3 @@ index 8ab1f0f..cbd3835 100644
121
116
  return True
122
117
  except OSError: pass
123
118
  return False
124
- --
125
- 2.43.0
126
-
@@ -94,6 +94,29 @@ def _registry_worker_recs(by_id):
94
94
  # false "not scheduled".
95
95
  FIRING_WINDOW = 420
96
96
 
97
+ # --- Stall thresholds (single source of truth) --------------------------------
98
+ # The longest a fully healthy drain can take end to end: a draft job can wait
99
+ # minutes for a worker slot (per-minute scheduler cadence, workers busy on other
100
+ # jobs) and the drafting turn itself runs multiple minutes. Observed healthy
101
+ # episodes reach 8-10 min (2026-07-13); alarm at 20 min for margin. Every stall
102
+ # detector derives from this ONE number so they can't drift apart again (99e695bf
103
+ # retuned the two per-state timers to 1200s but left the end-to-end DRAFT_STUCK
104
+ # check at 300s, which then false-alarmed on healthy 8-minute drains). The Node
105
+ # server can't import this module — mcp/src/index.ts::AUTOPILOT_STALL_MS must be
106
+ # kept in sync by hand.
107
+ HEALTHY_DRAIN_MAX_SECONDS = 1200
108
+
109
+ # A pending draft job unclaimed this long: no routine is draining the queue
110
+ # (an idle queue has no pending job at all, so a quiet pipeline never trips it).
111
+ STALL_SECONDS = HEALTHY_DRAIN_MAX_SECONDS
112
+ # A job claimed but still sitting in running/ this long: the worker picked it up
113
+ # and then wedged mid-run (the claude -p drafting child died / never spawned).
114
+ RUNNING_STALL_SECONDS = HEALTHY_DRAIN_MAX_SECONDS
115
+ # The producer's "drafting Nm" label alive this long: the end-to-end clock. This
116
+ # is the only timer that spans claim→die→requeue churn loops, which reset the two
117
+ # per-state timers above on every transition while nothing ever completes.
118
+ DRAFT_STUCK_SECONDS = HEALTHY_DRAIN_MAX_SECONDS
119
+
97
120
  # Grace for a JUST-scheduled task that hasn't fired yet. When the user runs
98
121
  # "Set up draft schedule", create_scheduled_task registers both worker tasks
99
122
  # (cron "* * * * *", enabled) but lastRunAt is null until the host fires them the
@@ -34,5 +34,5 @@ RUN_START=$(date +%s)
34
34
  EXIT_CODE=$?
35
35
  RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
36
36
 
37
- echo "[$(date +%H:%M:%S)] === done in ${RUN_ELAPSED}s (exit=${EXIT_CODE}) ==="
37
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] === done in ${RUN_ELAPSED}s (exit=${EXIT_CODE}) ==="
38
38
  exit "$EXIT_CODE"
@@ -14,7 +14,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
14
14
  mkdir -p "$LOG_DIR"
15
15
  LOG_FILE="$LOG_DIR/audit-dm-staleness-$(date +%Y-%m-%d_%H%M%S).log"
16
16
 
17
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
17
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
18
18
 
19
19
  # HTTP-only lane (2026-06-01): both staleness UPDATEs run server-side via the
20
20
  # s4l.ai API (POST /api/v1/dms/staleness-sweep). No DATABASE_URL, no psql.
@@ -6,7 +6,7 @@
6
6
  # State: ~/.claude/social-autoposter/linkedin.killswitch
7
7
  # Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
8
8
  if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
9
- echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
9
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
10
10
  echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
11
11
  exit 0
12
12
  fi
@@ -38,7 +38,7 @@ AUDIT_HELPER="$REPO_DIR/scripts/audit_helper.py"
38
38
  mkdir -p "$LOG_DIR"
39
39
  LOG_FILE="$LOG_DIR/audit-reddit-resurrect-$(date +%Y-%m-%d_%H%M%S).log"
40
40
 
41
- log() { echo "[$(date +%H:%M:%S)] $*" >> "$LOG_FILE"; echo "[$(date +%H:%M:%S)] $*"; }
41
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG_FILE"; echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
42
42
 
43
43
  RUN_START=$(date +%s)
44
44
  log "=== Reddit resurrect audit: $(date) ==="
package/skill/audit.sh CHANGED
@@ -62,10 +62,10 @@ case "${PLATFORM:-all}" in
62
62
  _LI_BOOT_RC=0
63
63
  ( source "$(dirname "$0")/lib/linkedin-backend.sh"; ensure_linkedin_browser_for_backend ) || _LI_BOOT_RC=$?
64
64
  if [ "$_LI_BOOT_RC" -eq 78 ]; then
65
- echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: peer pipeline is driving the 9556 Chrome; skipping this fire"
65
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock: peer pipeline is driving the 9556 Chrome; skipping this fire"
66
66
  exit 0
67
67
  elif [ "$_LI_BOOT_RC" -ne 0 ]; then
68
- echo "[$(date +%H:%M:%S)] ERROR: linkedin browser bootstrap failed (rc=$_LI_BOOT_RC)"
68
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: linkedin browser bootstrap failed (rc=$_LI_BOOT_RC)"
69
69
  exit "$_LI_BOOT_RC"
70
70
  fi
71
71
  ;;
@@ -84,10 +84,10 @@ case "${PLATFORM:-all}" in
84
84
  _LI_BOOT_RC=0
85
85
  ( source "$(dirname "$0")/lib/linkedin-backend.sh"; ensure_linkedin_browser_for_backend ) || _LI_BOOT_RC=$?
86
86
  if [ "$_LI_BOOT_RC" -eq 78 ]; then
87
- echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: peer pipeline is driving the 9556 Chrome; skipping this fire (all-platform audit)"
87
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock: peer pipeline is driving the 9556 Chrome; skipping this fire (all-platform audit)"
88
88
  exit 0
89
89
  elif [ "$_LI_BOOT_RC" -ne 0 ]; then
90
- echo "[$(date +%H:%M:%S)] ERROR: linkedin browser bootstrap failed (rc=$_LI_BOOT_RC)"
90
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: linkedin browser bootstrap failed (rc=$_LI_BOOT_RC)"
91
91
  exit "$_LI_BOOT_RC"
92
92
  fi
93
93
  python3 "$REPO_DIR_FOR_LOCK/scripts/reddit_browser_lock.py" acquire --timeout 3600 --ttl 90 2>&1 || \
@@ -113,7 +113,7 @@ mkdir -p "$LOG_DIR"
113
113
  LOG_TAG="${PLATFORM:-all}"
114
114
  LOG_FILE="$LOG_DIR/audit-${LOG_TAG}-$(date +%Y-%m-%d_%H%M%S).log"
115
115
 
116
- log() { echo "[$(date +%H:%M:%S)] $*" >> "$LOG_FILE"; echo "[$(date +%H:%M:%S)] $*"; }
116
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG_FILE"; echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
117
117
 
118
118
  RUN_START=$(date +%s)
119
119
  log "=== Audit Pipeline Run (${LOG_TAG}): $(date) ==="
@@ -35,7 +35,7 @@ mkdir -p "$LOG_DIR"
35
35
  PYTHON_BIN="${PYTHON_BIN:-/opt/homebrew/bin/python3.11}"
36
36
  [ -x "$PYTHON_BIN" ] || PYTHON_BIN="/usr/bin/python3"
37
37
 
38
- log() { echo "[$(date +%H:%M:%S)] $*" >> "$LOG_DIR/web-chat.log"; }
38
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG_DIR/web-chat.log"; }
39
39
 
40
40
  # Step 1: query Postgres for unread threads.
41
41
  CHATS=$("$PYTHON_BIN" "$SCRIPTS_DIR/check_unread_web_chats.py" 2>>"$LOG_DIR/web-chat.log")
@@ -12,7 +12,7 @@ set -euo pipefail
12
12
  # State: ~/.claude/social-autoposter/linkedin.killswitch
13
13
  # Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
14
14
  if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
15
- echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
15
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
16
16
  echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
17
17
  exit 0
18
18
  fi
@@ -37,10 +37,10 @@ acquire_lock "linkedin-browser" 3600
37
37
  _LI_BOOT_RC=0
38
38
  ensure_linkedin_browser_for_backend || _LI_BOOT_RC=$?
39
39
  if [ "$_LI_BOOT_RC" -eq 78 ]; then
40
- echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: peer pipeline is driving the 9556 Chrome; skipping this fire"
40
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock: peer pipeline is driving the 9556 Chrome; skipping this fire"
41
41
  exit 0
42
42
  elif [ "$_LI_BOOT_RC" -ne 0 ]; then
43
- echo "[$(date +%H:%M:%S)] ERROR: linkedin browser bootstrap failed (rc=$_LI_BOOT_RC)"
43
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: linkedin browser bootstrap failed (rc=$_LI_BOOT_RC)"
44
44
  exit "$_LI_BOOT_RC"
45
45
  fi
46
46
  acquire_lock "dm-outreach-linkedin" 2700
@@ -56,7 +56,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
56
56
  mkdir -p "$LOG_DIR"
57
57
  LOG_FILE="$LOG_DIR/dm-outreach-linkedin-$(date +%Y-%m-%d_%H%M%S).log"
58
58
 
59
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
59
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
60
60
 
61
61
  RUN_START=$(date +%s)
62
62
  log "=== LinkedIn DM Outreach Run: $(date) ==="
@@ -50,7 +50,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
50
50
  mkdir -p "$LOG_DIR"
51
51
  LOG_FILE="$LOG_DIR/dm-outreach-reddit-$(date +%Y-%m-%d_%H%M%S).log"
52
52
 
53
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
53
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
54
54
 
55
55
  RUN_START=$(date +%s)
56
56
  log "=== Reddit DM Outreach Run: $(date) ==="
@@ -44,7 +44,7 @@ SKILL_FILE="$REPO_DIR/SKILL.md"
44
44
  # dm-outreach-linkedin.sh (migrated 2026-05-12).
45
45
  # (LOG_DIR/LOG_FILE bootstrapped at top of script.)
46
46
 
47
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
47
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
48
48
 
49
49
  RUN_START=$(date +%s)
50
50
  log "=== Twitter DM Outreach Run: $(date) ==="
@@ -35,7 +35,7 @@ fi
35
35
  # State: ~/.claude/social-autoposter/linkedin.killswitch
36
36
  # Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
37
37
  if [ "$PLATFORM" = "linkedin" ] && [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
38
- echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn DM-replies pipeline."
38
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn DM-replies pipeline."
39
39
  echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
40
40
  exit 0
41
41
  fi
@@ -116,7 +116,7 @@ LOG_SUFFIX=""
116
116
  [ -n "$PLATFORM" ] && LOG_SUFFIX="-$PLATFORM"
117
117
  LOG_FILE="$LOG_DIR/engage-dm-replies${LOG_SUFFIX}-$(date +%Y-%m-%d_%H%M%S).log"
118
118
 
119
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
119
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
120
120
 
121
121
  RUN_START=$(date +%s)
122
122
  log "=== DM Reply Engagement Run: $(date) (platform: ${PLATFORM:-all}) ==="
@@ -17,7 +17,7 @@ set -euo pipefail
17
17
  # State: ~/.claude/social-autoposter/linkedin.killswitch
18
18
  # Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
19
19
  if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
20
- echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
20
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
21
21
  echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
22
22
  exit 0
23
23
  fi
@@ -84,7 +84,7 @@ LOG_FILE="$LOG_DIR/engage-linkedin-$(date +%Y-%m-%d_%H%M%S).log"
84
84
  BATCH_ID="enli-$(date +%Y%m%d_%H%M%S)-$$"
85
85
  export SA_CYCLE_ID="$BATCH_ID"
86
86
 
87
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
87
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
88
88
 
89
89
  RUN_START=$(date +%s)
90
90
  log "=== LinkedIn Engagement Run: $(date) ==="
@@ -23,7 +23,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
23
23
  mkdir -p "$LOG_DIR"
24
24
  LOG_FILE="$LOG_DIR/engage-moltbook-$(date +%Y-%m-%d_%H%M%S).log"
25
25
 
26
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
26
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
27
27
 
28
28
  RUN_START=$(date +%s)
29
29
  log "=== MoltBook Engage Run: $(date) ==="
@@ -25,12 +25,12 @@ source "$(dirname "$0")/lock.sh"
25
25
  # Sets MCP_CONFIG_FILE, BROWSER_INSTRUCTIONS, exports TWITTER_CDP_URL=9555.
26
26
  source "$(dirname "$0")/lib/twitter-backend.sh"
27
27
 
28
- echo "[$(date +%H:%M:%S)] Acquiring twitter-browser lock (pid=$$)..." | tee -a "$LOG_FILE"
28
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Acquiring twitter-browser lock (pid=$$)..." | tee -a "$LOG_FILE"
29
29
  acquire_lock "twitter-browser" 3600 2>>"$LOG_FILE"
30
- echo "[$(date +%H:%M:%S)] twitter-browser lock held (pid=$$)" | tee -a "$LOG_FILE"
30
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] twitter-browser lock held (pid=$$)" | tee -a "$LOG_FILE"
31
31
  # Probe + launch harness Chrome on port 9555 if needed, then sweep leftover tabs.
32
32
  ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
33
- echo "[$(date +%H:%M:%S)] Acquiring twitter (pipeline) lock (pid=$$)..." | tee -a "$LOG_FILE"
33
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Acquiring twitter (pipeline) lock (pid=$$)..." | tee -a "$LOG_FILE"
34
34
  acquire_lock "twitter" 3600 2>>"$LOG_FILE"
35
35
 
36
36
  # Load secrets
@@ -47,7 +47,7 @@ BATCH_SIZE=500
47
47
  ENGAGE_TWITTER_HELPER="$REPO_DIR/scripts/engage_twitter_helper.py"
48
48
  # (LOG_DIR/LOG_FILE bootstrapped at top of script.)
49
49
 
50
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
50
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
51
51
 
52
52
  RUN_START=$(date +%s)
53
53
  log "=== Twitter Engagement Run: $(date) ==="
@@ -20,7 +20,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
20
20
  mkdir -p "$LOG_DIR"
21
21
  LOG_FILE="$LOG_DIR/github-engage-$(date +%Y-%m-%d_%H%M%S).log"
22
22
 
23
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
23
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
24
24
 
25
25
  # Per-cycle batch id stamped onto every claude_sessions row spawned by this
26
26
  # engagement run (via SA_CYCLE_ID env -> log_claude_session.py). 2026-05-10
@@ -154,7 +154,7 @@ cleanup_harness_tabs() {
154
154
  if ! $_probe 2>/dev/null; then
155
155
  sleep 1
156
156
  if ! $_probe 2>/dev/null; then
157
- echo "[$(date +%H:%M:%S)] cleanup_harness_tabs: SKIPPED (linkedin-harness CDP /json/version unreachable after 10s+retry)" >&2
157
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (linkedin-harness CDP /json/version unreachable after 10s+retry)" >&2
158
158
  return 0
159
159
  fi
160
160
  fi
@@ -221,7 +221,7 @@ _acquire_linkedin_pipeline_lock() {
221
221
  echo "$$" > "$_LI_PIPELINE_LOCK_DIR/pid"
222
222
  echo "$_who" > "$_LI_PIPELINE_LOCK_DIR/holder"
223
223
  export _LI_PIPELINE_LOCK_HELD=1
224
- echo "[$(date +%H:%M:%S)] linkedin-pipeline lock ACQUIRED by $_who (pid $$)" >&2
224
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock ACQUIRED by $_who (pid $$)" >&2
225
225
  return 0
226
226
  fi
227
227
  local _h_pid _h_who
@@ -236,11 +236,11 @@ _acquire_linkedin_pipeline_lock() {
236
236
  return 0
237
237
  fi
238
238
  if [ -z "$_h_pid" ] || ! kill -0 "$_h_pid" 2>/dev/null; then
239
- echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: reclaiming stale lock (dead holder ${_h_who} pid ${_h_pid:-unknown})" >&2
239
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock: reclaiming stale lock (dead holder ${_h_who} pid ${_h_pid:-unknown})" >&2
240
240
  rm -rf "$_LI_PIPELINE_LOCK_DIR"
241
241
  continue
242
242
  fi
243
- echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: held by ${_h_who} (pid ${_h_pid}); ${_who} skipping this fire (rc=78) to avoid two drivers on the 9556 Chrome" >&2
243
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] linkedin-pipeline lock: held by ${_h_who} (pid ${_h_pid}); ${_who} skipping this fire (rc=78) to avoid two drivers on the 9556 Chrome" >&2
244
244
  return 78
245
245
  done
246
246
  }
@@ -271,10 +271,10 @@ ensure_linkedin_browser_for_backend() {
271
271
  if [ "${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" != "$_BH_LINKEDIN_DEFAULT_URL" ]; then
272
272
  local _ext_url="${LINKEDIN_CDP_URL}"
273
273
  if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
274
- echo "[$(date +%H:%M:%S)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
274
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
275
275
  return 0
276
276
  fi
277
- echo "[$(date +%H:%M:%S)] ERROR: LINKEDIN_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
277
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: LINKEDIN_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
278
278
  return 1
279
279
  fi
280
280
  # Cross-pipeline whole-run lock: only one LinkedIn browser pipeline drives
@@ -288,14 +288,14 @@ ensure_linkedin_browser_for_backend() {
288
288
  _acquire_linkedin_pipeline_lock || return $?
289
289
  # Probe + launch harness Chrome on port 9556 if needed.
290
290
  if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
291
- echo "[$(date +%H:%M:%S)] LinkedIn harness Chrome down on port 9556, launching..." >&2
291
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LinkedIn harness Chrome down on port 9556, launching..." >&2
292
292
  # Dated relaunch stamp for central observability (see twitter-backend.sh).
293
293
  echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) port=9556" \
294
294
  >> "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/logs/chrome-relaunch-events.log" 2>/dev/null || true
295
295
  local _chrome_bin
296
296
  _chrome_bin=$(_resolve_chrome_bin)
297
297
  if [ -z "$_chrome_bin" ]; then
298
- echo "[$(date +%H:%M:%S)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
298
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
299
299
  return 1
300
300
  fi
301
301
  # On Linux + no display, run headless. On root, add --no-sandbox.
@@ -323,7 +323,7 @@ ensure_linkedin_browser_for_backend() {
323
323
  local _stale_pids
324
324
  _stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
325
325
  if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
326
- echo "[$(date +%H:%M:%S)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
326
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
327
327
  kill $_stale_pids 2>/dev/null || true
328
328
  sleep 2
329
329
  _stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
@@ -357,10 +357,10 @@ os.execv(sys.argv[1], sys.argv[1:])' \
357
357
  sleep 1
358
358
  done
359
359
  if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
360
- echo "[$(date +%H:%M:%S)] ERROR: LinkedIn harness Chrome failed to start within 12s" >&2
360
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: LinkedIn harness Chrome failed to start within 12s" >&2
361
361
  return 1
362
362
  fi
363
- echo "[$(date +%H:%M:%S)] LinkedIn harness Chrome up on port 9556" >&2
363
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LinkedIn harness Chrome up on port 9556" >&2
364
364
  fi
365
365
  # Always close leftover tabs from prior runs. Safe under acquire_lock
366
366
  # "linkedin-browser" serialization.
@@ -395,7 +395,7 @@ _linkedin_session_detect_gate() {
395
395
  "$_py" "$HOME/social-autoposter/scripts/linkedin_killswitch.py" detect-gate \
396
396
  --cdp-url "${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" >&2 || _rc=$?
397
397
  if [ "$_rc" = "2" ]; then
398
- echo "[$(date +%H:%M:%S)] detect-gate tripped the LinkedIn killswitch; aborting this fire" >&2
398
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] detect-gate tripped the LinkedIn killswitch; aborting this fire" >&2
399
399
  return 1
400
400
  fi
401
401
  return 0
@@ -123,7 +123,7 @@ cleanup_harness_tabs() {
123
123
  if ! $_probe 2>/dev/null; then
124
124
  sleep 1
125
125
  if ! $_probe 2>/dev/null; then
126
- echo "[$(date +%H:%M:%S)] cleanup_harness_tabs: SKIPPED (reddit-harness CDP /json/version unreachable after 10s+retry)" >&2
126
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (reddit-harness CDP /json/version unreachable after 10s+retry)" >&2
127
127
  return 0
128
128
  fi
129
129
  fi
@@ -156,19 +156,19 @@ ensure_reddit_browser_for_backend() {
156
156
  if [ "${REDDIT_CDP_URL:-$_BH_REDDIT_DEFAULT_URL}" != "$_BH_REDDIT_DEFAULT_URL" ]; then
157
157
  local _ext_url="${REDDIT_CDP_URL}"
158
158
  if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
159
- echo "[$(date +%H:%M:%S)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
159
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
160
160
  return 0
161
161
  fi
162
- echo "[$(date +%H:%M:%S)] ERROR: REDDIT_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
162
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: REDDIT_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
163
163
  return 1
164
164
  fi
165
165
  # Probe + launch harness Chrome on port 9557 if needed.
166
166
  if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
167
- echo "[$(date +%H:%M:%S)] Reddit harness Chrome down on port 9557, launching..." >&2
167
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Reddit harness Chrome down on port 9557, launching..." >&2
168
168
  local _chrome_bin
169
169
  _chrome_bin=$(_resolve_chrome_bin)
170
170
  if [ -z "$_chrome_bin" ]; then
171
- echo "[$(date +%H:%M:%S)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
171
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
172
172
  return 1
173
173
  fi
174
174
  # On Linux + no display, run headless. On root, add --no-sandbox.
@@ -196,7 +196,7 @@ ensure_reddit_browser_for_backend() {
196
196
  local _stale_pids
197
197
  _stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
198
198
  if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
199
- echo "[$(date +%H:%M:%S)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
199
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
200
200
  kill $_stale_pids 2>/dev/null || true
201
201
  sleep 2
202
202
  _stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
@@ -216,10 +216,10 @@ ensure_reddit_browser_for_backend() {
216
216
  sleep 1
217
217
  done
218
218
  if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
219
- echo "[$(date +%H:%M:%S)] ERROR: Reddit harness Chrome failed to start within 12s" >&2
219
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: Reddit harness Chrome failed to start within 12s" >&2
220
220
  return 1
221
221
  fi
222
- echo "[$(date +%H:%M:%S)] Reddit harness Chrome up on port 9557" >&2
222
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Reddit harness Chrome up on port 9557" >&2
223
223
  fi
224
224
  # Always close leftover tabs from prior runs. Safe under acquire_lock
225
225
  # "reddit-browser" serialization.
@@ -138,7 +138,7 @@ cleanup_harness_tabs() {
138
138
  if ! $_probe 2>/dev/null; then
139
139
  sleep 1
140
140
  if ! $_probe 2>/dev/null; then
141
- echo "[$(date +%H:%M:%S)] cleanup_harness_tabs: SKIPPED (harness CDP /json/version unreachable after 10s+retry)" >&2
141
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (harness CDP /json/version unreachable after 10s+retry)" >&2
142
142
  return 0
143
143
  fi
144
144
  fi
@@ -202,13 +202,13 @@ ensure_twitter_browser_for_backend() {
202
202
  # 180s per downstream attach against a wedged browser.
203
203
  local _ext_verdict
204
204
  if ! _ext_verdict=$(_bh_cdp_ready "$_ext_url"); then
205
- echo "[$(date +%H:%M:%S)] ERROR: external Chrome at ${_ext_url} is WEDGED (/json/version answers but the CDP handshake never completes). Host must restart it (AppMaker /opt/startup.sh, etc)." >&2
205
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: external Chrome at ${_ext_url} is WEDGED (/json/version answers but the CDP handshake never completes). Host must restart it (AppMaker /opt/startup.sh, etc)." >&2
206
206
  echo "twitter_cdp_wedge: detected url=${_ext_url} action=none-external" >&2
207
207
  _bh_record_cdp_health external_wedged "$_ext_verdict"
208
208
  return 1
209
209
  fi
210
210
  _bh_record_cdp_health ok-external "$_ext_verdict"
211
- echo "[$(date +%H:%M:%S)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
211
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
212
212
  # Restore the Twitter login if the sandbox was substituted. AppMaker
213
213
  # Hobby-tier sandboxes have a 1h TTL; on substitution /root is reseeded
214
214
  # from /etc/skel-root and the harness profile (cookies) is wiped. This
@@ -217,7 +217,7 @@ ensure_twitter_browser_for_backend() {
217
217
  python3 "$_BH_REPO_DIR/scripts/restore_twitter_session.py" 2>&1 | sed 's/^/[restore] /' >&2 || true
218
218
  return 0
219
219
  fi
220
- echo "[$(date +%H:%M:%S)] ERROR: TWITTER_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host (AppMaker /opt/startup.sh, etc)." >&2
220
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: TWITTER_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host (AppMaker /opt/startup.sh, etc)." >&2
221
221
  return 1
222
222
  fi
223
223
  # Probe + launch harness Chrome on port 9555 if needed. Two-stage probe:
@@ -229,12 +229,12 @@ ensure_twitter_browser_for_backend() {
229
229
  local _bh_prof_dir="$HOME/.claude/browser-profiles/browser-harness"
230
230
  if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
231
231
  _need_launch=1; _launch_reason="http_down"
232
- echo "[$(date +%H:%M:%S)] Harness Chrome down on port 9555, launching..." >&2
232
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Harness Chrome down on port 9555, launching..." >&2
233
233
  else
234
234
  local _ready_verdict
235
235
  if ! _ready_verdict=$(_bh_cdp_ready "$_BH_DEFAULT_URL"); then
236
236
  _need_launch=1; _launch_reason="cdp_wedge"
237
- echo "[$(date +%H:%M:%S)] Harness Chrome WEDGED on port 9555 (/json/version answers but the CDP handshake never completes: ${_ready_verdict:-no verdict}); killing and relaunching..." >&2
237
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Harness Chrome WEDGED on port 9555 (/json/version answers but the CDP handshake never completes: ${_ready_verdict:-no verdict}); killing and relaunching..." >&2
238
238
  # Machine-greppable marker (same stderr-marker convention as
239
239
  # twitter_access_gate; bin/server.js parses these).
240
240
  echo "twitter_cdp_wedge: detected url=$_BH_DEFAULT_URL action=restart" >&2
@@ -260,7 +260,7 @@ ensure_twitter_browser_for_backend() {
260
260
  local _chrome_bin
261
261
  _chrome_bin=$(_resolve_chrome_bin)
262
262
  if [ -z "$_chrome_bin" ]; then
263
- echo "[$(date +%H:%M:%S)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
263
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
264
264
  return 1
265
265
  fi
266
266
  # On Linux + no display, run headless. On root, add --no-sandbox.
@@ -302,7 +302,7 @@ ensure_twitter_browser_for_backend() {
302
302
  local _stale_pids
303
303
  _stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
304
304
  if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
305
- echo "[$(date +%H:%M:%S)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
305
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
306
306
  kill $_stale_pids 2>/dev/null || true
307
307
  sleep 2
308
308
  _stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
@@ -335,7 +335,7 @@ os.execv(sys.argv[1], sys.argv[1:])' \
335
335
  sleep 1
336
336
  done
337
337
  if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
338
- echo "[$(date +%H:%M:%S)] ERROR: harness Chrome failed to start within 12s" >&2
338
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: harness Chrome failed to start within 12s" >&2
339
339
  _bh_record_cdp_health launch_failed null
340
340
  return 1
341
341
  fi
@@ -344,13 +344,13 @@ os.execv(sys.argv[1], sys.argv[1:])' \
344
344
  # the cycle loudly, not feed 180s hangs downstream.
345
345
  local _post_verdict
346
346
  if ! _post_verdict=$(_bh_cdp_ready "$_BH_DEFAULT_URL"); then
347
- echo "[$(date +%H:%M:%S)] ERROR: harness Chrome answers HTTP after relaunch but the CDP handshake is STILL failing: ${_post_verdict:-no verdict}" >&2
347
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: harness Chrome answers HTTP after relaunch but the CDP handshake is STILL failing: ${_post_verdict:-no verdict}" >&2
348
348
  echo "twitter_cdp_wedge: detected url=$_BH_DEFAULT_URL action=relaunch_failed" >&2
349
349
  _bh_record_cdp_health relaunch_failed "$_post_verdict"
350
350
  return 1
351
351
  fi
352
352
  _bh_record_cdp_health relaunched "$_post_verdict"
353
- echo "[$(date +%H:%M:%S)] Harness Chrome up on port 9555" >&2
353
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Harness Chrome up on port 9555" >&2
354
354
  else
355
355
  _bh_record_cdp_health ok "${_ready_verdict:-null}"
356
356
  fi
@@ -392,7 +392,7 @@ defer_if_foreign_for_backend() {
392
392
  _sa_harness_log() {
393
393
  # Use the caller's log() FUNCTION when present; `declare -F` matches only a
394
394
  # shell function, never the macOS /usr/bin/log binary (command -v would).
395
- if declare -F log >/dev/null 2>&1; then log "$*"; else echo "[$(date +%H:%M:%S)] $*" >&2; fi
395
+ if declare -F log >/dev/null 2>&1; then log "$*"; else echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >&2; fi
396
396
  }
397
397
  _sa_resolve_uv() {
398
398
  local c
@@ -30,7 +30,7 @@ LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
30
30
  mkdir -p "$LOG_DIR"
31
31
  LOG_FILE="$LOG_DIR/link-edit-github-$(date +%Y-%m-%d_%H%M%S).log"
32
32
 
33
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
33
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
34
34
 
35
35
  RUN_START=$(date +%s)
36
36
  log "=== GitHub Link Edit Run: $(date) ==="
@@ -29,7 +29,7 @@ LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
29
29
  mkdir -p "$LOG_DIR"
30
30
  LOG_FILE="$LOG_DIR/link-edit-moltbook-$(date +%Y-%m-%d_%H%M%S).log"
31
31
 
32
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
32
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
33
33
 
34
34
  RUN_START=$(date +%s)
35
35
  log "=== Moltbook Link Edit Run: $(date) ==="
@@ -38,7 +38,7 @@ LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
38
38
  mkdir -p "$LOG_DIR"
39
39
  LOG_FILE="$LOG_DIR/link-edit-reddit-$(date +%Y-%m-%d_%H%M%S).log"
40
40
 
41
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
41
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
42
42
 
43
43
  RUN_START=$(date +%s)
44
44
  log "=== Reddit Link Edit Run: $(date) ==="
@@ -25,7 +25,7 @@ BATCH_ID="lipres-$(date +%Y%m%d_%H%M%S)-$$"
25
25
  export SA_CYCLE_ID="$BATCH_ID"
26
26
  export S4L_PIPELINE_NAME="linkedin-presence"
27
27
 
28
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
28
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
29
29
 
30
30
  DRY_RUN="${LINKEDIN_PRESENCE_DRY_RUN:-0}"
31
31
 
package/skill/lock.sh CHANGED
@@ -121,11 +121,11 @@ if [ -z "${_SA_LOCK_DIRS+x}" ]; then
121
121
  # is defect "owner=OTHER" (wipes a live peer's lock -> double-hold).
122
122
  if _sa_we_own_lock "$d"; then
123
123
  _sa_lock_event trap_rm "$_lname" "$(_sa_lock_owner_tag "$d")"
124
- echo "[lock] trap-released $_lname pid=$$ at $(date +%H:%M:%S)" >&2
124
+ echo "[lock] trap-released $_lname pid=$$ at $(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2
125
125
  rm -rf "$d"
126
126
  else
127
127
  _sa_lock_event trap_rm_skipped "$_lname" "$(_sa_lock_owner_tag "$d")"
128
- echo "[lock] trap-release SKIPPED $_lname pid=$$ (not owner) at $(date +%H:%M:%S)" >&2
128
+ echo "[lock] trap-release SKIPPED $_lname pid=$$ (not owner) at $(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2
129
129
  fi
130
130
  done
131
131
  for t in ${_SA_LOCK_TICKETS[@]+"${_SA_LOCK_TICKETS[@]}"}; do
@@ -149,7 +149,7 @@ if [ -z "${_SA_LOCK_DIRS+x}" ]; then
149
149
  _sa_exit_on_signal() {
150
150
  local _sig="$1" _code="$2"
151
151
  _sa_lock_event signal_exit "-" "signal=$_sig"
152
- echo "[lock] caught SIG${_sig} pid=$$; releasing locks and exiting $_code at $(date +%H:%M:%S)" >&2
152
+ echo "[lock] caught SIG${_sig} pid=$$; releasing locks and exiting $_code at $(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2
153
153
  _sa_release_locks
154
154
  trap - EXIT
155
155
  exit "$_code"
@@ -247,7 +247,7 @@ acquire_lock() {
247
247
  [ "$_existing" != "$ticket_file" ] && _new_t+=("$_existing")
248
248
  done
249
249
  _SA_LOCK_TICKETS=(${_new_t[@]+"${_new_t[@]}"})
250
- echo "[lock] acquired $name pid=$$ at $(date +%H:%M:%S) waited=${waited}s" >&2
250
+ echo "[lock] acquired $name pid=$$ at $(date -u +%Y-%m-%dT%H:%M:%SZ) waited=${waited}s" >&2
251
251
  _sa_lock_event acquired "$name" "waited=${waited}s"
252
252
  break
253
253
  fi
@@ -694,10 +694,10 @@ release_lock() {
694
694
  if _sa_we_own_lock "$lock_dir"; then
695
695
  _sa_lock_event release "$name" "$(_sa_lock_owner_tag "$lock_dir")"
696
696
  rm -rf "$lock_dir"
697
- echo "[lock] released $name pid=$$ at $(date +%H:%M:%S)" >&2
697
+ echo "[lock] released $name pid=$$ at $(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2
698
698
  else
699
699
  _sa_lock_event release_skipped "$name" "$(_sa_lock_owner_tag "$lock_dir")"
700
- echo "[lock] release SKIPPED $name pid=$$ (not owner) at $(date +%H:%M:%S)" >&2
700
+ echo "[lock] release SKIPPED $name pid=$$ (not owner) at $(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2
701
701
  fi
702
702
  # Rebuild the lock stack without this entry so the EXIT trap doesn't try to
703
703
  # rm it again (harmless, but keeps the stack honest if release_lock is paired
@@ -31,5 +31,5 @@ python3 "$REPO_DIR/scripts/precompute_dashboard_stats.py"
31
31
  EXIT_CODE=$?
32
32
  RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
33
33
 
34
- echo "[$(date +%H:%M:%S)] === done in ${RUN_ELAPSED}s (exit=${EXIT_CODE}) ==="
34
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] === done in ${RUN_ELAPSED}s (exit=${EXIT_CODE}) ==="
35
35
  exit "$EXIT_CODE"
@@ -19,7 +19,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
19
19
  mkdir -p "$LOG_DIR"
20
20
  LOG_FILE="$LOG_DIR/refresh-instagram-tokens-$(date +%Y-%m-%d_%H%M%S).log"
21
21
 
22
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
22
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
23
23
  log "=== refresh-instagram-tokens fire: $(date) ==="
24
24
 
25
25
  RUN_START=$(date +%s)
@@ -16,7 +16,7 @@ set -uo pipefail
16
16
  LOG_DIR="$HOME/social-autoposter/skill/logs"
17
17
  mkdir -p "$LOG_DIR"
18
18
  LOG_FILE="$LOG_DIR/refresh-twitter-following-$(date +%Y-%m-%d_%H%M%S).log"
19
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
19
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
20
20
 
21
21
  REPO_DIR="$HOME/social-autoposter"
22
22
 
@@ -36,7 +36,7 @@ if [ -f "$REPO_DIR/.env" ]; then
36
36
  set +a
37
37
  fi
38
38
 
39
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
39
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
40
40
 
41
41
  log "starting daily human-style synthesizer"
42
42
 
@@ -36,7 +36,7 @@ if [ -f "$REPO_DIR/.env" ]; then
36
36
  set +a
37
37
  fi
38
38
 
39
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
39
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
40
40
 
41
41
  # Run accounting for dashboard Job History (Post Threads · Instagram).
42
42
  # Each exit site updates POSTED_CT / SKIPPED_CT / FAILED_CT; the EXIT trap
@@ -43,7 +43,7 @@ if [ -f "$REPO_DIR/.env" ]; then
43
43
  set +a
44
44
  fi
45
45
 
46
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
46
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
47
47
 
48
48
  # Run accounting for dashboard Job History (Render · Instagram). Matches the
49
49
  # pattern in run-instagram-daily.sh / run-twitter-threads.sh / run-reddit-threads.sh:
@@ -40,7 +40,7 @@ mkdir -p "$LOG_DIR"
40
40
  # State: ~/.claude/social-autoposter/linkedin.killswitch
41
41
  # Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
42
42
  if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
43
- echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Skipping run-linkedin." \
43
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Skipping run-linkedin." \
44
44
  >> "$LOG_DIR/launchd-linkedin-stderr.log"
45
45
  exit 0
46
46
  fi
@@ -45,7 +45,7 @@ SOCIAL_ID=""
45
45
  REPLY_TO=""
46
46
  DO_POST=0
47
47
 
48
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
48
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
49
49
 
50
50
  while [ $# -gt 0 ]; do
51
51
  case "$1" in
@@ -85,7 +85,7 @@ LINKEDIN_BACKEND="${LINKEDIN_BACKEND:-browser}"
85
85
  # Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
86
86
  # Only gates the browser backend — UniPile has no headed session to compromise.
87
87
  if [ "$LINKEDIN_BACKEND" = "browser" ] && [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
88
- echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
88
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
89
89
  echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
90
90
  exit 0
91
91
  fi
@@ -43,7 +43,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
43
43
  mkdir -p "$LOG_DIR"
44
44
  LOG_FILE="$LOG_DIR/run-reddit-search-$(date +%Y-%m-%d_%H%M%S).log"
45
45
 
46
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
46
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
47
47
 
48
48
  log "=== Reddit Search Post Run: $(date) ==="
49
49
 
@@ -88,7 +88,7 @@ _parse_post_results() {
88
88
  # Bug observed 2026-05-08: a leaked "[14:57:04] Post phase: ..." line
89
89
  # produced "TOTAL_POSTED + [14:57:04]: syntax error" and `set -e` aborted
90
90
  # the script BEFORE the discover lane ran.
91
- echo "[$(date +%H:%M:%S)] Post phase: exit code $rc; counting as failed." >> "$LOG_FILE"
91
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Post phase: exit code $rc; counting as failed." >> "$LOG_FILE"
92
92
  echo "0 1"
93
93
  fi
94
94
  }
@@ -33,7 +33,7 @@ RUN_START_EPOCH=$(date +%s)
33
33
  # calls added in 5e41d96 (2026-05-10) fall through to macOS /usr/bin/log,
34
34
  # which barfs `Unknown subcommand` and ERR-traps the run at exit=64 before
35
35
  # the browser lease is ever acquired (silent kill since 2026-05-10).
36
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
36
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
37
37
 
38
38
  # Diagnostic: log the failing line and command before set -e kills the script.
39
39
  # Without this, silent deaths (e.g., Claude exits non-zero inside the $() below)
@@ -174,7 +174,7 @@ if [ -f "$REPO_DIR/.env" ]; then
174
174
  set +a
175
175
  fi
176
176
 
177
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
177
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
178
178
 
179
179
  log "=== Twitter Cycle (batch=$BATCH_ID): $(date) ==="
180
180
  log "Logic=D (no-ripen + 1h freshness + 2k_view_cap; experiment concluded 2026-05-31); discover_freshness=${FRESHNESS_HOURS_DISCOVER}h"
@@ -16,7 +16,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
16
16
  mkdir -p "$LOG_DIR"
17
17
  LOG_FILE="$LOG_DIR/scan-instagram-replies-$(date +%Y-%m-%d_%H%M%S).log"
18
18
 
19
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
19
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
20
20
  log "=== scan-instagram-replies fire: $(date) ==="
21
21
 
22
22
  RUN_START=$(date +%s)
@@ -37,7 +37,7 @@ LOG_FILE="$LOG_DIR/sentry-digest-$TS.log"
37
37
  SCAN_FILE="$LOG_DIR/sentry-digest-scan-$TS.json"
38
38
  OUTCOME_FILE="$LOG_DIR/outcome-sentry-digest-$TS.json"
39
39
 
40
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
40
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
41
41
 
42
42
  cd "$REPO_DIR"
43
43
 
@@ -15,7 +15,7 @@ LOG_DIR="$REPO_DIR/skill/logs"
15
15
  mkdir -p "$LOG_DIR"
16
16
  LOG_FILE="$LOG_DIR/stats-instagram-$(date +%Y-%m-%d_%H%M%S).log"
17
17
 
18
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
18
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
19
19
  log "=== stats-instagram fire: $(date) ==="
20
20
 
21
21
  RUN_START=$(date +%s)
@@ -55,7 +55,7 @@ set -euo pipefail
55
55
  # State: ~/.claude/social-autoposter/linkedin.killswitch
56
56
  # Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
57
57
  if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
58
- echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
58
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
59
59
  echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
60
60
  exit 0
61
61
  fi
@@ -85,7 +85,7 @@ SCRAPER_TIMEOUT_SEC=900 # 15min outer gtimeout. Inner JS deadline now defaults
85
85
 
86
86
  mkdir -p "$LOG_DIR"
87
87
  LOG_FILE="$LOG_DIR/stats-linkedin-$(date +%Y-%m-%d_%H%M%S).log"
88
- log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
88
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG_FILE"; }
89
89
 
90
90
  RUN_START=$(date +%s)
91
91
  log "=== LinkedIn Stats Run (unified): $(date) ==="
package/skill/stats.sh CHANGED
@@ -87,7 +87,7 @@ mkdir -p "$LOG_DIR"
87
87
  LOG_TAG="${PLATFORM:-all}"
88
88
  LOGFILE="$LOG_DIR/stats-${LOG_TAG}-$(date +%Y-%m-%d_%H%M%S).log"
89
89
 
90
- log() { echo "[$(date +%H:%M:%S)] $*" >> "$LOGFILE"; echo "[$(date +%H:%M:%S)] $*"; }
90
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOGFILE"; echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
91
91
 
92
92
  RUN_START=$(date +%s)
93
93
  STEP1_EXIT=0; STEP2_EXIT=0; STEP3_EXIT=0; STEP4_EXIT=0
@@ -36,5 +36,5 @@ RUN_START=$(date +%s)
36
36
  EXIT_CODE=$?
37
37
  RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
38
38
 
39
- echo "[$(date +%H:%M:%S)] === done in ${RUN_ELAPSED}s (exit=${EXIT_CODE}) ==="
39
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] === done in ${RUN_ELAPSED}s (exit=${EXIT_CODE}) ==="
40
40
  exit "$EXIT_CODE"