-
Notifications
You must be signed in to change notification settings - Fork 4
Daemon Guide
Note
ops-daemon is a single launchd-managed process that supervises seven sub-services. It replaced per-service launchd agents in v0.5.0, then gained briefing-pre-warm (v0.6.0) and early-install (v0.8.0).
flowchart LR
D[ops-daemon] --> B[briefing-pre-warm<br/>every 2 min]
D --> W[wacli-sync<br/>continuous]
D --> M[memory-extractor<br/>every 30 min]
D --> I[inbox-digest<br/>every 4 hours]
D --> S[store-health<br/>daily 9am]
D --> C[competitor-intel<br/>weekly Mon 10am]
D --> L[message-listener<br/>continuous]
B --> BC[daemon-cache.json]
W --> WH[~/.wacli/.health]
M --> ME[memories/]
I --> IL[inbox log]
S --> SR[store-health report]
C --> CR[competitor report]
L --> LL[message log]
classDef svc fill:#6366f1,color:#fff,stroke:#4338ca
classDef out fill:#22c55e,color:#fff,stroke:#15803d
classDef root fill:#8b5cf6,color:#fff,stroke:#6d28d9
class D root
class B,W,M,I,S,C,L svc
class BC,WH,ME,IL,SR,CR,LL out
| Service | Cadence | Purpose |
|---|---|---|
briefing-pre-warm |
every 2 min | Runs bin/ops-gather and caches dashboard data for /ops:go
|
wacli-sync |
continuous | Keeps WhatsApp Web connection alive, auto-backfills @lid chats |
memory-extractor |
every 30 min | Haiku 4.5 summarizes new wacli + gog data into memories/
|
inbox-digest |
every 4 hours | Ranks unread messages across channels, writes digest |
store-health |
daily at 09:00 | Shopify inventory / order / fulfillment sanity check |
competitor-intel |
weekly Mon 10:00 | Scrapes competitor signals, writes weekly brief |
message-listener |
continuous | Polls wacli/Telegram for new messages, writes to local event log |
Important
All services are local-only. message-listener never sends messages outbound on its own — it only writes to a log that the daemon reads. See Privacy and Security for the exact list of network targets.
Pre-v0.8.0 the daemon was installed at Step 5b — after channels, MCPs, and registry. That meant users waited ~10s for their first /ops:go because the cache was empty.
v0.8.0 moved daemon install to Step 2c, immediately after CLIs. The briefing-pre-warm service starts working the moment the plist loads, and continues to warm the cache while the rest of the setup wizard runs.
sequenceDiagram
autonumber
participant U as User
participant W as Setup Wizard
participant D as ops-daemon
participant G as ops-gather
participant C as /ops:go
U->>W: /ops:setup
W->>W: Step 1 · Pick sections
W->>W: Step 2 · Install CLIs
W->>D: Step 2c · Install daemon (early!)
activate D
D->>G: briefing-pre-warm tick #1
G-->>D: registry, PRs, CI, unread, revenue...
Note over W,D: Wizard continues in parallel
W->>W: Step 3 · Configure channels (60–180s)
D->>G: briefing-pre-warm tick #2 (+2 min)
W->>W: Step 4 · MCPs · 5 · Registry · 6 · Prefs
D->>G: briefing-pre-warm tick #N
W->>W: Step 5b · Reconcile daemon services
W-->>U: Setup complete
U->>C: /ops:go
C->>D: read daemon-cache.json
D-->>C: cached data (<3s)
deactivate D
Tip
By the time the wizard finishes, the cache has 3–8 warm-up cycles banked. /ops:go loads from cache in under 3 seconds instead of cold-gathering in 10+.
Two health files, one per concern.
Written by wacli-sync on every successful poll.
{
"status": "ok",
"last_sync": "2026-04-14T07:45:00Z",
"wacli_pid": 12345,
"message_count": 1420
}| Field | Meaning |
|---|---|
status |
ok · degraded · down
|
last_sync |
ISO timestamp of last successful WhatsApp poll |
wacli_pid |
PID of the running wacli process |
message_count |
Total messages in local SQLite DB |
Written by the supervisor. Lives at ~/.claude/plugins/data/ops-ops-marketplace/daemon-health.json.
{
"supervisor_pid": 54321,
"uptime_seconds": 87442,
"services": {
"briefing-pre-warm": { "status": "ok", "last_run": "2026-04-14T07:44:12Z" },
"wacli-sync": { "status": "ok", "last_run": "2026-04-14T07:45:00Z" },
"memory-extractor": { "status": "ok", "last_run": "2026-04-14T07:30:04Z" },
"inbox-digest": { "status": "ok", "last_run": "2026-04-14T04:00:00Z" },
"store-health": { "status": "ok", "last_run": "2026-04-14T09:00:00Z" },
"competitor-intel": { "status": "idle", "next_run": "2026-04-21T10:00:00Z" },
"message-listener": { "status": "ok", "last_event": "2026-04-14T07:44:58Z" }
}
}hooks/whatsapp-health-check.sh (registered in hooks.json) fires before any wacli command. It reads ~/.wacli/.health and:
| Condition | Behavior |
|---|---|
status === "ok" AND last_sync < 10 min
|
Proceeds silently |
status === "degraded" OR last_sync >= 10 min
|
Surfaces warning + restart instructions |
| Health file missing | Surfaces "daemon not running" + install instructions |
Warning
The hook fails closed. Skills warn instead of silently retrying with stale auth. This is intentional — per Rule 3 and the privacy policy, users must see every health regression rather than get misleading "no new messages" results.
bin/ops-brain runs inside briefing-pre-warm. Every 2 minutes it:
- Calls
bin/ops-gather— which probes registry projects, GitHub PRs, CI, AWS ECS, Shopify, Klaviyo, Meta Ads, unread counts, GSD phases - Writes the result to
daemon-cache.json - Detects urgent patterns (your name mentions, "urgent", "ASAP", "fire", "down", "broken") in unread messages
- Flips the
urgent_flagindaemon-health.jsonso/ops:gocan surface them instantly
Result: /ops:go reads one cached JSON file instead of spawning 8 parallel gather scripts.
After channels + registry are configured, Step 5b reconciles the daemon:
- Adds
wacli-syncif WhatsApp is configured - Adds
message-listenerif WhatsApp OR Telegram is configured - Adds
inbox-digestif >=2 channels are configured - Adds
store-healthif Shopify is configured - Adds
competitor-intelif/ops:marketingis selected - Verifies all services launch, prints a green-check summary
Because the daemon was installed at Step 2c with a minimal service list, Step 5b's job is to add channel-dependent services — not reinstall everything from scratch.
# Start (first-time install or after edits)
launchctl load ~/Library/LaunchAgents/com.claude-ops.daemon.plist
# Stop
launchctl unload ~/Library/LaunchAgents/com.claude-ops.daemon.plist
# Force restart (common fix for stuck services)
launchctl kickstart -k gui/$UID/com.claude-ops.daemon
# Check status
launchctl list | grep claude-ops~/.claude/plugins/cache/ops-marketplace/ops/<version>/scripts/ops-daemon.sh start
~/.claude/plugins/cache/.../scripts/ops-daemon.sh status
~/.claude/plugins/cache/.../scripts/ops-daemon.sh stop/ops:setup daemon # re-install, regenerate plist with current paths
/ops:doctor # auto-repair (runs ops-autofix + kicks the daemon)Primary log location:
~/.claude/plugins/data/ops-ops-marketplace/logs/ops-daemon.logFollow live:
tail -f ~/.claude/plugins/data/ops-ops-marketplace/logs/ops-daemon.logFilter by service:
grep briefing-pre-warm ~/.claude/plugins/data/ops-ops-marketplace/logs/ops-daemon.log
grep memory-extractor ~/.claude/plugins/data/ops-ops-marketplace/logs/ops-daemon.logNote
Logs are rotated at 10 MB with 3 backup files kept. Nothing in the log file is ever transmitted off-machine — see Privacy and Security.
Explicit list of non-behaviors
- No telemetry. No phone-home, no crash reports, no usage metrics.
-
No outbound network except to the user's own configured APIs (Anthropic for Haiku calls, WhatsApp's servers via
wacli, the user's Shopify store, etc.) -
No sending messages unless a skill (running in an interactive Claude Code session) explicitly calls
send_messagewith user confirmation. -
No disk-wide scans. Every read is a targeted path: registry projects,
~/.wacli/,~/.claude/plugins/data/ops-ops-marketplace/. -
No credential harvesting post-setup. Auto-scan runs only inside
/ops:setup.
| Symptom | See |
|---|---|
/ops:go is slow (>10s) |
Troubleshooting#daemon — pre-warm cache not updating |
launchctl load fails |
Troubleshooting#daemon — launchd errors |
wacli health degraded warning |
Troubleshooting#daemon — health file stale |
| Memories not extracting | Troubleshooting#memories-not-extracting + Memories System |
| Still stuck | Run /ops:doctor --verbose
|
-
Memories System — what the
memory-extractorservice produces - Privacy and Security — exact list of daemon network targets
- Plugin Rules — why the hook fails closed (Rule 3)
- Architecture — where the daemon sits in the overall system