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
- Variables
- func BucketConfig(session string) *nats.KeyValueConfig
- func BucketName(session string) string
- func BuildScoutPrompt(f *Fleet, m *Member, reason string, screen []string, posts []string, ...) string
- func DurationText(d time.Duration) string
- func MemberLabel(f *Fleet, paneID string) string
- func RenderGoals(f *Fleet, now int64) string
- func RenderStatus(f *Fleet, now int64) string
- func ScoutEligible(c Checkin, w Watch) bool
- func ServeQueries(nc *nats.Conn, r *Registry, reconciledAt func() int64) (*nats.Subscription, error)
- func ServeUpdates(nc *nats.Conn, r *Registry, onChange func(u Update, reply UpdateReply)) (*nats.Subscription, error)
- func ValidWork(w string) bool
- func ValidateName(name string) error
- type Checkin
- type Fleet
- type Goal
- type GoalExec
- type GoalStatus
- type GoalUpdate
- type HealthChange
- type KV
- type LivePanesFunc
- type Member
- type MemberStatus
- type Persistence
- type Query
- type Registry
- func (r *Registry) CheckinSent(fleetName, paneID, cmdID string)
- func (r *Registry) Declare(fleetName, paneID string, u StatusUpdate, now int64) (StatusResult, error)
- func (r *Registry) DueCheckins(now int64) []Checkin
- func (r *Registry) Evaluate(now int64) []HealthChange
- func (r *Registry) FleetOfPane(paneID string) string
- func (r *Registry) Forget(name string) bool
- func (r *Registry) Get(name string) *Fleet
- func (r *Registry) GoalAskSent(fleetName, paneID, cmdID string, now int64)
- func (r *Registry) Len() int
- func (r *Registry) List() []Fleet
- func (r *Registry) Observe(paneID, activity string, now int64)
- func (r *Registry) Reconcile(livePaneIDs map[string]bool) int
- func (r *Registry) Register(f Fleet) (*Fleet, error)
- func (r *Registry) RemoveMember(name, paneID string) bool
- func (r *Registry) ScoutAnswered(fleetName, paneID, health string, now int64)
- func (r *Registry) SetGoal(fleetName string, gu GoalUpdate, now int64) (*Fleet, error)
- func (r *Registry) SetGoalExec(name string, g GoalExec) (*Fleet, error)
- func (r *Registry) SetGoalStatus(name string, gs GoalStatus)
- func (r *Registry) SetState(name, state string) error
- func (r *Registry) SetStatus(fleetName, paneID string, u StatusUpdate, now int64) (string, *Member, string, error)
- func (r *Registry) SetWatch(name string, w Watch) (*Fleet, error)
- func (r *Registry) UpsertMember(name string, m Member) error
- type Reply
- type ScoutAnswer
- type StatusResult
- type StatusUpdate
- type Update
- type UpdateReply
- type Watch
Constants ¶
const ( GoalOpen = "open" GoalMet = "met" GoalDropped = "dropped" )
Goal states.
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.
const ( DefaultGoalExecMinSec = 300 DefaultGoalExecMaxSec = 1800 )
Executor cadence defaults — the brief's "every 5 minutes or every 30".
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.
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.
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.
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.
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.
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.
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.
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.
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".
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.
const DeclaredByScout = "scout"
DeclaredByScout marks a declaration the scout made for an agent.
const DefaultScoutModel = "haiku"
DefaultScoutModel is the scout's model when none is set: the claude CLI's alias for the current Haiku.
const ScoutMinConfidence = 0.7
ScoutMinConfidence is the least confidence at which a scout answer is used.
const ScoutSystemPrompt = `` /* 1153-byte string literal not displayed */
ScoutSystemPrompt is the scout's instruction.
Variables ¶
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 ¶
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
DurationText renders a duration for a message: 10m, 1h, 1h30m, 45s.
func MemberLabel ¶ added in v0.2.11
MemberLabel is the display name of a pane in f: its label, else pane-<8>.
func RenderGoals ¶ added in v0.2.11
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
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
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 ValidateName ¶
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.
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.
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.
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 ¶
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
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 (*Registry) CheckinSent ¶ added in v0.2.11
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
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 ¶
FleetOfPane reports which fleet a pane belongs to, or "".
func (*Registry) Forget ¶
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 ¶
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
GoalAskSent records the cmd id of an executor ask (HURRY / REALIGN), so the member's next declaration answers it on its thread.
func (*Registry) List ¶
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
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 ¶
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 ¶
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 ¶
RemoveMember drops one member by pane id.
func (*Registry) ScoutAnswered ¶ added in v0.2.11
ScoutAnswered records that the scout answered this member's current verdict.
func (*Registry) SetGoal ¶ added in v0.2.11
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
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) 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) UpsertMember ¶
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.
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 ¶
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
IntervalText renders the check-in interval for a message: 10m0s → "10m", 1h0m0s → "1h", 90s → "1m30s", 20s → "20s".