Skip to content

launchd plist template

The framework’s scheduling layer. Three files, fully documented.

Each non-comment, non-blank line is a record with up to six tab-separated fields:

FieldWhat
1. labellaunchd job label (also the .plist filename stem)
2. scriptpython file in $HERMES_HOME/bin/ to invoke
3. scheduleone of: interval:<seconds> / cron:<HH>:<MM> (daily) / cron:<weekday>:<HH>:<MM> (weekly; 0=Sun)
4. needs_javayes or no. yes prepends openjdk@21 + fnm bins to PATH and sets JAVA_HOME
5. log_stembasename used for /tmp/<stem>.{stdout,stderr}. Empty falls back to label
6. roleone-line operational descriptor surfaced in alfred agents and Slack post prefixes

Example:

my.fleet.lucius lucius.py interval:1200 yes Feature developer
my.fleet.bane bane.py cron:2:00 yes Test coverage
my.fleet.gordon gordon.py cron:8:00 no Deploy health
my.fleet.weekly-cleanup cleanup.py cron:0:21:00 no my.fleet.cleanup Weekly cleanup

Tabs are required between fields. Trailing empty fields can be omitted.

render.sh substitutes these in _template.plist:

TokenSource
__LABEL__agents.conf field 1
__SCRIPT__agents.conf field 2
__SCHEDULE_BLOCK__rendered from field 3 (StartInterval or StartCalendarInterval)
__PATH__colon-joined PATH for EnvironmentVariables (varies by needs_java)
__JAVA_BLOCK__JAVA_HOME entry (empty when needs_java=no)
__GH_ORG_BLOCK__GH_ORG entry (omitted if env unset)
__HERMES_BIN__$HERMES_HOME/bin
__HERMES_HOME__resolved at render time
__WORKSPACE_ROOT__resolved at render time
__HOME__$HOME at render time
__LOG_STEM__agents.conf field 5 (or label if empty)
__AGENT_SHORT__label suffix, rendered as AGENT_CODENAME
__AGENT_ROLE_BLOCK__ALFRED_<CODENAME>_ROLE env var rendered from field 6 when present

launchd does not source shell rc files. The rendered plist calls agent-launch, which sources ~/.alfredrc at firing time and then execs the agent script from $HERMES_HOME/bin.

  1. Drop bin/<your-codename>.py into your fleet repo.
  2. Append a row to launchd/agents.conf.
  3. Run bash deploy.sh: renders + bootstraps.
  4. Verify with bash bin/doctor.sh.

Pause persists across deploy.sh invocations via marker files at $HERMES_HOME/state/_paused/<short-name> (where short-name is the label minus the <prefix>. prefix).

Terminal window
# Manual pause:
launchctl bootout "gui/$(id -u)/my.fleet.lucius"
mkdir -p $HERMES_HOME/state/_paused
date -u +"%Y-%m-%dT%H:%M:%SZ" > $HERMES_HOME/state/_paused/lucius
# Resume:
rm $HERMES_HOME/state/_paused/lucius
launchctl bootstrap "gui/$(id -u)" \
~/Library/LaunchAgents/my.fleet.lucius.plist

Each plist writes to /tmp/<log_stem>.stdout and /tmp/<log_stem>.stderr. Use tail -f /tmp/my.fleet.lucius.std{out,err} to watch a firing live.

/tmp/ is wiped on macOS reboot. The framework’s per-firing JSONL transcripts (under $HERMES_HOME/state/transcripts/) survive, so post-hoc analysis isn’t dependent on /tmp/.

The shipped template sets RunAtLoad = false, so bash deploy.sh does not immediately fire any agent. They wait for their scheduled trigger (or launchctl kickstart).

For immediate on-deploy firing of a specific agent, render its plist with RunAtLoad = true (edit _template.plist for that agent only). No per-agent override in agents.conf yet; tracked.