Help us shape the future of Go! Take the 2026 Go Developer Survey

watcher

package
v1.24.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package watcher defines watchers: scripts ask runs on a schedule. A run whose script prints nothing does nothing; a run whose script prints anything hands that output, with the watcher's instructions, to one model run — a chat session, a named agent, or a workflow.

This package owns the definitions and their store, the schedule grammar, the state every ask window shares, the lock that picks the one window that runs watchers, and the script runner. Running the model is the TUI's job (cmd/ask).

Index

Constants

View Source
const (
	// DefaultTimeout bounds a script that sets no timeout of its own.
	DefaultTimeout = 2 * time.Minute
	// MaxTimeout is the longest a script may run before it is killed.
	MaxTimeout = 30 * time.Minute
)
View Source
const (
	// MinInterval is the shortest interval a schedule may name.
	MinInterval = 10 * time.Second
	// MaxInterval is the longest.
	MaxInterval = 30 * 24 * time.Hour
)
View Source
const (
	EnvName     = "ASK_WATCHER_NAME"
	EnvStateDir = "ASK_WATCHER_STATE_DIR"
	EnvLastRun  = "ASK_WATCHER_LAST_RUN"
)

Environment a watcher script runs with, on top of ask's own.

View Source
const (

	// MaxRuns is how many runs each watcher keeps in the shared state.
	MaxRuns = 20
	// MaxStoredOutput caps the script output a run keeps for display; the
	// model gets the whole output, up to MaxOutputBytes.
	MaxStoredOutput = 16 * 1024
	// MaxStoredResponse caps the model's final answer a run keeps.
	MaxStoredResponse = 8 * 1024
	// MaxStoredStderr caps the tail of stderr a run keeps.
	MaxStoredStderr = 4 * 1024
)
View Source
const MaxOutputBytes = 100 * 1024

MaxOutputBytes caps the script output handed to the model; longer output keeps its head and tail.

Variables

View Source
var ErrNotFound = errors.New("watcher not found")

ErrNotFound is returned for a watcher with no definition.

View Source
var RunScript = runScript

RunScript runs one check. Swappable so tests never start a process.

Functions

func Clip

func Clip(s string, max int) string

Clip keeps at most max bytes of s, cut at a rune boundary, marking the cut.

func ClipTail

func ClipTail(s string, max int) string

ClipTail keeps at most the last max bytes of s.

func DefPath

func DefPath(r Ref) string

DefPath is where a watcher's definition is stored.

func Delete

func Delete(r Ref) error

Delete removes a watcher: its definition, script, scratch directory, and shared run state.

func Dir

func Dir() string

Dir is ~/.config/ask/watchers: a directory per project holding each watcher's <name>.json, its <name>.script when it has one, and its <name>.state/ scratch directory.

func MutateState

func MutateState(fn func(*State) error) error

MutateState runs fn on the shared state under a cross-process lock and writes the result.

func NextDue

func NextDue(d Def, st *Status) time.Time

NextDue is when the watcher's next check is due: what the running window last computed, else what the schedule says. Zero for a paused watcher or an unparseable schedule.

func ProjectDir

func ProjectDir(project string) string

ProjectDir is the store directory a project's watchers live in: the project's base name, sanitized, and a hash of its path, so two projects with one name never share it.

func ProjectOf

func ProjectOf(dir string) string

ProjectOf is the project a directory belongs to: the root of its git checkout — an ask worktree maps back to the checkout it was made from — else the directory itself.

func Prompt

func Prompt(d Def, res ScriptResult, at time.Time) string

Prompt is the first message of a watcher's model run: its instructions, then what its script printed. A workflow target gets the same text as the run's source.

func ReadScript

func ReadScript(r Ref) (string, error)

ReadScript returns a watcher's stored script, or "" when it runs a command.

func ScriptPath

func ScriptPath(r Ref) string

ScriptPath is where a watcher's stored script lives.

func StateDir

func StateDir(r Ref) string

StateDir is the directory a watcher's script may keep its own state in (what it reported last time, a cursor, a cache). The runner creates it and passes it as ASK_WATCHER_STATE_DIR; deleting the watcher removes it.

func StatePath

func StatePath() string

StatePath is ~/.config/ask/watchers.state.json.

func Summary

func Summary(d Def, st *Status) string

Summary describes what a watcher is doing, in a sentence, for the agent.

func ValidName

func ValidName(name string) error

ValidName reports whether name can name a watcher: lowercase letters, digits, dashes, and underscores, starting with a letter or digit, at most 63 characters. Names are file names in the store.

Types

type Def

type Def struct {
	Project      string    `json:"project"`
	Name         string    `json:"name"`
	Description  string    `json:"description,omitempty"`
	Schedule     string    `json:"schedule"`
	Command      string    `json:"command,omitempty"`
	Cwd          string    `json:"cwd"`
	TimeoutSecs  int       `json:"timeoutSeconds,omitempty"`
	Instructions string    `json:"instructions,omitempty"`
	Agent        string    `json:"agent,omitempty"`
	Workflow     string    `json:"workflow,omitempty"`
	Provider     string    `json:"provider,omitempty"`
	Model        string    `json:"model,omitempty"`
	Paused       bool      `json:"paused,omitempty"`
	CreatedAt    time.Time `json:"createdAt"`

	// HasScript reports a script file stored with the definition. It is
	// derived from the store, never serialized.
	HasScript bool `json:"-"`
}

Def is one watcher. The script is either Command, run by the shell, or a script file stored beside the definition (HasScript), run directly.

A watcher belongs to a project: the project its working directory is in (ProjectOf), set by Save. Names are unique within a project; a watcher is named by its Ref (project and name), or as one string by its ID.

func Get

func Get(r Ref) (Def, error)

Get returns the watcher r names.

func List

func List() []Def

List returns every stored watcher in every project, by project then name. Malformed files, and files that disagree with where they are stored, are skipped.

func Resolve

func Resolve(project, name string) (Def, error)

Resolve finds a watcher by name: in project when it has one by that name, else the one watcher of that name anywhere. More than one elsewhere is ambiguous.

func Save

func Save(old Ref, d Def, script *string) (Def, error)

Save stores d in the project its working directory belongs to (which Save sets as d.Project). old is the watcher being edited, the zero Ref to create one. script replaces the stored script when non-nil (an empty string removes it); nil keeps whatever is stored. A rename, or a move to another project, carries the script, the scratch directory, and the shared run state along. HasScript is derived from the result, and the definition is validated against it.

func SetPaused

func SetPaused(r Ref, paused bool) (Def, error)

SetPaused pauses or resumes a watcher.

func (Def) ID

func (d Def) ID() string

ID is the watcher's key (Ref.ID).

func (Def) Ref

func (d Def) Ref() Ref

Ref is the watcher's reference.

func (Def) RunsLabel

func (d Def) RunsLabel() string

RunsLabel is what the watcher runs, for display: the command, or the stored script.

func (Def) Target

func (d Def) Target() TargetKind

Target reports what the watcher hands its output to.

func (Def) TargetLabel

func (d Def) TargetLabel() string

TargetLabel names the target for display: "chat session", "agent x", "workflow y".

func (Def) Timeout

func (d Def) Timeout() time.Duration

Timeout is how long the script may run.

func (Def) Validate

func (d Def) Validate() error

Validate checks a definition before it is stored.

type Lead

type Lead struct {
	// contains filtered or unexported fields
}

Lead is held by the one ask process that runs watchers. It is an advisory lock on ~/.config/ask/watchers.lock kept for as long as the process leads; the kernel drops it when the process dies, however it dies, so there is never a stale lead to clean up.

func TryLead

func TryLead() (*Lead, error)

TryLead takes the lead without waiting. It returns (nil, nil) when another process holds it.

func (*Lead) Release

func (l *Lead) Release()

Release gives the lead up.

type Leader

type Leader struct {
	PID   int       `json:"pid,omitempty"`
	Since time.Time `json:"since,omitempty"`
}

Leader identifies the ask process that runs watchers.

type Phase

type Phase string

Phase is what a watcher is doing right now.

const (
	PhaseIdle     Phase = ""
	PhaseScript   Phase = "script"
	PhaseModel    Phase = "model"
	PhaseApproval Phase = "approval"
)

type Ref

type Ref struct {
	Project string
	Name    string
}

Ref names a watcher: its project and its name in that project.

func (Ref) ID

func (r Ref) ID() string

ID is the watcher's key in the shared state and everywhere one string has to name it: "<project dir>/<name>".

type Run

type Run struct {
	ID          string    `json:"id"`
	StartedAt   time.Time `json:"startedAt"`
	EndedAt     time.Time `json:"endedAt,omitempty"`
	Status      RunStatus `json:"status"`
	Error       string    `json:"error,omitempty"`
	ExitCode    int       `json:"exitCode"`
	Output      string    `json:"output,omitempty"`
	OutputBytes int       `json:"outputBytes,omitempty"`
	Stderr      string    `json:"stderr,omitempty"`
	Target      string    `json:"target,omitempty"`
	Provider    string    `json:"provider,omitempty"`
	Model       string    `json:"model,omitempty"`
	// SessionID and SessionCwd locate the model run's session on disk, so
	// any window can show its transcript and continue it as a chat.
	SessionID  string  `json:"sessionID,omitempty"`
	SessionCwd string  `json:"sessionCwd,omitempty"`
	Response   string  `json:"response,omitempty"`
	CostUSD    float64 `json:"costUSD,omitempty"`
	CostKnown  bool    `json:"costKnown,omitempty"`
}

Run is one check that did something: its script failed, or it printed output that went to the model. Checks that printed nothing are not runs; they only move Status.LastCheck.

func (Run) Finished

func (r Run) Finished() bool

Finished reports whether the run has ended.

type RunStatus

type RunStatus string

RunStatus is where a run stands.

const (
	RunRunning RunStatus = "running"
	RunDone    RunStatus = "done"
	RunFailed  RunStatus = "failed"
	RunStopped RunStatus = "stopped"
)

type Schedule

type Schedule struct {
	// contains filtered or unexported fields
}

Schedule is a parsed watcher schedule, in one of three shapes:

every 5m                        an interval, counted from the end of the last run
every 10m weekdays 09:00-18:00  an interval that only runs inside those days and hours
daily 09:00                     times of day ("weekdays", "weekends", "mon,thu", "mon-fri" too)
hourly :15,:45                  minutes past every hour

Times are local. Whatever the shape, a run is never due until the previous one has ended: an interval counts from that end, and a time of day is the first one after it.

func ParseSchedule

func ParseSchedule(s string) (Schedule, error)

ParseSchedule parses a schedule string. See Schedule for the grammar.

func (Schedule) IsInterval

func (s Schedule) IsInterval() bool

IsInterval reports whether the schedule counts an interval from the end of the last run.

func (Schedule) Next

func (s Schedule) Next(lastEnd, anchor time.Time) time.Time

Next is when the next run is due: for an interval, its length after lastEnd (or anchor, the moment the watcher was created, when it never ran — so a new interval watcher runs straight away), moved into the schedule's days and hours; for times of day, the first one after lastEnd (or anchor). A due time in the past means "run now": missed runs are never made up, so at most one run follows.

func (Schedule) String

func (s Schedule) String() string

String is the schedule in its canonical spelling.

type ScriptResult

type ScriptResult struct {
	ExitCode int
	// Stdout is what the script printed, cut to MaxOutputBytes;
	// StdoutBytes is how much it printed in all.
	Stdout      string
	StdoutBytes int
	// Stderr is the tail of what the script wrote to stderr.
	Stderr    string
	TimedOut  bool
	Cancelled bool
	// Err is set when the script could not be started.
	Err error
}

ScriptResult is how a script ended and what it printed.

func (ScriptResult) Failed

func (r ScriptResult) Failed() bool

Failed reports whether the check failed: it did not start, timed out, was stopped, or exited non-zero.

func (ScriptResult) FailureText

func (r ScriptResult) FailureText() string

FailureText describes a failed check in a few words.

type ScriptSpec

type ScriptSpec struct {
	Dir string
	// Command runs through Shell -c; when empty, Path is executed directly.
	Command string
	Path    string
	Shell   string
	Env     []string
	Timeout time.Duration
}

ScriptSpec is one script invocation.

func SpecFor

func SpecFor(d Def, shell string, lastCheck time.Time) ScriptSpec

SpecFor builds the invocation for d: its command or stored script, in its working directory, with its timeout, and the watcher environment — its name, its scratch directory (created here), and when it last ran (RFC 3339, empty the first time).

type State

type State struct {
	Version  int                `json:"version"`
	Leader   Leader             `json:"leader"`
	Watchers map[string]*Status `json:"watchers,omitempty"`
}

State is shared by every ask window: the one that runs watchers writes it, the rest read it to show the same status, and any window may file a request (run now, stop, seen).

func LoadState

func LoadState() (State, error)

LoadState reads the shared state; a missing file is an empty state.

func (State) Clone

func (s State) Clone() State

Clone deep-copies the state so a reader can hold it without a lock.

func (*State) Watcher

func (s *State) Watcher(name string) *Status

Watcher returns the named watcher's status, creating it.

type StateStamp

type StateStamp struct {
	ModTime time.Time
	Size    int64
}

StateStamp identifies a version of the state file on disk, so a reader re-reads it only when it changed.

func CurrentStateStamp

func CurrentStateStamp() (StateStamp, bool)

CurrentStateStamp stats the state file; ok is false when it is missing.

type Status

type Status struct {
	Phase    Phase  `json:"phase,omitempty"`
	Activity string `json:"activity,omitempty"`
	// Approval names the tool a running model is waiting on the user to
	// approve.
	Approval string `json:"approval,omitempty"`
	// LastCheck is when the script last started; LastEnded when the last
	// check (and its model run) ended. The schedule counts from LastEnded.
	LastCheck time.Time `json:"lastCheck,omitempty"`
	LastEnded time.Time `json:"lastEnded,omitempty"`
	// LastNote describes the last check when it produced no run ("no
	// output"); empty when it did.
	LastNote string    `json:"lastNote,omitempty"`
	NextRun  time.Time `json:"nextRun,omitempty"`
	Runs     []Run     `json:"runs,omitempty"`
	// RunRequested and StopRequested are requests from any window to the
	// one that runs watchers: run now, and stop the run with that id.
	RunRequested  time.Time `json:"runRequested,omitempty"`
	StopRequested string    `json:"stopRequested,omitempty"`
	// SeenRun is the newest run the user has looked at.
	SeenRun string `json:"seenRun,omitempty"`
}

Status is one watcher's live state and recent runs.

func (*Status) AddRun

func (st *Status) AddRun(r Run)

AddRun records a new run, newest first, keeping MaxRuns.

func (*Status) LatestRun

func (st *Status) LatestRun() (Run, bool)

LatestRun is the newest run, if any.

func (*Status) Unseen

func (st *Status) Unseen() bool

Unseen reports whether the newest run finished and the user has not looked at it yet.

func (*Status) UpdateRun

func (st *Status) UpdateRun(id string, fn func(*Run)) bool

UpdateRun applies fn to the run with id; false when it is gone.

type TargetKind

type TargetKind int

TargetKind is what a watcher's output is handed to.

const (
	// TargetChat is a normal ask session running the watcher's instructions.
	TargetChat TargetKind = iota
	// TargetAgent is a named agent definition.
	TargetAgent
	// TargetWorkflow is a workflow run on the output.
	TargetWorkflow
)

Jump to

Keyboard shortcuts

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