CLI
Every
dotagentsubcommand, what it does, and a minimal example.
dotagent <COMMAND> [ARGS] [FLAGS]Run dotagent --help for the cheat-sheet, dotagent <command> --help for per-command flags. Every subcommand documented below mirrors the clap-generated help.
Run a single schedule of an agent in the foreground.
One-shot dispatch pass (what the daemon does on each cycle).
Long-lived adaptive scheduler. Invoked by launchd / systemd.
Raw JSONL bridge to the daemon's local Unix socket.
Textual health dashboard.
Send the end-of-day health summary.
Mark every schedule's window as ok (one-shot, post-install). Not implemented yet.
Generate + write the daemon unit file (launchd / systemd).
Remove the daemon unit file.
Validate manifests, resolve plugin references, warn on drift.
List discovered plugins.
Invoke a plugin manually for debugging. Not implemented yet.
Verify the audit log's hash chain and report how far back the guarantee reaches.
Tail the daemon-captured stdout/stderr for one or all agents.
Dump heartbeat + manifest hash + schedule state for one agent.
Send SIGHUP to the running daemon.
Force-run an agent immediately, ignoring schedule windows.
Serve every discovered agent as an MCP tool over stdio.
Read and curate the long-term memory workspace.
Print a shell completion script (bash / zsh / fish / elvish / powershell).
run
Run a single schedule of an agent in the foreground. Useful for development — no daemon involvement, no on_* plugin hooks, no notifiers fire.
NAME
The agent's agent.name (not the directory name).
--schedule ID
Which schedule to use — picks the args and the slug from this schedule.
--dry-run
Inject AGENT_DRY_RUN=true. The script can decide what to do (typically: skip side effects). dotagent skips writing the heartbeat.
What it does: discovers the manifest, injects the env vars, spawns the command, captures stdout/stderr, prints stdout to your terminal, and exits with the agent's exit code. Notifiers and sinks do NOT fire — use run-now for that.
Example:
tick
One-shot dispatch pass. Same logic the daemon loop runs once, but without sleeping or installing signal handlers. Used by the daemon internally; exposed so you can debug "what would dotagent do RIGHT NOW".
--dry-run
Don't actually spawn anything. Prints scanned N; would dispatch M; next event: ts.
--verbose
(Reserved — currently a no-op.)
Example:
When debugging "is my agent gonna fire?", run this. It tells you which schedules dotagent considers due.
daemon
The long-lived adaptive scheduler. You don't invoke this directly — launchd / systemd does, via the unit file dotagent install writes.
If you run it manually (e.g., for development), it stays in the foreground until you Ctrl+C (SIGINT) or kill it (SIGTERM). It:
Discovers manifests + plugins
Writes
state/daemon.pidInitializes structured JSON logging into
logs/daemon/dotagent.logInitializes OTel export if
[telemetry]is configuredLoops forever:
tick → sleep until next event → wake on SIGHUPCleans up the PID file on exit (Drop guard)
The daemon process responds to:
SIGHUP
Wake immediately, re-read manifests + plugins on the next tick.
SIGTERM
Graceful shutdown. Audit log gets a DaemonStopped entry.
SIGINT
Same as SIGTERM.
See guides/daemon-lifecycle.md for how to install / start / stop / reload the daemon end-to-end.
api
Bridge raw JSON Lines between stdin/stdout and the daemon's local Unix socket. The command does not parse, render, or persist messages, so it can be used as a backend for scripts or a TUI.
By default it connects to $DOTAGENT_HOME/api.sock (normally ~/.config/dotagent/api.sock). --socket overrides the path for tests or an explicit endpoint. Every input frame is forwarded with its original bytes and line ending; every socket frame is forwarded to stdout the same way.
When stdin reaches EOF, the client half-closes its request side and continues reading responses/events until the daemon closes the socket. Diagnostics go to stderr. There is no prompt or TUI rendering.
Example:
See Local Client API for the wire contract.
status
Textual health dashboard. Read-only — never writes to audit, never dispatches. Takes no flags.
Output:
One row per (agent, schedule) pair — inbox-triage above declares two schedules and gets two rows, each with its own health. The counter at the top counts rows, not agents: 2/5 there is five schedules across four agents.
Rows are ordered most-urgent-first: failing → degraded → stale → ok. Within a bucket, discovery order.
AGENT/SCHEDULE
agent.name + the schedule's id.
STATE
One of the four health states.
LAST RUN
finished_at_iso from the heartbeat — when the run ended, success or not. never if there is no heartbeat yet.
REASON
Why that state, see below.
Agents with monitor = false in their manifest are excluded (those are typically one-shot/manual examples).
Reading the REASON column
The state comes from the heartbeat and from the window file for the window currently due — the same file the daemon writes retries against. The reason tells you which of the two decided:
ok
ok
The due window succeeded on the first try.
ok
no window today · last success ok
Nothing is due — a weekday-only schedule on a Sunday, an hour outside hours — and the last run succeeded.
degraded
recovered after N attempts
The window did succeed, but burned N failed attempts first.
failing
N attempts, will retry
The window passed without success; the retry budget is not spent yet.
failing
gave up after N attempts
Retry budget exhausted — the window is marked given_up.
failing
window due Nmin ago, no attempt
The window is overdue and no window file exists — the daemon never dispatched. Usually means the daemon is not running.
stale
window missed Nmin ago (stale)
Older than stale_after_minutes; retrying is no longer useful.
stale
never ran
No heartbeat and no window due — the schedule has never fired.
The count is singular-aware: 1 attempt, 2 attempts.
N in recovered after N attempts counts failed attempts, not dispatches. The window file's attempts counter is bumped after every dispatch including the one that succeeded, so a first-try success lands on disk as attempts: 1 and still reports ok. See Health states.
For stale, an interval schedule is judged against the first window it missed after its last success, not the rolling window dispatch uses — so a schedule broken for weeks reports the full age.
Live subprocesses
When the daemon is running and has children alive, status appends a second table read from the supervisor snapshot (state/supervisor.json). It is omitted entirely when the snapshot is missing or empty.
KIND is one of agent, persistent, preflight, sink, notify, skill, plugin_info, plugin_validate. Rows sort by deadline pressure, highest first, and are flagged ⚠️ at ≥80% of the deadline and 🔴 at ≥100%.
A persistent row never gets a warning icon: its clock is the idle window before recycling, not a run running late, so reaching it is the pool working as designed. Those rows mark the clock idle, and their AGE is time since the last answer. See Lifecycle.
Agents launched by dotagent mcp or run-now run in that process, not under the daemon, so they never appear here.
The snapshot carries two fields this table deliberately does not show: pgid and command. They are identity proof for the next daemon's boot orphan reap, not information an operator reads at a glance. Full schema in paths.md.
daily-summary
Send the end-of-day health summary. The daemon delivers it once a day at [daily_summary].time (default 22:45 local) and schedules its own wake-up for that moment. grace_minutes (default 30) is the tail that still counts as on-time when the wake-up could not happen at all — laptop closed, machine off. Both are configurable; see [daily_summary].
--dry-run
Print the message and its destinations to stdout instead of delivering it.
Typing the command delivers even when enabled = false. That flag governs the daemon's scheduled delivery; someone who ran this asked for this one.
Destinations come from [[daily_summary.notifiers]], which takes the same entries a manifest's [[notifiers]] takes. With none configured the summary goes to the desktop driver — the only one that needs no address and sends nothing off the machine.
Output — the same classification status renders, folded into a message short enough for a phone. Healthy schedules collapse into the count; only the unhealthy ones are named, and empty buckets are dropped:
An all-green day is three lines: the header, N/N ok, and nothing else.
--dry-run appends where it would have gone:
Each delivery is audited as plugin_invoked with plugin: "notifier:<driver>", on failure as well as on success — a summary that reached nobody is answerable from dotagent audit instead of from silence.
bootstrap
Not yet implemented. Calling it returns
bootstrap — not yet implemented.
Intent: mark every schedule's "current window" as ok in one shot, so a fresh install doesn't trigger a flood of failing notifications for schedules that haven't had a chance to run yet.
Workaround for now: run the daemon for one full cycle of each schedule and it'll fill in.
install
Generate and write the daemon unit file. One unit per system — not one per agent.
--all
(Accepted for backwards compat — no-op, prints a notice.)
NAME
(Accepted for backwards compat — no-op, prints a notice.)
--all and NAME are accepted but ignored — dotagent now uses one daemon unit (run.avelino.dotagent) that manages every discovered manifest internally.
What gets written:
macOS:
~/Library/LaunchAgents/run.avelino.dotagent.plistRunAtLoad=true,KeepAlive=true,ThrottleInterval=10
Linux:
~/.config/systemd/user/run.avelino.dotagent.serviceRestart=always,RestartSec=10
The unit points at the currently-running dotagent binary (resolved via std::env::current_exe). If you move the binary, re-run install.
After writing the file, the command prints the platform-specific "Next step" line to actually load the unit:
uninstall
Remove the daemon unit file. Idempotent (no error if nothing to remove).
Same flag caveats as install — both are no-ops.
Does NOT stop the daemon if it's running — launchctl bootout / systemctl --user disable --now first if you want a clean stop.
doctor
Validate every discovered manifest, resolve plugin references, and warn on inconsistencies. Always safe to run — read-only.
What it checks:
Manifest TOML is parseable.
agent.nameand[run].commandare non-empty.Schedule ids are unique within each manifest.
Every plugin referenced by
[[preflight]]/[[on_success]]/[[on_failure]]/[[notifiers]] driver = "plugin"resolves to a binary on$PATH/ the discovery dirs.Each manifest has a
[security]section. (If absent, emits a warning — schema-only in v0; seesecurity/threat-model.md.)Compares each manifest's sha256 against
state/known_manifests.json. Mismatch → "manifest drift since last daemon run" warning.
Exit code: 0 if 0 errors, non-zero otherwise. Warnings do not trigger a non-zero exit.
Example:
doctor also reports inbound Telegram when [telegram] is present in config.toml — whether the ingress is on, and the two ways it can be half-configured:
It stays silent when the section is absent, since the ingress is off by default. A dispatcher_agent that does not resolve is a warning: every accepted message would fail after passing the allowlist.
It also prints where long-term memory lives:
A path set in [memory] workspace that holds no outl workspace is a warning — the default path is scaffolded automatically, a configured one is not.
And how many skills it found, across every search root:
A SKILL.md that fails to parse is a warning, not an error — nothing stops running, an assistant just answers without a procedure it should have had. Two skill names that sanitize to the same tool name are also warned about: only the first is callable, and a skill you wrote and cannot call is otherwise a mystery.
Commands get the same treatment, with one extra check. They carry two derived names — the MCP tool and the Telegram menu entry — and Telegram's [a-z0-9_]{1,32} has no hyphens, so two commands can be distinct in the catalog and collide in the menu:
The files are named, not just the commands: two names differing only by - versus _ are near-identical on screen, and the useful question is which one to rename. See Commands.
Manifests that fail to parse are listed with ✗ and counted as errors. They no longer abort the scan — the healthy agents are still reported below them.
plugin list
List every plugin referenced by any discovered manifest, with its resolved path + advertised version + kinds.
Output (tab-separated):
If a plugin is referenced but not on $PATH, you'll see (not found: ...). Run dotagent doctor for a friendlier report.
plugin invoke
Not yet implemented. Currently returns
plugin invoke <name> — not yet implemented.
Intent: run a plugin manually with a JSON payload. Until this lands, invoke directly:
See reference/plugin-protocol.md for the payload shape.
audit verify
Verify the hash chain in $DOTAGENT_HOME/audit.log and print what verification could actually establish.
The daemon runs the same check at boot, but it only asks yes/no. This asks how far back, which is the question a rotated log makes real: a history the operator pruned must not read the same as one somebody beheaded.
Verdicts:
chain intact from GENESIS
Every entry still on disk was checked, back to the first line ever written
0
chain intact since <ts>
Every entry checked links, and the oldest is a seam naming where the rest went. The text says whether that segment is still on disk (re-run with --full) or gone (retention — or evidence removed; the chain cannot tell those apart)
0
unexplained truncation
The oldest entry links to a hash nothing accounts for: no GENESIS, no seam. The head of the log was removed
1
chain broken at position N
A link mismatch, or a line nobody can parse, inside the data that is present. Names the segment and both hashes
1
Without --full the walk stops at the first seam, which is what the daemon does at boot — the live file is the only one that changes, so it is the cheap check worth running every time. --full is the one to run when you actually want the guarantee to reach GENESIS.
What the chain does and does not prove (short version: it catches partial edits, never a total rewrite) is in security/threat-model.md.
logs
Tail the daemon-captured stdout/stderr.
NAME (optional)
Tail one agent's logs. Omit to tail every agent at once (each chunk is prefixed by tail with ==> path <==).
-n LINES
Print the last N lines (default 50).
--follow / -f
tail -F-style follow. Survives rotation.
--schedule ID
(Reserved — currently unused.)
Reads from ~/.config/dotagent/logs/agents/<name>/<name>.log plus any rolled <name>.log.YYYY-MM-DD files (skips .gz — tail can't follow compressed files).
Examples:
For structured logs (the daemon's own tracing output) read ~/.config/dotagent/logs/daemon/dotagent.log directly:
See guides/observability.md for the log schema.
inspect
Dump heartbeat + manifest hash + schedule state for one agent.
Output:
Use this when "is the heartbeat fresh?" / "did the last run succeed?" is your question.
reload
Send SIGHUP to the running daemon. The daemon picks up new manifests and plugin changes on its next tick.
Reads ~/.config/dotagent/state/daemon.pid and sends SIGHUP. Fails if:
The PID file is missing (daemon not running).
The PID exists but the process is gone (stale pidfile).
If you swapped the dotagent binary itself, SIGHUP isn't enough — restart the daemon via launchctl/systemctl. See guides/daemon-lifecycle.md.
run-now
Force-run an agent immediately, ignoring schedule windows. Unlike run, this DOES fire preflight, sinks, and notifiers — it's a single-shot version of what the daemon would do.
NAME
The agent's agent.name.
--schedule ID
Which schedule's args to use. If omitted, uses the first schedule declared.
--json
Emit one machine-parseable JSON line instead of the human report.
Example:
A preflight abort names the plugin that stopped it instead of a run result:
Colour follows the TTY and honours NO_COLOR. Truncated output says so explicitly rather than trailing off.
Use this to:
Trigger an agent after fixing a problem (don't wait for the next window).
Manually exercise the full plugin chain (preflight → spawn → sink → notify).
mcp
Serve every discovered agent as an MCP tool over stdio. JSON-RPC 2.0, one object per line.
Takes no flags — the catalog comes from the manifests already on disk. Point any MCP client at it:
Example — list the catalog by hand:
The catalog is not only agents: skill-* tools carry the procedures found under ~/.config/dotagent/skills/ and ~/.claude/skills/, command-get and command-list resolve the commands under ~/.config/dotagent/commands/, and the five memory-* tools appear when [memory] is on. See Skills and Commands.
Agents run in this process, like run-now — not through the daemon — so the subprocess tree does not appear in dotagent status. State is keyed off the trigger-mcp slug so an on-demand run never overwrites the scheduled history of the same agent.
stdout carries protocol only; logging goes to stderr, so RUST_LOG=debug dotagent mcp stays safe to run under a client.
Full protocol reference: MCP server.
memory
Read and curate the long-term memory workspace — the same store dotagent mcp exposes to agents, reachable from a shell and without a running daemon. That last part is the point: consolidating a day's raw facts into durable ones is a job an agent should be able to do on a schedule, and it can only do that if the verbs exist outside the daemon process.
recall ranks by shared words first and recency second; an empty query lists the most recent facts. --topic asks the graph instead — every fact linked to that subject, which is the better question when you know the subject.
Example — find a fact, then correct it:
Every line leads with the fact's id, because the follow-up move takes it. The ×2 suffix means the fact was stated more than once — restating a fact reinforces the one already stored rather than filing a copy.
supersede keeps the old fact readable in its journal and stops recalling it; forget deletes outright. Correcting something that changed wants the first; removing something that should never have been stored wants the second.
stats reports what the store holds:
--json emits one JSON object per line, provenance included, for scripting.
Full reference: Memory.
completions
Print a shell completion script. The script wires dynamic completion of agent names: pressing <TAB> after dotagent run, inspect, run-now, logs, install, or uninstall runs dotagent _list-agents (a hidden helper) and lists every manifest currently on disk — no stale baked-in list to refresh.
SHELL
bash · zsh · fish · elvish · powershell
Dynamic agent-name completion is wired for
bash,zsh, andfish.elvish/powershellget subcommand + flag completion only.
Install:
Example:
Exit codes
0
Success.
1
Generic failure (manifest invalid, plugin not found, etc.).
124
dotagent run only — the agent timed out (SIGTERM + SIGKILL).
Anything else
dotagent run only — the agent's exit code is passed through.
Environment variables
dotagent reads a small set of env vars for configuration overrides (DOTAGENT_HOME, DOTAGENT_ROOT, DOTAGENT_PLUGIN_PATH, RUST_LOG, OTEL_EXPORTER_OTLP_HEADERS). See env-vars.md for the complete list.
dotagent INJECTS env vars into the agent subprocess (AGENT_NAME, AGENT_TMPDIR, etc.). Same doc.
Related
Daemon lifecycle — install / start / stop / reload
Troubleshooting — sintoma → diagnostic
Agent spec —
agent.tomlschemaPlugin protocol — for
plugin list/plugin invokeObservability — log format, OTel
Last updated
Was this helpful?