pulse

package
v0.16.0-dev.c839379e Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Index

Constants

View Source
const (
	ConfigFile = "pulse.toml"
	SeenFile   = "pulse.config.seen"
)

ConfigFile is the loop's configuration, in the queue directory. SeenFile is the snapshot of the last load, which is how a CONFIG line can say what changed and a DEFAULT line can be printed once rather than every tick.

View Source
const (
	DefaultSlotsStudio       = 8
	DefaultSlotsSpace        = 8
	DefaultSlotsLocal        = 2
	DefaultHeadroomStudio    = 2
	DefaultHeadroomSpace     = 2
	DefaultHeadroomLocal     = 1
	DefaultTickSeconds       = 60
	DefaultRefillCadence     = 5
	DefaultRunnersPerMachine = 8
	// DefaultLaunchFiles is the file budget every launched card carries. `nova-swarm batch`
	// requires --files and refuses to guess one (issue #869): a launch that names none is
	// refused before a single card starts, so the loop carries the documented number.
	DefaultLaunchFiles = 40
)

The documented defaults. A missing key takes the value here and says so once.

View Source
const (
	FleetRootsFile  = "PULSE_ROOTS"
	FleetLabelsFile = "runner-labels.tsv"
)

FleetRootsFile and FleetLabelsFile are written by `fleet add` alone: the roots the loop holds and the runner labels the admitted bench brings. Nothing else writes them.

View Source
const (
	MatchEquals    = "equals"   // the value is exactly Want
	MatchContains  = "contains" // the value holds Want
	MatchNonempty  = "nonempty" // the value is not empty
	MatchNotEquals = "not"      // the value is anything but Want
	MatchAtLeast   = "at-least" // the value is a number >= Want
)

The matchers a check may ask for. They are strings rather than functions so the whole table is data a test (and a reader) can hold against the standard.

View Source
const (
	HygieneDiagDaysDefault     = 2
	HygieneDiagMaxBytesDefault = int64(2 * 1024 * 1024 * 1024)
)

The runner `_diag` prune's defaults.

Measured on hulk, 2026-09-18: 24 runner directories, 3.5 GB of `_diag`, 18,296 files, and the OLDEST file on the bench two days old. A runner rolls its own diagnostics at a rate nobody chose, so the seven-day window the bench ran with took nothing, ever, and neither would three days.

The two rules divide the job. On a busy bench the window is what bites day by day (a dry run on hulk at --diag-days 1 selects 10,391 files, 1.95 GB); the cap is the backstop for a burst, or for a runner that starts writing faster than anyone watches. 2 GiB per runner over hulk's 24 runners is a 48 GiB ceiling on a 1.8 TB disk, and it takes nothing there today, because no runner directory is over 220 MB. That is what a ceiling is for.

View Source
const AdmitShown = 5

AdmitShown is how many refusals a launch prints before it prints the count instead.

View Source
const DefaultBase = "dev"

DefaultBase is the base a card that names none is measured and opened against.

View Source
const DefaultBranchPrefix = "rowan/"

DefaultBranchPrefix is the branch prefix a bench harvest takes when the caller names none: this line's own branches and nobody else's.

View Source
const DefaultCardDeadline = 25 * time.Minute

DefaultCardDeadline is how long a card may run before the reaper collects it: the batch deadline the hand loop passed nova-swarm, 25 minutes.

View Source
const DefaultLaunchTokens = "unmetered"

DefaultLaunchTokens is the token budget the launch names. The native runner the bench's shim has always used has no live token accounting -- the deadline is the stop -- and `unmetered` is the word nova-swarm requires a caller to say out loud rather than a number nothing observes.

View Source
const DefaultResume = "read this section, run nova-pulse status, act on REDS/STOP/HUMAN first"

DefaultResume is the rule a beat writes when --resume names none: the first thing the next window does with the section it just read.

View Source
const DefaultRunnerIdle = 5 * time.Minute

DefaultRunnerIdle is how long a runner may be busy with nothing running before it is stuck. Five minutes: longer than the longest gap between a job finishing and the next starting, far shorter than the forty minutes the jam cost.

View Source
const DefaultTick = 10 * time.Second

DefaultTick is the manager's tick (SPEC-PULSE, "Rate and convergence" rule 1).

View Source
const FillCap = 30

FillCap is the most cards one bench may take in a tick: fill-loop.sh holds this reserve back so a filling bench never eats the machine its own CI needs.

View Source
const FillInterval = 300 * time.Second

FillInterval is how often the loop ticks when --once is absent (fill-loop.sh's sleep 300).

View Source
const FirstCard = 1

FirstCard is the number a fresh queue's first card takes. It is 1 and not 0 so a card number is never the empty value of anything.

View Source
const FleetProbeRecordFile = "fleet-probe.tsv"

FleetProbeRecordFile is where the fleet-probe read-back is kept under the queue unless the verb is given an explicit --probe path.

View Source
const NewRule = "NEW"

NewRule is the third field of a verdict that no row in the table decided.

View Source
const PacketMax = 5000

PacketMax is the ceiling on a triage card, in bytes. Five thousand is about 1.2k tokens on the text route: enough for the evidence and the rule table, few enough that the decision is cheaper than the coordinator reading the same lines once.

View Source
const RoutesLogFile = "ROUTES.log"

RoutesLogFile is the log rule 8 demands: one ROUTE line per card, beside the card's label and the time, under the queue directory.

View Source
const RunnerBusyFile = "runners.busy.tsv"

RunnerBusyFile is where the moment each runner was first seen busy-with-nothing-running is kept, so the rule survives a tick and a restart of the loop.

View Source
const RunnerServicesFile = "runner-services.tsv"

RunnerServicesFile maps a runner name to the service that restarts it: name<TAB>host<TAB>kind<TAB>target, kind being systemd (target is the unit) or svc (target is the runner directory holding svc.sh). A host of "-" is this machine.

View Source
const SlotDash = "-"

SlotDash is the cards.tsv slot every cut card carries until launch allocates one.

View Source
const StateFile = "pulse.state"

StateFile is the loop's counters, in the queue directory beside ConfigFile.

View Source
const StatusLineMax = 400

StatusLineMax is the ceiling on the line, in bytes. Four hundred is about a hundred tokens: the price of knowing the whole day, paid once per window.

View Source
const StopFile = "STOP"

StopFile is the halt, in the queue directory. Its line 1 is the gate's own record -- MAIN-RED <sha> run=<id> job=<name> test=<name> -- and its line 2 is the admission name admission.go reads.

View Source
const StopMark = "MAIN-RED"

StopMark is what makes a STOP the gate's own. A STOP whose first line does not begin with it was written by a person, and no green lifts it.

View Source
const TriageRoute = "deepseek-v4-flash"

TriageRoute is the model a triage card is cut for: the cheapest route that can hold a text decision (SPEC-PULSE rule 7's cost table gives the same answer; this is the name the card carries so a hand reading the card knows what it was written for).

Variables

View Source
var Benches = []string{"studio", "space", "local"}

Benches are the benches this configuration knows, in the order a line prints them.

View Source
var CutKinds = []string{"read", "fix", "replay", "spec", "rebase"}

CutKinds are the kinds this cutter knows, in the order help prints them.

View Source
var DefaultIntegrationBranches = []string{"main", "dev"}

DefaultIntegrationBranches is the one list of integration branches (class B, bug 8: dev inherited main's cancellation bug because the branch name was written twice).

View Source
var SendWakeOnLAN = func(packet []byte, addr string) error {
	conn, err := net.Dial("udp4", addr)
	if err != nil {
		return err
	}
	defer conn.Close()
	_, err = conn.Write(packet)
	return err
}

SendWakeOnLAN is the real transport: one UDP datagram to the broadcast address. It is a variable so a test replaces it and never opens a socket.

View Source
var TriageKinds = []string{"signature", "scope", "docs-only", "nosha", "orphan", "fence", "hold-line"}

TriageKinds are the seven cases that took a coordinator turn each on 2026-09-16 and which a rule row plus a packet decides instead. A kind outside this list is refused: a case nobody named is a case with no rule rows to offer, and a packet with no candidate rows is a packet asking a model to invent policy.

View Source
var TriageVerdicts = []string{"ADMIT", "REFUSE", "HOLD", "RETRY", "ESCALATE"}

TriageVerdicts are the words the third field may be. They are the dispositions the loop already has: admit the card, refuse it, hold it for a person, retry it once, or escalate.

Functions

func AdmissionName

func AdmissionName(stop []byte) string

AdmissionName is STOP's line 2: the issue or the test the red belongs to, and the one name a card must carry to launch while it stands.

func Admit

func Admit(stop []byte, cardLine1 string) (ok bool, reason string)

Admit says whether one card may launch under the STOP in hand, and why not when it may not. An empty STOP is no state to be in and admits everything. A STOP whose line 2 is an admission name admits the cards whose line 1 carries that name. A STOP with no line 2 -- a person's hold, or a red the gate could not name -- admits nothing.

func AppendLedger

func AppendLedger(queue string, rows ...LedgerRow) error

AppendLedger appends rows under the queue's lock: one writer, append only, one take of the lock however many rows a sweep has to mark.

func AppendRule

func AppendRule(queue string, r RuleRow) error

AppendRule writes one row to <queue>/RULES.tsv marked pending. It is what a NEW verdict costs: the decision is recorded as policy the same hour, and a person reading the table sees which rows have not been read yet.

func Beat

func Beat(in BeatInput) int

Beat appends one section to the cairn file, commits it when the cairn lives in a git repo (git add and one commit, never a push), and prints the two lines the next window reads: BEAT OK with the cairn's line count, then the restart command.

func Cut

func Cut(in CutInput) int

Cut writes one card per pool.tsv candidate from its typed template and returns the exit code: 0 when every candidate was cut, 1 when any candidate was skipped, 2 when the pool, the templates or the routing table could not be read. Routing is the cost table (benches.tsv, or routes.tsv beside it) when it is there; a templates directory with only a models.tsv falls back to the two-line flash/pro table cut wrote before the cost table landed (issue #635), so a spend rule that holds every card to one model still works.

func CutKind

func CutKind(in CutKindInput) int

CutKind writes one card of one kind under the next number and prints one line. It returns 0 when the card was written and 2 when the invocation was refused.

func CutValidated

func CutValidated(in CutValidatedInput) int

CutValidated cuts cards from one input (an issue, a table of rows, or a PR's head branch) through a template with named slots. It writes the cards under --out and their rows to cards.tsv, and nothing else. Before a byte is written it runs five checks in order -- branch, base, STEP 1, row, result -- and stops at the first that fails, exit 2 with one refusal line. Success prints one CUT OK line naming the source.

func Fill

func Fill(in FillInput) int

Fill holds the loop: one fillTick per bench set, one FILL line per tick, until killed -- or exactly one tick when --once is set. It returns 0, 1 when every bench failed the tick (a fleet nobody can reach is not a quiet success), or 2 on a refusal that never started.

func FleetAdd

func FleetAdd(in FleetAddInput) int

FleetAdd admits one bench or refuses it. A bench whose probe carries a job that is not green, or a probe with no jobs at all, is refused with the offending runner name and the run id on the line; a bench whose every runner name is green is admitted and its PULSE_ROOTS and runner labels written.

func FleetJoin

func FleetJoin(in FleetJoinInput) int

FleetJoin joins one bench to the tailnet and prints one line: `FLEET <bench> JOINED ip=<addr>`. Exit 0 when the bench joined, 2 when the invocation was refused (no key in the named variable, a tailscale path that is not absolute, a bench the file does not carry, `studio`), 3 when the bench could not be reached or tailscale refused.

func FleetMirror

func FleetMirror(in FleetMirrorInput) int

FleetMirror creates or refreshes one bare mirror on one bench and prints one line: `FLEET <bench> MIRROR <path> created|refreshed head=<sha> size=<n>K`. Exit 0 when the mirror is there, 2 when the invocation was refused, 3 when the bench could not be reached or git failed on it.

func FleetReboot

func FleetReboot(in FleetRebootInput) int

FleetReboot boots each named bench, waits for its runners, and prints one FLEET line per bench. A bench not in the file, and `studio` by name, are refused (exit 2); a bench that never answers is unreachable (exit 3). Refusals are read before any ssh starts.

func FleetRegistry

func FleetRegistry(in FleetRegistryInput) int

FleetRegistry prints one MACHINE line per machine, in file order:

MACHINE hulk ssh=hulk os=linux/x64 roles=bench,runner seat=swarm-hulk cores=64 notes="..."

Exit 0 when the registry read, 2 when it did not or when --role names a role no machine carries (a role nobody carries is far more likely a typo than a fleet fact).

func FleetSecrets

func FleetSecrets(in FleetSecretsInput) int

FleetSecrets runs the seat check on every bench in parallel and prints one FLEET line per bench. It exits 0 when every seat checked OK, 2 when any refused or found no seat, and 3 when any bench was unreachable.

func FleetSleep

func FleetSleep(in FleetSleepInput) int

FleetSleep suspends one idle bench over ssh and prints one line: `FLEET <bench> SUSPENDED`, `FLEET <bench> BUSY <what>` (exit 2, and nothing is suspended), `FLEET <bench> SKIP <what>` under --if-idle, or `FLEET <bench> UNREACHABLE <reason>` (exit 3).

func FleetStandard

func FleetStandard(in FleetStandardInput) int

FleetStandard runs the standard's checks on one bench and prints one `STANDARD <bench> <check> OK|DRIFT ...` line per check and one verdict line: exit 0 when every check is met, 2 when any drifted or the bench was refused, 3 when the bench could not be reached.

func FleetSuspend

func FleetSuspend(in FleetSuspendInput) int

FleetSuspend sleeps each named bench: a bench holding a running card (a lease or a job directory with a live pid under ~/rowan-swarm-root or ~/stella-swarm-root) or a busy runner (a Runner.Worker process) is BUSY and refused (exit 2) unless --force, and is skipped under --if-idle. A bench that cannot be reached is exit 3; every bench prints one FLEET line.

func FleetWake

func FleetWake(in FleetWakeInput) int

FleetWake sends each named bench one Wake-on-LAN magic packet to its mac, then polls ssh until the bench answers, printing `FLEET <name> AWAKE wall=<s>` or `FLEET <name> WAKE TIMEOUT` (exit 3). A bench with no mac is refused (exit 2); every bench prints one FLEET line.

func Gate

func Gate(in GateInput) int

Gate reads the branch's latest ci run and writes, holds or lifts the STOP. It prints exactly ONE line: GATE RED (exit 1, the check says NO), GATE HELD or GATE GREEN (exit 0), or GATE REFUSED (exit 2) when the source could not answer -- a dead source is never green.

func Harvest

func Harvest(in HarvestInput) int

Harvest folds one pulse's cards: it pushes and opens a draft PR for every card whose RESULT.md line 1 equals its contract line, sends every abstain (or missing RESULT) to retry.tsv with the harness log's last refusal line, cuts a read card per PR, and then pulses again -- queue first. It never merges.

func Hygiene

func Hygiene(in HygieneInput) int

Hygiene runs one hygiene subcommand and returns its exit code: 0 when it ran, 2 on every refusal. One HYGIENE line is the only stdout a run prints; each deletion is one line in <home>/hygiene.log.

func KnownTriageKind

func KnownTriageKind(kind string) bool

KnownTriageKind reports whether kind is one of the seven.

func Launch

func Launch(in LaunchInput) int

Launch allocates free slots, admits the cards that fit as one nova-swarm batch in its CARD form (--id --cards --deadline --runner --root --files, the only form that runs a card; the POOL form wants --tokens as well, which no launch flag supplies, issue #630), and queues the rest when --queue is set. The card form still carries the file budget `nova-swarm batch` refuses an admission without (issue #869). It returns the process exit code: 0 when the pulse was admitted, 2 when it could not be.

func MagicPacket

func MagicPacket(mac string) ([]byte, error)

MagicPacket is the Wake-on-LAN packet for one mac: six 0xFF bytes then the six-byte hardware address repeated sixteen times, 102 bytes and nothing else.

func Main

func Main(name string, args []string, stamp string, out, errs io.Writer) int

Main is nova-pulse's entry point; it parses the verb and its flags and runs harvest.

func Manager

func Manager(in ManagerInput) int

Manager runs one bounded shift: 0 when it ended by itself, 2 on a refusal that never started.

func NextCardNumber

func NextCardNumber(queue string) (int, error)

NextCardNumber takes the next card number under the queue's lock and writes the state file back one higher. Two cutters never see the same number, whatever else is running.

func ParseTimeout

func ParseTimeout(s string) (time.Duration, error)

ParseTimeout reads a bound on a child, for the flag layer. A bare number is seconds, because that is what every caller of this tool has typed for a year; anything else is a Go duration, because the progress line prints one and a flag that will not accept what the tool prints is a trap. There is no default here and no zero: a bound of nothing is not a bound.

func Pool

func Pool(in PoolInput) int

Pool enumerates bounded open work from the declared sources into pool.tsv. It runs no model: every candidate is read through gh, git or the files the sources name. One unreadable source is one refusal and no pool.tsv is written.

func Probe

func Probe(in ProbeInput) int

Probe runs the learned checklist against one card and returns 0 when it passes and 1 when a learned class refuses it; 2 when the history could not be read. A refusal names the class and its remedy so the per-class count stays comparable to the history.

func Progress

func Progress(in ProgressInput) int

Progress prints the PROGRESS line and the ESTIMATE line and returns 0, or 2 when it could not run.

func ReadFleetBenches

func ReadFleetBenches(path string) (map[string]FleetBench, error)

ReadFleetBenches reads the benches file: one bench per line, `name<TAB>ssh target<TAB>home<TAB>mac`. Blank lines and `#` comments are skipped, and a line that is not four fields is a refusal naming the file and the line -- a partially-read fleet is worse than none.

func ReadRunnerServices

func ReadRunnerServices(queue string) (map[string]RunnerService, error)

ReadRunnerServices reads the service map. A map nobody wrote is empty, and every restart against it is then a refusal naming the file.

func Reap

func Reap(in ReapInput) int

Reap runs one collection over the roots and the queue and prints one REAP line. It returns 0 when it ran, 2 when it could not.

func Run

func Run(in RunInput) int

Run holds the loop for --hours and prints one WIDTH line per tick. It returns 0 when the shift ended by itself and 2 on a refusal that never started.

func Status

func Status(in StatusInput) int

Status prints the eight line kinds and returns 0, or 2 when it could not run.

func StatusHTML

func StatusHTML(in StatusHTMLInput) int

StatusHTML writes the fleet page to --html, appends one seven-column metrics.tsv row beside it, ships both where --publish says, and prints the one STATUS HTML line.

func StatusLine

func StatusLine(in StatusInput) int

StatusLine prints the one line and returns 0, or 2 when it could not run.

func Sweep

func Sweep(in SweepInput) int

Sweep walks every open ledger row once and prints one SWEEP line. It returns 0 when it ran, 2 when it could not.

func Triage

func Triage(in TriageInput) int

Triage cuts one packet to a card file and prints one line.

func Watch

func Watch(in WatchInput) int

Watch waits on the queue, the bus and the job roots and prints one line per change. It starts from the state of its first poll, so a backlog is never printed; a named --until event ends it at the poll that sees it, before the cap, with WATCH OK and exit 0; the cap prints WATCH CAP and exits 3. Every unusable invocation is WATCH REFUSED, exit 2, one remedy, and makes no poll.

func Wire

func Wire(run *RunInput, w *Wiring)

Wire fills every seam of a RunInput this caller left nil. It is what `nova-pulse run` does and what a test that wants the real steps does, with fakes in WiringInput.

Types

type BeatInput

type BeatInput struct {
	Queue  string // the queue directory: pending, launched, done, failed and the state files
	Cairn  string // the cairn file this beat is appended to
	Title  string // the one-line title of this beat
	Resume string // the resume rule; empty takes DefaultResume
	Now    func() time.Time
	Stdout io.Writer
	Stderr io.Writer
}

BeatInput is everything the beat verb needs, held apart from flag parsing so a test can drive it against a temp queue and a temp git repo.

type BenchReading

type BenchReading struct {
	Name    string
	Live    int
	Cores   int
	Load    int
	FreeGB  int
	MemGB   int
	Allowed int
	Hygiene int // reaps and deletions on this bench since the window opened
	// Down is the bench that did not answer. It is NOT a bench with nothing to do, and the
	// difference is what the 2026-09-17 pit stop cost: a row of zeros reads as an idle
	// bench, so a fleet nobody could see looked like a fleet with nothing to do. The page
	// says DOWN, the metrics row writes a dash for its disk rather than a number the bench
	// never gave, and the STATUS HTML line counts it.
	Down bool
	// Note says WHY, when the bench answered well enough to tell us. A bench whose home is
	// not there answers every other question perfectly and then reports no disk, which read
	// as "out of disk, allowed 0" on every row of a page that was otherwise right.
	Note string
}

BenchReading is one bench's numbers for the page: the live cards counted from running card processes, the capacity the allowed arithmetic folds, and the hygiene actions the same round trip counted.

type BenchShell

type BenchShell interface {
	Run(bench, script string) (string, error)
}

BenchShell runs one command on a bench and returns its stdout. The shipped one is ssh; a test's is a fake, so no test of this package opens a connection.

type CIJob

type CIJob struct {
	Name        string `json:"name"`
	Log         string `json:"log"`
	StartedAt   string `json:"startedAt"`
	CompletedAt string `json:"completedAt"`
}

CIJob is the failing job of a run: its name, and its log, which is where the failing test and the issue the red belongs to are named. StartedAt/CompletedAt are the job's wall, which the ci-wall rule reads to name the long pole.

type CIRun

type CIRun struct {
	ID         int64  `json:"databaseId"`
	Status     string `json:"status"`
	Conclusion string `json:"conclusion"`
	HeadSHA    string `json:"headSha"`
	Workflow   string `json:"workflowName"`
	Event      string `json:"event"`
	CreatedAt  string `json:"createdAt"`
	UpdatedAt  string `json:"updatedAt"`
}

CIRun is one run as `gh run list --json status,conclusion,headSha,databaseId,workflowName,event,createdAt,updatedAt` gives it. The gate reads only the ci workflow's push runs, so it can tell the branch's own run from a certification, workflow_dispatch, schedule or merge_group run at the same sha. CreatedAt/UpdatedAt are the run's wall, which the ci-wall rule (issue #888) reads.

type Candidate

type Candidate struct {
	Source   string
	ID       string
	Kind     string
	Title    string
	Template string
}

Candidate is one row of bounded work: a source kind, an item id, a card kind, a title and the template to render it from. pool.tsv, queue.tsv and next.tsv all hold candidates in the same five-field shape.

type Capacity

type Capacity interface {
	Capacity(bench string) (int, error)
}

Capacity answers how many cards the named bench can take this tick -- card 9316's formula, the min of core, disk and memory headroom. The real one runs it over ssh; tests inject a fixed number.

type CardLauncher

type CardLauncher interface {
	Launch(bench, card string) error
}

CardLauncher launches one card that Fill has already moved into --launched. It is the per-card seam of the launch verb (internal/pulse/launch.go): one call, in order, one card. The real one shells flash-native-bench.sh on the bench; tests inject a recorder.

type CardRow

type CardRow struct {
	Label string
	Slot  string
	Model string
	Card  string
}

CardRow is one row of cards.tsv: the label the card was admitted under, the slot it holds ("-" until launch allocates one), the model, and the card file's path.

type Config

type Config struct {
	Slots map[string]int
	// Headroom is a ratio of the bench's cores, so it is a decimal: the spec writes it
	// `cores x 1.5` and 2.5 is a value a person writes (issue #869).
	Headroom            map[string]float64
	TickSeconds         int
	RefillCadence       int
	IntegrationBranches []string
	RunnersPerMachine   int
	Files               int    // the --files budget every launched card carries
	Tokens              string // the --tokens budget: a number, or the word `unmetered`
}

Config is the loop's whole configuration: slots and headroom per bench, the tick, the refill cadence, the integration branches and the runners each machine runs.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig is the documented configuration, for a caller that has not read a file yet.

func LoadConfig

func LoadConfig(dir string, out io.Writer, max int) (Config, error)

LoadConfig reads <dir>/pulse.toml, takes the documented default for every key it does not carry, and prints ONE CONFIG line naming the keys that changed since the last load, plus one DEFAULT line per key taking a default it has not announced before, bounded at max. A missing file is every default and no refusal: a first tick on a fresh queue still runs.

type CutInput

type CutInput struct {
	Pool      string // path to pool.tsv: source, id, kind, title, template per line
	Templates string // the directory holding read.md fix.md text.md replay.md drift.md tone.md and benches.tsv (or routes.tsv, or a models.tsv fallback)
	Out       string // the directory the card files go into
	Root      string // the state root; skipped.tsv and retry.tsv are read and written here
	Local     string // an optional ollama tag that overrides the flash model for read and text cards
	Max       int    // cap on the cards cut, and on the skipped lines printed; 0 means no cap
	Probe     bool   // run the learned admission checklist before writing each card (#585)
	History   string // the abstain history the checklist is cut from, when Probe is set
	Budget    int    // the card's byte budget, when Probe is set; 0 means unbounded
	Stdout    io.Writer
	Stderr    io.Writer
}

CutInput is everything the cut verb needs, held apart from flag parsing so a test can drive it with a fake templates directory and a fake pool.tsv.

type CutKindInput

type CutKindInput struct {
	Kind      string
	Repo      string // owner/name; line 1 names the repo for every kind
	PR        int    // read, rebase
	Head      string // read
	Issue     int    // fix
	Title     string // fix, spec, rebase, and the parenthesised title of a read
	Branch    string // rebase: the branch rebased onto the base
	Base      string // rebase: the branch it is rebased onto
	BodyFile  string // fix, spec: the numbered steps this card carries
	Prior     string // fix: what a prior attempt did, so the worker never repeats it
	Names     string // replay: the replay names, comma separated
	SpecLines string // replay: the spec lines the replays are named at
	Out       string // the directory the card is written into
	Queue     string // the queue directory holding the state file and its lock
	Stdout    io.Writer
	Stderr    io.Writer
}

CutKindInput is everything `cut --kind` takes. Flag parsing lives in cmd/nova-pulse.

type CutValidatedInput

type CutValidatedInput struct {
	Source     string // issue, rows or branch-from
	Issue      string // <owner>/<repo>#<n> when Source is issue
	Rows       string // the rows.tsv path when Source is rows
	BranchFrom string // <owner>/<repo>#<n> when Source is branch-from
	Templates  string // the directory holding the source's .md template
	Out        string // the directory the cards go into
	Repo       string // the clone every git call runs in: `git -C <repo> ...`, never the working directory
	Base       string // the base branch a source that names none is cut onto; "" is dev
	Cards      string // where the cards.tsv goes; "" is <out>/cards.tsv, which puts a table in a queue directory
	Max        int    // cap on the cards cut; 0 lifts it
	Stdout     io.Writer
	Stderr     io.Writer
}

CutValidatedInput is everything the validated-template cut needs, held apart from flag parsing so a test can drive it with a fixture gh and a fixture git.

type Decider

type Decider interface {
	Decide(ctx context.Context, state string, qs map[string]decide.Question) (map[string]decide.Answer, decide.Usage, error)
}

Decider is the typed decision the gate asks about a failing job: one call, a verdict choice and a noul. The shipped one is *decide.Client; a test passes a fake, so no test reaches the network.

type Enqueuer

type Enqueuer interface {
	Enqueue(repo string, pr int) error
}

Enqueuer puts one PR in the merge queue, exactly once per approval.

type FillInput

type FillInput struct {
	Ready    string        // the queue/ready directory the card-<n>.md are popped from
	Launched string        // the queue/launched directory they are moved into; its cards are live
	Lanes    string        // the lanes file: <name>\t<path prefixes> per line; empty names no lane
	Machines string        // the machines registry; a bench whose roles lack `bench` is refused
	Session  string        // the session id stamped into every launched card's marker
	Benches  []string      // the benches to fill, in order
	Only     []string      // glob patterns over a card's filename; empty takes every ready card
	Once     bool          // true runs exactly one tick and returns
	Interval time.Duration // how long between ticks; 0 takes FillInterval
	Stdout   io.Writer
	Stderr   io.Writer
	Now      func() time.Time
	Sleep    func(time.Duration)
	Launcher CardLauncher
	Capacity Capacity
}

FillInput is the fill verb apart from flag parsing, so a test drives one tick with fake directories and stub seams.

type FleetAddInput

type FleetAddInput struct {
	Bench  string
	Queue  string
	Roots  string
	Probe  string
	Stdout io.Writer
	Stderr io.Writer
}

FleetAddInput is `fleet add <bench>`.

type FleetBench

type FleetBench struct {
	Name string
	SSH  string
	Home string
	MAC  string
}

FleetBench is one line of the benches file: name, ssh target, home, mac (mac is "-" when the bench never sleeps). The file is shared with `fleet survey`.

type FleetJoinInput

type FleetJoinInput struct {
	Benches    string // the fleet file
	Machines   string // the machines registry; a machine whose roles lack `bench` is refused
	Name       string // the one bench, which is also its tailnet hostname
	SSH        string // the ssh program; empty is "ssh"
	Tailscale  string // the absolute path of tailscale ON THE BENCH
	AuthKeyEnv string // the environment variable holding the auth key
	Getenv     func(string) string
	Timeout    time.Duration
	Runner     FleetRunner
	Stdout     io.Writer
	Stderr     io.Writer
}

FleetJoinInput is everything `fleet join` needs apart from flag parsing.

type FleetMirrorInput

type FleetMirrorInput struct {
	Benches  string // the fleet file
	Machines string // the machines registry; a machine whose roles lack `bench` is refused
	Name     string // the one bench
	SSH      string // the ssh program; empty is "ssh"
	Repo     string // the https remote to mirror
	Path     string // the absolute path of the bare mirror on the bench
	Timeout  time.Duration
	Runner   FleetRunner
	Stdout   io.Writer
	Stderr   io.Writer
}

FleetMirrorInput is everything `fleet mirror` needs apart from flag parsing.

type FleetProbeJob

type FleetProbeJob struct {
	Runner     string
	Conclusion string
}

FleetProbeJob is one fleet-probe job read back by runner_name.

type FleetProbeRecord

type FleetProbeRecord struct {
	Run   string
	Bench string
	Jobs  []FleetProbeJob
}

FleetProbeRecord is the read-back of one fleet-probe dispatch: the run id and the job of every runner slot.

func ReadFleetProbeRecord

func ReadFleetProbeRecord(path string) (FleetProbeRecord, error)

ReadFleetProbeRecord reads the read-back of the fleet-probe job (ci.yml, #872): `run` and `bench` lines, then one `runner<TAB><name><TAB><conclusion>` line per runner slot. A line that is not one of those three fields is a refusal naming the file and the line, because a partially-read probe would read as a green one.

type FleetReader

type FleetReader func(benches []FleetBench, now time.Time) []BenchReading

FleetReader reads every bench's numbers in file order. The production reader reads them over ssh with ps/pgrep liveness; a test injects a fake returning per-bench process counts and disk/mem, so no test starts ssh.

type FleetRebootInput

type FleetRebootInput struct {
	Benches  string
	Machines string // the machines registry; empty leaves the older refusals as the guard
	Names    []string
	SSH      string // the ssh program; empty is "ssh"
	Wait     time.Duration
	Timeout  time.Duration
	Max      int // at most this many FLEET lines; 0 is all
	Now      func() time.Time
	Sleep    func(time.Duration)
	Stdout   io.Writer
	Stderr   io.Writer
}

FleetRebootInput is everything `fleet reboot` needs: the file, the names, the ssh path, the wait and child bound, and the clock and sleep a test replaces.

type FleetRegistryInput

type FleetRegistryInput struct {
	Machines string // the machines file
	Role     string // list only the machines carrying this role; empty is all of them
	Max      int    // at most this many MACHINE lines; 0 is all
	Stdout   io.Writer
	Stderr   io.Writer
}

FleetRegistryInput is everything the verb needs apart from flag parsing.

type FleetRunner

type FleetRunner interface {
	Run(ctx context.Context, target, script string) (string, error)
}

FleetRunner is the one remote step: run a script on a bench and return what it said. The real one is `ssh <target> bash -s` with the script on stdin.

type FleetSecretsInput

type FleetSecretsInput struct {
	Benches string
	SSH     string
	Timeout time.Duration
	Max     int
	Stdout  io.Writer
	Stderr  io.Writer
}

FleetSecretsInput is everything the fleet secrets verb needs, apart from flag parsing, so a test can drive it against a benches file and a fake ssh on PATH.

type FleetSleepInput

type FleetSleepInput struct {
	Benches  string // the fleet file
	Machines string // the machines registry; a machine whose roles lack `bench` is refused
	Name     string // the one bench to put to sleep
	SSH      string // the ssh program; empty is "ssh"
	Force    bool   // sleep even a busy bench
	IfIdle   bool   // skip a busy bench instead of refusing it
	Timeout  time.Duration
	Max      int
	Stdout   io.Writer
	Stderr   io.Writer
}

FleetSleepInput is everything `fleet sleep` needs apart from flag parsing.

type FleetStandardInput

type FleetStandardInput struct {
	Benches   string // the fleet file: name<TAB>ssh target<TAB>home<TAB>mac
	Machines  string // the machines registry; a machine whose roles lack `bench` is refused
	Name      string // the one bench to check
	SSH       string // the ssh program; empty is "ssh"
	OS        string // "linux" or "darwin"; empty asks the bench with uname -s
	Go        string // the Go toolchain the fleet is on; empty is go1.26.5
	Want      string // the nova bins' stamp; empty reports what the bench has
	MinFreeGB int    // the floor on free space under HOME
	Timeout   time.Duration
	Max       int // at most this many lines; 0 is all
	Runner    FleetRunner
	Stdout    io.Writer
	Stderr    io.Writer
}

FleetStandardInput is everything `fleet standard` needs apart from flag parsing.

type FleetSuspendInput

type FleetSuspendInput struct {
	Benches  string
	Machines string // the machines registry; empty leaves the older refusals as the guard
	Names    []string
	SSH      string // the ssh program; empty is "ssh"
	Force    bool   // suspend even a busy bench
	IfIdle   bool   // skip a busy bench instead of refusing it
	Timeout  time.Duration
	Max      int // at most this many FLEET lines; 0 is all
	Stdout   io.Writer
	Stderr   io.Writer
}

FleetSuspendInput is everything `fleet suspend` needs, apart from flag parsing, so a test can drive it against a benches file and a fake ssh on PATH.

type FleetWakeInput

type FleetWakeInput struct {
	Benches  string
	Machines string // the machines registry; empty leaves the older refusals as the guard
	Names    []string
	SSH      string // the ssh program; empty is "ssh"
	Wait     time.Duration
	Timeout  time.Duration
	Max      int // at most this many FLEET lines; 0 is all
	Now      func() time.Time
	Sleep    func(time.Duration)
	Send     func(packet []byte, addr string) error
	Stdout   io.Writer
	Stderr   io.Writer
}

FleetWakeInput is everything `fleet wake` needs, apart from flag parsing, so a test can drive it against a benches file, a fake ssh on PATH and a sender that captures the packet instead of opening a socket.

type Forge

type Forge interface {
	FindPR(repo, branch string) (int, error)
	CreatePR(repo, base, branch, title, body string) (int, error)
}

Forge opens pull requests. The shipped one runs gh; a test's records what it was asked for. FindPR answers 0 when the branch has no open PR.

type GHEnqueuer

type GHEnqueuer struct{ Timeout time.Duration }

GHEnqueuer is the real Enqueuer, and it is now a THIN ADAPTER ONTO THE ONE DOOR (internal/merge.Enqueuer).

It used to run `gh pr merge <n> -R <repo> --auto`, once per approval, on the reasoning that the sweep only reached it when every check was green and none pending. That reasoning was wrong in the one way that matters: --auto enqueues nothing. It leaves a STANDING INSTRUCTION on the forge, which fires later, with nobody in the room, against whatever the head has become. On 2026-09-18 twenty-seven open pull requests were carrying one, and four of them walked into the dev merge queue on their own. Glenn locked it the same day: nothing reaches the dev merge queue but a batch.

So this reads the pull request's head and offers it to the one door, which admits a batch's head -- or a head with that head's BATCH OK receipt -- and refuses everything else. A refusal is an error like any other here, and the sweep leaves the row open. There is no path from this type to a merge of any kind.

func (GHEnqueuer) Enqueue

func (g GHEnqueuer) Enqueue(repo string, pr int) error

type GHRunners

type GHRunners struct{ Timeout time.Duration }

GHRunners is the real table: `gh api repos/<o/n>/actions/runners` for the runners and `gh run list --status in_progress` for what is actually running, each bounded.

func (GHRunners) InProgress

func (g GHRunners) InProgress(repo string) (int, error)

func (GHRunners) Runners

func (g GHRunners) Runners(repo string) ([]Runner, error)

type GHSource

type GHSource struct{ Timeout time.Duration }

GHSource is the real PRSource: one bounded `gh pr view` per row, the one JSON call the sweep makes per open approval.

func (GHSource) View

func (g GHSource) View(repo string, pr int) (PRView, error)

type GHWork

type GHWork struct{ Timeout time.Duration }

GHWork is the real open work: one `gh pr list` and one `gh issue list`, each bounded.

func (GHWork) OpenIssues

func (g GHWork) OpenIssues(repo string) ([]OpenIssue, error)

func (GHWork) OpenPRs

func (g GHWork) OpenPRs(repo string) ([]OpenPR, error)

type GateInput

type GateInput struct {
	Repo   string
	Branch string
	Queue  string // the queue directory STOP lives in
	Source RunSource
	Stdout io.Writer
	Stderr io.Writer

	// Decide turns on the gate's typed decision: a failing job is classified behind Floor,
	// and a flaky verdict reruns the failed jobs once for the head. A decision below the
	// floor is a suggestion, never an authorization, and today's STOP stands.
	Decide  bool
	Floor   float64
	Decider Decider
}

GateInput is the verb's input, held apart from flag parsing.

type Gater

type Gater interface {
	// Gate reports whether the branch is red this tick, the sha or reason that says so,
	// and an error when the probe itself could not run (never a verdict).
	Gate(tick int) (red bool, why string, err error)
}

Gater is the mechanical gate on the branch this bench merges into: it answers whether that branch is red, and the loop does the rest (STOP, the note, the skip). It is a seam: the gh probe and the admission gates live on the sibling branch rowan/pulse-config-gate-admission. A nil Gater is the STOP file alone.

type HarvestInput

type HarvestInput struct {
	ID           string
	Root         string
	Sources      string
	Templates    string
	MaxBodyBytes int
	Max          int
	Stdout       io.Writer
	Stderr       io.Writer
	Now          func() time.Time

	// Decide turns on the harvest's one typed decision per finished job: before
	// any push each done card's RESULT.md is classified behind Floor as fixed,
	// already-fixed, no-change, failed or off-branch. Decider is the typed
	// decision seam (the shipped *decide.Client; a test's fake). A nil Decider
	// leaves the harvest exactly as it was.
	Decide  bool
	Floor   float64
	Decider Decider

	// Bench turns the harvest around: the jobs are read on that bench over the
	// BenchShell seam rather than under a local root, and Root names the swarm
	// root ON the bench (comma separated for more than one). A bench harvest
	// wants no --sources, no --templates and no --id -- which a `cut --rows`
	// never produces -- and never relaunches: it folds what is there.
	Bench string
	// Machines is the machines registry --bench is held against before the first ssh
	// (Glenn's lock of 2026-09-18: runner hosts are CI-only). An empty path leaves the
	// verb unguarded, which is what a test and a by-hand run want; cmd/nova-pulse names
	// the registry on every real invocation.
	Machines     string
	SSH          string   // the ssh program the shipped shell runs; "" is "ssh"
	Clones       []string // <owner>/<name>=<dir>, or a bare <dir> for any repo
	Session      string   // only jobs whose RESULT.md names this session
	BranchPrefix string   // only branches under this prefix; "" is rowan/
	Base         string   // the base of a card that names none; "" is dev
	Since        time.Duration
	Shell        BenchShell
	Forge        Forge

	// Launched is the queue directory `fill` moves live cards into. When it is
	// named, harvest drains it: a card whose job has finished leaves it for Done
	// or Failed, its launched marker goes with it, and its lane is free.
	Launched string
	Done     string
	Failed   string
}

HarvestInput is everything the harvest verb needs, held apart from command-line parsing so a test can drive it with fake directories and a fake nova-swarm/nova-pulse on PATH.

type Harvester

type Harvester interface {
	Harvest(tick int) (done int, undecided []Undecided, err error)
}

Harvester folds every card whose job has written a RESULT.md. Seam: the shipped `nova-pulse harvest` verb, driven per pulse id.

type HygieneDisk

type HygieneDisk interface {
	FreeGB() int
	Free() string
	SizeGB(path string) int
}

HygieneDisk answers the two numbers the run verb's one line needs.

type HygieneInput

type HygieneInput struct {
	Verb   string // run, reap, delete-job, delete-slot, drop-cache, log
	Slot   string
	Job    string
	DryRun bool
	N      int // log: how many trailing lines

	Home      string   // every root, the log and the cache hang under it
	Roots     []string // default <home>/rowan-swarm-root, <home>/rowan-working/tmp
	LogPath   string   // default <home>/hygiene.log
	CachePath string   // default <home>/.cache/go-build
	Hostname  string   // default the short local host name

	// The runner `_diag` prune's two rules. Zero means the default.
	DiagDays     int   // age window in days, default HygieneDiagDaysDefault
	DiagMaxBytes int64 // per-runner-directory cap, default HygieneDiagMaxBytesDefault

	Now    func() time.Time
	Procs  HygieneProcs
	Disk   HygieneDisk
	Stdout io.Writer
	Stderr io.Writer
}

HygieneInput is the hygiene verb family's input, held apart from flag parsing.

type HygieneProcs

type HygieneProcs interface {
	Busy(path string) bool
}

HygieneProcs answers whether any process names a slot in its command line or its working directory. The real one reads /proc; tests drive a fake.

type LaunchInput

type LaunchInput struct {
	Cards    string // path to cards.tsv: label<TAB>slot<TAB>model<TAB>card
	Root     string // the pulse root; the swarm pool lives under <root>/pool
	Slots    int    // the ceiling on free slots launch considers
	Deadline string // the whole pulse's deadline, whole seconds
	Queue    bool   // true keeps the overflow in queue.tsv instead of refusing
	Files    int    // the --files budget every card in the batch carries; 0 takes the default
	Tokens   string // the --tokens budget; empty takes the default
	QueueDir string // the queue directory STOP lives in; empty falls back to Root (admission.go)
	// Routes is the routes.tsv the typed decision reads to pick each card's worker. Empty
	// means no routing: the cards group by their own model column, exactly as before.
	Routes  string
	Floor   float64 // answers below the floor keep the card's own model as the default worker
	KeyEnv  string  // the environment variable the decision's key comes from
	BaseURL string  // the decision provider endpoint
	Stdout  io.Writer
	Stderr  io.Writer
	Now     func() time.Time
	// Log is where the structured JSON event line goes, beside the stdout line and never
	// instead of it. nil writes no JSON line, which is how the tests that predate the
	// slice keep their exact stdout and stderr; cmd/nova-pulse passes stderr, which on a
	// bench is the systemd journal.
	Log io.Writer
	// GUID is the source of this run's guid. nil reads the real /proc in production; a
	// test injects a fixed one so it reads no /proc.
	GUID func() string
}

LaunchInput is everything the launch verb needs, held apart from the command-line parsing so a test can drive it with fake directories and a fake nova-swarm on PATH.

type Launcher

type Launcher interface {
	Launch(tick int) (launched, free int, err error)
}

Launcher fills every free slot on every bench. Seam: the shipped `nova-pulse launch`.

type LedgerRow

type LedgerRow struct {
	PR         int
	Head       string
	Card       string
	Verdict    string // APPROVE, HOLD, STALE
	At         string // when the verdict was recorded, RFC3339 UTC
	EnqueuedAt string // when the merge was enqueued, or "-"
	ClosedAt   string // when the row was closed, or "-"
}

LedgerRow is one row of the approvals ledger.

func OpenRows

func OpenRows(rows []LedgerRow) []LedgerRow

OpenRows folds the log into one row per PR — the last one written — and keeps the rows that are not closed, in PR order.

func ReadLedger

func ReadLedger(queue string) ([]LedgerRow, error)

ReadLedger reads every row of <queue>/ledger.tsv in file order.

type LoopCount

type LoopCount struct {
	Label string
	N     int
}

LoopCount is one long-running loop the caller named and how many of it are up. The patterns come from --loop and never from a name baked into this tool: a verb carrying `harvest-loop.sh` in its source would freeze the scripts it exists to retire.

type ManagerInput

type ManagerInput struct {
	Policy string // the finite policy this shift executes
	Queue  string // the queue directory: pending, launched, done, failed and the state files
	Roots  string // comma-separated swarm roots, the benches this shift harvests
	Bus    string // the nova-bus clone this shift is the single waiter on
	As     string // the name this shift waits and receipts as
	Hours  float64
	Max    int
	Stdout io.Writer
	Stderr io.Writer
	Now    func() time.Time
}

ManagerInput is the verb's input, held apart from flag parsing so a test can drive a whole shift against a fake queue and fake gh, git, nova-bus and nova-swarm on PATH.

type Notifier

type Notifier interface {
	Note(subject, body string) error
}

Notifier is the ONE call a tick may make to a person. The real one shells `nova-bus send`; with no --bus the loop appends the note to <queue>/ESCALATE, which is the same inbox the hand loop wrote to.

type OSDisk

type OSDisk struct{ Home string }

OSDisk is the real disk: df for the free space, du for the build cache. It fails open -- a number it cannot read never turns into a deletion.

func (OSDisk) Free

func (o OSDisk) Free() string

func (OSDisk) FreeGB

func (o OSDisk) FreeGB() int

func (OSDisk) SizeGB

func (o OSDisk) SizeGB(path string) int

type OSProcs

type OSProcs struct{ Timeout time.Duration }

OSProcs is the real process table: one bounded listing, and a signal per overdue pid.

func (OSProcs) Alive

func (o OSProcs) Alive(pid int) bool

func (OSProcs) Busy

func (o OSProcs) Busy(path string) bool

Busy is the authoritative liveness probe: any process whose cwd is at or below the path, or whose command line names it, holds the slot. It reads /proc and never signals anything.

func (OSProcs) Kill

func (o OSProcs) Kill(pid int) error

func (OSProcs) List

func (o OSProcs) List() ([]Proc, error)

type OpenIssue

type OpenIssue struct {
	Number int
	Title  string
	Body   string
}

OpenIssue is one open issue, as the refill rule reads it.

type OpenPR

type OpenPR struct {
	Number int
	Head   string
	Draft  bool
	Title  string
	Body   string
}

OpenPR is one open pull request, as the refill rule reads it.

type PRCheck

type PRCheck struct {
	Name  string
	State string
}

PRCheck is one check on a PR, normalised to one state token.

type PRSource

type PRSource interface {
	View(repo string, pr int) (PRView, error)
}

PRSource answers what a pull request looks like right now. The real one runs gh; a test drives a fake, so no test of this package ever reaches the network.

type PRView

type PRView struct {
	Number  int
	State   string // OPEN, MERGED, CLOSED
	IsDraft bool
	Head    string
	Labels  []string
	Title   string
	Checks  []PRCheck
	// AutoMerge says the forge is carrying a STANDING INSTRUCTION to merge this pull
	// request when its checks go green. It is not an enqueue and it is not ours: the sweep
	// reads it as a hold, and `nova-merge queue audit` takes it off.
	AutoMerge bool
}

PRView is everything the sweep needs to know about one pull request.

type Packet

type Packet struct {
	Case    string
	Ref     string
	Result  []string // the RESULT lines of the card the case is about
	Refusal string   // the harness's refusal line, one line
	Rules   []RuleRow
}

Packet is the decision packet: everything the triage route is given.

func BuildPacket

func BuildPacket(queue, kind, ref string, result []string, refusal string) Packet

BuildPacket assembles a packet for one case from the queue's rule table.

func (Packet) Card

func (p Packet) Card() string

Card renders the packet as the card the route is handed: line 1 the RESULT contract line (`RESULT <label> sha=<sha12 of everything below>`, SPEC-PULSE "The card"), then the wall, the evidence, the candidate rows, and the one line the verdict must be.

It never exceeds PacketMax bytes. Over the ceiling it drops EVIDENCE first, from the end, and says how many lines it dropped: the rule rows and the verdict shape are the decision and are never the thing cut.

type PoolInput

type PoolInput struct {
	Sources string        // path to the declared sources TSV (kind, locator, template)
	Root    string        // root directory holding seen.tsv and the default pool.tsv
	Out     string        // path pool.tsv is written to; empty means <root>/pool.tsv
	Timeout time.Duration // bound on every gh child, default 120s
	Max     int           // bound on admitted candidates; 0 means no bound
	Stdout  io.Writer
	Stderr  io.Writer
}

PoolInput holds everything the pool verb reads, apart from command-line parsing, so a test can drive it with fake gh invocations on PATH.

type PoolRow

type PoolRow = Candidate

PoolRow is a candidate as pool.tsv holds it: pool.tsv, queue.tsv and next.tsv share the Candidate shape, and pool names it for the file it lives in.

type ProbeInput

type ProbeInput struct {
	Label   string
	Card    string
	History string
	Budget  int
	Stdout  io.Writer
	Stderr  io.Writer
}

ProbeInput is one card under the checklist. History is the abstain history: one class per line, an optional tab-separated count after it. Budget is the card's byte budget; 0 means unbounded. The probe makes no model call and touches no network.

type Proc

type Proc struct {
	PID  int
	Age  time.Duration
	Args string
}

Proc is one row of the process table: a pid, its age, and the argv as one string.

type ProcessTable

type ProcessTable interface {
	List() ([]Proc, error)
	Alive(pid int) bool
	Kill(pid int) error
}

ProcessTable is the machine's processes, and the two things the reaper does with them.

type ProgressInput

type ProgressInput struct {
	Queue  string // the queue directory: pending, launched, done and the POLICY
	Roots  string // comma-separated swarm roots, the benches whose usage.tsv is read
	Day    string // YYYY-MM-DD the day window starts at; empty means today (UTC)
	Stdout io.Writer
	Stderr io.Writer
	Now    func() time.Time
}

ProgressInput is the progress verb's input, held apart from flag parsing so a test can drive it against fake usage files and a fake gh on PATH.

type PublishFile

type PublishFile struct {
	Name string
	Body []byte
}

PublishFile is one file shipped to where the page is served.

type Publisher

type Publisher func(dest string, files []PublishFile, timeout time.Duration) error

Publisher ships the page and its series to where they are served. Injected: a test that published for real would need a host.

type ReapInput

type ReapInput struct {
	Roots    string // comma-separated swarm roots
	Queue    string // the queue directory holding pending, launched and failed
	Deadline time.Duration
	DryRun   bool
	Procs    ProcessTable
	TempGlob string        // default "/tmp/*swarmtest*"; a test points it at its own directory
	TempRoot string        // the directory the temp glob must sit under; empty derives it from the glob's literal prefix
	TempAge  time.Duration // default 30 minutes

	// Rule E2 (runners.go): a runner busy with no in-progress run on Repo for longer than
	// RunnerIdle is restarted through its service. With no Repo and no Runners table the
	// rule does not run and the count is zero: a bench with no self-hosted runner has no
	// jam to clear.
	Repo       string
	Runners    RunnerTable
	Restarter  RunnerRestarter
	RunnerIdle time.Duration // default DefaultRunnerIdle (5 minutes)
	Now        func() time.Time
	Stdout     io.Writer
	Stderr     io.Writer
}

ReapInput is the reap verb's input, held apart from flag parsing.

type Reaper

type Reaper interface {
	Reap(tick int) (requeued, failed int, undecided []Undecided, err error)
}

Reaper requeues or fails a card whose slot went quiet. Seam: rowan/pulse-ledger-reap-cut. A card it can neither requeue nor fail by a rule comes back Undecided.

type Refiller

type Refiller interface {
	Refill(tick int) (added int, err error)
}

Refiller tops the queue up to its floor from the declared sources. Seam: rowan/pulse-config-gate-admission (the policy's floor and scope live with the config).

type Rerunner

type Rerunner interface {
	RerunFailed(repo string, runID int64) error
}

Rerunner is the optional half of a RunSource that can ask the provider to rerun a run's failed jobs. The shipped ghRunSource implements it; a source that cannot rerun leaves the gate's STOP as it is today.

type RuleRow

type RuleRow struct {
	Kind      string
	Condition string
	Verdict   string
	State     string
}

RuleRow is one row of <queue>/RULES.tsv: the case kind it decides, the condition in the evidence that selects it, the verdict it gives, and its state (`pending` until a person has read it).

func CandidateRules

func CandidateRules(rules []RuleRow, kind string) []RuleRow

CandidateRules are the rows that could decide this case: the rows of its own kind, and the rows written for every kind (`*`). Nothing else is put in front of the route, because a row of another kind is a row that can only mislead it.

func ReadRules

func ReadRules(queue string) []RuleRow

ReadRules reads <queue>/RULES.tsv. A missing file is no rows and not an error: the first day of a queue has no table yet, and every verdict that day is NEW.

type RunInput

type RunInput struct {
	Queue  string // the queue directory: pending, launched, done, failed, STOP, RULES.tsv
	Roots  string // the benches, comma separated
	Repo   string // owner/name, the repo the gate watches
	Branch string // the branch the gate watches
	Hours  float64
	Tick   time.Duration
	Once   bool
	Max    int
	Bus    string // a nova-bus clone; empty sends notes to <queue>/ESCALATE
	As     string // the name notes are sent to
	Stdout io.Writer
	Stderr io.Writer

	Now   func() time.Time
	Sleep func(time.Duration)

	// GateEvery is how many ticks apart the gate's own probe runs; 0 is gateEvery. It is a
	// field and not a constant so a bench with a slow gh and a bench with a fixture can
	// each say how often the probe is worth its seconds.
	GateEvery int

	// Configured, when set, is handed the configuration after every re-read, so the seams
	// run on the same values this loop does and pulse.toml has ONE reader per tick.
	Configured func(Config)

	Gate     Gater
	Harvest  Harvester
	Sweep    Sweeper
	Reap     Reaper
	Refill   Refiller
	Launcher Launcher
	Notifier Notifier
}

RunInput is the run verb, apart from flag parsing, so a test drives it against fake directories, a fake clock and stub seams -- no network, no model, no sleep.

type RunSource

type RunSource interface {
	LatestRun(repo, branch string) ([]CIRun, error)
	FailedJob(repo string, runID int64) (CIJob, error)
}

RunSource is where the gate's verdict comes from. The shipped one wraps gh; a test's answers a fixture, so no test in this package reaches the network.

func NewFileRunSource

func NewFileRunSource(path string) (RunSource, error)

NewFileRunSource reads a source file: {"runs":[...],"job":{"name":...,"log":...}}, or a bare array of runs.

func NewGHRunSource

func NewGHRunSource(timeout time.Duration) RunSource

NewGHRunSource is the gate's source when no --source file is given.

type Runner

type Runner struct {
	Name   string `json:"name"`
	Status string `json:"status"` // online, offline
	Busy   bool   `json:"busy"`
}

Runner is one self-hosted runner as the runners API reports it.

type RunnerRestarter

type RunnerRestarter interface {
	Restart(name string) error
}

RunnerRestarter restarts one runner through its own service.

type RunnerService

type RunnerService struct {
	Name   string
	Host   string // "-" or empty is this machine
	Kind   string // systemd or svc
	Target string // the unit, or the runner directory holding svc.sh
}

RunnerService is one row of the service map.

type RunnerTable

type RunnerTable interface {
	Runners(repo string) ([]Runner, error)
	InProgress(repo string) (int, error)
}

RunnerTable is the two facts the rule needs: which runners are busy, and how many runs are actually in progress on the repo they serve.

type SSHRunner

type SSHRunner struct {
	Program string
}

SSHRunner is the real runner. Program is the ssh command from --ssh; empty is `ssh` on PATH.

func (SSHRunner) Run

func (r SSHRunner) Run(ctx context.Context, target, script string) (string, error)

type SelfReader

type SelfReader func(name string, loops []string, now time.Time) SelfReading

SelfReader reads the host the verb runs on. Injected for the same reason.

type SelfReading

type SelfReading struct {
	Name      string
	CIRunners int
	Cores     int
	Load      int
	FreeGB    int
	Orphans   int
	Loops     []LoopCount
	Unknown   bool // the host could not be read; it renders like a DOWN bench
}

SelfReading is the host running the verb, which is a bench too. The Studio drowned at load 147 on 2026-09-17 and no page showed it, because the page only ever read the machines it ssh'd to.

type ServiceRestarter

type ServiceRestarter struct {
	Queue   string
	Timeout time.Duration
}

ServiceRestarter is the real restarter: per the service map, `systemctl restart <unit>` over ssh for a systemd runner (Space) and `<dir>/svc.sh stop && <dir>/svc.sh start` for a launchd one (the Studio). A runner with no row in the map is a refusal and not a guess: restarting the wrong service is worse than the jam.

func (ServiceRestarter) Restart

func (s ServiceRestarter) Restart(name string) error

type StandardCheck

type StandardCheck struct {
	Name  string // the token the STANDARD line carries
	OS    string // "linux", "darwin", or "" for every bench
	Probe string // one line of POSIX shell printing the value on stdout
	Match string // one of the Match* constants
	Want  string // what Match compares against
	// Root, when set, is the TOOLCHAIN ROOT this check is the standard's own copy of, in the
	// slash form internal/swarm/toolchain.go declares it: HOME-relative (`sdk`) or absolute
	// (`/opt/homebrew/Cellar/go`). The wall grants exactly these paths, so the standard and
	// the wall are ONE LIST checked in both directions, per OS, by the class test in
	// internal/ci -- a root the wall grants that no bench provisions is a wall naming a path
	// that will not be there, and a root a bench has that the wall never names is the hurt of
	// 2026-09-18 returning. The KIND of each grant is the wall's decision and lives with the
	// wall, not here: a bench only has to HAVE a root.
	Root string
}

StandardCheck is one line of the bench provisioning standard: what it is called, which benches it applies to, the one-line shell that reads the bench, and the answer the standard demands.

func FleetStandardChecks

func FleetStandardChecks(goos, goWant, stamp string, minFreeGB int) []StandardCheck

FleetStandardChecks is the standard itself: the checks for one operating system, in the order they print. goWant is the Go toolchain the fleet is on, stamp is the nova bins' build stamp (empty means "report whatever this bench has"), and minFreeGB is the floor under HOME (launch refuses below 25 GB).

The Linux list is tools/bench-standard.sh's toolchain half: the Go SDK, sbcl, the safe-rm helper every bench script sources, and the nova stamp. The darwin list is fleet/macos/provision-mac-bench.sh: the Go SDK and sbcl under ~/sdk, real git ahead of the Xcode shim, and each runner's .path carrying it (INSTALL-batman.md, 2026-09-18: /usr/bin/git is the shim and the sandbox cannot read /var/db/xcode_select_link).

THE TOOLCHAIN ROOTS (the `toolchain-*` checks, Root set) are this table's half of the one list the sandbox wall grants -- internal/swarm/toolchain.go is the other half, and the class test in internal/ci fails when they drift apart in either direction, per OS. A LINUX root is DEMANDED, because the standard's own installer is what puts it there and a bench missing one kills every Go card on it. A DARWIN root is REPORTED and never drifted on: a Mac's toolchains are installed and on PATH rather than unpacked into a home, so which trees exist is the machine's shape -- a Mac with no .NET is a Mac with no .NET -- while that the bench has a working Go at all is what the `go` check above asserts. The wall skips an absent root either way, so the report is the wall's argv read in advance.

func (StandardCheck) Met

func (c StandardCheck) Met(value string) bool

Met says whether one check's value meets the standard.

type State

type State struct {
	NextCard   int
	Tick       int
	RefillTick int
	Gate       int
}

State is what a restart must not lose: the next card number (the one cutter's counter), the tick count, the tick the last refill ran on, and how many times the gate has run.

func LoadState

func LoadState(dir string) (State, error)

LoadState reads <dir>/pulse.state. A file that is not there is a fresh queue: the first card number and zero counters. A file that is malformed is a REFUSAL and never a zero state -- a zeroed next_card reissues numbers that are live.

func (State) Save

func (s State) Save(dir string) error

Save writes the counters whole, through a temp file and a rename, so a tick killed mid-write leaves the last good state rather than half of this one.

type StatusHTMLInput

type StatusHTMLInput struct {
	HTML string // the local file to write
	// Machines is the fleet's truth: the machines registry (internal/fleet), seven columns
	// and roles. The benches read over ssh are its rows carrying role `bench`, and the page
	// says which machines serve the merge group's shards, instead of leaving that to a
	// second file nobody updates.
	Machines string
	// Benches is the retired four-column fleet file: name, ssh target, home, mac. It reads
	// for one more release, and every run of it says on stderr what to pass instead.
	Benches  string
	Queue    string   // the queue whose depth, fill log and REPO the page reads
	SSH      string   // the ssh program; empty is "ssh"
	Publish  string   // host:dir to ship the page and the series to; empty ships nothing
	GhConfig string   // GH_CONFIG_DIR for the gh children; empty leaves the caller's own
	DayStart string   // when the merged counter resets, as HH:MMZ; empty is 02:00Z
	Branch   string   // the branch whose merge queue and tip the page shows; empty is dev
	Self     string   // the name of the host running the verb; empty omits the row
	Loops    []string // label=pattern pairs counted on the self host
	Timeout  time.Duration
	Reader   FleetReader
	SelfRead SelfReader
	Ship     Publisher
	Now      func() time.Time
	Stdout   io.Writer
	Stderr   io.Writer
}

StatusHTMLInput is everything `status --html` needs, apart from flag parsing, so a test can drive it against fake directories and injected readers.

type StatusInput

type StatusInput struct {
	Queue          string // the queue directory: pending, launched, done, failed and the state files
	Roots          string // comma-separated bench roots, the benches in scope
	SlotsStores    string // comma-separated bench slot-lease stores to report utilisation for
	Batches        string // the directory holding the swarm's batch-*.out outputs; empty claims nothing
	Day            string // YYYY-MM-DD the day window starts at; empty means today (UTC)
	Max            int
	Timeout        time.Duration
	ExpandingHours int // the sustained window the EXPANDING verdict requires, in whole hours; <= 0 means 2
	Stdout         io.Writer
	Stderr         io.Writer
	Now            func() time.Time
}

StatusInput is everything the status verb needs, apart from flag parsing so a test can drive it against fake directories and a fake gh on PATH.

type SweepInput

type SweepInput struct {
	Repo     string
	Queue    string
	Source   PRSource
	Enqueuer Enqueuer
	Now      func() time.Time
	Stdout   io.Writer
	Stderr   io.Writer
}

SweepInput is the sweep verb's input, held apart from flag parsing.

type Sweeper

type Sweeper interface {
	Sweep(tick int) (swept int, err error)
}

Sweeper retires what is finished: merged PRs, closed issues, stale job directories. Seam: rowan/pulse-ledger-reap-cut.

type TriageInput

type TriageInput struct {
	Case     string
	Queue    string
	Out      string
	Ref      string
	Evidence string // a file of the RESULT lines and the refusal line; default <queue>/UNDECIDED/<case>.txt
	Stdout   io.Writer
	Stderr   io.Writer

	// Decide turns on the verb's typed decision: the bounded packet is put to the provider
	// as one verdict question behind Floor, and the answer is printed as one advisory line
	// beside the verb's own. A decision below the floor is a suggestion, never an
	// authorization: the packet is cut and written exactly as it was before the flag.
	Decide  bool
	Floor   float64
	Decider Decider
}

TriageInput is the triage verb, apart from flag parsing.

type Undecided

type Undecided struct {
	Case    string
	Ref     string
	Result  []string
	Refusal string
}

Undecided is one thing no rule could decide, and the whole evidence for it. It is what a step hands back when its rule table has no row: the case kind (one of TriageKinds), the thing it is about, the RESULT lines and the harness's refusal line. Nothing else -- a log, a transcript or a report body in here would be the coordinator's turn again.

type Verdict

type Verdict struct {
	Case    string
	Verdict string
	Rule    string // the condition of the row that decided it, or NEW
}

Verdict is the one line a triage comes back as.

func ParseTriage

func ParseTriage(s string) (Verdict, error)

ParseTriage reads the one line a triage card demands. Anything else -- a preamble, a second line, a verdict that is not one of the five, a kind that is not one of the seven -- is an error naming what was wrong, because a verdict this tool half-read is a verdict nobody approved.

func (Verdict) Apply

func (v Verdict) Apply(queue, condition string) (bool, error)

Apply records a verdict: a NEW rule becomes a pending row of the table, and anything else is already policy and writes nothing. It returns whether a row was written.

func (Verdict) Line

func (v Verdict) Line() string

Line renders a verdict in the shape the card demands, so that a test can round-trip a packet through the shape it asked for without a model in the loop.

type WatchInput

type WatchInput struct {
	Queue  string        // the merge queue: entry rows, the MERGED line and removal rows
	Bus    string        // the nova-bus checkout whose addressed notes are watched
	Jobs   string        // the job roots: <root>/<slot>/jobs/<label>/RESULT.md
	Until  string        // the event that ends the watch: pr=<n> merged, note-from=<name>, job=<label> done
	Cap    time.Duration // the wall it never runs past
	Now    func() time.Time
	Sleep  func(time.Duration)
	Stdout io.Writer
	Stderr io.Writer
}

WatchInput is everything the watch verb needs, held apart from flag parsing so a test can drive it against a fake queue, a fake bus checkout, a fake bench tree and a fake clock.

type Wiring

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

Wiring is the six seams of RunInput at once: it satisfies Gater, Harvester, Sweeper, Reaper, Refiller and Launcher, so one value fills every seam of a run.

func NewWiring

func NewWiring(in WiringInput) *Wiring

NewWiring fills in the defaults and returns the seams. It makes no call of its own.

func (*Wiring) Gate

func (w *Wiring) Gate(tick int) (bool, string, error)

Gate runs the gate verb over the branch this bench merges into. The verb owns the STOP file -- it writes the admission name on line 2, which is what admission.go reads while red -- so run.go's own STOP is only ever the fallback for a bench with no gate wired.

func (*Wiring) Harvest

func (w *Wiring) Harvest(tick int) (int, []Undecided, error)

Harvest folds every bench that has cards in flight. A root with no cards.tsv has never launched anything and is skipped rather than refused.

func (*Wiring) Launch

func (w *Wiring) Launch(tick int) (int, int, error)

Launch fills every free slot on every bench from queue/pending, oldest card first. The cards it takes are moved to queue/launched before the batch starts, so a card is in exactly one place at every moment -- the stale listing that launched card-892 on both benches at once is what a move, and not a copy, prevents.

The STOP admission is launch.go's: while the gate's STOP stands, only a card whose line 1 names the red launches. That is class C, and it is why the red's own fix card no longer goes out by hand.

func (*Wiring) Reap

func (w *Wiring) Reap(tick int) (int, int, []Undecided, error)

Reap collects what the benches leak, and restarts a runner stuck busy with nothing running (rule E2). The counts are the reap verb's own line.

func (*Wiring) Refill

func (w *Wiring) Refill(tick int) (int, error)

Refill is the rule of bin/refill.sh, in the tool: a READ card for every open non-draft PR in <queue>/WORKSET with no card at its current head, and a FIX card for every WORKSET issue with no open PR naming it and no card pending or launched for it, the prior attempts named so the worker fixes the prompt's failure instead of repeating it (Glenn: fix the prompt, not retry). Every card is cut by `cut --kind`, which is the only numberer.

It runs on the configured cadence and only while the pool is under the floor -- the floor being every slot on every bench, because a queue deeper than the benches is a queue that went stale before it ran. WORKSET is the whole admission: an issue outside it is work nobody asked for (bug: 25 reads refused by mistake at 15:20Z, and the answer was the WORKSET, not a wider net).

func (*Wiring) Sweep

func (w *Wiring) Sweep(tick int) (int, error)

Sweep walks the approvals ledger: green, undrafted, unheld approvals are enqueued once, a moved head is marked stale, and a merged PR closes its row and writes the MERGED record `status --oneline` counts.

type WiringInput

type WiringInput struct {
	Queue    string
	Roots    string
	Repo     string
	Branch   string
	Deadline time.Duration // the batch deadline; 0 is DefaultCardDeadline
	Timeout  time.Duration // the bound on every child; 0 is childTimeout
	Max      int
	TempGlob string // the swarm test leftovers reap collects; empty is reap's own default
	TempRoot string // the directory the temp glob must sit under; empty derives it from the glob's prefix
	Now      func() time.Time
	Config   func() Config // the tick's configuration, re-read by run.go each tick
	Log      io.Writer     // where each verb's one line goes; nil is <queue>/pulse.log

	Runs      RunSource
	PRs       PRSource
	Enqueuer  Enqueuer
	Procs     ProcessTable
	Runners   RunnerTable
	Restarter RunnerRestarter
	Work      WorkSource
}

WiringInput is everything the wired seams need. Every interface field is optional: nil takes the shipped implementation, and a test puts a fake in its place.

type WorkSource

type WorkSource interface {
	OpenPRs(repo string) ([]OpenPR, error)
	OpenIssues(repo string) ([]OpenIssue, error)
}

WorkSource is the open work of a repo: the PRs a read is owed on, and the issues a fix is owed on. The shipped one is two bounded gh calls; a test drives a fake.

Jump to

Keyboard shortcuts

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