notify

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package notify is §10's local notifications: the OS notifier, the mapping from engine event kinds to a short title and one-line body, the delivery loop that walks the event log once per event, and the optional webhook (notify.webhook) that POSTs the same moments as plain JSON.

The package is deliberately generic — nothing here knows how events are produced, only how the log's rows read. Further remote channels (Telegram and the like) ride the relay a webhook already covers: the POST is plain JSON, with no per-service adapters.

Index

Constants

View Source
const (
	KindPlanDone               = "plan_done"                 // the plan's last open task finished (§6.2)
	KindTaskCheckFailed        = "task_check_failed"         // the check failed; the task waits for the foreman (§3)
	KindTaskNeedsHelp          = "task_needs_help"           // the worker asked its question (§3)
	KindTaskFailed             = "task_failed"               // the run failed; the foreman decides (§3)
	KindReviewChanges          = "review_changes"            // the reviewer wants changes (§7)
	KindReviewNoVerdict        = "review_no_verdict"         // the review ended without a usable verdict (§7)
	KindTaskOutOfRounds        = "task_out_of_rounds"        // the task spent its rounds; arrives with the rounds work
	KindForemanReport          = "foreman_report"            // a done foreman turn's final message, its report to the user (§3, §10)
	KindForemanLimited         = "foreman_limited"           // the foreman's CLI hit its subscription limit (§4.4, §10)
	KindForemanWakeBudgetSpent = "foreman_wake_budget_spent" // the day's foreman wake budget is spent (§4.3, §10)
	KindAgentBroken            = "agent_broken"              // an agent was marked broken (§8 "Health", §10)
	KindPlanUnattended         = "plan_unattended"           // a parked plan the foreman does not hold (§10)
	KindTaskUnattended         = "task_unattended"           // a task parked at needs_foreman that the foreman does not hold (§10)
	// KindReviewUnservable: a task's queued review can never be taken —
	// no agent of another family offers review (§7, §10).
	KindReviewUnservable = "review_unservable"
	// KindPlanAwaitingApproval: a plan is over the approval limit and
	// waits for the user's yes (§6.3, §10).
	KindPlanAwaitingApproval = "plan_awaiting_approval"
)

The event kinds the engine's log can carry that wake the user (§10). The set is the notification vocabulary: a plan finishing, the foreman's report after a headless turn, the foreman's limit and its budget out (§4.4, §4.3), and the reasons a task parks at needs_foreman — the same kinds the "needs you" queue reads in `brigada status`. Everything else in the log never notifies.

View Source
const (
	ModeAuto = "auto"
	ModeNone = "none"
)

The config file's notify values (lifecycle's FileConfig): auto picks the OS tool on PATH, none silences notifications. Anything else is refused when the config is read.

Variables

This section is empty.

Functions

func Message

func Message(ev Event) (t, body string, ok bool)

Message maps one event to its notification's title and one-line body (§10). ok is false for every event kind that never wakes the user — the log's routine entries — which the delivery loop skips while still advancing its cursor past them.

func NameOf

func NameOf(n Notifier) string

NameOf is n's log name: the tool it shells out to, or none.

func ValidWebhookURL

func ValidWebhookURL(raw string) error

ValidWebhookURL reports whether raw is an https URL with a host — the shape a relay publishes — or an http one on a loopback host, where nothing crosses a network. The URL may carry a token, so plain http is refused anywhere else. The config reader refuses anything else, so a typo cannot point notifications nowhere.

Types

type Event

type Event struct {
	ID        int64
	Kind      string
	TaskID    int64 // 0 when the event names no task
	PlanID    int64 // 0 when the event names no plan
	PlanTitle string
	// JobID is the foreman job a foreman_report names (§9): the id the
	// pointer to the whole text names. 0 when the event names no job.
	JobID int64
	// Project is the event's project repo URL — the log's own project
	// reference first, else the plan its task or payload names. Empty
	// when no referent survives; the webhook carries it as-is.
	Project string
	// At is the log's created_at stamp: the time the webhook reports.
	At string
	// Reason is the payload's words for why the user is woken — the
	// check's tail, the worker's question, the review's findings —
	// already picked for the kind and flattened to one line, except a
	// report's, whose own lines the mapper cuts to the first.
	Reason string
	// Parked is true when the event parked its task (its payload names
	// needs_foreman or needs_user as the task's state); ParkedAtUser is
	// true when that state is needs_user. A needs-you kind wakes the user
	// only for a park at needs_user: a park at needs_foreman is the
	// foreman's, and task_unattended speaks when nobody handles it.
	Parked       bool
	ParkedAtUser bool
	// ParkKind is the kind of the parking event a task_unattended names;
	// the webhook's text reads it to keep a check's output off the relay.
	ParkKind string
}

Event is one log row in the shape the mapper needs: the delivery loop resolves the event's references (the task's plan, the title) and its payload's reason before mapping, so Message stays pure.

type Fake

type Fake struct {

	// Err is returned by every Notify while set.
	Err error
	// contains filtered or unexported fields
}

Fake is the recording notifier the tests stand in for the OS tools: every Notify appends to its list. Err, when set, is what every Notify returns — the delivery-loop tests use it to play a broken notifier.

func (*Fake) Name

func (f *Fake) Name() string

Name is the log name.

func (*Fake) Notes

func (f *Fake) Notes() []Note

Notes returns the notifications recorded so far, in order.

func (*Fake) Notify

func (f *Fake) Notify(_ context.Context, title, body string) error

Notify records the notification and returns f's Err.

type Loop

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

Loop is the delivery loop (§13): it polls the event log for rows past a persisted cursor and notifies each mapped event once. The cursor lives in the store's meta row, so a restart resumes exactly after the last delivered event — never re-notifying old ones, never missing new ones — and advances even past events whose notification failed, so one broken banner never repeats forever.

func NewLoop

func NewLoop(st *store.Store, n Notifier, log *slog.Logger) *Loop

NewLoop returns a delivery loop over st, delivering through n. A nil logger keeps slog's default.

func (*Loop) Flush

func (l *Loop) Flush(ctx context.Context) error

Flush delivers everything pending, in passes of batch events: read the cursor and the events past it, notify the mapped ones — advancing the cursor after each event — then read again, until a pass finds nothing or the cap or ctx says stop. A failing notifier is logged and does not stop the loop; the store is what fails the flush.

func (*Loop) WithWebhook

func (l *Loop) WithWebhook(h *Webhook) *Loop

WithWebhook adds the optional webhook sink (§10): every notification the banner fires is also queued for one small JSON POST to the URL, beside the OS notification, never instead of it. nil h keeps the loop local-only. The posts ride a background worker on their own context, not the engine's tick: the loop queues and moves on, so a relay that hangs, fails or backs up never delays the sweep, the snapshot or the idle exit — fire and forget.

type None

type None struct{}

None is the notifier for a host with no banner tool, or for `notify = none`: it drops every notification and reports success, so the delivery loop above it runs unchanged — the cursor keeps advancing and flipping the config to auto later never re-notifies ancient events.

func (None) Name

func (None) Name() string

Name is none's log name.

func (None) Notify

func (None) Notify(context.Context, string, string) error

Notify drops the notification.

type Note

type Note struct {
	Title string
	Body  string
}

Note is one recorded notification: what the banner would have shown.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, title, body string) error
}

Notifier delivers one local notification (§10). Title is the short name the OS banner shows bold; body is one line of detail. ctx bounds the attempt: shutdown and the delivery loop's per-pass deadline cancel it, and implementations cap it regardless, so a hung banner tool never stalls the engine's tick or its idle exit. An error means the banner did not reach the OS; the delivery loop logs it and goes on, so one broken notification never stalls or repeats forever.

func Detect

func Detect() Notifier

Detect picks the OS's notification tool from GOOS and PATH: osascript on macOS, notify-send on Linux, and the silent none when neither exists — a host with no banner service is normal, not an error.

func Resolve

func Resolve(mode string) Notifier

Resolve turns a config value into a Notifier: auto detects the OS tool, none (or the empty value, the default) silences. The result always implements Notifier, so callers never branch on the mode.

type NotifySend

type NotifySend struct {
	// Tool is the notify-send path; empty keeps "notify-send" (PATH lookup).
	Tool string
}

NotifySend is the Linux notifier: it calls notify-send, which takes the title and body as plain arguments — no shell, no interpolation.

func (NotifySend) Name

func (n NotifySend) Name() string

Name is the log name: the tool it shells out to.

func (NotifySend) Notify

func (n NotifySend) Notify(ctx context.Context, title, body string) error

Notify displays one banner through notify-send. The attempt is bounded by the caller's context, capped at execTimeout, and by the exec's own wait delay past the kill.

type Osascript

type Osascript struct {
	// Tool is the osascript path; empty keeps "osascript" (PATH lookup).
	Tool string
}

Osascript is the macOS notifier: it asks osascript to display a notification. Title and body reach the script only as the run handler's argv — after `--`, as whole arguments — never interpolated into the script source, so no text in a plan or task title can become AppleScript (TestOsascriptArgsGolden in notify_test.go pins the argv golden).

func (Osascript) Name

func (o Osascript) Name() string

Name is the log name: the tool it shells out to.

func (Osascript) Notify

func (o Osascript) Notify(ctx context.Context, title, body string) error

Notify displays one banner through osascript. The attempt is bounded by the caller's context, capped at execTimeout, and by the exec's own wait delay past the kill — a shutdown never waits on the exec past the hook's grace.

type Webhook

type Webhook struct {
	// URL is the relay's http or https address.
	URL string
	// Client posts; nil uses the default. Tests inject one.
	Client *http.Client
	// Timeout caps one attempt; zero keeps webhookTimeout.
	Timeout time.Duration
}

Webhook POSTs one small JSON document per delivered notification to a single URL (§10), set by the config file's notify.webhook — never by the project's own files, for the URL may carry a token. The URL is the one place the token lives: errors are stripped of it before they surface, so it never reaches a log line, the status or the history.

func (*Webhook) Post

func (w *Webhook) Post(ctx context.Context, ev Event, body string) error

Post delivers one notification's JSON for ev, body being the same one-line text the banner shows. A failed attempt is retried once — unless ctx already ended, when there is nothing to retry against. The returned error names no URL, so logging it leaks nothing.

Jump to

Keyboard shortcuts

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