Skip to main content

Module trigger

Module trigger 

Source
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.
StoreLock
Holds the store’s writer lock for as long as it lives.
Trigger
One scheduled prompt.
TriggerStore

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