Documentation
¶
Overview ¶
Package runner executes jobs on behalf of the engine. It registers with a runner token and advertises its adapters, leases jobs over a long-poll, materializes its own workspace by fetching the task's repo URL into a per-task worktree, runs the agent CLI headless through an adapter, runs the task's check itself once the worker reports done, and pushes the result branch to the remote. A foreman turn has none of that material side: it runs the project's foreman session headless in a per-project scratch directory, with no workspace and no check to run. It heartbeats progress and completes with a fencing token, so lease expiry plus fencing make crashes safe.
Index ¶
- func ForemanScratchDir(stateDir string, project int64) (string, error)
- func PrepareForemanCheckout(ctx context.Context, scratch, repoURL, landTarget, defaultBranch string, ...) (foreman.Checkout, error)
- func RemoveMCPConfig(path string) error
- func ReportLanded(ctx context.Context, client *api.Client, repoURL, branch string, ...) error
- func Run(ctx context.Context, cfg Config) error
- func WriteMCPConfig(stateDir, socket, token string) (string, error)
- type CheckResult
- type ClassCaps
- type Clock
- type Config
- type Ticker
- type WorkerResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ForemanScratchDir ¶
ForemanScratchDir is the foreman's scratch directory for a project (§4): a directory under the runner's state, created on demand and kept between turns. It holds the reading checkout (PrepareForemanCheckout) and is not where the harness runs: the foreman never edits code, so there is no worktree to make, and the harness runs in the checkout.
The checkout, not this directory, is the one working directory — a rule that lives with PrepareForemanCheckout because two processes must agree on it (§4.2 item 1): the runner's headless turns and the client's attached chat are the same session, and a harness keys its sessions by working directory — an attach that ran anywhere else could not resume what the turns left, so the attach prepares the checkout through the same function and runs there.
func PrepareForemanCheckout ¶
func PrepareForemanCheckout(ctx context.Context, scratch, repoURL, landTarget, defaultBranch string, gitEnv []string) (foreman.Checkout, error)
PrepareForemanCheckout refreshes the project's reading copy for both a headless turn and attach. The checkout, `<scratch>/repo`, is the one working directory of the foreman's harness, headless or attached: the harness keys its sessions and project memory by it, and the repository's own CLAUDE.md, AGENTS.md and .claude/ load there as in any clone. Only this preparation writes the checkout; the foreman must receive read-only access from its harness.
The checkout reads the project's land target (issue #122), so the foreman sees what has landed even when the default branch lags it. An empty landTarget means the default branch. A land target the remote does not hold yet (before the first landing creates it) falls back to the default branch; the returned Checkout says so.
func RemoveMCPConfig ¶
RemoveMCPConfig removes one MCP config WriteMCPConfig wrote — the config.json first, then the private directory whole, for an adapter may have written its own CLI's shape of the bridge beside it (§5.2), and that file carries the same token — whatever ended the session. The token inside is dead with its lease or hold; the file must not outlive it. A path already gone is not an error, so the removal is idempotent; any other failure is the caller's to report.
func ReportLanded ¶
func ReportLanded(ctx context.Context, client *api.Client, repoURL, branch string, checkout foreman.Checkout) error
ReportLanded scans the branch a freshly refreshed foreman checkout reads — the land target when one is set, else the default branch — for Brigada-Task trailers and reports each task it finds to the engine as landed (§7.1): a landing made outside the land job — a hand squash, or a brigada land in the user's clone. It is called from a headless turn and from attach, the two places the checkout is refreshed; a failure to scan or report is returned, and the callers treat it as a warning, never a turn or attach failure.
func Run ¶
Run is the runner (§5.1): it registers the runner and its advertisement, then long-polls leases and runs up to MaxParallel jobs at once (one when unset), each in its own goroutine — workspace, worker, result contract, check, push — until ctx is done; it polls only while a slot is free, and on the way out waits for its jobs. It takes cfg by value and fills its defaults in its own copy, so the caller's Config is never mutated; the job runs hold the filled copy.
The engine going away is not a reason to stop: registration and lease failures that are not the caller's own mistake — the socket blinking, the engine restarting — are retried with a doubling backoff, and the runner re-registers before it polls again, because a restarted engine may have a fresh store with no row for it. Run returns only on a configuration error (the caller's, or its own) or ctx cancellation; a single job's failure is carried to the engine, not into the loop.
func WriteMCPConfig ¶
WriteMCPConfig writes the harness's MCP config for one foreman session (§12.2) — a runner's headless turn or a client's attached chat, which share the machinery (§4.2): one server, `brigada` — the caller's own binary (os.Executable) run as the mcp bridge over the engine's socket — with the session's foreman token (a turn's is lease-scoped, a hold's is hold-scoped, §12.1) in the bridge's environment alone. The file is mode 0600 in a private 0700 directory under the runner's state, a fresh unguessable one per session — never a path another session could share, never the foreman's scratch directory (that is the working directory the harness reads at will), and never a command line, which anyone listing processes could read. The token reaches this file and the bridge it names; the caller deletes both with RemoveMCPConfig when the session ends. The directory is the session's alone, and an adapter may write its own CLI's shape of the bridge beside config.json (opencode's opencode.json): the removal takes the directory whole.
Every failure after the directory is made removes what it made — a partial file included — so the caller's error path has no debris to clean: it returns "" and nothing else.
Types ¶
type CheckResult ¶
type CheckResult struct {
ExitCode int `json:"exit_code"`
TimedOut bool `json:"timed_out"`
Output string `json:"output_tail"`
// Log is the whole captured output, for the check log artifact (§9);
// the tail above is what the result carries.
Log string `json:"-"`
}
CheckResult is what the runner reports about a check it ran (§5.5): the exit code, whether the timeout cut it short, and the last of its output. A non-zero exit is a failed check whatever it printed.
func (CheckResult) Failed ¶
func (c CheckResult) Failed() bool
Failed reports whether the check failed: a non-zero exit or a timeout. The kill a timeout performs usually shows up as a non-zero exit too, but the timeout is named on its own so a killed check can never read as a pass. A check that did not run (no command) has neither.
func (CheckResult) JSON ¶
func (c CheckResult) JSON() string
JSON renders the check result for the engine's check_result column.
type ClassCaps ¶
ClassCaps are one job class's own bounds (§5.6): how long a single tool call may stay open — the bound on the suspension it buys the stall clock — and how long the whole run may wall. The shipped defaults sit just under the engine's hard caps (§5.6: 45 m work and fix, 25 m review, 30 m foreman), so a runner that reaches its cap completes the job with a useful result instead of racing the engine's cap to a lost lease. A negative ToolCap leaves an in-flight call suspending the stall clock unbounded — silence inside a tool call is then never a stall — so switch it off only for a class whose long calls are trusted.
type Clock ¶
type Clock struct {
// Now reports the current time; every timestamp the run keeps —
// the activity clock, the stall checks — comes from here.
Now func() time.Time
// NewTicker is time.NewTicker behind the Ticker interface, so a
// manual clock can fire the beats itself.
NewTicker func(d time.Duration) Ticker
}
Clock is the runner's sense of time (§5.6): the wall it reads the worker's activity against, and the tickers its heartbeats beat on. Both are injectable so a test can move a run ten minutes in a millisecond of real time instead of sleeping the stall out. The zero Clock is the real one; Config.defaults fills a nil Now or NewTicker in, so a jobRun never checks for them again.
type Config ¶
type Config struct {
// Socket is the engine API's Unix socket path (§12.1).
Socket string
// Token is the runner token's secret; its name is the runner's
// identity (§12.3).
Token string
// StateDir holds the runner's workspace: the per-repo mirror cache
// under mirrors/, the per-task worktrees under worktrees/ and the
// run records (transcripts, check logs) under records/.
StateDir string
// Agents is the runner's advertisement (§5.1): one entry per
// (adapter, harness, model) it offers, persisted by register.
Agents []api.Agent
// AgentSpecs records the local launch configuration for init comparisons,
// including agent-file, which is not part of the engine advertisement.
AgentSpecs []string
// Adapters are the adapter implementations the runner can drive;
// each agent's Adapter names one of them.
Adapters []adapter.Adapter
// Labels are offered on every lease poll (§8). The git label is
// added by defaults when absent (§7.1), so a land job — which needs
// no agent, only the git label — can always be leased.
Labels []string
// MaxParallel is the runner's own cap on parallel jobs (§8.1),
// advertised at register; 0 means unset.
MaxParallel int
// HeartbeatEvery, LeaseWait and CheckTimeout override the defaults;
// zero keeps them.
HeartbeatEvery time.Duration
LeaseWait time.Duration
CheckTimeout time.Duration
// BackoffMin and BackoffMax bound the wait between lease attempts
// after a failure; zero keeps leaseBackoffMin and leaseBackoffMax.
// A test shortens them so a burst of transient failures on a loaded
// host costs milliseconds, not the minute the defaults add up to.
BackoffMin time.Duration
BackoffMax time.Duration
// SetupTimeout, when set, replaces the time bound a task's recorded
// setup carries (§5.7); zero keeps the recorded one.
SetupTimeout time.Duration
// StallAfter is how long without any worker activity a run counts
// as stalled (§5.6); zero keeps the 10-minute default, negative
// turns the silence watch off.
StallAfter time.Duration
// ClassCaps overrides the per-class caps (§5.6), keyed by job kind
// — work, fix, review, foreman. A zero entry keeps that class's
// default, a set field overrides it, a negative one switches that
// bound off for the class.
ClassCaps map[string]ClassCaps
// KillGrace is how long a stopped worker's process tree has between
// the runner's SIGTERM and its SIGKILL (§5.6); zero keeps 10 s.
KillGrace time.Duration
// ProbeEvery is how often the runner asks the engine for due
// broken-agent probes (§8 "Health"); zero keeps 1 minute.
ProbeEvery time.Duration
// Clock is the runner's time (§5.6): the wall the activity clock
// reads and the tickers the heartbeats beat on. Nil is real time;
// tests inject a manual one so no test sleeps minutes.
Clock Clock
// GitEnv is extra environment for the runner's own git commands —
// where its credentials ride when they come from config (§5.1).
// They never reach the worker's argv or prompt, and the variables they
// name are stripped from a worker's, a setup command's and a check's
// environment. The ambient credentials of scrub.go go whether or not
// this is set; the command line leaves it empty.
GitEnv []string
// HarnessEnv is resolved environment per harness, "VAR=value" entries
// (§5.1): a job's worker gets the entries of its own harness and no
// other's. It is never logged, stored or put in an error.
HarnessEnv map[string][]string
// Ready, when set, runs once the runner holds its state directory's
// lock and its configuration has passed — the moment it is up. The
// caller's startup line belongs here: printed before Run, a second
// process on the same state directory would announce itself up and
// then be refused at the lock.
Ready func()
// Log is the runner's logger; nil uses the default.
Log *slog.Logger
}
Config is what a runner needs to run (§5.1). The zero value is not usable; Run fills the defaults for the tuning fields and refuses the rest.
type Ticker ¶
Ticker is the channel-and-stop face of time.Ticker: what a heartbeat loop needs and nothing more, so a manual clock's ticker is one buffered channel.
type WorkerResult ¶
type WorkerResult struct {
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Question string `json:"question,omitempty"`
Branch string `json:"branch,omitempty"`
Reason string `json:"reason,omitempty"`
// Discoveries is what the worker learned that matters beyond its own
// task (§5.4); the runner keeps it so the engine can hand it to the
// foreman (§4.2).
Discoveries string `json:"discoveries,omitempty"`
// CommitMessage is the commit message the worker wrote for the land
// job (§5.4): subject and body in the repo's own style, which the
// worker reads from the project's instructions and the log. The land
// job uses it when it is there and falls back to its own format when
// it is not (§7.1); the runner passes it on untouched.
CommitMessage string `json:"commit_message,omitempty"`
// Progress is the last progress events, attached when the runner
// ends a stalled or time-capped run (§5.6) so what the worker was
// doing rides with the outcome. A worker's own block never carries
// it.
Progress []string `json:"progress,omitempty"`
// ExtraRepos is the commit each extra repository resolved to (§5.8),
// by name, so the history shows what the job saw. The runner sets it;
// a worker's own block never carries it.
ExtraRepos map[string]string `json:"extra_repos,omitempty"`
// Head is the commit the runner pushed as the task's branch (§7), so
// a later re-review can ask for the fix round's diff alone. The
// runner sets it; a worker's own block never carries it.
Head string `json:"head,omitempty"`
// ChangedPaths is the task's real changed paths, base...branch
// (§6.3, §7.1): what the land gate's protected-path rule matches,
// recorded with the work job's result. The runner sets it; a
// worker's own block never carries it.
ChangedPaths []string `json:"changed_paths,omitempty"`
}
WorkerResult is the parsed result block of a worker's final message (§5.4): what the job ended saying. The runner completes the job with this as the result, adding fields of its own (reason) when the contract itself failed.