Expand description
Triggers: prompts that run on a schedule, unattended.
This is what turns the harness into an assistant rather than a REPL —
nobody types “check my inbox” every morning. A trigger is a prompt, a cron
schedule, and the policy an unattended run needs; everything that makes such
a run safe already existed (the outbox stages what would be sent, the
interlock refuses exfiltration, the sandbox confines shell, budgets bound
the spend, and the session recording feeds reflect). This module only adds
the clock and the ledger.
Four decisions carry the design:
Triggers live in the user’s own store, never in the layered config.
[[hook]], [[mcp]] and [[subagent]] are all declarable in a project’s
mecha.toml, which is a file that arrives with a cloned repository. A
trigger is a scheduled unattended agent run, and a repository that can
contribute one has been handed a cron slot on your machine. So they are
files under ~/.mecha/triggers/, one per trigger, and a trigger run reads
the global config only — crate::config::Config::load_global exists for
exactly this.
The schedule is answered backwards. “Is this due?” asks for the most
recent slot at or before now (crate::cron::Schedule::prev_at_or_before)
and compares it against the last slot that fired. A laptop closed for a week
therefore wakes up owing one briefing, not forty, and a tick that arrives
late has lost nothing — which is what lets the scheduler be a dumb
once-a-minute loop with no state of its own.
A manual run is evidence, not a fire. mecha trigger run briefing at
noon records a row with no slot, so it never advances the marker and never
cancels tomorrow morning’s. Testing a trigger must not silently disarm it.
Read-only unless the file says otherwise. Nobody is watching to approve
anything, and PermissionMode::Ask in that situation means “deny with a
message telling you to pass --yes”, which is useless advice at 03:00. A
trigger states its permission mode; the default is the narrow one, and
widening it is a line someone wrote down. Note what read-only does not
block: an outbox-routed call still stages, because staging executes nothing.
Draft-my-replies-overnight is the safe default shape, and it needs no
privilege at all.
Storage follows the outbox’s rules — one file per trigger so $EDITOR and
git diff work on it, temp-sibling-and-rename for every write, an advisory
flock for read-modify-write, and an append-only JSONL ledger of every fire.
Structs§
- RunLock
- Holds one trigger’s run lock — proof that no other run of it is in flight.
- RunMarker
- Who is running a trigger right now.
- RunRecord
- One line of the ledger.
- Store
Lock - Holds the store’s writer lock for as long as it lives.
- Trigger
- One scheduled prompt.
- Trigger
Store
Enums§
- CatchUp
- How long after a missed slot it is still worth running.
- Due
- RunStatus
- What happened on one fire.
Constants§
- DEFAULT_
TIMEOUT - How long a run may take before it is cancelled, when the trigger does not say. Long enough for a real briefing over a local model, short enough that a wedged run does not hold the scheduler until someone notices.
Functions§
- parse_
duration 90s,30m,2h,1d. A bare number is seconds.- render_
duration