Documentation
¶
Overview ¶
Package runner owns everything that executes outside the app process: the Claude Code session, the git worktree, the gates, and the preflight that proves the environment can run any of it.
Index ¶
- Constants
- func DefaultBase(ctx context.Context, dir string) string
- func GHReady(ctx context.Context) error
- func ReapTmuxSession(log *slog.Logger, name string)
- func TmuxSessionName(issue int64, attempt int) string
- type Check
- type Diff
- type PublishResult
- type Report
- type Result
- type Session
- type Status
- type Workspace
- func (w *Workspace) Commit(ctx context.Context, msg, agent, model, runID string, issue int64) (string, error)
- func (w *Workspace) Diff(ctx context.Context) (Diff, error)
- func (w *Workspace) Push(ctx context.Context, remote, branch string) error
- func (w *Workspace) Remove(ctx context.Context)
- func (w *Workspace) RevertOutside(ctx context.Context, allowed []string, files []string) ([]string, error)
Constants ¶
const MinClaudeVersion = "2.0.0"
MinClaudeVersion is the oldest Claude Code known to support the flags the runner depends on (--session-id, --output-format stream-json, --allowedTools).
Variables ¶
This section is empty.
Functions ¶
func DefaultBase ¶
DefaultBase asks the remote which branch a PR should target.
Hardcoding "main" is wrong on any repo that uses master, develop or trunk, and a PR opened against a branch that does not exist fails late and confusingly. Falls back to the local HEAD's upstream, then to main.
func GHReady ¶
GHReady reports whether gh exists AND is authenticated.
Checked before pushing, not before opening the PR: discovering that gh is not logged in AFTER the branch is already on the remote leaves a dangling branch nobody asked for.
func ReapTmuxSession ¶ added in v0.2.0
ReapTmuxSession kills a session left behind by a run this process did not finish.
The one leak the kill-on-completion policy cannot close by itself is a builder that dies mid-run: the tmux server outlives it and keeps the session, and nothing in the finished-run path ever executes. The reconciler, which already adopts those orphans to expire their rows and remove their worktrees, calls this to reap the session too.
Silent when tmux is absent or the session is already gone. Both are the normal case, not a fault.
func TmuxSessionName ¶ added in v0.2.0
TmuxSessionName builds the deterministic name for an issue run.
Deterministic on purpose: an operator watching issue 412's second attempt can type the name without looking it up. Unique on purpose too — the attempt number is part of it, and an issue is only ever claimed by one runner at a time, so two live sessions cannot collide.
Types ¶
type Check ¶
type Check struct {
ID int `json:"id"`
Key string `json:"key"`
Label string `json:"label"`
Status Status `json:"status"`
Detail string `json:"detail,omitempty"`
// Remedy is the exact thing the operator should run or change. A preflight
// that reports a failure without saying how to fix it just moves the
// problem into a support channel.
Remedy string `json:"remedy,omitempty"`
// Required marks a probe the wizard will not let you skip. gh auth, claude
// auth and the live execution probe are required because the development
// loop shells out to both binaries — a project that proceeds without them
// fails later, in an agent run, where the cause is much harder to see.
Required bool `json:"required"`
Took time.Duration `json:"took_ns"`
}
Check is one probe result.
type Diff ¶
type Diff struct {
Files []string
Added int
Removed int
// New files versus existing ones, counted apart.
//
// Blast radius is what a change DISTURBS, not what it adds. Two thousand
// lines across five brand-new files cannot break one line of what already
// works — nothing imports them yet. Three hundred lines rewritten across
// twenty existing files can break all twenty. Measured as one number those
// two are indistinguishable, and the second is by far the more dangerous.
//
// So the caps are applied to Touched* only, and the totals serve as a
// runaway guard rather than as the design limit.
NewFiles []string
TouchedFiles []string
TouchedAdded int
TouchedRemoved int
HeadSHA string
HasChanges bool
}
Diff is what the runner DERIVED from git — never what the agent claimed.
The agent's verdict is a hint about intent. The facts come from the repository, because an agent that believes it changed three files and actually changed thirty is exactly the case the caps exist to catch.
type PublishResult ¶
PublishResult is what the operator gets back. Pushed can be true while URL is empty: the branch reached the remote but `gh` could not open the PR. Those are genuinely different states and the caller reports them differently — a branch on the remote with no PR still needs a human to know it is there.
func OpenPR ¶
func OpenPR(ctx context.Context, dir, base, branch, title, body string) (PublishResult, error)
OpenPR creates the pull request with gh and returns its URL and number.
The body is passed via a file rather than -b: a verdict containing backticks, $(...) or a newline is ordinary prose from a model, and interpolating it into a shell would make an agent's summary able to run commands. exec.Command does not use a shell, but the file also sidesteps ARG_MAX on a long diff summary.
type Report ¶
Report is the full preflight.
func Preflight ¶
Preflight runs every probe. It is the single code path behind both GET /api/builder/preflight and `togo-builder doctor`, so the wizard and the CLI can never disagree about whether a machine is ready.
Probes are ordered cheapest-first so an obvious failure surfaces immediately, and the two that cost real money or time (18, 20) run last.
type Result ¶
type Result struct {
Text string `json:"text"`
IsError bool `json:"is_error"`
Subtype string `json:"subtype"`
CostUSD float64 `json:"cost_usd"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
NumTurns int `json:"num_turns"`
DurationMS int64 `json:"duration_ms"`
SessionID string `json:"session_id"`
// TmuxSession is the session the run actually executed in, or empty when
// tmux was unavailable and the process was spawned directly. Returned so a
// caller that did not choose the name can still persist and display it.
TmuxSession string `json:"tmux_session,omitempty"`
Raw string `json:"-"`
Took time.Duration `json:"-"`
}
Result is the terminal `result` event plus what we derived from it.
type Session ¶
type Session struct {
// ID is minted by us and passed as --session-id, so the mapping from a
// database row to a running process exists BEFORE the process does. A crash
// between spawn and first write still leaves a row that can be reconciled.
ID string
// Dir is the working tree. For an implement run this is a git worktree
// isolated from the operator's checkout.
Dir string
// Prompt is the full instruction. Untrusted content inside it must already
// be wrapped by the caller — see orchestrator.wrapUntrusted.
Prompt string
// Model: haiku | sonnet | opus. Empty inherits the CLI default.
Model string
// AllowedTools restricts the surface. Empty string means NO tools, which is
// what a classification pass wants.
AllowedTools string
// MaxTurns bounds a runaway loop.
MaxTurns int
// PermissionMode: acceptEdits for implement runs. Never bypassPermissions.
PermissionMode string
Timeout time.Duration
// Env is extra environment for the session, as "KEY=value".
//
// This is how the control plane tells the in-session guard hooks what the
// operator configured. Without it the hooks read .claude/autonomy.yaml and
// nothing else, so a ceiling edited in the agent settings UI had no effect
// inside the run — the two disagreed and the file silently won.
Env []string
// TmuxSession is the name of the tmux session the run is spawned inside, so
// an operator can `tmux attach -t <name>` and watch the work happen.
//
// Empty means "name it for me" — every run gets a session, because the
// requirement is that every running task is attachable and a per-call-site
// opt-in would silently miss whichever call site is added next. Set it
// explicitly where the name should be GUESSABLE: an issue run uses
// TmuxSessionName(number, attempt) so the operator can type it from the
// board without looking anything up.
TmuxSession string
// Log is where the tmux wrapper reports. nil takes slog.Default().
Log *slog.Logger
}
Session is one headless Claude Code invocation.
type Workspace ¶
type Workspace struct {
Repo string // the main checkout
Dir string // the worktree
Branch string
BaseSHA string
}
Workspace is an isolated git worktree for one agent run.
A worktree, not a branch in the main checkout: two agents working concurrently in one directory would overwrite each other, and an operator editing the same files while an agent runs would see their work vanish under a checkout. Each run gets its own directory and its own branch.
func NewWorkspace ¶
NewWorkspace creates a worktree on a fresh branch off the base ref.
func (*Workspace) Commit ¶
func (w *Workspace) Commit(ctx context.Context, msg, agent, model, runID string, issue int64) (string, error)
Commit records the work with provenance in the trailer.
Never `git add -A` at this point — Diff already staged deliberately, and the blast-radius check has run against that staged set. Committing a wider set than was checked would defeat the check.
func (*Workspace) Diff ¶
Diff derives what actually changed. This is the only source of truth about a run's effect.
func (*Workspace) Push ¶
Push sends the branch to the remote.
--force-with-lease, never --force: if someone else moved the branch since we last saw it, this fails instead of overwriting their work. For a branch the agent just created the distinction rarely matters; on a retry of an issue whose branch a human has already edited, it is the difference between a failed push and lost work.
func (*Workspace) Remove ¶
Remove tears the worktree down. The branch survives so a PR can be opened from it after the directory is gone.
func (*Workspace) RevertOutside ¶
func (w *Workspace) RevertOutside(ctx context.Context, allowed []string, files []string) ([]string, error)
RevertOutside undoes any change to a path outside the allowlist.
Belt and braces with the guard hooks: a hook constrains the agent's own shell, but a subagent, a script the agent ran, or a tool that writes indirectly can all land a file the hook never saw. This runs after the fact against the real diff, so it catches whatever got through.