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

fleet

package
v0.2.11 Latest Latest
Warning

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

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

Documentation

Overview

Package fleet is the session's registry of agent fleets (design 028 §6.5): which fleets exist, who is in them, and which board each one talks on.

THE PACKAGE BOUNDARY IS THE POINT, and it is the same one internal/board draws. This package must not import internal/actors or internal/tui, and it does no NATS, no I/O and no clock of its own. What it holds is a MODEL — a registry that can be tested exhaustively without standing anything up — while the actor around it owns subscriptions, persistence and the question of who is alive.

WHY THIS EXISTS AT ALL, since the fleet layer is a Python script and worked without it. The registry was `.rysh/fleet/<name>.json`: untracked, shared by every fleet in the workspace, and written by whichever `fleetctl` happened to run. Two failures are on record from live sessions — a sibling fleet's teardown DELETED a live fleet's manifest, taking the tool down for everyone; and `fleetctl worktree` reported `created: true` for a directory that did not exist. Both are registry-truth problems, and both get linearly worse with the number of fleets. The manifest stays as a cache; this is the authority.

Index

Constants

View Source
const (
	GoalOpen    = "open"
	GoalMet     = "met"
	GoalDropped = "dropped"
)

Goal states.

View Source
const (
	// GoalExecAdaptive looks every Min while anything is wrong, and backs off
	// toward Max while the fleet is on track.
	GoalExecAdaptive = "adaptive"
	// GoalExecFixed looks every Min.
	GoalExecFixed = "fixed"
	// GoalExecOff runs no executor for the fleet.
	GoalExecOff = "off"
)

Goal executor modes.

View Source
const (
	DefaultGoalExecMinSec = 300
	DefaultGoalExecMaxSec = 1800
)

Executor cadence defaults — the brief's "every 5 minutes or every 30".

View Source
const (
	// RetentionTTL is how long one fleet record lives without being rewritten.
	RetentionTTL = 90 * 24 * time.Hour
	// RetentionMaxBytes bounds the bucket on disk.
	RetentionMaxBytes = 8 << 20
)

Durability: the registry SURVIVES A DAEMON RESTART, and that is most of the reason it moved out of `.rysh/fleet/*.json` (design 028 §6.5).

One KV entry per fleet, keyed by name. Unlike the board's history there is no ordinal and no ordering to preserve — a fleet is a record that is overwritten in place, so a second writer here costs a lost UPDATE rather than deleted history. That is a real difference and it is why this file is short: the board's whole single-writer apparatus exists to protect an append-only sequence, and this is not one.

RETENTION IS DELIBERATELY LONGER THAN THE BOARD'S. Board history is monitoring and ages out in a week; a fleet's identity is what lets a human resume `claude -r <session_id>` in a worktree weeks later, and losing it turns a set of live agents into anonymous panes. It is bounded rather than unbounded because a session that registers and forgets fleets forever should still not grow without limit.

View Source
const (
	StateRegistered = "registered"
	StateUp         = "up"
	StateDown       = "down"
)

State is where a fleet is in its lifecycle.

REGISTERED IS NOT A LESSER FORM OF UP, it is the cheap half of the split: a registered fleet is a row, a board id and a roadmap — no panes, no claude sessions, no spend. Bringing it up is what costs; since R11 that cost is bounded by the session's token ceiling, not by a count of fleets here.

View Source
const (
	AnswerAgent = "agent" // the agent itself (default)
	AnswerScout = "scout" // a cheap model first; the agent only if it cannot tell
)

Who answers a check-in.

View Source
const (
	WorkIdle     = "idle"     // no task: available for work
	WorkProgress = "progress" // working on Task
	WorkBlocked  = "blocked"  // cannot continue until every BlockedBy is resolved
	WorkDone     = "done"     // finished Task; waiting for the next one
)

Work states — declared by the agent.

View Source
const (
	ActivityWorking = "working"
	ActivityIdle    = "idle"
	ActivityUnknown = "unknown" // the pane could not be read; never treated as idle
)

Activity — observed by the actor from the pane.

View Source
const (
	HealthOK           = "ok"
	HealthSilent       = "silent"       // never declared a status, idle, and past the check-in interval
	HealthStalled      = "stalled"      // says progress; idle on screen for >= the stall threshold
	HealthUnblocked    = "unblocked"    // says blocked; every blocker is resolved
	HealthDeadlock     = "deadlock"     // in a blocked_by cycle — nobody in it can move
	HealthAvailable    = "available"    // idle or done, and idle on screen: needs work
	HealthUnresponsive = "unresponsive" // ignored MaxMissedCheckins check-ins in a row
)

Health — derived from work, activity and the blocked_by graph.

View Source
const (
	// BlockerHuman is "waiting on the human". Common and legitimate: a founder
	// gate, a credential, a question only the human can answer.
	BlockerHuman = "human"
	// BlockerExternalPrefix marks anything else outside the fleet: "ext:ci",
	// "ext:D-14". Free text after the prefix, kept so the reason is visible.
	BlockerExternalPrefix = "ext:"
)

Blocker refs that are not members.

View Source
const (
	// PolicySmart (default) checks in only when something is wrong: a silent,
	// stalled or newly unblocked member. A member working with a fresh status
	// is never interrupted — every check-in costs a working agent a turn.
	PolicySmart = "smart"
	// PolicyAlways checks in with every member every interval.
	PolicyAlways = "always"
	// PolicyOff observes and derives health, and sends nothing.
	PolicyOff = "off"
)

Watch policies.

View Source
const (
	DefaultCheckinIntervalSec = 600
	DefaultStallSec           = 300
	// MaxMissedCheckins is how many unanswered check-ins make a member
	// unresponsive and escalate to its parent.
	MaxMissedCheckins = 2
)

Defaults. The interval is the founder's "every 10 minutes or so".

View Source
const (
	OpRegister     = "register"
	OpForget       = "forget"
	OpState        = "state"
	OpMemberUpsert = "member-upsert"
	OpMemberRemove = "member-remove"
	// OpStatus is a member declaring its work (design 054). PaneID is the
	// member; Name may be empty, meaning the pane's own fleet.
	OpStatus = "status"
	// OpWatch sets a fleet's check-in policy.
	OpWatch = "watch"
	// OpGoal sets or closes the fleet's goal or a member's (design 055).
	OpGoal = "goal"
	// OpGoalExec configures the fleet's goal executor.
	OpGoalExec = "goal-exec"
)

Update operations.

View Source
const DeclaredByScout = "scout"

DeclaredByScout marks a declaration the scout made for an agent.

View Source
const DefaultScoutModel = "haiku"

DefaultScoutModel is the scout's model when none is set: the claude CLI's alias for the current Haiku.

View Source
const ScoutMinConfidence = 0.7

ScoutMinConfidence is the least confidence at which a scout answer is used.

View Source
const ScoutSystemPrompt = `` /* 1153-byte string literal not displayed */

ScoutSystemPrompt is the scout's instruction.

Variables

View Source
var ErrNoRegistry = errors.New("fleet: the registry did not answer")

ErrNoRegistry means nobody answered. Callers MUST branch on it.

Functions

func BucketConfig

func BucketConfig(session string) *nats.KeyValueConfig

BucketConfig is the bucket the bus should create. It lives here, beside the retention constants, so the numbers and the thing they configure cannot drift apart.

func BucketName

func BucketName(session string) string

BucketName is the KV bucket for a session's fleet registry, namespaced by session exactly like every other bucket in internal/bus.

func BuildScoutPrompt added in v0.2.11

func BuildScoutPrompt(f *Fleet, m *Member, reason string, screen []string, posts []string, toolRunning bool, now int64) string

BuildScoutPrompt renders the evidence for one member.

func DurationText added in v0.2.11

func DurationText(d time.Duration) string

DurationText renders a duration for a message: 10m, 1h, 1h30m, 45s.

func MemberLabel added in v0.2.11

func MemberLabel(f *Fleet, paneID string) string

MemberLabel is the display name of a pane in f: its label, else pane-<8>.

func RenderGoals added in v0.2.11

func RenderGoals(f *Fleet, now int64) string

RenderGoals is `rysh fleet goal show`: the fleet goal, the executor's latest verdict and cadence, and every member's goal and progress.

func RenderStatus added in v0.2.11

func RenderStatus(f *Fleet, now int64) string

RenderStatus is the ledger of one fleet as a table: one row per member, declared work, observed activity, derived health, and the blocking graph in both directions. Pure, so `rysh fleet status` and `##fleet status` print the same thing and it is testable without a daemon.

func ScoutEligible added in v0.2.11

func ScoutEligible(c Checkin, w Watch) bool

ScoutEligible reports whether the scout may answer this check-in.

func ServeQueries

func ServeQueries(nc *nats.Conn, r *Registry, reconciledAt func() int64) (*nats.Subscription, error)

ServeQueries answers registry reads for as long as the returned subscription lives.

IT ANSWERS FROM THE CACHED REGISTRY AND NEVER CALLS BACK INTO THE WORKSPACE, and that is a correctness requirement rather than a performance choice. The WORKSPACE is a caller here — `##fleet list` runs inside it — and the fleet actor's reconcile asks the workspace for a snapshot. If this handler reconciled on demand, the workspace would block in Receive waiting for an answer that requires the workspace to answer a snapshot first: a deadlock broken only by a timeout, which would surface as "the registry did not answer" on a healthy session. Reconciliation therefore happens on the actor's own schedule, and the answer says how old it is.

func ServeUpdates

func ServeUpdates(nc *nats.Conn, r *Registry, onChange func(u Update, reply UpdateReply)) (*nats.Subscription, error)

ServeUpdates accepts registry writes. onChange is called after every accepted update, so the caller can persist — and, for a status, tell the board — without this file knowing what a KV or a board is.

func ValidWork added in v0.2.11

func ValidWork(w string) bool

ValidWork reports whether w is a declarable work state.

func ValidateName

func ValidateName(name string) error

ValidateName rejects a fleet name that cannot also be a board id.

ONE NAMESPACE, DELIBERATELY. A fleet's board id defaults to its name (design 028 §6.5), and a board id becomes a NATS subject token — so a fleet called "epic.07" would name a board nobody can subscribe to. Rejecting at registration is the only place this can be caught before the fleet exists; afterwards the failure is a board that is silently empty.

Types

type Checkin added in v0.2.11

type Checkin struct {
	Fleet   string
	BoardID string
	Member  Member // a copy, status included
	Reason  string // the member's health, or "scheduled" under PolicyAlways
	Missed  int    // unanswered check-ins before this one
}

Checkin is one check-in the actor should send.

type Fleet

type Fleet struct {
	Name       string `json:"name"`
	BoardID    string `json:"board_id"`
	Tab        string `json:"tab,omitempty"`
	Source     string `json:"source,omitempty"` // the epic doc it was cut from
	RoadmapDir string `json:"roadmap_dir,omitempty"`
	State      string `json:"state"`
	Created    int64  `json:"created,omitempty"` // unix millis, stamped by the caller

	// Watch is the fleet's check-in policy (design 054); nil means the
	// defaults (smart, every 10 minutes, stalled after 5).
	Watch *Watch `json:"watch,omitempty"`

	// Goal is what the whole fleet is for (design 055); nil means none set.
	Goal *Goal `json:"goal,omitempty"`
	// GoalExec configures the fleet's goal executor; nil means the defaults
	// (adaptive, every 5 to 30 minutes).
	GoalExec *GoalExec `json:"goal_exec,omitempty"`
	// GoalStatus is the executor's latest verdict (written by it only).
	GoalStatus *GoalStatus `json:"goal_status,omitempty"`

	Members []Member `json:"members"`
}

Fleet is one fleet: its identity, where its work is described, and who is in it.

func Normalise

func Normalise(f Fleet) Fleet

Normalise fills in what a caller may leave blank and returns the fleet it will be stored as. It does not validate; Register does.

type Goal added in v0.2.11

type Goal struct {
	Text     string `json:"text"`
	DoneWhen string `json:"done_when,omitempty"` // the acceptance line: how anyone can tell it is met
	DueAt    int64  `json:"due_at,omitempty"`    // unix millis; 0 = no deadline
	State    string `json:"state,omitempty"`     // open (default) | met | dropped
	SetBy    string `json:"set_by,omitempty"`    // pane id, or "human"
	SetAt    int64  `json:"set_at,omitempty"`
	MetAt    int64  `json:"met_at,omitempty"`
	Note     string `json:"note,omitempty"`
}

Goal is a fleet's or a member's goal.

func (*Goal) IsMet added in v0.2.11

func (g *Goal) IsMet() bool

IsMet reports whether g was met.

func (*Goal) IsOpen added in v0.2.11

func (g *Goal) IsOpen() bool

IsOpen reports whether g is a goal still being worked toward.

type GoalExec added in v0.2.11

type GoalExec struct {
	Mode   string `json:"mode,omitempty"`
	MinSec int    `json:"min_sec,omitempty"`
	MaxSec int    `json:"max_sec,omitempty"`
}

GoalExec configures a fleet's goal executor. Zero values mean the defaults.

func (*GoalExec) Resolved added in v0.2.11

func (g *GoalExec) Resolved() GoalExec

Resolved returns the config with every default filled in.

func (GoalExec) Validate added in v0.2.11

func (g GoalExec) Validate() error

Validate refuses a config the executor cannot run.

type GoalStatus added in v0.2.11

type GoalStatus struct {
	State      string   `json:"state"`
	Pct        int      `json:"pct"`                 // -1 unknown
	FinishAt   int64    `json:"finish_at,omitempty"` // critical-path finish; 0 unknown
	Unknown    int      `json:"unknown,omitempty"`   // open paths with no estimate
	Bottleneck string   `json:"bottleneck,omitempty"`
	Waiting    int      `json:"waiting,omitempty"`
	Summary    string   `json:"summary,omitempty"` // the report's first line
	Why        []string `json:"why,omitempty"`
	AssessedAt int64    `json:"assessed_at"`
	NextAt     int64    `json:"next_at,omitempty"`
	Sent       int      `json:"sent,omitempty"` // directives sent in that cycle
	// Members is the primary condition per member pane id.
	Members map[string]string `json:"members,omitempty"`
}

GoalStatus is the goal executor's latest verdict on a fleet, written by the executor after every cycle so any process can read it with the fleet (`rysh fleet goal show`). A cache of the executor's mind, never an input to it: the executor judges from the ledger and the board, not from this.

type GoalUpdate added in v0.2.11

type GoalUpdate struct {
	// Member selects a member (pane id, prefix, label); empty means the
	// fleet's own goal.
	Member   string `json:"member,omitempty"`
	Text     string `json:"text,omitempty"`
	DoneWhen string `json:"done_when,omitempty"`
	DueSec   int    `json:"due_sec,omitempty"` // seconds from now; 0 keeps the due time (or none)
	// State: "" sets the goal (Text required); met / dropped closes it.
	State string `json:"state,omitempty"`
	Note  string `json:"note,omitempty"`
	SetBy string `json:"set_by,omitempty"`
}

GoalUpdate sets or closes a fleet's goal, or one member's.

type HealthChange added in v0.2.11

type HealthChange struct {
	Fleet  string
	Member Member // a copy, status included
	From   string
	To     string
	// Cycle is set on a deadlock: the pane ids in the cycle, in order.
	Cycle []string
}

HealthChange is one member whose health moved.

type KV

type KV interface {
	Put(key string, value []byte) (uint64, error)
	Get(key string) (nats.KeyValueEntry, error)
	Delete(key string, opts ...nats.DeleteOpt) error
	Keys(opts ...nats.WatchOpt) ([]string, error)
}

KV is the slice of nats.KeyValue this package uses. Narrow on purpose: it keeps internal/fleet out of the bus's dependency tree and makes the store testable against a fake as well as against a real JetStream.

type LivePanesFunc

type LivePanesFunc func() (map[string]bool, error)

LivePanesFunc reports the panes that exist in the session right now.

THE ERROR MUST NOT BE SWALLOWED INTO AN EMPTY MAP. "I could not ask" and "I asked and nobody is there" are different facts and only one of them licenses dropping members — Registry.Reconcile treats the empty set as "caller does not know" for exactly this reason.

type Member

type Member struct {
	PaneID    string `json:"pane_id"`
	Label     string `json:"label,omitempty"`
	Role      string `json:"role,omitempty"`
	Unit      string `json:"unit,omitempty"`
	Parent    string `json:"parent,omitempty"` // parent's pane id
	Worktree  string `json:"worktree,omitempty"`
	SessionID string `json:"session_id,omitempty"` // resumable with `claude -r`

	// Status is the member's row in the status ledger (design 054): declared
	// work, observed activity, derived health. Owned by the fleet actor; a
	// membership upsert that carries none keeps the one already recorded.
	Status *MemberStatus `json:"status,omitempty"`

	// Goal is this agent's individual goal (design 055). Kept across a
	// membership upsert that carries none, like Status.
	Goal *Goal `json:"goal,omitempty"`
}

Member is one agent in a fleet.

PaneID is the identity and the only field this package treats as unique — the same rule the board applies to posts, and for the same reason: a label is a given-name, given-names are unique per LANE rather than per session (TabActor.IsGivenNameTakenInLane), and two fleets in different lanes may legally hold the same one.

func ResolveMember added in v0.2.11

func ResolveMember(f *Fleet, ref string) (*Member, error)

ResolveMember finds a member of f by pane id, pane id prefix (>= 4 chars), label, or label prefix — the addressing fleetctl already accepts. Ambiguity is an error, never a guess: a blocker attributed to the wrong agent is a wrong edge in the graph every nudge is derived from.

type MemberStatus added in v0.2.11

type MemberStatus struct {
	// Declared.
	Work      string   `json:"work,omitempty"`
	Task      string   `json:"task,omitempty"`
	Note      string   `json:"note,omitempty"`
	BlockedBy []string `json:"blocked_by,omitempty"` // member pane ids, "human", or "ext:<what>"
	UpdatedAt int64    `json:"updated_at,omitempty"` // last declaration
	WorkSince int64    `json:"work_since,omitempty"` // when Work last changed

	// Derived from every member's BlockedBy. Never declared.
	BlockerFor []string `json:"blocker_for,omitempty"`

	// Observed.
	Activity   string `json:"activity,omitempty"`
	ActiveAt   int64  `json:"active_at,omitempty"`   // last time seen working
	ObservedAt int64  `json:"observed_at,omitempty"` // last observation
	JoinedAt   int64  `json:"joined_at,omitempty"`   // first time the ledger saw this member

	// Derived.
	Health      string `json:"health,omitempty"`
	HealthSince int64  `json:"health_since,omitempty"`

	// Check-in bookkeeping.
	CheckinAt     int64  `json:"checkin_at,omitempty"`     // last check-in sent
	CheckinCmd    string `json:"checkin_cmd,omitempty"`    // that check-in's cmd id, until answered
	CheckinMissed int    `json:"checkin_missed,omitempty"` // consecutive unanswered

	// Progress toward the CURRENT task (design 055), declared with
	// `--pct` / `--eta`. A new task text resets both: they described the old
	// one. Pct is a pointer because 0% is a real answer and "never said" is
	// a different one.
	Pct      *int  `json:"pct,omitempty"`
	EtaAt    int64 `json:"eta_at,omitempty"`     // unix millis the agent expects to finish
	EtaSetAt int64 `json:"eta_set_at,omitempty"` // when that estimate was given

	// The goal executor's pending ask (HURRY / REALIGN): answered, like a
	// check-in, by the member's next declaration.
	GoalAskAt  int64  `json:"goal_ask_at,omitempty"`
	GoalAskCmd string `json:"goal_ask_cmd,omitempty"`

	// The status scout (design 056): who made the last declaration ("" the
	// agent, "scout"), and the verdict the scout last answered — it answers
	// a verdict once; if it stands, the agent is asked.
	DeclaredBy  string `json:"declared_by,omitempty"`
	ScoutAt     int64  `json:"scout_at,omitempty"`
	ScoutHealth string `json:"scout_health,omitempty"`
}

MemberStatus is one member's row in the ledger.

type Persistence

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

Persistence writes fleet records to a KV and replays them on restart.

The nil *Persistence is usable and does nothing — that is what a session without JetStream gets, and it degrades to a registry that forgets on restart rather than a session that refuses to start.

func NewPersistence

func NewPersistence(kv KV) *Persistence

NewPersistence returns a Persistence over kv. A nil kv yields a nil Persistence, whose methods are no-ops.

func (*Persistence) Delete

func (p *Persistence) Delete(name string) error

Delete removes one fleet record. A missing key is not an error: forgetting a fleet that was never persisted is the same outcome the caller asked for.

func (*Persistence) Restore

func (p *Persistence) Restore(r *Registry) (int, error)

Restore replays every persisted fleet into r and returns how many were applied.

A record that fails to fetch or decode is SKIPPED, not fatal, and the count returned is what really loaded — the same rule the board's restore follows. A registry that refuses to start because one fleet's record went bad is worse than one missing a fleet, because it takes every other fleet down with it.

func (*Persistence) Save

func (p *Persistence) Save(f *Fleet) error

Save writes one fleet record, overwriting the previous one.

type Query

type Query struct {
	// Name selects one fleet. Empty means every fleet.
	Name string `json:"name,omitempty"`
}

Query asks for one fleet, or for all of them.

type Registry

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

Registry is the set of fleets in one session. Safe for concurrent use: the actor's subscription goroutines read it while its reconcile loop writes.

func New

func New() *Registry

New returns an empty Registry.

func (*Registry) CheckinSent added in v0.2.11

func (r *Registry) CheckinSent(fleetName, paneID, cmdID string)

CheckinSent records the cmd id of a check-in just sent, so the next status declaration can answer it and an unanswered one can be counted.

func (*Registry) Declare added in v0.2.11

func (r *Registry) Declare(fleetName, paneID string, u StatusUpdate, now int64) (StatusResult, error)

Declare is SetStatus with everything a declaration did: it also reports the goal executor's ask it answered (design 055).

func (*Registry) DueCheckins added in v0.2.11

func (r *Registry) DueCheckins(now int64) []Checkin

DueCheckins returns the check-ins to send now, and stamps them as sent: the caller sends each and then calls CheckinSent with the cmd id it minted.

Rate-limited per member: at most one per interval — except that a member whose health CHANGED since its last check-in into stalled or unblocked is asked at once, because those are the two verdicts it can act on now. (Found live: re-asking every stall window counted two "misses" in a minute.) A member working with a fresh status is never pinged under the smart policy.

func (*Registry) Evaluate added in v0.2.11

func (r *Registry) Evaluate(now int64) []HealthChange

Evaluate derives every member's health and returns the members whose health changed. The actor posts alerts for changes only, so a member that stays stalled is reported once, not every sweep.

func (*Registry) FleetOfPane

func (r *Registry) FleetOfPane(paneID string) string

FleetOfPane reports which fleet a pane belongs to, or "".

func (*Registry) Forget

func (r *Registry) Forget(name string) bool

Forget removes a fleet from the registry and reports whether it was there.

IT DOES NOT TOUCH PANES. Forgetting is a registry operation; the panes of a forgotten fleet keep running, which is exactly what happened live when a sibling's teardown deleted a manifest — the agents carried on while the tool that could address them lost their names. Naming that here is the point: callers that want the agents stopped must stop them, and this is not that.

func (*Registry) Get

func (r *Registry) Get(name string) *Fleet

Get returns a copy of one fleet, or nil.

A COPY, so a caller cannot mutate the registry by holding a pointer it was handed for reading. The board store makes the opposite trade and documents it; here the objects are small and the readers are many (a CLI, a ## verb, the interrupt path), so copying is cheaper than the discipline.

func (*Registry) GoalAskSent added in v0.2.11

func (r *Registry) GoalAskSent(fleetName, paneID, cmdID string, now int64)

GoalAskSent records the cmd id of an executor ask (HURRY / REALIGN), so the member's next declaration answers it on its thread.

func (*Registry) Len

func (r *Registry) Len() int

Len is how many fleets the registry holds.

func (*Registry) List

func (r *Registry) List() []Fleet

List returns every fleet, ordered by name so a listing is stable. Go randomises map iteration, and a fleet list that reorders itself between two calls looks like the session changed.

func (*Registry) Observe added in v0.2.11

func (r *Registry) Observe(paneID, activity string, now int64)

Observe records what the actor saw in a member's pane. Unknown is recorded as the observation time only: it says nothing about whether the agent is working, and must not move ActiveAt or turn an agent idle.

func (*Registry) Reconcile

func (r *Registry) Reconcile(livePaneIDs map[string]bool) int

Reconcile drops members whose panes no longer exist and reports how many were dropped.

THE EMPTY SET MEANS "THE CALLER DOES NOT KNOW" and retains everything. This is board.Store.RetainRoster's rule, restated because the failure it prevents is worse here: a momentarily empty or failed snapshot would empty every fleet's roster, and a registry that reports a fleet as having no members is indistinguishable from a fleet that finished. A registry that overcounts is bad; one that silently empties itself is unrecoverable.

It never removes the FLEET, only members. A fleet whose panes have all gone is a fleet that is down — which is a state, decided by whoever owns the lifecycle, not something to infer from a snapshot.

func (*Registry) Register

func (r *Registry) Register(f Fleet) (*Fleet, error)

Register adds or REPLACES a fleet's identity, keeping its members.

Members are kept rather than replaced because registration and membership arrive from different places at different times: `fleetctl up` registers the fleet, then adds each agent as its pane is created. A Register that emptied the roster would make a re-registration — which happens on every restart of the driver — silently forget everyone who was already there.

func (*Registry) RemoveMember

func (r *Registry) RemoveMember(name, paneID string) bool

RemoveMember drops one member by pane id.

func (*Registry) ScoutAnswered added in v0.2.11

func (r *Registry) ScoutAnswered(fleetName, paneID, health string, now int64)

ScoutAnswered records that the scout answered this member's current verdict.

func (*Registry) SetGoal added in v0.2.11

func (r *Registry) SetGoal(fleetName string, gu GoalUpdate, now int64) (*Fleet, error)

SetGoal sets or closes a goal. It returns a copy of the fleet as stored.

It REFUSES rather than repairs: a goal with no text, closing a goal that was never set, an unknown member. Every assessment the executor makes is measured against these, and a goal that was quietly half-applied is a fleet judged against the wrong target.

func (*Registry) SetGoalExec added in v0.2.11

func (r *Registry) SetGoalExec(name string, g GoalExec) (*Fleet, error)

SetGoalExec sets a fleet's goal executor config.

func (*Registry) SetGoalStatus added in v0.2.11

func (r *Registry) SetGoalStatus(name string, gs GoalStatus)

SetGoalStatus records the executor's latest verdict.

func (*Registry) SetState

func (r *Registry) SetState(name, state string) error

SetState moves a fleet through its lifecycle.

func (*Registry) SetStatus added in v0.2.11

func (r *Registry) SetStatus(fleetName, paneID string, u StatusUpdate, now int64) (string, *Member, string, error)

SetStatus records a declaration from the member with paneID. fleetName may be empty: the pane's own fleet is used. Returns the fleet name, a copy of the member as stored, and the id of the check-in this declaration answered ("" if none was pending) — the actor files the status on that check-in's thread, so the board shows the check-in as answered.

It REFUSES rather than repairs: blocked with no blocker, a blocker on a non-blocked state, self-blocking, an unknown blocker. Each of those, stored, is an edge in the graph that is wrong — and every nudge, escalation and deadlock verdict is derived from that graph.

func (*Registry) SetWatch added in v0.2.11

func (r *Registry) SetWatch(name string, w Watch) (*Fleet, error)

SetWatch sets a fleet's check-in policy.

func (*Registry) UpsertMember

func (r *Registry) UpsertMember(name string, m Member) error

UpsertMember adds a member or updates the one with the same pane id.

Keyed on PaneID and never on Label: a fleet legitimately holds two agents called `wkr-01` in different lanes, and keying on the label would silently merge them into one — which reads as a fleet half its real size.

type Reply

type Reply struct {
	Fleets []Fleet `json:"fleets"`

	// ReconciledAt is when membership was last checked against the panes that
	// actually exist, in unix millis; 0 means never.
	//
	// IT IS REPORTED RATHER THAN HIDDEN because the answer is served from the
	// actor's cache and is therefore as old as the last reconcile. That is a
	// deliberate design constraint, not an oversight — see ServeQueries — and a
	// caller that cannot tell a fresh roster from a stale one would eventually
	// act on a member that closed ten minutes ago. Same discipline as the
	// board's RosterReconciled.
	ReconciledAt int64 `json:"reconciled_at,omitempty"`

	// Err carries a refusal. A registry that cannot answer says so; it never
	// answers with an empty list.
	Err string `json:"err,omitempty"`
}

Reply is what the fleet actor answers.

func Ask

func Ask(nc *nats.Conn, q Query, timeout time.Duration) (*Reply, error)

Ask puts a Query to the registry and returns its answer.

It returns (nil, ErrNoRegistry) when nobody answers — no responder, a timeout, or no connection — and NEVER a usable-looking empty reply alongside an error.

type ScoutAnswer added in v0.2.11

type ScoutAnswer struct {
	Answerable bool     `json:"answerable"`
	Work       string   `json:"work"`
	Task       string   `json:"task"`
	BlockedBy  []string `json:"blocked_by"`
	Note       string   `json:"note"`
	Confidence float64  `json:"confidence"`
	Reason     string   `json:"reason"`
}

ScoutAnswer is the scout model's reply.

func ParseScoutAnswer added in v0.2.11

func ParseScoutAnswer(text string) (ScoutAnswer, error)

ParseScoutAnswer reads the model's reply, tolerating a code fence or text around the JSON object.

type StatusResult added in v0.2.11

type StatusResult struct {
	Fleet  string
	Member *Member // a copy, as stored
	// AnsweredCheckin is the watch's check-in this declaration answered.
	AnsweredCheckin string
	// AnsweredGoalAsk is the goal executor's ask it answered.
	AnsweredGoalAsk string
}

StatusResult is what a declaration did.

type StatusUpdate added in v0.2.11

type StatusUpdate struct {
	Work      string   `json:"work"`
	Task      string   `json:"task,omitempty"`
	Note      string   `json:"note,omitempty"`
	BlockedBy []string `json:"blocked_by,omitempty"` // refs: pane id, id prefix, label, "human", "ext:<what>"
	// KeepTask leaves Task as it was when Task is empty, so `status blocked
	// --blocked-by mgr` does not erase what the agent is blocked ON.
	KeepTask bool `json:"keep_task,omitempty"`

	// Pct (0–100) and EtaSec (seconds from now) are progress toward the
	// task (design 055). Optional; nil / 0 leave what was declared before,
	// unless the task text changes.
	Pct    *int `json:"pct,omitempty"`
	EtaSec int  `json:"eta_sec,omitempty"`

	// By is who declares: "" the agent itself, "scout" the status scout on
	// its behalf (design 056). A scout's declaration answers nothing that was
	// asked of the agent: its pending check-in and the executor's ask stay
	// pending, so an agent never loses a question it was meant to answer.
	By string `json:"by,omitempty"`
}

StatusUpdate is a declaration from an agent.

func ScoutUpdate added in v0.2.11

func ScoutUpdate(a ScoutAnswer, model string) (StatusUpdate, error)

ScoutUpdate turns a scout answer into a declaration, or says why it must not be used (and the agent asked instead).

type Update

type Update struct {
	Op string `json:"op"`

	Fleet  Fleet  `json:"fleet,omitempty"`  // OpRegister
	Name   string `json:"name,omitempty"`   // every other op
	State  string `json:"state,omitempty"`  // OpState
	Member Member `json:"member,omitempty"` // OpMemberUpsert
	PaneID string `json:"pane_id,omitempty"`

	Status   StatusUpdate `json:"status,omitempty"`    // OpStatus
	Watch    Watch        `json:"watch,omitempty"`     // OpWatch
	Goal     GoalUpdate   `json:"goal,omitempty"`      // OpGoal
	GoalExec GoalExec     `json:"goal_exec,omitempty"` // OpGoalExec
	// contains filtered or unexported fields
}

Update is a write against the registry.

type UpdateReply

type UpdateReply struct {
	OK    bool   `json:"ok"`
	Fleet *Fleet `json:"fleet,omitempty"`
	// Member is the member as stored after an OpStatus.
	Member *Member `json:"member,omitempty"`
	// AnsweredCheckin is the check-in cmd an OpStatus answered, if any.
	AnsweredCheckin string `json:"answered_checkin,omitempty"`
	// AnsweredGoalAsk is the goal executor's ask an OpStatus answered.
	AnsweredGoalAsk string `json:"answered_goal_ask,omitempty"`
	Err             string `json:"err,omitempty"`
}

UpdateReply reports what the registry did.

func Send

func Send(nc *nats.Conn, u Update, timeout time.Duration) (*UpdateReply, error)

Send puts an Update to the registry.

A refusal from the registry comes back as an error, so a caller cannot mistake "recorded" for "sent and ignored" — `fleetctl` puts agents into a fleet immediately after registering it, and a registration that quietly failed would produce a fleet whose members belong to nothing.

type Watch added in v0.2.11

type Watch struct {
	Policy      string `json:"policy,omitempty"`
	IntervalSec int    `json:"interval_sec,omitempty"`
	StallSec    int    `json:"stall_sec,omitempty"`
	// Answer is who answers a status check-in: "agent" (default) or
	// "scout" — a cheap model reads the agent's screen and posts and answers
	// for it, and the agent is asked only when it cannot tell (design 056).
	Answer string `json:"answer,omitempty"`
	// ScoutModel is the scout's model (claude CLI name or alias); default haiku.
	ScoutModel string `json:"scout_model,omitempty"`
}

Watch is a fleet's check-in policy. Zero values mean the defaults.

func (Watch) IntervalText added in v0.2.11

func (w Watch) IntervalText() string

IntervalText renders the check-in interval for a message: 10m0s → "10m", 1h0m0s → "1h", 90s → "1m30s", 20s → "20s".

func (*Watch) Resolved added in v0.2.11

func (w *Watch) Resolved() Watch

Resolved returns the watch with every default filled in.

func (Watch) Validate added in v0.2.11

func (w Watch) Validate() error

Validate refuses a policy the actor cannot run.

Jump to

Keyboard shortcuts

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