--- title: internal/orch description: Go API reference for the orch package. --- # orch ```go import "github.com/jbcom/radioactive-ralph/internal/orch" ``` Package orch is the orchestration layer: the correctness backbone where completion is ORCHESTRATOR\-VERIFIED, never agent\-asserted \(spec §10; .agent\-state/decisions.ndjson "completion\-verification"\). Orchestrator reads a plan \(internal/plan\), dispatches workers \(internal/provider onto internal/agent\) with PLAN\-SCOPED context \(only the ready step\(s\) \+ their group heading \+ the plan title — never the whole plan document\), and verifies each worker's submitted evidence against the task's acceptance criteria before ever marking a task done. A worker terminating is not completion; it is a signal to verify. ## Index - [Constants](<#constants>) - [func EnforcementPrompt\(ctx context.Context, a \*agent.Agent, interval time.Duration\)](<#EnforcementPrompt>) - [func HandleWatchdogSignal\(sig agent.Signal\) \(shouldKill bool\)](<#HandleWatchdogSignal>) - [type Acceptance](<#Acceptance>) - [type AcceptanceChecker](<#AcceptanceChecker>) - [type BindingResolver](<#BindingResolver>) - [type Clock](<#Clock>) - [type ErrProviderTurnInFlight](<#ErrProviderTurnInFlight>) - [func \(e \*ErrProviderTurnInFlight\) Error\(\) string](<#ErrProviderTurnInFlight.Error>) - [type ErrSpendCapExceeded](<#ErrSpendCapExceeded>) - [func \(e \*ErrSpendCapExceeded\) Error\(\) string](<#ErrSpendCapExceeded.Error>) - [type Option](<#Option>) - [func WithAcceptanceChecker\(c AcceptanceChecker\) Option](<#WithAcceptanceChecker>) - [func WithBaseContext\(ctx context.Context\) Option](<#WithBaseContext>) - [func WithBindingResolver\(f BindingResolver\) Option](<#WithBindingResolver>) - [func WithClock\(c Clock\) Option](<#WithClock>) - [func WithDecisionLogRoot\(dir string\) Option](<#WithDecisionLogRoot>) - [func WithMaxParallel\(n int\) Option](<#WithMaxParallel>) - [func WithRunnerFactory\(f RunnerFactory\) Option](<#WithRunnerFactory>) - [func WithSpendCap\(providerName string, capUSD float64\) Option](<#WithSpendCap>) - [func WithWatchdog\(cfg agent.WatchdogConfig\) Option](<#WithWatchdog>) - [type Orchestrator](<#Orchestrator>) - [func New\(st \*store.Store, opts ...Option\) \*Orchestrator](<#New>) - [func \(o \*Orchestrator\) AbsorbDecisionLog\(ctx context.Context, projectID, planID, taskID, workerID string\) error](<#Orchestrator.AbsorbDecisionLog>) - [func \(o \*Orchestrator\) DispatchNext\(ctx context.Context, projectID, planID string\) \(dispatched int, err error\)](<#Orchestrator.DispatchNext>) - [func \(o \*Orchestrator\) HandleContextEnd\(ctx context.Context, a \*agent.Agent, planID, taskID, sessionID string\) error](<#Orchestrator.HandleContextEnd>) - [func \(o \*Orchestrator\) KillWorker\(workerID string\) bool](<#Orchestrator.KillWorker>) - [func \(o \*Orchestrator\) PlanProgress\(ctx context.Context, planID string\) \(Progress, error\)](<#Orchestrator.PlanProgress>) - [func \(o \*Orchestrator\) SetBaseContext\(ctx context.Context\)](<#Orchestrator.SetBaseContext>) - [func \(o \*Orchestrator\) VerifyAndComplete\(ctx context.Context, planID, taskID string, ev a2a.Evidence\) \(done bool, err error\)](<#Orchestrator.VerifyAndComplete>) - [func \(o \*Orchestrator\) Wait\(\)](<#Orchestrator.Wait>) - [func \(o \*Orchestrator\) WriteWorkerDecision\(workerID, decision string\) error](<#Orchestrator.WriteWorkerDecision>) - [type Progress](<#Progress>) - [type RunnerFactory](<#RunnerFactory>) ## Constants EnforcementPromptText is what an Orchestrator periodically writes to a running worker's stdin, per .agent\-state/decisions.ndjson "context\-management": rather than trying to detect when a CLI auto\-compacts its context, send a periodic nudge that keeps the worker on task and, if it is itself capable of fanning out subagents/workflows, encourages it to do so; otherwise it should self\-check at the next convenient point. ```go const EnforcementPromptText = "Stay on task. If you can fan out to subagents or workflows, do. Otherwise, self-check your progress against the assigned step now.\n" ``` ## func [EnforcementPrompt]() ```go func EnforcementPrompt(ctx context.Context, a *agent.Agent, interval time.Duration) ``` EnforcementPrompt runs a ticker for interval that writes EnforcementPromptText to a via WriteInput on every tick, until ctx is canceled or a is done. It never blocks waiting on the agent — WriteInput is a direct, non\-blocking pty write, and a closed/exited agent's write error is swallowed \(there is nothing left to nudge\). Callers typically run this in its own goroutine alongside a dispatched worker's provider.Runner.Run call, canceling ctx when the worker finishes. ## func [HandleWatchdogSignal]() ```go func HandleWatchdogSignal(sig agent.Signal) (shouldKill bool) ``` HandleWatchdogSignal reacts to one agent.Signal from agent.Watch per the control invariant: NEVER wait. A Prompt or Stall is treated as kill\+reclaim \(a worker that would block or has gone quiet cannot be trusted to make progress\). Progress and Exited require no action here — Exited is handled by the dispatch loop's normal evidence/verification path, and Progress is a pure observation. Returns true if the caller should kill a and release the task claim \(via HandleContextEnd or an equivalent MarkFailed/MarkBlocked call\). ## type [Acceptance]() Acceptance is the done\-criteria a task's acceptance\_json column describes. A task with an empty/absent Acceptance is judgment\-only: its completion cannot be mechanically re\-verified, so VerifyAndComplete treats a present, non\-empty Evidence.Output as sufficient \(there is no stronger signal available without a verifier agent — see the package doc's "prefer pure\-Go verification where mechanical; a Ralph verifier only for judgment criteria" note. A judgment verifier is not implemented in this phase\). ```go type Acceptance struct { // Command, if set, must exit 0 when re-run in the project directory. // This is the primary mechanical check: e.g. "go test ./..." or // "golangci-lint run". Command string `json:"command,omitempty"` // FileExists, if set, must exist (stat succeeds) in the project // directory for acceptance to pass. FileExists string `json:"file_exists,omitempty"` // Dir is the working directory Command runs in / FileExists is // resolved against. Empty means the orchestrator's configured project // directory (callers pass this in via VerifyOpts/AcceptanceChecker // wiring — see mechanicalAcceptanceCheck). Dir string `json:"dir,omitempty"` } ``` ## type [AcceptanceChecker]() AcceptanceChecker re\-runs a task's acceptance criteria in pure Go and reports whether it passes. dir is the project working directory the check should run in. ```go type AcceptanceChecker func(ctx context.Context, dir string, acceptanceJSON string, ev a2a.Evidence) (ok bool, reason string, err error) ``` ## type [BindingResolver]() BindingResolver picks the provider binding to use for one dispatch. Exists so tests and callers can supply a fixed/fake binding without a real config.toml. Defaults to always resolving "claude" via provider.ResolveBinding with zero File/Local config \(i.e. the built\-in claude capability record\). ```go type BindingResolver func(ctx context.Context, projectID string, parallelGroup bool) (provider.Binding, error) ``` ## type [Clock]() Clock abstracts time.Now for deterministic tests \(enforcement\-prompt cadence, spend windows\). ```go type Clock func() time.Time ``` ## type [ErrProviderTurnInFlight]() ErrProviderTurnInFlight is returned by checkSpendCap when a CAPPED provider already has an in\-flight \(dispatched\-but\-not\-yet\-recorded\) turn for this project. It is a transient admission refusal — NOT "cap exceeded" — so the ready step is simply retried on a later pass once that turn settles. Kept distinct so DispatchNext's refusal event doesn't misreport a sub\-cap balance as an overspend. ```go type ErrProviderTurnInFlight struct { Provider string } ``` ### func \(\*ErrProviderTurnInFlight\) [Error]() ```go func (e *ErrProviderTurnInFlight) Error() string ``` ## type [ErrSpendCapExceeded]() ErrSpendCapExceeded is returned by checkSpendCap when a provider is over its configured cap. DispatchNext treats this as a per\-step admission refusal, not a fatal error — other ready steps \(possibly on an uncapped provider\) may still dispatch. ```go type ErrSpendCapExceeded struct { Provider string SpentUSD float64 CapUSD float64 } ``` ### func \(\*ErrSpendCapExceeded\) [Error]() ```go func (e *ErrSpendCapExceeded) Error() string ``` ## type [Option]() Option configures an Orchestrator at construction time. ```go type Option func(*Orchestrator) ``` ### func [WithAcceptanceChecker]() ```go func WithAcceptanceChecker(c AcceptanceChecker) Option ``` WithAcceptanceChecker overrides the mechanical acceptance checker used by VerifyAndComplete. Primarily for tests. ### func [WithBaseContext]() ```go func WithBaseContext(ctx context.Context) Option ``` WithBaseContext sets the long\-lived context the async dispatch goroutines run under \(provider turn \+ store writes \+ verification\). The supervisor passes its run context so dispatched work survives past the per\-request IPC context that drove it. A nil ctx is ignored \(keeps the Background default\). ### func [WithBindingResolver]() ```go func WithBindingResolver(f BindingResolver) Option ``` WithBindingResolver overrides how an Orchestrator picks a provider binding for a dispatch. Primarily for tests. ### func [WithClock]() ```go func WithClock(c Clock) Option ``` WithClock overrides the Orchestrator's time source. Primarily for tests. ### func [WithDecisionLogRoot]() ```go func WithDecisionLogRoot(dir string) Option ``` WithDecisionLogRoot overrides the XDG\-ish root directory used for per\-worker decision logs \(see lifecycle.go\). Primarily for tests. ### func [WithMaxParallel]() ```go func WithMaxParallel(n int) Option ``` WithMaxParallel bounds how many steps DispatchNext will dispatch in one call for a parallel group. Zero/negative means unbounded \(bounded only by the number of ready steps\). ### func [WithRunnerFactory]() ```go func WithRunnerFactory(f RunnerFactory) Option ``` WithRunnerFactory overrides how an Orchestrator resolves a provider.Runner for a binding. Primarily for tests. ### func [WithSpendCap]() ```go func WithSpendCap(providerName string, capUSD float64) Option ``` WithSpendCap sets a per\-provider spend cap in USD. A provider with no configured cap \(or a cap of 0\) is treated as uncapped. ### func [WithWatchdog]() ```go func WithWatchdog(cfg agent.WatchdogConfig) Option ``` WithWatchdog overrides the stall/prompt watchdog configuration used for dispatched workers. ## type [Orchestrator]() Orchestrator dispatches workers against a plan and is the sole authority that may mark a task done \(via VerifyAndComplete\). ```go type Orchestrator struct { // contains filtered or unexported fields } ``` ### func [New]() ```go func New(st *store.Store, opts ...Option) *Orchestrator ``` New constructs an Orchestrator against st. Defaults: provider.NewRunner as the runner factory, a claude binding resolver, real time, unbounded parallelism, a 5\-minute stall timeout, no spend caps, and the built\-in mechanical acceptance checker. ### func \(\*Orchestrator\) [AbsorbDecisionLog]() ```go func (o *Orchestrator) AbsorbDecisionLog(ctx context.Context, projectID, planID, taskID, workerID string) error ``` AbsorbDecisionLog reads workerID's XDG decision log \(if any\) and emits its content into the store's project event history as a single worker.decision\_log event, so the orchestrator's NEXT dispatch — and any operator inspecting project history — can see what this worker decided without needing filesystem access to the XDG state root. Absorbing is idempotent\-ish in effect \(re\-running it re\-emits the same content as a new event row\), so callers should call it once per worker lifecycle end \(normal completion, verification rejection, or kill\+reclaim\). A missing decision log file is not an error — most workers write no decisions and that's fine; AbsorbDecisionLog is a no\-op in that case. ### func \(\*Orchestrator\) [DispatchNext]() ```go func (o *Orchestrator) DispatchNext(ctx context.Context, projectID, planID string) (dispatched int, err error) ``` DispatchNext loads the plan for planID, computes what's ready right now, and dispatches workers for as many ready steps as capacity \(maxParallel\) and spend caps allow. It returns the number of steps actually dispatched. DispatchNext does NOT wait for dispatched workers to finish — each dispatch runs its provider turn in its own goroutine, wired through agent.Watch for stall/prompt handling \(kill\+reclaim, never wait\), and reports its result back for VerifyAndComplete via the store's task/event log. The worker's termination or self\-reported result does NOT mark the task done. ### func \(\*Orchestrator\) [HandleContextEnd]() ```go func (o *Orchestrator) HandleContextEnd(ctx context.Context, a *agent.Agent, planID, taskID, sessionID string) error ``` HandleContextEnd is called when a worker signals it has hit its own "manual end" \(e.g. the underlying CLI's own end\-of\-context marker, or an operator\-visible equivalent\) rather than a stall or a normal turn completion. Per decision "context\-management": kill the worker and re\-dispatch fresh from plan\-scoped context — this is cheap because all durable state lives in the store, not in the worker's own memory. The caller \(DispatchNext's dispatch loop or an equivalent driver\) is responsible for actually re\-dispatching; HandleContextEnd's job is only to kill cleanly and release the task claim so it becomes ready again. ### func \(\*Orchestrator\) [KillWorker]() ```go func (o *Orchestrator) KillWorker(workerID string) bool ``` KillWorker cancels the in\-flight agent run for workerID, if one is currently registered, and reports whether it did. This is the process half of a worker\-kill: exec.CommandContext propagates the cancellation to the provider subprocess so it stops consuming tokens and touching the checkout. The store half \(requeue the task, terminate the row\) is store.ReclaimWorker; the supervisor's HandleWorkerKill invokes both. A false return means no run was live under that id \(already finished, or a fan\-out member id\) — harmless, and the store side still runs. ### func \(\*Orchestrator\) [PlanProgress]() ```go func (o *Orchestrator) PlanProgress(ctx context.Context, planID string) (Progress, error) ``` PlanProgress computes Progress for planID by parsing its stored markdown and comparing the full step\-id universe \(plan.Plan.StepIDs\) against the store's done\-set \(the same done\-set DispatchNext feeds into plan.DecomposeRefs\). ### func \(\*Orchestrator\) [SetBaseContext]() ```go func (o *Orchestrator) SetBaseContext(ctx context.Context) ``` SetBaseContext sets the long\-lived context async dispatch goroutines run under. The supervisor calls this once at the top of Run with its run context — the orchestrator is constructed before that context exists, so it can't be a construction option there. Must be called before the first DispatchNext. A nil ctx is ignored. ### func \(\*Orchestrator\) [VerifyAndComplete]() ```go func (o *Orchestrator) VerifyAndComplete(ctx context.Context, planID, taskID string, ev a2a.Evidence) (done bool, err error) ``` VerifyAndComplete is THE BACKBONE: it never trusts a worker's termination or self\-report. It checks ev against task's acceptance criteria — re\-running mechanical checks in pure Go — and only marks the task done in the store if verification passes. Otherwise it marks the task failed \(retryable, per the task's normal retry budget\) and emits a worker.verification\_failed event carrying the rejection reason. ### func \(\*Orchestrator\) [Wait]() ```go func (o *Orchestrator) Wait() ``` Wait blocks until every in\-flight dispatched worker goroutine has finished \(its provider turn \+ verification returned\). The supervisor calls this during shutdown AFTER cancelling the run context — cancellation aborts the in\-flight runner.Run subprocesses via the runningWorkers registry, so this drain is bounded, not a hang. Tests also call it to observe dispatch results deterministically now that dispatch is asynchronous. ### func \(\*Orchestrator\) [WriteWorkerDecision]() ```go func (o *Orchestrator) WriteWorkerDecision(workerID, decision string) error ``` WriteWorkerDecision appends one decision line to workerID's XDG decision log markdown file, creating the file \(and its parent directories\) on first write. Each worker owns its own log; AbsorbDecisionLog later folds these into store events so the next dispatch's context can be informed by what a prior worker decided. ## type [Progress]() Progress reports done/total step counts for a plan, derived from plan.Decompose's notion of what's left. Surfaced to the macro TUI \(Phase 7\) via supervisor status. ```go type Progress struct { Done int Total int } ``` ## type [RunnerFactory]() RunnerFactory resolves a provider.Runner for a binding. Exists so tests can inject a fake runner without a real agent CLI. Defaults to provider.NewRunner. ```go type RunnerFactory func(provider.Binding) (provider.Runner, error) ``` Generated by [gomarkdoc]()