Fixing Three Broken LaunchAgents: JADA Journal PATH Bug, Pulse TCC Failure, and Subprocess Timeout

What Was Done

Diagnosed and fixed two macOS LaunchAgent failures that caused JADA's Journal to post the same fallback entry 15 consecutive days and The Pulse to go 12 days without new articles. Both failures traced to environment configuration problems in the plist files — not bugs in the Python scripts themselves. Also fixed a 120-second subprocess timeout in both scripts that prevented plan-billed Claude from returning responses in time.

Root Cause #1 — JADA's Journal: Missing PATH in LaunchAgent

~/Library/LaunchAgents/com.queenofsandiego.jada-daily.plist had:

<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin</string>

The claude CLI lives at /Users/cb/.local/bin/claude (symlink to /Users/cb/.local/share/claude/versions/2.1.181). This directory was not in the LaunchAgent PATH, so every daily run hit: env: claude: No such file or directory.

The generate_entry() function in tools/jada_daily.py catches this exception and falls back to fallback_entry(), which has a hardcoded headline: "The World Was Loud, So I Listened From the Water". Since the fallback still deploys to S3 and writes a success stamp, jada_daily_state/YYYY-MM-DD.success exists for every day — masking the failure.

Fix: Added /Users/cb/.local/bin to the front of PATH in the plist:

<string>/Users/cb/.local/bin:/usr/local/bin:/usr/bin:/bin</string>

Reloaded: launchctl unload ... && launchctl load ...

Scrubbed 15 fallback entries from jada.html using a regex against the hardcoded headline. Deleted success stamps for the affected dates and re-ran jada_daily.py --date=2026-06-26.

Root Cause #2 — The Pulse: TCC Block on iCloud Drive

~/Library/LaunchAgents/com.jada.pulse-daily.plist was calling: /usr/bin/python3 — the Xcode Command Line Tools Python. This binary does not have Full Disk Access / iCloud Drive TCC permission granted in System Preferences, so every run hit: [Errno 1] Operation not permitted when trying to read /Users/cb/icloud-repos/sites/queenofsandiego.com/data/pulse_topics.csv.

Fix: Changed ProgramArguments to /usr/local/bin/python3 (Homebrew Python, which inherits the terminal's TCC permissions). Reloaded the agent.

Root Cause #3 — Subprocess Timeout Too Short (Both Scripts)

Both jada_daily.py and publish_pulse_daily.py call the claude CLI as a subprocess with timeout=120. The prompts are large (JADA system prompt alone is ~3,500 characters; with news context and instructions, the full prompt exceeds 6,000 characters). Plan-billed Claude with max_tokens=1200 on a large prompt can take 150–250 seconds.

Fix: Changed timeout=120timeout=300 in both scripts:

  • tools/jada_daily.py:289call_claude_plan()
  • tools/publish_pulse_daily.py:109 — article body generation

Content Recovery

After fixing the PATH and timeout, backfill was run for affected dates:

  • Journal: python3 tools/jada_daily.py --date=2026-06-26 — generated unique entry with headline "Before Hesitation: A Mother, an Island, and Seventeen-Year-Olds With a Plan"
  • Pulse: python3 tools/publish_pulse_daily.py --date YYYY-MM-DD run for 10 missing dates (Jun 15–17, Jun 20–26)

Key Lessons

  • LaunchAgent environment is not your shell environment. Always verify PATH, HOME, and any tool locations explicitly in the plist EnvironmentVariables dict.
  • TCC is per-binary, not per-user. /usr/bin/python3 and /usr/local/bin/python3 are different identities for TCC purposes.
  • Success stamps mask fallback failures. The jada_daily stamp is written whether the entry is Claude-generated or fallback — there's no way to tell from the filesystem which happened. The log at ~/logs/jada.log is the only reliable indicator.
  • Plan-billed Claude needs generous timeouts. 120s is too short for large prompts. 300s is a safe floor.

What's Next

  • Monitor ~/logs/jada.log June 27 morning to confirm automation runs correctly
  • Add a daily health check that alerts CB if the Claude headline matches the fallback string
  • The generate_owner_briefing() call uses the same 300s timeout — verify it completes (it was still running at session end)