Skip to Content
OperationsScheduling

Scheduling

One command, and then the thing that will actually bite you — which is not the cron syntax, it is which binary the scheduler runs.

karani hunt # install and load the agents (macOS launchd) karani unschedule # remove them

What gets installed

AgentIntervalWhat it does
com.karani.hourly1hOne hunt pass; timestamps a marker file
com.karani.daily06:00, 13:00Digest and worklist to Slack, in-process heartbeat
com.karani.watchdog1hsh and curl; alerts if the marker is stale
com.karani.ollama-envat loginRe-pins the Ollama environment, when qualify runs local

The hourly pass

karani hourly # run one by hand
karani hourly
karani hourly
hunting 9 sources
fetchedall sources1,656
droppedpre-filter1,597
qualified3 new rows above the fit floor--
draftedStaff Backend Engineer, Platform at Northwind Robotics
delivered1 review card to Slack
skipped2 rows: per-run draft cap reached--
pass complete in 11m42s: 1 pack drafted, $0.31 of $15.00 this month

Ingest every source, qualify the arrivals, draft packs for new top-fit roles, deliver review cards. A graph with per-node retry and a Slack alert on failure (ADR 0013).

Quiet by design: an hour with nothing new posts nothing. Summary pushes stay twice daily so the channel is not a feed.

Spending is triple-bounded — a fit floor, a per-run draft cap, and one shared daily budget across all 24 runs — on top of the monthly cap that every billed task obeys.

Every path in a launchd job must be absolute

launchd has no working directory. A relative path in a plist fails to spawn with exit 78, which looks nothing like “your path was relative”. karani hunt renders absolute paths into the agents it writes, and the thing to remember is only this: if you hand-edit a plist, do not use a relative path.

Every pass has a deadline

A hunt pass runs under a hard ceiling — 3000 seconds by default, clamped below the hourly tick.

This is not a performance guard, it is an availability one. karani once went silent for 40 hours while launchctl list showed the job present and exit code 0. The scheduler was not dead, it was running: one pass had blocked on a request to a local model server that restarted mid-request, the socket stayed open, the caller had no timeout, and launchd will not start a second instance of a job while the first is alive. One wedged call silenced the whole pipeline (ADR 0025).

So a pass that exceeds the deadline is abandoned, logged, and reported as a failed run with a non-zero exit — so the launchd record shows the lost hour rather than a silent success. The next tick starts clean.

The ceiling is at the pass boundary and not on each client on purpose. Which call hangs is unpredictable; a per-client timeout only protects the client somebody remembered to configure.

KARANI_PASS_DEADLINE_SECONDS=3000

Linux

karani hunt writes launchd agents, so on Linux wire the same commands into systemd timers or cron yourself:

# Hourly: one hunt pass. 0 * * * * /path/to/karani hourly >> /path/to/logs/hourly.log 2>&1 # Twice daily: summaries. 0 6,13 * * * /path/to/karani notify --kind digest >> /path/to/logs/daily.log 2>&1 0 6,13 * * * /path/to/karani notify --kind actions >> /path/to/logs/daily.log 2>&1 # Weekly: widen the staleness sweep. 0 4 * * 0 /path/to/karani sweep --days 14 >> /path/to/logs/sweep.log 2>&1

Use an absolute path to the binary, for the same reason launchd needs one.

The watcher is not scheduled

karani watch is deliberately a verb and not an agent. Wire it into the chain once you trust what it reports — and read a few passes with --no-slack first, because the thing worth checking is what it refuses to match.

Logs

logs/ in repo mode, ~/.karani/logs/ installed. The hourly pass timestamps logs/.last-pass on completion, which is the only file the watchdog reads.

Next

Monitoring and liveness — how to tell it is still alive, and why the watchdog is sh and curl and nothing else.

Last updated on