automation

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
const (
	StatusRunning     = "running"
	StatusSuccess     = "success"
	StatusError       = "error"
	StatusInterrupted = "interrupted"
	StatusSkipped     = "skipped"
)

Run terminal-status values (mirrored onto session.SessionMeta.TerminalStatus).

View Source
const (
	KindScheduled = "scheduled"
	KindManual    = "manual"
)

Trigger-kind values stamped onto a run's session.

View Source
const (
	SourceManual = "manual"
	SourceAgent  = "agent"
)

Source values record how an automation was created.

View Source
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.)

View Source
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

View Source
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

func Badge(t Trigger) string

Badge renders a short cadence label for cards: "Daily" / "Weekly" / "Hourly" / "Manual".

func ComputeNextRun

func ComputeNextRun(after time.Time, t Trigger) (time.Time, bool)

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

func HumanSchedule(t Trigger) string

HumanSchedule renders a trigger for display, e.g. "Daily at 09:00", "Weekly on Mon at 14:30", "Hourly at :05", "Manual".

func IsLocalPath

func IsLocalPath(p string) bool

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

func SlotKey(t time.Time) string

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 Cadence

type Cadence string

Cadence is the recurrence granularity for a scheduled trigger.

const (
	CadenceHourly Cadence = "hourly"
	CadenceDaily  Cadence = "daily"
	CadenceWeekly Cadence = "weekly"
)

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

func NewScheduler(store *Store, runner Runner) *Scheduler

NewScheduler builds a scheduler. interval<=0 defaults to 30s.

func (*Scheduler) Run

func (s *Scheduler) Run(ctx context.Context)

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

func (s *Scheduler) SetInterval(d time.Duration)

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 NewStore

func NewStore() (*Store, error)

NewStore opens (and lazily creates) the automation store under ~/.jcode.

func NewStoreDir

func NewStoreDir(dir string) (*Store, error)

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) Delete

func (s *Store) Delete(id string) error

Delete removes an automation and its run-state.

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) State

func (s *Store) State(id string) RunState

State returns a copy of the run-state for an automation (zero value if none).

func (*Store) TryMarkRunning

func (s *Store) TryMarkRunning(id string) (bool, error)

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

func (s *Store) UpdateState(id string, mutate func(*RunState)) error

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

func (s *Store) UpdateStateAndMaybeDisable(id string, mutate func(*RunState)) (bool, error)

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"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL