Documentation
¶
Overview ¶
Package automation implements jcode Automations: scheduled and manual agent runs. The package is a leaf domain layer — it owns the data model, validation, scheduling math, persistence (two-file + flock), built-in templates, and the single-owner scheduler loop. It does NOT depend on web/tui/runner; callers inject a Runner to actually execute a run. See docs/automations-prd.md.
Index ¶
- Constants
- Variables
- func Badge(t Trigger) string
- func ComputeNextRun(after time.Time, t Trigger) (time.Time, bool)
- func ExecuteRun(ctx context.Context, store *Store, runner Runner, a *Automation, kind string) (string, error)
- func HumanSchedule(t Trigger) string
- func IsLocalPath(p string) bool
- func SlotKey(t time.Time) string
- func ValidateAutomation(a *Automation) error
- type Automation
- type Cadence
- type RunState
- type Runner
- type Scheduler
- type SkipNotifier
- type Store
- func (s *Store) Create(a Automation) (*Automation, error)
- func (s *Store) Delete(id string) error
- func (s *Store) Get(id string) *Automation
- func (s *Store) List() []*Automation
- func (s *Store) SetEnabled(id string, enabled bool) (*Automation, error)
- func (s *Store) State(id string) RunState
- func (s *Store) TryMarkRunning(id string) (bool, error)
- func (s *Store) Update(id string, mutate func(*Automation)) (*Automation, error)
- func (s *Store) UpdateState(id string, mutate func(*RunState)) error
- func (s *Store) UpdateStateAndMaybeDisable(id string, mutate func(*RunState)) (bool, error)
- type Template
- type Trigger
- type TriggerType
Constants ¶
const ( StatusRunning = "running" StatusSuccess = "success" StatusError = "error" StatusInterrupted = "interrupted" StatusSkipped = "skipped" )
Run terminal-status values (mirrored onto session.SessionMeta.TerminalStatus).
const ( KindScheduled = "scheduled" KindManual = "manual" )
Trigger-kind values stamped onto a run's session.
const ( SourceManual = "manual" SourceAgent = "agent" )
Source values record how an automation was created.
const AutoDisableThreshold = 5
AutoDisableThreshold is the number of consecutive failures (missing project, provider gone, etc.) after which a scheduled automation auto-disables so it stops re-failing — and re-notifying — every night. (PRD §11 open item N.)
const ScheduledRunCeiling = 30 * time.Minute
ScheduledRunCeiling bounds a scheduled (headless) run's wall-clock time. This is a liveness bound, not a safety guardrail: with ask_user/automation_create excluded from headless runs, the remaining hang vector is an agent loop, and an unbounded run would hold an engine forever. Manual runs are not capped.
Variables ¶
var ErrNotFound = errors.New("automation not found")
ErrNotFound is returned (wrapped) when an operation targets an automation id that does not exist, so HTTP handlers can map it to 404 rather than 400.
Functions ¶
func Badge ¶
Badge renders a short cadence label for cards: "Daily" / "Weekly" / "Hourly" / "Manual".
func ComputeNextRun ¶
ComputeNextRun returns the next instant strictly after `after` that matches the trigger, evaluated in after's own location (the host's local tz). It is a pure function (no time.Now) so it is fully unit-testable, including DST transitions — see schedule_test.go.
DST handling: each candidate is built with time.Date, which normalizes non-existent wall-clock times (spring-forward, e.g. a 02:30 daily on the day the clock jumps 02:00→03:00 lands on a real instant). Fall-back (a slot that occurs twice) is deduped by the caller via SlotKey/LastFiredSlot, not here.
Returns ok=false for non-schedule triggers (manual never auto-fires).
func ExecuteRun ¶
func ExecuteRun(ctx context.Context, store *Store, runner Runner, a *Automation, kind string) (string, error)
ExecuteRun runs an automation through the runner and records terminal state. It blocks until completion. Shared by the scheduler (scheduled fires) and the manual ▶ path so state bookkeeping is identical. For scheduled runs, repeated errors increment ConsecutiveFails and auto-disable past the threshold.
func HumanSchedule ¶
HumanSchedule renders a trigger for display, e.g. "Daily at 09:00", "Weekly on Mon at 14:30", "Hourly at :05", "Manual".
func IsLocalPath ¶
IsLocalPath reports whether p is a usable local project path (non-empty, not a remote scheme). Used at fire time to skip+disable an automation pointed at a vanished or remote target.
func SlotKey ¶
SlotKey is a stable dedup key for a fire instant: the local calendar minute. Two fires at the same wall-clock minute (e.g. a DST fall-back repeat) share a key, so LastFiredSlot can suppress the duplicate.
func ValidateAutomation ¶
func ValidateAutomation(a *Automation) error
ValidateAutomation is the single validation rule shared by every creation path (HTTP API, agent tool, CLI) — mirroring tools.ValidateGoalObjective. It mutates nothing; callers assign ID/timestamps/defaults around it.
Key invariants (PRD §7.5, §10.4): a local non-empty ProjectPath is required (no-project headless runs are unsafe and unsupported); remote (ssh://docker://) targets are rejected for v1.
Types ¶
type Automation ¶
type Automation struct {
ID string `json:"id"`
Name string `json:"name"`
Prompt string `json:"prompt"`
Trigger Trigger `json:"trigger"`
ProjectPath string `json:"project_path"` // required, must be a local path
Mode string `json:"mode"` // approval|plan|full_access
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
RunInCloud bool `json:"run_in_cloud"` // reserved; always false in v1
Enabled bool `json:"enabled"`
Source string `json:"source"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
Automation is the user-edited definition. It persists in ~/.jcode/automations.json (low-frequency, human writes). Volatile scheduler bookkeeping lives separately in RunState (automation-state.json) so the scheduler's frequent writes never collide with human edits.
type RunState ¶
type RunState struct {
LastRunAt string `json:"last_run_at,omitempty"`
LastStatus string `json:"last_status,omitempty"` // running|success|error|interrupted|skipped
LastError string `json:"last_error,omitempty"`
LastSessionID string `json:"last_session_id,omitempty"`
NextRunAt string `json:"next_run_at,omitempty"`
LastFiredSlot string `json:"last_fired_slot,omitempty"` // SlotKey dedup guard (DST fall-back)
ConsecutiveFails int `json:"consecutive_fails,omitempty"`
}
RunState is the volatile per-automation scheduler bookkeeping. It persists in ~/.jcode/automation-state.json, written frequently by the scheduler and the run-completion callback — kept apart from Automation so the two write paths don't clobber each other.
type Runner ¶
type Runner interface {
StartRun(ctx context.Context, a *Automation, kind string) (sessionID string, err error)
}
Runner executes one automation run to completion. Implementations (internal/web) reuse the Engine: build a headless engine for the automation's project + mode, inject the prompt, and block until the agent is done. The returned sessionID identifies the recorded session.
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler owns periodic firing for the process that wins the election lock. Non-owner processes can still manage definitions and trigger manual runs (which bypass the scheduler entirely).
func NewScheduler ¶
NewScheduler builds a scheduler. interval<=0 defaults to 30s.
func (*Scheduler) Run ¶
Run blocks until ctx is cancelled. It first contends for the election lock; if another process owns it, Run returns immediately (this process won't fire scheduled runs, but manual runs still work). The flock is released by the OS on process exit, so a crashed owner never deadlocks the election.
func (*Scheduler) SetInterval ¶
SetInterval overrides the tick interval (used in tests).
func (*Scheduler) SetSkipNotifier ¶
func (s *Scheduler) SetSkipNotifier(fn SkipNotifier)
SetSkipNotifier registers a callback for skipped fires.
type SkipNotifier ¶
type SkipNotifier func(a *Automation, reason string)
SkipNotifier is called when the scheduler skips a fire without running (e.g. the bound project is gone). Optional.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists automations across processes. Writes take an OS file lock and re-read from disk before mutating, so concurrent jcode processes (web, TUI, CLI) never lose updates. Definitions and volatile run-state live in separate files so the scheduler's frequent state writes don't collide with human edits.
func NewStoreDir ¶
NewStoreDir opens a store rooted at an explicit directory (used by tests).
func (*Store) Create ¶
func (s *Store) Create(a Automation) (*Automation, error)
Create validates, assigns id/timestamps/defaults, and persists a new automation. The input is copied; the stored automation is returned.
func (*Store) Get ¶
func (s *Store) Get(id string) *Automation
Get returns a copy of the automation, or nil if not found.
func (*Store) List ¶
func (s *Store) List() []*Automation
List returns all automations sorted by creation time.
func (*Store) SetEnabled ¶
func (s *Store) SetEnabled(id string, enabled bool) (*Automation, error)
SetEnabled flips the enabled flag (re-enabling clears ConsecutiveFails via Update's shared reset, so a recovered automation isn't immediately re-disabled by the next single failure).
func (*Store) TryMarkRunning ¶
TryMarkRunning atomically claims a run for id: it sets LastStatus=running (clearing LastError, stamping LastRunAt) only if a run is not ALREADY in progress, returning whether the claim succeeded. This is the single authoritative guard against overlapping runs across the scheduler, manual "Run Now", and other processes — the local in-flight maps are only fast-path hints that can't see each other or another process. A crashed run left at "running" is cleared by the scheduler's reconcileStale on the next election.
func (*Store) Update ¶
func (s *Store) Update(id string, mutate func(*Automation)) (*Automation, error)
Update applies a mutation to an existing automation under lock, re-validating the result. The mutate callback receives a pointer it may modify in place.
Re-enabling (Enabled false -> true) also clears ConsecutiveFails so a recovered automation isn't immediately re-disabled by the next single failure. Centralizing the reset here means EVERY enable path gets it — the web UI's partial-patch PUT (handleUpdateAutomation), the CLI's SetEnabled, and any future caller — not just SetEnabled.
func (*Store) UpdateState ¶
UpdateState mutates only the volatile run-state file (never the definitions), so scheduler/run-completion writes can never clobber a concurrent human edit.
func (*Store) UpdateStateAndMaybeDisable ¶
UpdateStateAndMaybeDisable mutates the run-state (e.g. recording a failure and bumping ConsecutiveFails) and, in the SAME lock scope, disables the definition when ConsecutiveFails has reached AutoDisableThreshold. Folding the disable into the run-state mutation closes the TOCTOU window that exists when the disable is a separate SetEnabled(false) call: a concurrent successful run can no longer reset ConsecutiveFails between the threshold check and the disable. Returns whether the definition was disabled by this call.
type Template ¶
type Template struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Badge string `json:"badge"` // Daily|Weekly|Manual (display)
Prompt string `json:"prompt"`
Trigger Trigger `json:"trigger"`
SuggestMode string `json:"suggest_mode"`
}
Template is a built-in starting point shown on the Templates page. Selecting one pre-fills the editor; the user picks a project and confirms.
func BuiltinTemplates ¶
func BuiltinTemplates() []Template
BuiltinTemplates returns the curated templates (aligned with the reference UI).
type Trigger ¶
type Trigger struct {
Type TriggerType `json:"type"`
Cadence Cadence `json:"cadence,omitempty"`
Hour int `json:"hour,omitempty"` // 0-23, used by daily/weekly
Minute int `json:"minute,omitempty"` // 0-59, used by all cadences
Weekday int `json:"weekday,omitempty"` // 0=Sun..6=Sat, used by weekly
}
Trigger describes when an automation fires. Times are interpreted in the host's local timezone (see ComputeNextRun).
type TriggerType ¶
type TriggerType string
TriggerType is how an automation fires.
const ( // TriggerSchedule fires on a recurring wall-clock cadence. TriggerSchedule TriggerType = "schedule" // TriggerManual never fires automatically; only via an explicit run. TriggerManual TriggerType = "manual" )