agent

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package agent is the think -> act -> observe loop, extracted from cmd/agent so it can be both DRIVEN by a CLI and MEASURED by an eval harness (HP-11). The loop prints nothing itself: progress flows through an Observer, and the terminal state flows back as a structured RunResult — so one run produces data (a trace + a typed outcome), not stdout noise. cmd/agent wires a printing Observer to reproduce the old live output exactly; the eval harness passes a silent one and reads the RunResult.

It implements the cycle in the most transparent way possible: no native function-calling, no framework. The model emits ONE line of plain text; OUR code parses it, runs the tool, and feeds the result back. Every dial below is annotated with which of the seven principles it embodies.

The seven principles, in one breath:

  1. State lives in YOUR code, never in the model. The LLM is (context) -> text.
  2. The cycle is think -> act -> observe (the model proposes, the harness disposes).
  3. Context is the only state, so managing it IS the engineering.
  4. Every iteration must observe REAL external state, not its own prior text.
  5. Termination is YOUR job, and needs multiple conditions.
  6. Tool failures are observations, not crashes.
  7. The control flow is yours; the model only fills in the next action.

Long-term memory for the agent — the natural extension of Principle 1 (state lives in YOUR code) and Principle 3 (context is the only state, so managing it IS the engineering). The in-loop `messages` slice is state for ONE run; long-term memory is state that survives ACROSS runs. We recall relevant facts before thinking and store what we concluded after answering. The model never drives this — the harness decides what to remember and what to surface (Principle 7).

Index

Constants

View Source
const (
	// DefaultMaxIterations is the (P5) hard cap when Config.MaxIterations is unset:
	// non-negotiable termination, prevents infinite spend. Exported so the CLI can
	// use it as a flag default. A longer/complex task raises this via Config.
	DefaultMaxIterations = runspec.DefaultMaxIterations

	// UncappedIterations is a MaxIterations sentinel meaning "effectively no cap" —
	// for interactive front-ends where a human watches the loop and can interrupt.
	// It is a large positive value so it flows through the loop's `maxIter <= 0 ->
	// DefaultMaxIterations` fallback unchanged.
	UncappedIterations = math.MaxInt32
	// DefaultMaxTokens is the fallback output cap used when Config.MaxTokens is
	// unset (see agent/loop_shared.go). 1024 was anomalously low and silently
	// truncated long final answers or write_file/edit_file content blocks mid-block,
	// forcing a recovery round-trip. 8192 aligns with provider/anthropic.DefaultMaxTokens.
	DefaultMaxTokens = runspec.DefaultMaxTokens
)

---- Principle 7: the control flow is yours. These are OUR dials, not the model's. ----

View Source
const (
	BinaryIdentityDriver = "driver"

	InvocationSurfaceDriverRun   = "driver-run"
	InvocationSurfaceDriverAgent = "driver-agent"
	InvocationSurfaceDriverTUI   = "driver-tui"
)

Binary and invocation-surface identifiers are stable transcript values.

View Source
const (
	FateRepaired = "repaired" // blocked a round, was fed back, and a later round no longer stood in the way.
	FateRefuted  = "refuted"  // its repro command PASSED and confidence was low — the claim was refuted by execution; downgraded to a note.
	FateExpired  = "expired"  // still blocking when the rounds (or the run) ran out.
	FateAdvised  = "advised"  // an unconfirmed blocker under the confidence gate but at/above the advisory floor: fed back for repair, never blocked.
	FateNote     = "note"     // never blocked or fed back: severity "note", or a blocker under the advisory floor.
	FateDropped  = "dropped"  // failed deterministic validation (quote not found verbatim / repro dirtied the workspace).
)

Finding fates — the calibration telemetry (docs/specs/REVIEW-GATE.md finding 8), recorded from day one so reviewer false-positive rates are measurable (FP rate = refuted+expired / total blockers).

View Source
const DefaultReviewRounds = 2

DefaultReviewRounds bounds the reviewer↔solver repair loop when Config.ReviewRounds is unset. Refine-loop gains concentrate in round 1 with documented diminishing returns, and unbounded loops flip correct patches to wrong (Self-Refine; Huang ICLR'24) — so the default is deliberately small.

View Source
const TranscriptSchemaVersion = "9"

TranscriptSchemaVersion is the on-disk RunRecord schema. Bump on any shape change so a reader can refuse an incompatible record instead of misreading absent-vs-zero fields.

v1 = original; Usage had no json tags → PascalCase keys on disk.
v2 = Usage carries snake_case json tags (llm.Usage.UnmarshalJSON dual-accepts
     both shapes for backward-compat with v1-era records).
v3 = RunRecord carries Messages, the continuation seam for resuming a
     conversation into agent.Session.
v4 = RunRecord carries RescuedFrom, preserving the pre-upgrade outcome for
     cap/kill/deadline/budget runs rescued by harness gates.
v5 = RunRecord carries per-role cost fields shared with the delegation ledger.
v6 = RunRecord carries typed guarantee evidence.
v7 = RunRecord carries the config record: canonical effective config,
     prompt/tool-schema/config hashes, build identity, and reserved profile fields.
v8 = RunRecord carries resolved termination policy and detector telemetry.
v9 = ConfigRecord replaces the lazily re-derived EffectiveConfig with the
     canonical policy-value + resolution-trace serializations and the
     runtime-resolution event sequence (PROFILES.md §7.3 / S6c); old
     records still decode via the legacy `effective` pointer field.

Variables

View Source
var ErrReviewParse = errors.New("review parse error")

ErrReviewParse marks reviewer output that could not be parsed even after the reviewer implementation's corrective retry.

Functions

func AppendLedger added in v0.2.0

func AppendLedger(rec RunRecord, meta LedgerMeta) error

func DefaultTools

func DefaultTools(sb sandbox.Sandbox, runTimeout time.Duration, opts ...ReadOptions) map[string]Tool

DefaultTools is the standard toolbox, every tool acting THROUGH the sandbox (P2/P4): file reads and command runs cross the isolation boundary, not the bare host.

The Desc is the API the model programs against (P2): it picks and invokes a tool from this string alone, never the implementation. Each one states what it does, WHEN to use it over the overlapping alternatives, the exact ARG format, and what it RETURNS — single-line, because buildSystemPrompt prints one line per tool. runTimeout is threaded in (not a const) so a caller can lengthen it for a longer-running build/test suite; it is interpolated into the `run` Desc below so the model is told the REAL limit it is working against (P2 — the Desc is the API; a stale "30s" would mislead it once the value is configurable).

func LedgerDir added in v0.2.0

func LedgerDir() (string, error)

func ReadOnlyTools

func ReadOnlyTools(sb sandbox.Sandbox, runTimeout time.Duration, opts ...ReadOptions) map[string]Tool

ReadOnlyTools is DefaultTools with the two dedicated MUTATION tools (write_file, edit_file) removed: list_dir + read_file + run only. It exists for agents whose whole job is to OBSERVE and reason about a codebase without changing it — e.g. the issue-review bot, which grounds a discussion in the real code and must never modify the repo it analyses.

Honest scope note: this is "no dedicated write tools", not a hermetic read-only jail. `run` stays in (grep/build/test is how the agent grounds "does X already exist?"), and `run` executes shell, so it CAN touch the filesystem. That is acceptable here because the caller pairs it with an ephemeral, confined sandbox (the CI checkout) and a trusted-author gate upstream — the bot's task only ever comes from accounts you trust. Drop `run` too if you need a stricter set.

It derives from DefaultTools by deletion so the kept tools' descriptions and schemas never drift from the canonical set.

func ScrubReplayReasoning added in v0.2.0

func ScrubReplayReasoning(msgs []llm.Message) []llm.Message

ScrubReplayReasoning removes opaque reasoning traces from assistant history before a transcript is replayed in a new session. Provider-signed reasoning can only be replayed in its original request chain, so persisted sessions must not carry it.

func StoreMemoryAsync

func StoreMemoryAsync(ctx context.Context, obs Observer, mem memory.Store, scope memory.Scope, task, answer string) <-chan struct{}

StoreMemoryAsync lets an orchestrator that deliberately suppressed in-run storage (for example, ladder loser isolation) store the single accepted winner without disabling recall for the attempts. It has the same best-effort, detached semantics as the loop's own post-answer store.

func TranscriptDir

func TranscriptDir() string

TranscriptDir is the default run-transcript root, honoring $XDG_STATE_HOME (run history is STATE, not the council DATA corpus) and falling back to ~/.local/state. Override with $DRIVER_OS_TRANSCRIPT_DIR.

$XDG_STATE_HOME/driver-os/runs/   (default ~/.local/state/driver-os/runs/)

func WriteTranscript

func WriteTranscript(dir string, rec RunRecord) (string, error)

WriteTranscript persists a run two ways under dir: the full record as "<id>.json" (replayable on its own), and a one-line summary appended to "runs.jsonl" (the longitudinal index — every run this machine made, newest last, cheap to tail or scan for correlation/dedup). dir is created if absent. Returns the per-run file path.

Types

type Config

type Config struct {

	// BinaryLabel is the legacy v1-v3 conflated binary label retained for
	// transcript compatibility. New callers should set BinaryIdentity instead.
	BinaryLabel string

	// BinaryIdentity identifies the built artifact for transcript config records.
	// It is recording-only and never affects behavior.
	BinaryIdentity string

	// InvocationSurface identifies how the artifact was invoked (for example,
	// driver-run). It is recording-only and never affects behavior.
	InvocationSurface string

	// RequestedProtocol and ProtocolFallbackReason are CLI provenance supplied by
	// the caller. The effective protocol is selected by Run or RunNative.
	RequestedProtocol      string
	ProtocolFallbackReason string

	// TrustProfile identifies the selected trust profile for transcript config
	// records. It is recording-only and never affects behavior.
	TrustProfile string

	// ExecProfileName identifies the selected execution profile for transcript
	// config records. It is recording-only and never affects behavior, mirroring
	// TrustProfile.
	ExecProfileName string

	// ExecProfileHash identifies the selected execution profile content for
	// transcript config records. It is recording-only and never affects behavior,
	// mirroring TrustProfile.
	ExecProfileHash string

	// CLIOverrides records the CLI override provenance for transcript config
	// records. It is recording-only and never affects behavior, mirroring
	// TrustProfile.
	CLIOverrides []string

	// Resolution provenance is recording-only and never affects behavior.
	RequiredTrust   string
	Canonical       bool
	FieldProvenance map[string]string

	// ApprovalPolicyName identifies the selected approval policy for transcript
	// config records. It is recording-only and never affects behavior.
	ApprovalPolicyName string

	// ApprovalPolicyHash identifies the selected approval policy content for
	// transcript config records. It is recording-only and never affects behavior.
	ApprovalPolicyHash string

	Model   llm.Provider    // required: the (context) -> text engine.
	Sandbox sandbox.Sandbox // required: the isolation boundary every effect flows through (P2).
	Memory  memory.Store    // optional: cross-run long-term memory; nil = stateless.

	// DisableMemoryStore suppresses the post-answer memory Add call while leaving
	// recall enabled. Ladder attempts use this so losing attempts can benefit from
	// prior memory without leaking their own failed observations into future runs.
	DisableMemoryStore bool

	// Persona is an optional identity block prepended to the system prompt — a
	// stable character the agent keeps across runs (e.g. "You are Adam, an
	// energetic builder…"). Empty = the bare tool-using harness prompt. It leads
	// the prompt so identity frames the tool instructions; the task and recalled
	// memory still follow. Used by multi-agent callers (see ../duet) to give two
	// agents distinct voices on top of the same loop.
	Persona string

	// MemoryScope namespaces this run's long-term memory (mneme isolates facts per
	// scope on both write and recall). The zero value falls back to the package
	// default scope, preserving single-agent behavior. Set it to a per-agent scope
	// (e.g. {AgentID: "adam"}) so several agents can share ONE store without their
	// memories bleeding into each other.
	MemoryScope memory.Scope

	// VerifySandbox is the sandbox the CLOSING gates (VerifyCmd/DiagnoseCmd) run on,
	// when it must differ from Sandbox. nil ⇒ Sandbox (the historical behavior; every
	// session-off caller, incl. all eval sweeps, leaves it nil). It exists for the
	// -session mode: the model's `run` tool acts through a STATEFUL session
	// (Sandbox), but the verification commands must run in a clean context — a model
	// that `cd`s away, or `export`s something odd, must not bend `go build ./...` or
	// the diagnostics feed. Both still hit the same warm container, so there is no
	// cache cost. See ../SESSION.md.
	VerifySandbox sandbox.Sandbox
	Tools         map[string]Tool // optional: nil = DefaultTools(Sandbox).
	Task          string          // required: the goal (this turn's user input when continuing a conversation).
	TaskImages    []llm.ImagePart // optional image parts for THIS turn's user message; cfg.Task stays the text projection used for recall/memory/plan/RunResult.
	// History is a prior conversation to CONTINUE from (the continuation seam, see
	// Session). When non-empty, the loop seeds its message slice with these and
	// appends Task as the next user turn — so the model sees the whole prior
	// exchange instead of a fresh "TASK:" framing. Empty (the default) is the
	// historical single-shot behavior: the run starts from "TASK: " + Task. The
	// loop clones it before appending, so the caller's slice is never mutated.
	History []llm.Message
	Root    string // optional: the dir Sandbox is rooted at; recorded in RunResult.Root.

	BootContext bool // include the boot-context Go dependency digest in the opening ENVIRONMENT preamble; set by the -boot-context CLI flag (default true)

	// StandingContext opts into persisted user messages summarizing the model's
	// diff since run start plus the last run/verify status. A changed block is
	// appended, never rewritten, because OpenAI-family prefix caching requires each
	// request to extend the previous request. Off at the library level (zero value)
	// so callers that don't opt in pay no extra git or prompt cost; the interactive
	// TUI (cmd/driver) opts in by default, headless cmd/agent leaves it off.
	StandingContext bool

	Obs Observer // optional: live progress sink; nil = silent.

	// PerfMark receives optional latency instrumentation from the native tool
	// loop. It is nil for callers that do not collect performance timelines.
	PerfMark func(event string, attrs map[string]any)

	// ModelInfo describes the selected solver model's known limits. A zero value
	// means unknown and disables proactive context-window telemetry.
	ModelInfo llm.ModelInfo

	// Stream opts this run into token streaming: when set AND the provider reports
	// Capabilities().Streaming, each model call goes through Provider.Stream and the
	// incremental text deltas are pushed to Obs if it implements DeltaObserver — the
	// Claude-Code-style live-typing feel a chat front-end wants. Off by default, so
	// every existing caller (eval, issue-bot, council) keeps the single Generate call
	// and byte-identical behavior. A non-streaming provider silently uses Generate
	// even when this is set. Adapters yield the turn's reasoning trace as an
	// assembled Reasoning chunk (llm.Chunk) and collectStream keeps it, so a
	// streamed turn replays its trace — and sees the reasoning-aware no-progress
	// window — the same as a Generate turn.
	Stream bool

	// MinIsolation is the SAFETY PRECONDITION (P2/§5): the weakest sandbox isolation
	// this run will tolerate. Before the first model call, Run/RunNative refuse with
	// Outcome RefusedUnsafe if Sandbox.Capabilities().Isolation < MinIsolation — so
	// untrusted, model-authored code never executes on a boundary too weak to
	// contain it (e.g. requiring IsolationKernel forces a gVisor backend; a `local`
	// or plain-container sandbox is refused). The default zero is IsolationNone,
	// which admits every backend and preserves today's behavior for trusted callers
	// (issue-bot, eval). This is the ONE enforcement point; CLI -untrusted just sets
	// it. Fails CLOSED: a nil Sandbox with MinIsolation > IsolationNone is a refusal,
	// not a panic.
	MinIsolation sandbox.Isolation

	// RequireNetworkOff is a SAFETY PRECONDITION: before the first model call,
	// Run/RunNative refuse when Sandbox.Capabilities reports network access. It
	// fails closed for a nil Sandbox. The zero value preserves trusted callers'
	// existing behavior.
	RequireNetworkOff bool

	// All three default sensibly when zero (P5/P7 — termination knobs are OURS):
	MaxIterations int           // 0 = DefaultMaxIterations. The hard cap on think->act->observe turns.
	MaxTokens     int           // 0 = DefaultMaxTokens. Per-turn output cap on the model call.
	RunTimeout    time.Duration // 0 = defaultRunTimeout. Wall-clock kill for a single `run` command.

	// TerminationPolicy groups no-progress detector thresholds. Its zero value resolves to DefaultTerminationPolicy.
	TerminationPolicy TerminationPolicy

	// VerifyTimeout bounds the closing VerifyCmd executions (final gate,
	// pre-flight baseline, kill/cap upgrade check) separately from the
	// per-`run`-tool RunTimeout. 0 = max(resolved RunTimeout, 5 minutes):
	// a verify suite is routinely slower than a single interactive command,
	// and a too-short bound turns "couldn't finish checking" into a false
	// "did not pass".
	VerifyTimeout time.Duration

	// ReasoningEffort rides on every model call of the run (llm.Request
	// passthrough: "minimal".."xhigh", "" = provider default). It is a QUALITY
	// knob, not a termination knob — reasoning tokens still bill against
	// MaxTokens and MaxTotalTokens, so a higher effort spends more of both.
	ReasoningEffort string

	// PromptProfile selects the NATIVE loop's base system prompt (PROMPT-SKILLS
	// slice 2). "" or "legacy" = the historical four-sentence prompt (the
	// default, so every existing caller is byte-identical); "structured" = the
	// sectioned working-rules prompt measured against it. It is an independent
	// switch — not derived from the model — precisely so an A/B arm differs in
	// this field alone and acceptance/rollback is one flag flip. An unknown
	// value fails CLOSED before the first model call: a typo must not silently
	// run the wrong arm of a paid experiment. The text loop ignores it
	// (Run/buildSystemPrompt is protocol-shaped, not profile-shaped).
	PromptProfile string

	// CodeAct, when true, appends a "code-as-action" instruction block to the
	// native-loop system prompt (see resolveSystemPrompt): it steers the model to
	// treat `run` shell as its primary action — composing/executing code to make
	// and verify changes — rather than issuing many discrete file-tool calls.
	// An A/B experiment knob (docs/specs/CODEACT-SCREEN.md); arms differ in it alone.
	CodeAct bool

	// ReproFirst enforces a red-at-base/green-after-fix reproduction test in the
	// native tool protocol. The solver must write a new test file and call
	// declare_repro before answering, unless it explicitly skip_repro's with a
	// reason.
	ReproFirst bool

	// ReproGate, when true, appends a reproduction-first disposition to the solver system prompt.
	ReproGate bool

	// BatchReads, when true, appends a "batch independent reads" instruction
	// block to the native-loop system prompt (see resolveSystemPrompt): it
	// steers the model to emit several parallel-safe read-only tool calls in
	// ONE turn instead of one per turn, so the harness's parallel prefetch
	// (prefetchLeadingReadOnly) can fetch them concurrently. An A/B arm knob.
	BatchReads bool

	// ReadWindow overrides read_file's line window (max lines a range-less or
	// over-long read returns before clipping). 0 = the built-in default (150).
	// Only consulted when Config.Tools is nil and the default toolbox is built.
	ReadWindow int
	// ReadOutline, when true, appends a compact file structure map (symbols +
	// line numbers) to a CLIPPED read_file so the model can jump to the right
	// range. Only consulted when Config.Tools is nil (default toolbox path).
	ReadOutline bool

	// MaxWallClock bounds the WHOLE run's wall-clock (P5), checked between turns. It
	// is the universal backstop for a spiral that dodges every action/observation
	// detector — the DOGFOOD nano case that emitted ever-changing malformed tool
	// calls (premature finishes, truncated apply_patch blobs) and was only stopped by
	// an EXTERNAL `timeout`, exiting 124 with no typed outcome. With this set the loop
	// ends itself as HitDeadline (and runs the verify-on-terminate check). Especially
	// relevant with slow third-party providers, where iteration count and wall-clock
	// diverge. 0 = off (only the iteration cap bounds the run).
	MaxWallClock time.Duration

	// MaxTotalTokens bounds the run's CUMULATIVE token spend (prompt + completion,
	// summed across turns — RunResult.Usage.TotalTokens), checked at the turn
	// boundary like MaxWallClock. It is the cost cap the iteration cap only
	// approximates: with full-window re-send the prompt grows ~quadratically
	// (HP-8), so a spiral's later turns are far more expensive than its early
	// ones and N iterations can cost 10× what N/2 did. The turn that crosses the
	// budget still gets processed (it may BE the answer); the loop then ends as
	// HitBudget — which the closing verification can still upgrade, exactly like
	// a cap/deadline exit. Per-turn cumulative usage flows through Obs.Note so a
	// caller can measure before choosing a number. 0 = off.
	MaxTotalTokens int

	// MaxTotalCostUSD bounds the run's CUMULATIVE dollar spend, priced by CostFn
	// from the run's cumulative Usage. It mirrors MaxTotalTokens but in dollars:
	// checked at the turn boundary AFTER a paid generation, so the crossing turn
	// is kept and overshoot is bounded by one turn's spend; the loop then ends as
	// HitBudget and closing verification may still upgrade it. 0 = off.
	MaxTotalCostUSD float64

	// AllowUnpricedSpend permits a configured dollar budget to continue when spend
	// cannot be priced. The default false fails closed as HitBudget; set true only
	// when an advisory budget is acceptable.
	AllowUnpricedSpend bool

	// CostFn prices this run's solver model from cumulative Usage. The agent does
	// not know catalog model ids; callers close over the model id in this function.
	// When MaxTotalCostUSD is set but CostFn is nil or returns ok=false, the loop
	// fails closed unless AllowUnpricedSpend explicitly opts into note-once-and-
	// continue behavior.
	CostFn func(llm.Usage) (float64, bool)

	// SolverModel is the solver model's id, so the loop can price solver turns into
	// Spend (the agent otherwise does not know catalog model ids). Empty disables
	// solver pricing in Spend.
	SolverModel string

	// Spend is the run's shared role-aware dollar accumulator (solver + reviewer +
	// planner, each priced at its own model). When non-nil it is the source of truth
	// for MaxTotalCostUSD, superseding CostFn. Shared by POINTER so it accumulates
	// across repair rounds and gate/plan calls. nil = fall back to CostFn (solver-only).
	Spend *Spend

	// VerifyCmd is the closing VERIFICATION gate (P5/HP-5): a success command the
	// caller names (e.g. "go test ./...") that is re-run when the model finishes.
	// A non-zero exit downgrades the terminal Answered to Unverified — turning a
	// model that stopped while the task was still broken (DOGFOOD R9/R10's
	// termination-by-silence false positives: narrated intent, acknowledged
	// failure, hallucinated success) into an honest non-pass instead of exit-0
	// success. Empty = no closing check. The harness does NOT guess the success
	// criterion; the caller states it.
	VerifyCmd string

	// AutoVerify opts into deriving a conservative VerifyCmd from root project
	// markers when VerifyCmd is empty. It is off by default in agent core; the
	// daily driver enables it after prefs/flags resolve. Eval never arms it.
	AutoVerify bool

	// AutoVerifySoft marks a harness-chosen VerifyCmd whose disposition must never
	// be worse than the empty-gate baseline. autoVerifyProvenance names the marker
	// that produced it for notes and the boot preamble. autoVerifyResolved is a
	// session guard: once an interactive Session has attempted auto-verify
	// resolution, later turns must not re-derive or re-run the preflight on WIP.
	AutoVerifySoft bool

	// SkipVerifyBaseline, when true, opts out of the pre-flight verification
	// check. By default (false), when VerifyCmd is set, the harness runs it
	// once on the untouched workspace BEFORE the first model call to record
	// whether the gate starts red.
	SkipVerifyBaseline bool

	// AbortOnRedBaseline, when true AND the pre-flight baseline measures red,
	// causes BOTH loops to return immediately before the first model call with
	// Outcome Unverified, Iterations 0, and a Reason stating the verify command
	// was already failing on the untouched workspace. The caller sets this when
	// a red baseline means the gate is unsatisfiable and the run should not
	// spend any budget. Requires VerifyCmd; inert when SkipVerifyBaseline is
	// also set (skip wins — baseline is not measured, so there is no signal to
	// abort on).
	AbortOnRedBaseline bool

	// VerifyLastRun is the no-VerifyCmd FALLBACK: when set (and VerifyCmd is empty),
	// a silent finish is marked Unverified if the most recent `run` this session was
	// still failing and nothing succeeded after it. Off by default and opt-in
	// because a legitimate absence answer often follows a non-zero exit (e.g. `grep`
	// returns 1 on no match), which this would wrongly flag — VerifyCmd is the
	// precise gate, this is the cheap heuristic for an un-instrumented run.
	VerifyLastRun bool

	// ChurnNudgeRuns, when > 0, injects a ONE-TIME hint after this many FAILING `run`
	// results in a session: a suggestion to stop incremental editing and rewrite the
	// whole file in one write_file. It targets the residual cheap-model failure the
	// live runs surfaced — a capable model (gpt-oss-120b, grok) that passes when it
	// writes the file wholesale but burns to the iteration cap when it wanders in
	// edit_file/run churn on shifting line numbers. The runs that rewrite pass; the
	// runs that churn time out, so nudging a stuck model toward a rewrite is the lever
	// to consistent passing. 0 = off (the historical behavior).
	ChurnNudgeRuns int

	// VerifyContinue turns the VerifyCmd gate from TERMINAL into CONTINUE-ON-FAIL: a
	// finish that doesn't verify is not accepted as Unverified while iterations
	// remain — instead the failing output is fed back as an observation and the loop
	// keeps going. This is the lever that lifts weak-model pass rates: the dominant
	// cheap-model failure (DOGFOOD R9/R10) is a PREMATURE finish — a tool-call-free
	// turn that is really narration ("I'll implement now") or a hallucinated "done",
	// emitted before the work is complete. Re-grounding it with the real red test
	// output and "keep working" converts that false stop into actual progress, where
	// the plain terminal gate would just record a non-pass. Bounded by MaxIterations;
	// requires VerifyCmd.
	VerifyContinue bool

	// TestFence is the READ-ONLY glob list for the run (REVIEW-GATE slice 0;
	// recommended `*_test.go,testdata/**`): write_file/edit_file refuse matching
	// paths outright, every fenced file is hashed at run start, and ANY drift at
	// a closing gate — including a `run`-mediated shell redirect — makes the run
	// Unverified with the files named. It closes the hole the probe only papered
	// over with prompt text ("do not modify tests"): test-file immutability must
	// be enforced by the HARNESS, not asked of the model. Empty = off (today's
	// behavior byte-for-byte); opt-in for now — eval/challenge runs and -review
	// runs set it.
	TestFence []string

	// DiffScope is the WRITABLE allowlist for the run (the inverse of TestFence):
	// a list of path globs the solver's changes MAY touch; any change outside it
	// is a first-class failure (ScopeViolation), not a pass.
	//
	// Scope globs are anchored at the repository ROOT:
	//   - `dir/**`  matches paths that start with `dir/` (and `dir` itself).
	//     It does NOT match `pkg/dir/evil.go` — the scope is a prefix, not a
	//     substring.
	//   - `*.go`    (bare, no slash) matches only root-level files whose name
	//     matches the pattern.  It does NOT match `pkg/x.go`.
	//   - `ci/build.sh`, `.github/*` (slash present) — exact path.Match on the
	//     full root-relative path.
	//
	// Enforcement is layered, mirroring the test fence:
	//   1. Tool layer: write_file/edit_file (and append) REFUSE a path outside
	//      the scope with a recovery-shaped error — the cheap, immediate fence.
	//   2. Closing gate: a git-tree snapshot pair (run-start → gate-time) catches
	//      changes that went AROUND the tools (shell redirects, sed -i, git
	//      checkout, build artifacts); the run terminates ScopeViolation.
	//   3. Degrade loudly: if the run-start snapshot fails (non-git workspace),
	//      a Note is recorded and the tool-layer enforcement alone is active —
	//      a violation is never fabricated from an infrastructure fault.
	//
	// When BOTH TestFence and DiffScope are set: the fence WINS for fenced
	// paths (they are read-only even when in scope); a refusal message names
	// whichever mechanism refused.
	//
	// Empty = off (today's behavior byte-for-byte). Motivation: a solver asked
	// to add a guard test reordered production code the test was meant to pin,
	// to make its own test pass — reward-hacking the gate.
	DiffScope []string

	// Reviewer arms the REVIEW GATE (REVIEW-GATE slice 1): an injected,
	// independent model reviewer run at every path that can end Answered, ONLY
	// after the fence and VerifyCmd pass (execution-first). Blocking findings —
	// grounded by verbatim quote, over the confidence gate or confirmed by an
	// executed repro — are fed back to the solver VerifyContinue-style while
	// ReviewRounds remain, then mark the run Unverified. nil = gate off.
	// Implementations live outside agent (council.CodeReviewer) — council
	// imports agent, so the reviewer must be injected to avoid the cycle.
	Reviewer Reviewer

	// ReviewPolicy controls whether review failures block and whether an
	// unavailable baseline may be skipped. Its zero value preserves the default
	// behavior of requiring any configured Reviewer.
	ReviewPolicy ReviewPolicy

	// RequireDiff makes an empty final workspace diff a failed completion rather
	// than an answer. It requires a git baseline at startup and is opt-in because
	// some tasks are legitimate no-ops.
	RequireDiff bool

	// ReviewUnverified runs one advisory, report-only reviewer pass when a run
	// ends Unverified with a non-empty diff before the normal review gate would
	// fire. The verdict enriches salvage telemetry but never changes the run
	// outcome or reason. CLI defaults it on; callers may leave it false to opt out.
	ReviewUnverified bool

	// ReviewRounds caps the reviewer↔solver repair cycles (0 =
	// DefaultReviewRounds). Bounded on purpose: refine-loop gains concentrate in
	// round 1, and unbounded review loops flip correct patches to wrong.
	ReviewRounds int

	// Planner arms the PLAN STAGE (triad slice 3, plan.go): an injected,
	// independent planner model run ONCE before the first solver turn, whose
	// plan is appended to the seeded task. Fails OPEN — a planner error never
	// blocks the run. nil = stage off. Implementations live outside agent
	// (council.Planner) — same injection pattern as Reviewer.
	Planner Planner

	// FinishNudgeWindow arms HP-4's near-cap FINISHER. When > 0, and the run is
	// within this many turns of the iteration cap, AND the world looks SETTLED — the
	// most recent `run` exited 0 (build/test green) and no file has been mutated
	// (write_file/edit_file) for this many turns — a one-time hint is injected telling
	// the model the task appears complete and to finish now (or say what remains). It
	// manufactures the finish ATTEMPT the spinners never make: the gemini/grok runs
	// that burn to the cap on ALREADY-GREEN code (hit_cap-but-passing), which
	// upgradeIfVerified only rescues at the very end. The nudge is SAFE: the resulting
	// finish still routes through verifyTermination, so a premature/false "done" is
	// caught (and under VerifyContinue becomes more work, not a stop). Gated on a green
	// `run` on purpose — with no build/test executed there is no grounded done-signal,
	// so the finisher stays out and the cap/other detectors bound those runs. 0 = off.
	//
	// Eval-validated (selfhist, gemini-3.1-pro, window=3): it converts hit_cap-but-passing
	// into clean `answered` (fired on the 2 settled trials, stayed out on the red-build
	// one — 0 false-positives). NOTE the window governs WHAT it buys: a small window
	// fires LATE (iter 27–29 of 30), so it cleans the OUTCOME SIGNAL but does not cut
	// tokens — the run still reaches the cap. To cut spend, widen the window so it fires
	// earlier (e.g. 8–10), trading against interrupting a model still doing real work.
	FinishNudgeWindow int

	// DiagnoseCmd arms slice 1 of the code-intelligence work (see docs/specs/CODE-INTELLIGENCE.md):
	// a fast compile/type-check command (e.g. "go build ./...") run as a diagnostics
	// SOURCE when the model looks stuck with a broken build, whose errors are surfaced
	// into the loop as INFORMATION — never a gate (the gate stays at termination,
	// VerifyCmd). It targets the dominant build-broken failure: a model that edits
	// without self-checking and never learns it left a compile error (the glm-5 `"errors"
	// imported and not used` → hit_cap case). The source/surfacing split is deliberate —
	// a future persistent-gopls client becomes an alternative SOURCE behind this same
	// call, leaving the loops' SURFACING untouched, so only this command changes. Empty =
	// off. May name the same command as VerifyCmd or a cheaper one (build vs. test).
	DiagnoseCmd string

	// DiagnoseAfterEdits is the stuck threshold for DiagnoseCmd: the feed stays silent
	// until the model has made this many file edits WITHOUT reaching a green build/run in
	// between (the counter resets on any passing `run` or a clean DiagnoseCmd). The
	// threshold is what honors the multi-file reality — a multi-file change is legitimately
	// red mid-flight, so the feed must NOT nag after the first edit, only once the model is
	// clearly not converging. 0 = off (DiagnoseCmd is also required to arm).
	DiagnoseAfterEdits int

	// AnswerNudgeWindow arms a near-cap answer-forcer (native loop) for an OBSERVE-ONLY
	// agent: when > 0 and the run is within this many turns of the iteration cap, a
	// one-time hint tells the model to stop using tools and give its final answer NOW.
	// It is the observe-only sibling of FinishNudgeWindow: that finisher is gated on a
	// green `run` and stable files, which a read-only agent (no `run`, no edits) never
	// has — so it can never fire for a critic. DOGFOOD (council slice 4): a code critic
	// over a repo issued a read_file/search every single turn and NEVER emitted a
	// no-tool-call answer turn, hitting the cap with zero output on every budget tried —
	// the native loop only terminates on a text answer the model wouldn't produce on its
	// own. This nudge manufactures the answer attempt. It fires ONLY when the toolset is
	// observe-only (isObserveOnly — an allowlist of read-only built-ins, fail-closed);
	// an effectful/coding run leaves it inert and uses FinishNudgeWindow instead, so a
	// nudged premature "done" can never mask unverified broken work. 0 = off.
	AnswerNudgeWindow int

	// FinishTool names a first-class TERMINAL tool (native loop only): when the
	// model calls a tool with this name, the turn ends cleanly as Answered with that
	// call's "message" field as RunResult.Answer. It is the structured counterpart to
	// the prose-termination done-signal (stop calling tools, reply in text) — for a
	// caller whose whole turn IS "send a message" (duet's `say`), giving the model a
	// real finish ACTION beats steering it toward "reply with no tool", which cheap
	// models fight (they call a nonexistent `answer` tool or run `answer` as a shell
	// command and burn the turn, then idle to the cap: DUET-DOGFOOD F1/N1/N2). Any
	// non-finish calls in the same turn run first (a final cp/build is a legit last
	// action). An explicit finish is a STRONGER done-signal than silence, so it skips
	// the silence-reverification heuristic. Empty = no terminal tool (the default;
	// every existing caller terminates on the prose path, unchanged). The named tool
	// must still be present in Tools so it's advertised to the model.
	FinishTool string

	// FinishToolTrustsCaller lets a FinishTool call vouch for task completion while
	// still enforcing safety boundaries. By default a finish is NOT ground truth
	// that the task is done: when the caller configured VerifyCmd/VerifyLastRun, an
	// explicit finish routes through the SAME completion gate as prose termination
	// (a finish while the build is red is Unverified, not a false Answered — the
	// harness review's FinishTool hole). Set this true when the finish IS the
	// deliverable and there is nothing to re-verify — a conversational caller whose
	// whole turn is "send a message". Even then, RequireDiff, the test fence,
	// diff scope, caller-cancel check, empty-answer guard, and grounded-memory
	// policy still run.
	// Default false.
	FinishToolTrustsCaller bool
	// contains filtered or unexported fields
}

Config is everything Run needs. Model, Sandbox, and Task are required; the rest default sensibly (nil Tools -> DefaultTools, nil Memory -> no cross-run recall, nil Obs -> silent).

func (Config) Bindings added in v0.2.0

func (c Config) Bindings() (Runtime, Content)

Bindings projects cfg's runtime dependencies and per-run content.

func (Config) Requested added in v0.2.0

func (c Config) Requested() runspec.RequestedConfig

Requested projects cfg's policy fields into a presence-preserving RequestedConfig (zero value = unset, historical lazy semantics).

func (Config) Split added in v0.2.0

Split is Requested + Resolve + Bindings in one call — the whole requested- side conversion for callers that hold a Config template (eval, tests).

func (Config) Validate added in v0.2.0

func (c Config) Validate() error

Validate checks contradictory or unsafe pure configuration before setup does any environmental work. Workspace and sandbox capability checks remain in the stages that can inspect those resources.

type ConfigRecord added in v0.2.0

type ConfigRecord struct {
	SchemaVersion int `json:"schema_version"`
	// Binary is the legacy v1-v3 conflated invoking-binary label. It is retained
	// solely so old transcript JSON continues to decode.
	Binary            string `json:"binary,omitempty"`
	BinaryIdentity    string `json:"binary_identity,omitempty"`
	InvocationSurface string `json:"invocation_surface,omitempty"`
	// RequestedProtocol and ProtocolFallbackReason describe CLI routing provenance.
	// They intentionally remain outside Effective so they do not affect ConfigSHA256.
	RequestedProtocol      string `json:"requested_protocol,omitempty"`
	ProtocolFallbackReason string `json:"protocol_fallback_reason,omitempty"`
	HarnessCommit          string `json:"harness_commit,omitempty"`
	HarnessDirty           bool   `json:"harness_dirty,omitempty"`
	PromptSHA256           string `json:"prompt_sha256"`
	ToolSchemaSHA256       string `json:"tool_schema_sha256"`
	// ConfigSHA256 is the hash of the canonical POLICY-VALUE serialization
	// (runspec.ResolvedSpec.ConfigSHA256): two runs with the same effective
	// policy share it regardless of HOW each value was reached. It does not
	// prove semantic task equivalence, environment identity, unrecorded runtime
	// state, or provider-side behavior.
	ConfigSHA256 string `json:"config_sha256"`

	// EffectiveProtocol is the wire protocol the loop actually ran ("text" /
	// "tools") — loop-selected, so recorded beside the policy, not inside it.
	EffectiveProtocol string `json:"effective_protocol,omitempty"`

	// Policy is the canonical policy value the loop held (v9).
	Policy *runspec.PolicyValue `json:"policy,omitempty"`
	// ResolutionTrace is the per-field provenance captured at Resolve, under
	// its own hash — "same policy?" and "same way of asking for it?" stay
	// separable questions (§7.3).
	ResolutionTrace       *runspec.Trace `json:"resolution_trace,omitempty"`
	ResolutionTraceSHA256 string         `json:"resolution_trace_sha256,omitempty"`
	// RuntimeResolutions is the append-only runtime-derivation event sequence
	// (auto-verify, session /verify overrides) with its hash — included in
	// bundle identity so two runs sharing ConfigSHA256 cannot execute different
	// derived verify commands invisibly.
	RuntimeResolutions      []RuntimeResolution `json:"runtime_resolutions,omitempty"`
	RuntimeResolutionSHA256 string              `json:"runtime_resolution_sha256,omitempty"`
	// Bindings records which runtime dependencies were configured (presence
	// only — the bindings themselves are never serialized).
	Bindings RuntimeBindings `json:"bindings"`

	// Effective is the legacy v<=8 projection, retained ONLY so old transcript
	// JSON continues to decode; nil in v9 records.
	Effective          *EffectiveConfig  `json:"effective,omitempty"`
	TrustProfile       *string           `json:"trust_profile"`
	ExecProfile        *string           `json:"exec_profile"`
	ExecProfileHash    string            `json:"exec_profile_hash,omitempty"`
	CLIOverrides       []string          `json:"cli_overrides,omitempty"`
	RequiredTrust      string            `json:"required_trust,omitempty"`
	Canonical          bool              `json:"canonical"`
	FieldProvenance    map[string]string `json:"field_provenance,omitempty"`
	ApprovalPolicyName string            `json:"approval_policy_name,omitempty"`
	ApprovalPolicyHash string            `json:"approval_policy_hash,omitempty"`
}

ConfigRecord is the reproducibility record embedded in transcripts. It records today's effective config plus stable hashes and the selected profile identities and override provenance. Binary is the legacy, conflated binary label from schema v1-v3 records; it remains decodable for old transcripts. New records use BinaryIdentity and InvocationSurface for the two distinct facts.

type Content added in v0.2.0

type Content struct {
	Task       string          // required: the goal (this turn's user input when continuing).
	TaskImages []llm.ImagePart // optional image parts for THIS turn's user message.
	// History is a prior conversation to CONTINUE from (the continuation seam,
	// see Session). The loop clones it before appending.
	History []llm.Message
	Root    string // optional: the dir Sandbox is rooted at; recorded in RunResult.Root.
}

Content is the per-run payload (spec §7.1): recorded separately from policy (the task hash already rides in bundle identity), never part of ConfigSHA256.

type Degradation added in v0.2.0

type Degradation struct {
	Guarantee string            `json:"guarantee"`
	Reason    DegradationReason `json:"reason"`
	Detail    string            `json:"detail,omitempty"`
}

Degradation explains why a configured guarantee was weakened.

type DegradationReason added in v0.2.0

type DegradationReason string

DegradationReason is a closed enum. Consumers must switch on these values, rather than parsing the human Detail field.

const (
	ReasonGateTimeout   DegradationReason = "gate-timeout"
	ReasonFailOpen      DegradationReason = "fail-open"
	ReasonUnpricedSpend DegradationReason = "unpriced-spend"
	ReasonNoVCS         DegradationReason = "no-vcs"
	ReasonPolicySkip    DegradationReason = "policy-skip"
	ReasonInfraFault    DegradationReason = "infra-fault"
	ReasonNotReached    DegradationReason = "not-reached"
	ReasonStaleEvidence DegradationReason = "stale-evidence"
	ReasonNoOpAnswer    DegradationReason = "no-op-answer"
	ReasonArtifactWrite DegradationReason = "artifact-write"
)

type DeltaObserver

type DeltaObserver interface {
	ModelDelta(delta string) // one incremental text fragment from the streaming model call.
}

DeltaObserver is an OPTIONAL Observer extension (DESIGN decision 2: optional capabilities discovered via type-assertion). An Observer that ALSO implements it receives incremental text deltas as the model streams — the live-typing channel a chat REPL renders. The loop type-asserts for it and streams only when both Config.Stream is set and the provider supports streaming; an Observer that doesn't implement it is unaffected and still gets the whole reply via Model(). Keeping it separate from Observer means none of the existing implementations (ndjson, issue-bot, commit-msg, jarvis, duet) need to change.

type DetectorCounters added in v0.2.0

type DetectorCounters struct {
	Repeat           int    `json:"repeat"`
	ReasoningRepeat  int    `json:"reasoning_repeat"`
	ToolObsRepeat    int    `json:"tool_obs_repeat"`
	Stagnant         int    `json:"stagnant"`
	SpiralCycle      int    `json:"spiral_cycle"`
	SpiralWander     int    `json:"spiral_wander"`
	ChurnNudge       int    `json:"churn_nudge"`
	GreenRepeatNudge int    `json:"green_repeat_nudge"`
	FinishNudge      int    `json:"finish_nudge"`
	AnswerNudge      int    `json:"answer_nudge"`
	Diagnostics      int    `json:"diagnostics"`
	TerminatedBy     string `json:"terminated_by,omitempty"`
	TerminatedAtIter int    `json:"terminated_at_iter,omitempty"`
}

DetectorCounters is write-only run telemetry. It must never affect control flow.

type DiffEvidence added in v0.2.0

type DiffEvidence struct {
	Status          EvidenceStatus  `json:"status"`
	FilesChanged    []string        `json:"files_changed,omitempty"`
	PatchRef        string          `json:"patch_ref"`
	CaptureGen      int             `json:"capture_gen"`
	WorkspaceEffect WorkspaceEffect `json:"workspace_effect"`
}

DiffEvidence summarizes the captured workspace delta; patch content remains an artifact.

type EffectiveConfig added in v0.2.0

type EffectiveConfig struct {
	// EffectiveProtocol changes the model wire protocol and is therefore hashed.
	EffectiveProtocol       string            `json:"effective_protocol"`
	DisableMemoryStore      bool              `json:"disable_memory_store"`
	Persona                 string            `json:"persona,omitempty"`
	MemoryScope             memory.Scope      `json:"memory_scope"`
	BootContext             bool              `json:"boot_context"`
	StandingContext         bool              `json:"standing_context"`
	Stream                  bool              `json:"stream"`
	MinIsolation            sandbox.Isolation `json:"min_isolation"`
	RequireNetworkOff       bool              `json:"require_network_off"`
	MaxIterations           int               `json:"max_iterations"`
	MaxTokens               int               `json:"max_tokens"`
	RunTimeout              time.Duration     `json:"run_timeout"`
	VerifyTimeout           time.Duration     `json:"verify_timeout"`
	ReasoningEffort         string            `json:"reasoning_effort,omitempty"`
	PromptProfile           string            `json:"prompt_profile,omitempty"`
	CodeAct                 bool              `json:"code_act"`
	ReproFirst              bool              `json:"repro_first"`
	ReproGate               bool              `json:"repro_gate"`
	BatchReads              bool              `json:"batch_reads"`
	ReadWindow              int               `json:"read_window"`
	ReadOutline             bool              `json:"read_outline"`
	MaxWallClock            time.Duration     `json:"max_wall_clock"`
	MaxTotalTokens          int               `json:"max_total_tokens"`
	MaxTotalCostUSD         float64           `json:"max_total_cost_usd"`
	AllowUnpricedSpend      bool              `json:"allow_unpriced_spend"`
	SolverModel             string            `json:"solver_model,omitempty"`
	ModelConfigured         bool              `json:"model_configured"`
	MemoryConfigured        bool              `json:"memory_configured"`
	VerifySandboxConfigured bool              `json:"verify_sandbox_configured"`
	CostFnConfigured        bool              `json:"cost_fn_configured"`
	SpendConfigured         bool              `json:"spend_configured"`
	ModelInfo               llm.ModelInfo     `json:"model_info"`
	ReviewerIdentity        string            `json:"reviewer_identity,omitempty"`
	PlannerIdentity         string            `json:"planner_identity,omitempty"`

	VerifyCmd              string            `json:"verify_cmd,omitempty"`
	AutoVerify             bool              `json:"auto_verify"`
	AutoVerifySoft         bool              `json:"auto_verify_soft"`
	SkipVerifyBaseline     bool              `json:"skip_verify_baseline"`
	AbortOnRedBaseline     bool              `json:"abort_on_red_baseline"`
	VerifyLastRun          bool              `json:"verify_last_run"`
	ChurnNudgeRuns         int               `json:"churn_nudge_runs"`
	VerifyContinue         bool              `json:"verify_continue"`
	TestFence              []string          `json:"test_fence,omitempty"`
	DiffScope              []string          `json:"diff_scope,omitempty"`
	RequireDiff            bool              `json:"require_diff"`
	ReviewConfigured       bool              `json:"review_configured"`
	ReviewPolicy           int               `json:"review_policy"`
	ReviewUnverified       bool              `json:"review_unverified"`
	ReviewRounds           int               `json:"review_rounds"`
	PlannerConfigured      bool              `json:"planner_configured"`
	FinishNudgeWindow      int               `json:"finish_nudge_window"`
	DiagnoseCmd            string            `json:"diagnose_cmd,omitempty"`
	DiagnoseAfterEdits     int               `json:"diagnose_after_edits"`
	NavSpiralWindow        int               `json:"nav_spiral_window"`
	TerminationPolicy      TerminationPolicy `json:"termination_policy"`
	AnswerNudgeWindow      int               `json:"answer_nudge_window"`
	FinishTool             string            `json:"finish_tool,omitempty"`
	FinishToolConfigured   bool              `json:"finish_tool_configured"`
	FinishToolTrustsCaller bool              `json:"finish_tool_trusts_caller"`
}

EffectiveConfig is the serializable projection of Config fields that affect agent behavior. Runtime objects and task content are deliberately excluded.

func EffectiveConfigFromSpec added in v0.2.0

func EffectiveConfigFromSpec(spec runspec.ResolvedSpec, rt Runtime) EffectiveConfig

EffectiveConfigFromSpec is the exported golden-test projection for a spec + runtime pair OUTSIDE a running loop (pre-run verification state, native protocol).

type EvidenceStatus added in v0.2.0

type EvidenceStatus string

EvidenceStatus is the machine-readable state of one run guarantee. Its zero value means absent: the guarantee was neither configured nor attempted.

const (
	EvidenceAbsent       EvidenceStatus = ""
	EvidenceSkipped      EvidenceStatus = "skipped"
	EvidenceDegraded     EvidenceStatus = "degraded"
	EvidenceInconclusive EvidenceStatus = "inconclusive"
	EvidenceFailed       EvidenceStatus = "failed"
	EvidencePassed       EvidenceStatus = "passed"
)

type Gate

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

Gate is the closing gate detached from the agent loop, for gate-only evaluation (REVIEW-GATE-PLAN §3/§5.1): construct it over a prepared workspace (snapshotting the fence and the diff base), apply a candidate patch by any external means, then Check — the same fence → verify → review ladder a live run's finish flows through, without a solver.

func NewGate

func NewGate(ctx context.Context, spec runspec.ResolvedSpec, rt Runtime, content Content) (*Gate, error)

NewGate snapshots the workspace's gate baseline. Runtime needs Sandbox (and a Reviewer when the review stage should run); the spec carries whichever gate knobs apply (TestFence, VerifyCmd, RunTimeout); Model is not used — there is no solver. A setup failure (e.g. reviewer armed on a tree the review gate cannot baseline) is returned, never swallowed: a silently review-less verdict is exactly the fail-open the 2026-07-07 fail-closed change exists to prevent.

func (*Gate) Check

func (t *Gate) Check(ctx context.Context) GateReport

Check runs the composed closing gate once and reports every stage's verdict. Stage order and short-circuiting mirror the live loop: the reviewer runs ONLY when the fence and VerifyCmd are green (execution-first).

type GateReport

type GateReport struct {
	Blocked        bool          `json:"blocked"`
	FenceViolation string        `json:"fence_violation,omitempty"`
	VerifyReason   string        `json:"verify_reason,omitempty"`
	Review         *ReviewReport `json:"review,omitempty"`
}

GateReport is one standalone gate verdict. Blocked is the headline: a banked BAD patch must come back Blocked (CAUGHT), a good one must not (PASSED).

type Guarantees added in v0.2.0

type Guarantees struct {
	Verification      VerificationEvidence `json:"verification"`
	Review            ReviewEvidence       `json:"review"`
	CostBound         EvidenceStatus       `json:"cost_bound"`
	Isolation         EvidenceStatus       `json:"isolation"`
	ObservedIsolation string               `json:"observed_isolation"`
	ObservedNetwork   *bool                `json:"observed_network"`
	Diff              DiffEvidence         `json:"diff"`
	Degradations      []Degradation        `json:"degradations,omitempty"`
}

Guarantees is the typed evidence report attached to every RunResult.

type KindedNoteObserver added in v0.2.0

type KindedNoteObserver interface {
	KindedNote(kind NoteKind, msg string)
}

KindedNoteObserver is an OPTIONAL Observer extension, discovered by type-assertion like DeltaObserver. An Observer that implements it receives kinded notes through KindedNote INSTEAD of Note — the explicit opt-in that lets a front-end drop the notes whose content it already renders from typed events (usage, review round summaries) without string-prefix matching. Implementing UsageObserver/ReviewObserver alone changes nothing: observers that do not opt in keep receiving every line through Note, byte-identical.

type LedgerMeta added in v0.2.0

type LedgerMeta struct {
	Repo        string
	ExitCode    int
	ReviewModel string
	HasPatch    bool
}

type LoopFunc

LoopFunc is the shared signature of the two agent loops, Run (text protocol) and RunNative (native tool-calling). A Session is parameterized by one so the same multi-turn machinery drives either — the caller picks the loop that fits its provider.

type NoteKind added in v0.2.0

type NoteKind string

NoteKind labels the loop's machine-recognizable Note lines so a front-end can route them without parsing prose. Only the notes that duplicate a typed event carry a kind today; everything else is NoteGeneral.

const (
	NoteGeneral     NoteKind = ""             // an unclassified status line.
	NoteUsage       NoteKind = "usage"        // the per-turn "tokens: …" report (typed twin: UsageObserver.StepUsage).
	NoteReviewRound NoteKind = "review-round" // the "review: round n/m — …" summary (typed twin: ReviewObserver.ReviewVerdict).
)

type Observer

type Observer interface {
	Iteration(i, max int)    // a new turn begins.
	Model(reply string)      // the model's raw reply this turn.
	Observation(text string) // a tool result fed back (Run hands the FULL text; the observer decides how to render).
	Note(msg string)         // a miscellaneous status line (e.g. the not-stored memory note).
	Done(answer string)      // the model emitted a final answer.
}

Observer receives live progress events from a Run. It is the seam that lets ONE loop serve two callers: the CLI passes a printing Observer to reproduce the old think->act->observe output exactly, while the eval harness passes a silent one so a run yields data (the RunResult), not stdout noise. Run never calls fmt itself — every user-facing line goes through here.

func NewStdoutObserver

func NewStdoutObserver() Observer

NewStdoutObserver returns an Observer that prints the live loop trace to stdout. Retained for callers that want the historical stdout rendering; cmd/agent now routes the trace to stderr via NewWriterObserver (D1).

func NewWriterObserver

func NewWriterObserver(w io.Writer) Observer

NewWriterObserver returns an Observer that prints the live loop trace to w.

type Outcome

type Outcome string

Outcome is the typed terminal state of a Run. It replaces the old error-string encoding so a caller (the eval oracle) can branch on HOW a run ended — answered vs hit-cap vs which detector killed it — without parsing prose. KilledSpiral vs KilledRepeat is exactly the signal that told us fix-3 needed gating last dogfood round, so it is first-class, not inferred.

const (
	Answered        Outcome = "answered"          // the model emitted `answer` (and it verified, if a check was configured).
	Unverified      Outcome = "unverified"        // the model finished, but the closing verification (VerifyCmd / last-run check) failed — a non-pass (P5/HP-5).
	HitCap          Outcome = "hit_cap"           // ran out of iterations (P5 hard cap).
	KilledRepeat    Outcome = "killed_repeat"     // exact same action maxRepeats times.
	KilledSpiral    Outcome = "killed_spiral"     // frontier-aware explore-spiral: a discovery-only cycle (revisiting seen list_dir/search targets) or endless novel wandering (spiralState).
	KilledStagnant  Outcome = "killed_stagnant"   // the same failing `run` result maxStagnant times despite changing actions.
	HitDeadline     Outcome = "hit_deadline"      // exceeded the wall-clock budget (P5) — a spiral that dodged the action/observation detectors.
	HitBudget       Outcome = "hit_budget"        // exceeded the token budget (P5/HP-8, MaxTotalTokens) — cost is a first-class cap, not a side effect of the iteration count.
	ProviderErr     Outcome = "provider_error"    // a transport/auth failure talking to the model.
	HitContextLimit Outcome = "hit_context_limit" // (HP-1) the window overflowed AND reactive eviction couldn't compact it further — a graceful stop, not a crash.
	RefusedUnsafe   Outcome = "refused_unsafe"    // the Sandbox's isolation is weaker than Config.MinIsolation requires — refused BEFORE the first model call (P2/§5). Never ran hostile code on a too-weak boundary.
	ScopeViolation  Outcome = "scope_violation"   // the run's diff escaped the configured writable globs (Config.DiffScope) — a changed file was outside the declared task scope, e.g. rewriting production code to fit a guard test.
	Canceled        Outcome = "canceled"          // the caller canceled the run (SIGINT / ctx cancel) — not a provider fault (distinct from HitDeadline, which is the run's own wall-clock budget).
)

type PlanInput

type PlanInput struct {
	Task       string
	Root       string
	Continuing bool
}

PlanInput is what a Planner plans: the task and the workspace root it may explore READ-ONLY. Continuing marks a follow-up turn of an ongoing conversation (Config.History non-empty) — the task may reference earlier exchanges the planner cannot see, so it should plan from what the code itself can ground.

type PlanObserver

type PlanObserver interface {
	PlanStart()
	PlanDone(plan, skipped string)
}

PlanObserver is an OPTIONAL Observer extension, discovered by type-assertion like ReviewObserver: a front-end that implements it receives the typed plan-stage events (the TUI renders the plan as its own block); every other Observer only sees the Obs.Note progress lines.

type PlanReport

type PlanReport struct {
	Model      string    `json:"model,omitempty"`
	Plan       string    `json:"plan,omitempty"`
	Skipped    string    `json:"skipped,omitempty"` // why the solver ran unplanned (planner error / empty plan).
	PlannerRun string    `json:"planner_run,omitempty"`
	Usage      llm.Usage `json:"usage"`
}

PlanReport is the run's plan-stage record: the plan the solver was handed (or why there was none) and the planner's own token cost. Carried on RunResult.Plan and the transcript. Usage is deliberately SEPARATE from RunResult.Usage (the solver's bill), mirroring ReviewReport.Usage — per-role cost must stay attributable.

type PlanResult

type PlanResult struct {
	Plan           string
	Model          string
	Usage          llm.Usage
	RunID          string
	TranscriptPath string
}

PlanResult is a Planner's answer plus its token cost. Model is the planner's human-facing model id ("" if the impl doesn't know); RunID/TranscriptPath identify the planner's own persisted sub-run (empty when the implementation doesn't persist) — the same telemetry contract as ReviewVerdict.

type Planner

type Planner interface {
	Plan(ctx context.Context, in PlanInput) (*PlanResult, error)
}

Planner is the injected plan-stage implementation (nil = stage off). Implementations live OUTSIDE agent (council.Planner, or cmd wiring) to avoid the import cycle; the harness owns the injection point and the fail-open policy.

type Prepared added in v0.2.0

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

Prepared is a resolved, recordable run: the spec the loop executes, the trace that produced it, and the runtime/content bindings. Constructed only by Prepare.

func Prepare added in v0.2.0

func Prepare(req runspec.RequestedConfig, rt Runtime, content Content, meta RecordInputs) (Prepared, error)

Prepare resolves req exactly once and returns a runnable, recordable run. rt.Record is overwritten with the centrally-derived RecordMeta: a caller cannot smuggle in a provenance view that disagrees with the resolution.

req MUST carry TrustProfile: runspec.Resolve would default a nil trust to trusted-local (the weakest posture), so the seam refuses instead — the PROFILES.md contract is that omitting trust is a hard error, matching the headless flag layer's refusal, and native producers get no flag layer.

func (Prepared) ConfigSHA256 added in v0.2.0

func (p Prepared) ConfigSHA256() string

func (Prepared) Content added in v0.2.0

func (p Prepared) Content() Content

func (Prepared) Runtime added in v0.2.0

func (p Prepared) Runtime() Runtime

func (Prepared) Spec added in v0.2.0

func (p Prepared) Spec() runspec.ResolvedSpec

func (Prepared) Trace added in v0.2.0

func (p Prepared) Trace() runspec.Trace

type ReadOptions added in v0.2.0

type ReadOptions struct {
	// Window is the max lines a range-less or over-long read returns before it
	// clips (P1 context bound). <=0 means the default readLineCap.
	Window int
	// Outline, when true, appends a compact structure map (top-level symbols +
	// line numbers) to a CLIPPED read, so the model can request the right range
	// instead of paging linearly. No outline is added when the whole file fit in
	// the window (the model already sees everything).
	Outline bool
}

ReadOptions tunes read_file's context shaping. Zero value = today's behavior (window = readLineCap, no outline), so existing callers are unaffected.

type RecordInputs added in v0.2.0

type RecordInputs struct {
	BinaryLabel            string // legacy v1-v3 conflated label; new callers set BinaryIdentity.
	BinaryIdentity         string
	InvocationSurface      string
	RequestedProtocol      string
	ProtocolFallbackReason string
	CLIOverrides           []string
	ApprovalPolicyName     string
	ApprovalPolicyHash     string
}

RecordInputs is the surface-specific half of the recording identity: what only the invoking binary knows. Everything derivable from the resolution itself (trust profile, exec profile name, required trust, canonicality, per-field provenance) is derived by Prepare and must NOT be passed in — that derivation is the whole point of the seam.

type RecordMeta added in v0.2.0

type RecordMeta struct {
	// BinaryLabel is the legacy v1-v3 conflated binary label retained for
	// transcript compatibility. New callers set BinaryIdentity instead.
	BinaryLabel            string
	BinaryIdentity         string
	InvocationSurface      string
	RequestedProtocol      string
	ProtocolFallbackReason string
	TrustProfile           string
	ExecProfileName        string
	ExecProfileHash        string
	CLIOverrides           []string
	RequiredTrust          string
	Canonical              bool
	FieldProvenance        map[string]string
	ApprovalPolicyName     string
	ApprovalPolicyHash     string
}

RecordMeta is the recording-only invocation identity and resolution provenance embedded in ConfigRecord. Every field mirrors a former Config field documented as "recording-only and never affects behavior".

type ReproReport added in v0.2.0

type ReproReport struct {
	Status     string `json:"status"`
	Path       string `json:"path,omitempty"`
	Cmd        string `json:"cmd,omitempty"`
	SkipReason string `json:"skip_reason,omitempty"`
	RedExcerpt string `json:"red_excerpt,omitempty"`
	Green      *bool  `json:"green,omitempty"`
	Detail     string `json:"detail,omitempty"`
}

type ReproSolicitor

type ReproSolicitor interface {
	SolicitRepro(ctx context.Context, in ReviewInput, findings []ReviewFinding) ([]ReviewFinding, llm.Usage, error)
}

ReproSolicitor is an optional Reviewer extension: after a review round produces blocker findings without repro commands, the harness can ask the reviewer to supply runnable repro commands in a single follow-up call per round. The reviewer receives the batched list of eligible findings and returns each with either a repro_cmd or an explicit no_repro_reason. Implementations that don't implement this are simply not consulted — findings without repro commands expire as today. The harness discovers it via type assertion; the Reviewer interface itself stays unchanged so existing implementations (including the fake in tests) compile untouched.

type ReviewEvidence added in v0.2.0

type ReviewEvidence struct {
	Status              EvidenceStatus `json:"status"`
	Rounds              int            `json:"rounds"`
	FindingsTotal       int            `json:"findings_total"`
	BlockersFired       int            `json:"blockers_fired"`
	SurvivingFindingIDs []string       `json:"surviving_finding_ids,omitempty"`
	RepairedFindingIDs  []string       `json:"repaired_finding_ids,omitempty"`
	ReviewedGen         int            `json:"reviewed_gen"`
}

ReviewEvidence is a structured summary whose finding IDs refer to RunResult.Review.

type ReviewFinding

type ReviewFinding struct {
	ID              string `json:"id,omitempty"` // stable within a run; evidence summaries use it as a foreign key.
	File            string `json:"file"`
	Quote           string `json:"quote"`    // verbatim post-patch code — mismatch drops the finding.
	Severity        string `json:"severity"` // "blocker" | "note"
	Confidence      int    `json:"confidence"`
	FailureScenario string `json:"failure_scenario"`
	ReproCmd        string `json:"repro_cmd,omitempty"` // a command expected to FAIL on this code (slice 2).

	// NoReproReason is set when the reviewer was asked for a repro command
	// (via ReproSolicitor) but explicitly declined with a reason — the defect
	// cannot be demonstrated by a runnable command (e.g. a visual TUI defect).
	// A finding with a NoReproReason expires as today (unconfirmed).
	NoReproReason string `json:"no_repro_reason,omitempty"`

	// Severity-rubric fields (slice 2b) — the reviewer is asked to fill these
	// to make the gate's JSON report self-describing without re-reading the
	// full finding prose. Missing/invalid values parse as empty and never
	// affect the blocking decision.
	Category     string `json:"category,omitempty"`     // correctness|performance|security|maintainability|style
	Impact       string `json:"impact,omitempty"`       // high|medium|low
	Reproducible string `json:"reproducible,omitempty"` // "true"|"false" — could a runnable command demonstrate it?
}

ReviewFinding is one reviewer claim, exactly the structured-verdict schema (docs/specs/REVIEW-GATE.md): a verbatim post-patch quote that re-grounds it, a two-value severity, a 0-10 confidence, a concrete failure scenario, and an optional runnable repro the harness escalates to execution.

type ReviewInput

type ReviewInput struct {
	Task    string
	Diff    string
	Root    string
	Signals []string
	// SessionKey is a run-scoped handle, identical across the rounds of one run
	// and unique per run. A stateful reviewer may key a continuation on it —
	// round 2 re-uses round 1's exploration instead of re-reading the repo
	// (REVIEW-GATE-PLAN §5.3 follow-up (a): the re-read was most of the
	// two-round bill). Empty when the caller doesn't do rounds. Round is the
	// 1-based round number for the same purpose.
	SessionKey string
	Round      int
}

ReviewInput is what a Reviewer judges: the task, the solver's unified diff vs the run base, the workspace root it may explore READ-ONLY, and the deterministic substance signals (fence.go) the harness scanned from the diff. It is never told what produced the patch (independence policy).

type ReviewObserver

type ReviewObserver interface {
	// ReviewStart announces a round beginning. model is the reviewer model id
	// when the harness knows it at round start — from an earlier round's
	// verdict, or from a Reviewer implementing ReviewerModelNamer — and ""
	// otherwise; a front-end labeling the live round must not need to carry
	// the model out-of-band.
	ReviewStart(round int, model string)
	ReviewFinding(f ReviewFinding)
	ReviewVerdict(blocking int, round int, summary string)
}

ReviewObserver is an OPTIONAL Observer extension, discovered by type-assertion like DeltaObserver: a front-end (the TUI) that implements it receives typed review-gate events; every other Observer only sees the Obs.Note progress lines.

type ReviewPassOptions

type ReviewPassOptions struct {
	BaseTree string

	// SessionKey and RoundOffset are optional continuity hooks for callers that
	// stitch several one-pass reviews into one logical review gate. They keep the
	// reviewer continuation handle stable and make ReviewInput.Round match the
	// logical pass number while preserving the original BaseTree.
	SessionKey  string
	RoundOffset int
}

ReviewPassOptions configures a closing review gate run over an existing workspace. BaseTree, when set, is the git tree to diff the current workspace against. Best-of-N uses this to review the selected attempt against its pre-solve HEAD rather than accidentally snapshotting the already-patched tree as the review baseline.

type ReviewPolicy added in v0.2.0

type ReviewPolicy int

ReviewPolicy selects how an armed review gate handles infrastructure failures and unavailable workspace baselines. The zero value preserves the historical library default: a configured Reviewer is required and its baseline must be available.

const (
	// ReviewPolicyDefault requires an armed reviewer to succeed and requires its
	// workspace baseline. With no Reviewer, the review gate is disabled.
	ReviewPolicyDefault ReviewPolicy = iota
	// ReviewPolicyRequired explicitly requires review infrastructure to succeed.
	ReviewPolicyRequired
	// ReviewPolicyFailOpen records review infrastructure failures without
	// downgrading an otherwise verified run.
	ReviewPolicyFailOpen
	// ReviewPolicyOptional permits an unavailable review baseline, while review
	// infrastructure failures still block when the reviewer can be armed.
	ReviewPolicyOptional
	// ReviewPolicyRequiredOptional explicitly requires review infrastructure but
	// permits an unavailable review baseline.
	ReviewPolicyRequiredOptional
	// ReviewPolicyFailOpenOptional permits both an unavailable baseline and
	// fail-open handling of review infrastructure failures.
	ReviewPolicyFailOpenOptional
)

func ReviewPolicyFrom added in v0.2.0

func ReviewPolicyFrom(required, failOpen, optional bool) (ReviewPolicy, error)

ReviewPolicyFrom converts the legacy flag representation into a policy. Required and failOpen are contradictory; all other combinations are the configurations historically constructed by this repository.

func (ReviewPolicy) FailOpen added in v0.2.0

func (p ReviewPolicy) FailOpen() bool

FailOpen reports whether review infrastructure failures are advisory.

func (ReviewPolicy) Optional added in v0.2.0

func (p ReviewPolicy) Optional() bool

Optional reports whether an unavailable review baseline may be skipped.

type ReviewReport

type ReviewReport struct {
	Rounds        int               `json:"rounds"`
	Status        ReviewStatus      `json:"status,omitempty"`
	Blocked       bool              `json:"blocked"`                  // the run ended with blockers standing.
	Salvage       bool              `json:"salvage,omitempty"`        // advisory review on an Unverified run; never changed outcome.
	Skipped       string            `json:"skipped,omitempty"`        // why the gate never ran (not a git workspace, reviewer error, …).
	ReviewerModel string            `json:"reviewer_model,omitempty"` // from ReviewVerdict.Model — the per-reviewer calibration axis.
	ReviewerRuns  []string          `json:"reviewer_runs,omitempty"`  // per-round ReviewVerdict.RunID — links this transcript to the reviewer's own.
	Summaries     []string          `json:"summaries,omitempty"`      // per-round free-prose preambles; "" when absent.
	Findings      []ReviewedFinding `json:"findings,omitempty"`
	Usage         llm.Usage         `json:"usage"`

	// ConfirmedBlockers counts blocker findings whose repro command FAILED
	// (Confirmed=true, execution-evidenced). UnconfirmedBlockers counts
	// blocker findings that stood without execution evidence — either the
	// reviewer supplied no repro command, the command passed (and confidence
	// was high enough not to refute), or the repro couldn't run. A caller
	// can tell CAUGHT-with-confirmed-repro from CAUGHT-on-unconfirmed-claims
	// at a glance.
	ConfirmedBlockers   int `json:"confirmed_blockers"`
	UnconfirmedBlockers int `json:"unconfirmed_blockers"`
}

ReviewReport is the run's review-gate record: rounds used, every finding with its fate, and the reviewer's token cost. Carried on RunResult.Review, the result JSON, and the transcript.

func ReviewExistingWorkspace

func ReviewExistingWorkspace(ctx context.Context, spec runspec.ResolvedSpec, rt Runtime, content Content, runTimeout time.Duration, opts ReviewPassOptions) (feedback string, blockReason string, report *ReviewReport)

ReviewExistingWorkspace runs the existing closing review gate once against an already-mutated workspace. It exposes the gate without exposing the internal gates/reviewState types, so callers that did not start a normal agent loop can still reuse reviewer classification, repro execution, fates, usage reporting, and fail-open behavior.

type ReviewStatus

type ReviewStatus string

ReviewStatus is the terminal review-gate classification recorded in ReviewReport.Status.

const (
	ReviewClean       ReviewStatus = "clean"
	ReviewBlocked     ReviewStatus = "blocked"
	ReviewAdvisory    ReviewStatus = "advisory"
	ReviewUnavailable ReviewStatus = "unavailable"
	ReviewParseError  ReviewStatus = "parse_error"
	ReviewTimeout     ReviewStatus = "timeout"
	ReviewCanceled    ReviewStatus = "canceled"
)

type ReviewVerdict

type ReviewVerdict struct {
	Findings []ReviewFinding
	Usage    llm.Usage
	Model    string
	Summary  string // reviewer free-prose preamble before the findings array, when present.
	// RunID/TranscriptPath identify the reviewer's own persisted sub-run (a
	// reviewer that runs a real agent loop records it like any other run —
	// diagnosing a misbehaving reviewer must not require a temp dump). Empty
	// when the implementation doesn't persist.
	RunID          string
	TranscriptPath string
}

ReviewVerdict is a Reviewer's structured answer plus its token cost. Model is the reviewer's human-facing model id ("" if the impl doesn't know) — it flows into ReviewReport.ReviewerModel so finding-fates aggregate PER REVIEWER across transcripts (the calibration axis).

type ReviewedFinding

type ReviewedFinding struct {
	ReviewFinding
	Round     int    `json:"round"`
	Fate      string `json:"fate"`
	Confirmed bool   `json:"confirmed,omitempty"`    // its repro command FAILED — the defect is execution-confirmed.
	ReproOut  string `json:"repro_output,omitempty"` // the (clipped) repro output backing Confirmed/Refuted.
	DropWhy   string `json:"drop_reason,omitempty"`  // why a dropped finding was dropped.
}

ReviewedFinding is a finding plus what the harness decided about it.

type Reviewer

type Reviewer interface {
	Review(ctx context.Context, in ReviewInput) (*ReviewVerdict, error)
}

Reviewer is the injected review-gate implementation (nil = gate off). Implementations live OUTSIDE agent (council / cmd wiring) to avoid the import cycle; the harness owns validation, escalation, and the repair loop.

type ReviewerModelNamer added in v0.2.0

type ReviewerModelNamer interface {
	ReviewerModel() string
}

ReviewerModelNamer is an optional Reviewer extension, discovered by type-assertion: a Reviewer that knows which model it will run reports it so ReviewStart can carry the model from round 1 (ReviewVerdict.Model only becomes available after a round completes).

type RunRecord

type RunRecord struct {
	SchemaVersion string    `json:"schema_version"`
	ID            string    `json:"id"`
	StartedAt     string    `json:"started_at,omitempty"`
	EndedAt       string    `json:"ended_at,omitempty"`
	Model         string    `json:"model,omitempty"` // the caller's provider label (the loop doesn't know it).
	Task          string    `json:"task"`
	Outcome       Outcome   `json:"outcome"`
	RescuedFrom   Outcome   `json:"rescued_from,omitempty"`
	Answer        string    `json:"answer,omitempty"`
	Reason        string    `json:"reason,omitempty"`
	Iterations    int       `json:"iterations"`
	Usage         llm.Usage `json:"usage"`
	Err           string    `json:"error,omitempty"`
	// Review is the review-gate record (rounds, findings + fates, reviewer
	// usage) — the calibration telemetry the corpus tooling aggregates
	// (FP rate per reviewer = refuted+expired / total blockers). Absent when
	// the run had no Reviewer configured.
	Review            *ReviewReport     `json:"review,omitempty"`
	Guarantees        Guarantees        `json:"guarantees"`
	Config            *ConfigRecord     `json:"config,omitempty"`
	TerminationPolicy TerminationPolicy `json:"termination_policy"`
	DetectorCounters  DetectorCounters  `json:"detector_counters"`

	// Plan is the plan-stage record (the plan handed to the solver, or why
	// there was none, plus the planner's own usage — kept out of Usage so
	// per-role spend stays attributable). Absent when the run had no Planner.
	Plan *PlanReport `json:"plan,omitempty"`

	SolverCost   *float64 `json:"solver_cost_usd,omitempty"`
	ReviewerCost *float64 `json:"reviewer_cost_usd,omitempty"`
	PlannerCost  *float64 `json:"planner_cost_usd,omitempty"`
	SelectorCost *float64 `json:"selector_cost_usd,omitempty"`
	TotalCost    *float64 `json:"total_cost_usd,omitempty"`
	CostSource   *string  `json:"cost_source,omitempty"`

	Messages []llm.Message `json:"messages,omitempty"`
	Steps    []Step        `json:"steps,omitempty"`
}

RunRecord is the durable, self-describing envelope for one agent run — the P1 spine every consumer shares (a CLI transcript, an eval Trial, a council AgentTrace, a commit-msg dogfood record). It carries the full Step trace so a run is replayable from the file alone, plus enough header (id, model, timing, outcome, usage) to be read without it.

func LoadTranscript

func LoadTranscript(dir, runID string) (*RunRecord, error)

LoadTranscript reads one per-run transcript by run id from dir.

func RecordFrom

func RecordFrom(res *RunResult, model string) RunRecord

RecordFrom builds a RunRecord from a finished run. model is the caller's label for the provider (a slug like "openai/gpt-5.5"); pass "" when unknown.

type RunResult

type RunResult struct {
	// ID is a stable, time-sortable identifier for this run ("<YYYYMMDD-HHMMSS>-<hex>"),
	// stamped by Run/RunNative. It is the spine: a transcript, an eval Trial, a council
	// AgentTrace, and a commit-msg dogfood record can all reference the SAME run by ID
	// instead of each re-embedding their own copy.
	ID string
	// StartedAt/EndedAt bound the run's wall-clock (stamped by the loop). Distinct from
	// the per-step ModelMs/ToolMs, which sum only time spent IN model calls and tools.
	StartedAt   time.Time
	EndedAt     time.Time
	Task        string
	Root        string  // the dir the sandbox was rooted at (Config.Root).
	Outcome     Outcome // how the run ended.
	RescuedFrom Outcome // pre-upgrade Outcome when upgradeIfVerified rescued this run to Answered. Outcome == Answered && RescuedFrom != "" means the model never itself claimed completion — the harness gates proved the work complete after a cap/kill exit.
	Answer      string  // the final answer, set iff Outcome == Answered.
	Reason      string  // human explanation for a non-Answered outcome (kept so the CLI prints the old message verbatim).
	Steps       []Step  // the full trace.
	Iterations  int     // turns taken.
	Usage       llm.Usage
	Err         error // set iff Outcome == ProviderErr.
	// Review is the review-gate record — rounds, every finding with its fate,
	// and the reviewer's token cost (the calibration telemetry). nil when no
	// Reviewer was configured; populated on every loop exit when one was, even
	// if the gate never fired (Skipped says why).
	Review *ReviewReport

	// Guarantees is the typed, machine-readable evidence report for this run.
	Guarantees Guarantees `json:"guarantees"`
	// ConfigRecord is the transcript reproducibility snapshot for this run. It is
	// populated by the native loop after prompt and tool schemas are finalized.
	ConfigRecord      *ConfigRecord
	TerminationPolicy TerminationPolicy `json:"termination_policy"`
	DetectorCounters  DetectorCounters  `json:"detector_counters"`
	// Repro is the repro-first gate report. nil unless ReproFirst was enabled.
	Repro *ReproReport
	// Plan is the plan-stage record — the plan the solver was handed (or why
	// there was none) and the planner's own token cost, kept OUT of Usage so
	// per-role spend stays attributable. nil when no Planner was configured.
	Plan *PlanReport

	SolverCost   *float64 `json:"solver_cost_usd,omitempty"`
	ReviewerCost *float64 `json:"reviewer_cost_usd,omitempty"`
	PlannerCost  *float64 `json:"planner_cost_usd,omitempty"`
	SelectorCost *float64 `json:"selector_cost_usd,omitempty"`
	TotalCost    *float64 `json:"total_cost_usd,omitempty"`
	CostSource   *string  `json:"cost_source,omitempty"`
	// Messages is the FULL conversation as it stood when the run ended — the
	// system-framed TASK (or the seeded History plus this turn's input), every
	// assistant turn, and every tool result. It is the continuation seam: a chat
	// front-end feeds it back as the next run's Config.History so the model sees
	// the whole prior conversation (see Session). Populated on every path that
	// reaches the loop; nil for a pre-loop refusal (too-weak sandbox).
	Messages []llm.Message

	// VerifyBaselineRed is true when VerifyCmd was configured and it already
	// failed on the untouched workspace before the first model call.
	VerifyBaselineRed bool `json:"verify_baseline_red,omitempty"`
	// VerifyBaselineOut is the failing output of the baseline VerifyCmd run,
	// clipped like other observations. Empty when the baseline was green.
	VerifyBaselineOut string `json:"verify_baseline_out,omitempty"`
	// ClosingVerification records the authoritative closing gate run that accepted
	// the final state. It is nil when no closing verification succeeded.
	ClosingVerification *VerificationRecord `json:"closing_verification,omitempty"`
	// VerifyInfra is true when a closing VerifyCmd failed because the harness
	// recognized an environment fault rather than a code failure.
	VerifyInfra bool `json:"verify_infra,omitempty"`
	// VerifyInfraSignature is the matched environment-fault signature.
	VerifyInfraSignature string `json:"verify_infra_signature,omitempty"`

	// CacheSumExpectedCached is the run-level sum of expected cached tokens per
	// the prefix-cache model (Measurement 3 in eval/scripts/read_dup_pass.py):
	// turn 1 expected=0; turn i>1 expected=min(P_{i-1}, P_i).
	CacheSumExpectedCached int `json:"sum_expected_cached,omitempty"`
	// CacheSumCached is the run-level sum of actual cached tokens across turns.
	CacheSumCached int `json:"sum_cached,omitempty"`
	// CacheMiss is the run-level sum of cache misses (expected − actual, clamped >= 0).
	CacheMiss int `json:"cache_miss,omitempty"`
	// CacheHitPct is the run-level cache-hit percentage: 100 * sum_cached / sum_expected_cached,
	// or 0 when sum_expected_cached is 0.
	CacheHitPct float64 `json:"cache_hit_pct,omitempty"`
	// contains filtered or unexported fields
}

RunResult is the structured outcome of a Run: the typed Outcome, the answer (if any), the full Step trace, summed token Usage, and the Root the sandbox ran against. Root is the fixture hook (HP-11): a baseline diff must know whether a case ran against an immutable fixture or the live repo, so the working dir travels with the result instead of being implicit.

func ReviewAndRepairExistingWorkspace

func ReviewAndRepairExistingWorkspace(ctx context.Context, spec runspec.ResolvedSpec, rt Runtime, content Content, base *RunResult, opts ReviewPassOptions, loop LoopFunc) (*RunResult, error)

ReviewAndRepairExistingWorkspace applies the review gate to an already chosen result/workspace and, when review produces repair feedback, gives the normal agent loop continuation runs in the same workspace. Every review pass is against the original pre-solve base tree; repair runs have their reviewer disabled so they cannot silently re-baseline the gate to the already-patched workspace. A blocking, unrepaired review marks the chosen result Unverified; callers must not fall back to a lower-ranked candidate.

func Run

func Run(ctx context.Context, spec runspec.ResolvedSpec, rt Runtime, content Content) (out *RunResult, err error)

Run is the entire agent. Notice it is tiny — the loop is trivial (P3); the interesting work is the context policy and the termination conditions. It prints nothing: events go to rt.Obs, the terminal state to the returned RunResult. err is non-nil ONLY for a genuine infrastructure failure (the model call itself failed); a no-progress kill or a hit cap is a normal Outcome, not a Go error.

The spec is COMPLETE by construction (runspec.Resolve) — the loop asserts that at entry and never repairs a field (PROFILES.md §7.5 S6a).

func RunNative

func RunNative(ctx context.Context, spec runspec.ResolvedSpec, rt Runtime, content Content) (out *RunResult, err error)

RunNative executes the agent against a tool-capable provider. Its signature and RunResult match Run exactly, so a caller swaps loops without other changes.

The spec is COMPLETE by construction (runspec.Resolve) — the loop asserts that at entry and never repairs a field (PROFILES.md §7.5 S6a).

func RunNativePrepared added in v0.2.0

func RunNativePrepared(ctx context.Context, p Prepared) (*RunResult, error)

func RunPrepared added in v0.2.0

func RunPrepared(ctx context.Context, p Prepared) (*RunResult, error)

RunPrepared / RunNativePrepared are the Prepared-taking loop entry points. S6d.7 collapses them onto Run/RunNative once every producer is migrated and the bare-ResolvedSpec entries are deleted.

func (*RunResult) AwaitMemory

func (r *RunResult) AwaitMemory()

AwaitMemory blocks until the background memory store started for this run's answer (if any) has completed. The loops fire the store asynchronously so the result reaches the caller immediately (review #4); a caller that is about to EXIT the process must await it, or the extracted facts die with the process. Long-lived callers (a REPL, duet) can ignore it — the store finishes on its own. Returns immediately when no store was started; nil-safe, so a caller can await its "last result" without guarding the no-turns-yet case.

type Runtime added in v0.2.0

type Runtime struct {
	Model   llm.Provider    // required: the (context) -> text engine.
	Sandbox sandbox.Sandbox // required: the isolation boundary every effect flows through (P2).

	// VerifySandbox is the sandbox the CLOSING gates (VerifyCmd/DiagnoseCmd) run
	// on, when it must differ from Sandbox. nil ⇒ Sandbox. See ../SESSION.md.
	VerifySandbox sandbox.Sandbox

	Memory memory.Store    // optional: cross-run long-term memory; nil = stateless.
	Tools  map[string]Tool // optional: nil = DefaultTools(Sandbox).
	Obs    Observer        // optional: live progress sink; nil = silent.

	// PerfMark receives optional latency instrumentation from the native tool
	// loop. nil for callers that do not collect performance timelines.
	PerfMark func(event string, attrs map[string]any)

	// ModelInfo describes the selected solver model's known limits. A zero value
	// means unknown and disables proactive context-window telemetry.
	ModelInfo llm.ModelInfo

	// CostFn prices this run's solver model from cumulative Usage; Spend is the
	// shared role-aware dollar accumulator that supersedes it when non-nil.
	CostFn func(llm.Usage) (float64, bool)
	Spend  *Spend

	// Reviewer arms the review gate; Planner arms the opening plan stage.
	// Implementations live outside agent (council) — injected to avoid the cycle.
	Reviewer Reviewer
	Planner  Planner

	// Record is the recording-only invocation identity and resolution provenance
	// for the transcript ConfigRecord. It never affects behavior. (S6c replaces
	// this caller-supplied block with trace-derived serialization.)
	Record RecordMeta
	// contains filtered or unexported fields
}

Runtime is the injected dependency bundle for one run (spec §7.1 "runtime bindings"). Model and Sandbox are required; the rest default sensibly (nil Tools -> DefaultTools, nil Memory -> no cross-run recall, nil Obs -> silent).

type RuntimeBindings added in v0.2.0

type RuntimeBindings struct {
	ModelConfigured         bool          `json:"model_configured"`
	MemoryConfigured        bool          `json:"memory_configured"`
	VerifySandboxConfigured bool          `json:"verify_sandbox_configured"`
	CostFnConfigured        bool          `json:"cost_fn_configured"`
	SpendConfigured         bool          `json:"spend_configured"`
	ReviewConfigured        bool          `json:"review_configured"`
	PlannerConfigured       bool          `json:"planner_configured"`
	ModelInfo               llm.ModelInfo `json:"model_info"`
	ReviewerIdentity        string        `json:"reviewer_identity,omitempty"`
	PlannerIdentity         string        `json:"planner_identity,omitempty"`
}

RuntimeBindings records the PRESENCE of injected runtime dependencies plus their reproducibility-relevant identity (model limits, reviewer/planner implementation types). The bindings themselves are never serialized (§7.1).

type RuntimeResolution added in v0.2.0

type RuntimeResolution struct {
	Seq        int    `json:"seq"`
	Phase      string `json:"phase"` // "pre-run" | "session"
	Field      string `json:"field"` // "verify_cmd"
	Value      string `json:"value"`
	Provenance string `json:"provenance"` // project marker (go.mod, …) or "user"
	Consumer   string `json:"consumer"`   // "verify-gate"
	Disarmed   bool   `json:"disarmed,omitempty"`
	Note       string `json:"note,omitempty"`
}

RuntimeResolution is one runtime-derivation event (§7.3): the derived value, where it came from, and the gate that consumes it.

type Session

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

Session is a CONTINUING conversation over the agent loop — the statefulness a chat front-end needs that single-shot Run/RunNative lack. Each Send runs one user turn to completion (the loop iterates think->act->observe internally until it answers or hits a cap) and folds the resulting transcript back in, so the next Send sees the whole prior exchange.

The expensive context is held HERE, not rebuilt per turn: the Session keeps one warm Sandbox and one open Memory across the conversation (they ride in the Runtime and are passed unchanged to every loop call). The Session also owns the REQUESTED side of policy (runspec.RequestedConfig): live reconfiguration (SetMaxIterations, /model-adjacent commands) edits the request and re-resolves, so there is still exactly one place a policy number can come from — Resolve.

Session is NOT safe for concurrent Sends; a conversation is inherently serial (each turn depends on the last). Drive it from one goroutine.

func NewSession

func NewSession(req runspec.RequestedConfig, rt Runtime, root string, loop LoopFunc) (*Session, error)

NewSession resolves req once and returns a Session that runs each turn through loop with the resolved spec and rt as the fixed base. root is the workspace root every turn's Content carries. A nil loop defaults to Run (the text protocol); pass RunNative for native tool use.

func NewSessionFromConfig added in v0.2.0

func NewSessionFromConfig(cfg Config, loop LoopFunc, history []llm.Message) (*Session, error)

NewSessionFromConfig builds a Session from a Config template — the requested-side compatibility constructor for callers whose builders still assemble Config (deleted with Config in S6b). history may be nil.

func NewSessionWith

func NewSessionWith(req runspec.RequestedConfig, rt Runtime, root string, loop LoopFunc, history []llm.Message) (*Session, error)

NewSessionWith is NewSession plus an explicit conversation seed. The seed is used by resume paths that loaded a prior RunRecord.Messages transcript.

func (*Session) MaxIterations

func (s *Session) MaxIterations() int

MaxIterations returns the cap the next Send will run under.

func (*Session) MaxTokens

func (s *Session) MaxTokens() int

MaxTokens returns the output cap the next Send will run under.

func (*Session) Messages

func (s *Session) Messages() []llm.Message

Messages returns the conversation accumulated so far. The returned slice is the Session's own backing store — treat it as read-only; the next Send replaces it.

func (*Session) Model

func (s *Session) Model() llm.Provider

Model returns the provider the next Send will use.

func (*Session) Planner

func (s *Session) Planner() Planner

Planner returns the planner the next Send will open with (nil = stage off).

func (*Session) PrewarmAutoVerify added in v0.2.0

func (s *Session) PrewarmAutoVerify(ctx context.Context)

PrewarmAutoVerify starts the automatic verify derivation and untouched-tree baseline in the background. It is intentionally optional: callers that do not prewarm retain the synchronous auto-verify behavior of Send.

func (*Session) ReasoningEffort

func (s *Session) ReasoningEffort() string

ReasoningEffort returns the effort the next Send will ask for.

func (*Session) Reset

func (s *Session) Reset()

Reset clears the conversation, starting fresh on the next Send (the /clear command). The warm Sandbox and Memory are untouched — only the dialogue resets.

func (*Session) ReviewPolicy added in v0.2.0

func (s *Session) ReviewPolicy() ReviewPolicy

ReviewPolicy returns the policy the next Send will run under.

func (*Session) Reviewer

func (s *Session) Reviewer() Reviewer

Reviewer returns the reviewer the next Send will gate on (nil = gate off).

func (*Session) Send

func (s *Session) Send(ctx context.Context, input string) (*RunResult, error)

Send runs one user turn to completion and returns its result. The conversation grows: this turn's input, every assistant turn, and every tool result are retained so the next Send continues from them. On a result that reached the loop (Answered, a cap, a spiral kill — anything with a populated transcript) the history advances; a pre-loop refusal or a result with no Messages leaves the prior history intact, so a transient failure doesn't truncate the chat.

func (*Session) SendParts

func (s *Session) SendParts(ctx context.Context, text string, images []llm.ImagePart) (*RunResult, error)

SendParts runs one user turn with explicit content parts: text plus optional images. The text remains the task projection used by recall, memory, planning, and RunResult; images are attached only to this turn's user message.

func (*Session) SetMaxIterations

func (s *Session) SetMaxIterations(n int)

SetMaxIterations changes the per-turn iteration cap for every SUBSEQUENT turn (the /set max-iters command) — raising it after a hit_cap turn lets "continue" finish a big task without restarting the session. Same calling contract as SetModel: only from the driving goroutine, never mid-Send. The caller validates; values <= 0 are rejected by Resolve and keep the previous spec.

func (*Session) SetMaxTokens

func (s *Session) SetMaxTokens(n int)

SetMaxTokens changes the per-turn model output cap for every SUBSEQUENT turn (the /set max-tokens command). Same calling contract as SetModel.

func (*Session) SetModel

func (s *Session) SetModel(p llm.Provider)

SetModel swaps the provider used for every SUBSEQUENT turn while keeping the conversation, the warm Sandbox, and the open Memory — the /model command: switching models mid-chat must not cost you your context. Like Send, it must be called from the single goroutine driving the Session (never while a Send is in flight).

func (*Session) SetPlanner

func (s *Session) SetPlanner(p Planner)

SetPlanner arms (or, with nil, disarms) the PLAN STAGE for every SUBSEQUENT turn — the /plan command: an independent planner model explores the tree read-only before the solver's first turn and its plan rides into the task. Same calling contract as SetModel: only from the driving goroutine, never mid-Send.

func (*Session) SetReasoningEffort

func (s *Session) SetReasoningEffort(e string)

SetReasoningEffort changes the reasoning-effort passthrough for every SUBSEQUENT turn (the /set effort command; "" = provider default). The caller validates the tier name — the wire accepts any string, so a typo would otherwise surface as a provider 400 mid-run. Same calling contract as SetModel.

func (*Session) SetReviewPolicy added in v0.2.0

func (s *Session) SetReviewPolicy(p ReviewPolicy)

SetReviewPolicy swaps the review policy for every SUBSEQUENT turn. Callers that disarm the reviewer (SetReviewer(nil)) while a required policy is in force must downgrade the policy too — validateRun rejects a required policy with no Reviewer, which would fail every later Send. Same calling contract as SetModel: only from the driving goroutine, never mid-Send.

func (*Session) SetReviewer

func (s *Session) SetReviewer(r Reviewer)

SetReviewer arms (or, with nil, disarms) the REVIEW GATE for every SUBSEQUENT turn — the /review command: an independent reviewer model judges each turn's diff after the fence and VerifyCmd pass. Same calling contract as SetModel: only from the driving goroutine, never mid-Send.

func (*Session) SetTools

func (s *Session) SetTools(t map[string]Tool)

SetTools swaps the toolset used for every SUBSEQUENT turn — the /skills picker's seam: toggling a skill rebuilds the `skill` meta-tool (its Level-1 listing is baked into the tool description) and the whole map is swapped here. nil falls back to DefaultTools at loop time, like Runtime.Tools. Same calling contract as SetModel: only from the driving goroutine, never mid-Send. A skill already loaded into the conversation keeps its body in history — SetTools governs what the model can LOAD next, not what it read.

func (*Session) SetVerifyCmd

func (s *Session) SetVerifyCmd(cmd string)

SetVerifyCmd arms (or, with "", disarms) the closing VERIFICATION gate for every SUBSEQUENT turn — the /verify command: the user names the success command mid-session ("go test ./...") and each finish from then on must pass it or the turn ends Unverified (or keeps working, under VerifyContinue). The command is runtime-derived session state (like an auto-verify derivation), so it lives in the session verifyState, not the spec. Same calling contract as SetModel: only from the driving goroutine, never mid-Send.

func (*Session) SetWorkspace

func (s *Session) SetWorkspace(root string, sb sandbox.Sandbox)

SetWorkspace swaps the sandbox/root pair used for every SUBSEQUENT turn while keeping the conversation and memory. Interactive front-ends use this for per-turn worktree isolation: build a fresh sandbox rooted at the worktree, call SetWorkspace, Send once, then restore the normal workspace. Same calling contract as SetModel: only from the driving goroutine, never mid-Send.

func (*Session) Spec added in v0.2.0

func (s *Session) Spec() runspec.ResolvedSpec

Spec returns the resolved spec the next Send will run under.

func (*Session) Tools

func (s *Session) Tools() map[string]Tool

Tools returns the toolset the next Send will run with.

func (*Session) VerifyCmd

func (s *Session) VerifyCmd() string

VerifyCmd returns the verification command the next Send will gate on ("" = gate off).

func (*Session) Workspace

func (s *Session) Workspace() (string, sandbox.Sandbox)

Workspace returns the root/sandbox pair the next Send will use.

type SetupError added in v0.2.0

type SetupError struct {
	Kind string
	Msg  string
}

SetupError is a typed pre-solver configuration failure. Callers should report it as a setup error rather than as a model/provider run result.

func (*SetupError) Error added in v0.2.0

func (e *SetupError) Error() string

type Spend

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

Spend accumulates a run's cumulative dollar cost across ROLES (solver, reviewer, planner), pricing each role's tokens at its OWN model instead of forcing one model's rate onto all of them. Shared through Config as a POINTER so a single accumulator survives the closing review gate, mid-loop repair rounds, and the best-of-N/repair path's fresh loop() invocations. Safe for concurrent Add.

func NewSpend

func NewSpend(price func(model string, u llm.Usage) (float64, bool)) *Spend

NewSpend builds an accumulator over a role-aware pricer. price should prefer a provider-reported per-call cost (Usage.Cost) when present and fall back to a static table, returning ok=false for an unpriceable model.

func (*Spend) Add

func (s *Spend) Add(model string, u llm.Usage)

Add prices one role call's usage at its model and folds it in. Nil-safe and a no-op on empty usage, so callers Add unconditionally.

func (*Spend) Floor added in v0.2.0

func (s *Spend) Floor() (usd float64, priced bool)

Floor reports the cumulative total of successfully priced calls. priced is false until at least one call has been priced. It is nil-safe.

func (*Spend) USD

func (s *Spend) USD() (float64, bool)

USD reports cumulative dollars and whether the figure is COMPLETE. ok=false when nothing has been priced yet or some call could not be priced. Callers that can safely act on an under-count should use Floor.

type Step

type Step struct {
	Iter        int       // 1-based iteration number.
	Reply       string    // the model's full reply this turn.
	Verb        string    // parsed action verb ("" if unrecognized).
	Arg         string    // parsed action argument.
	Observation string    // the tool result fed back (empty on the answer turn).
	Grounded    bool      // had any tool returned a real observation by end of this step.
	Usage       llm.Usage // token accounting for THIS turn's model call.
	// FinishReason is why THIS turn's generation stopped — a turn property
	// (every step of a multi-call turn carries it, like ReasoningAdvanced).
	// The post-mortem discriminator for a mid-sentence answer: "stop" = the
	// model chose to end there, "length" = token cap, "" on a streamed call =
	// the stream ended with NO terminal chunk (silent EOF — likely truncated
	// upstream).
	FinishReason llm.FinishReason
	// ModelMs/ToolMs split the turn's wall-clock so provider latency and tool
	// execution can be told apart (a 25s `run` vs a 25s slow model look identical
	// in iteration counts). ToolMs is 0 on the answer turn (no tool dispatched).
	ModelMs int64
	ToolMs  int64
	// ReasoningAdvanced is true when this turn's (opaque) provider reasoning trace
	// DIFFERED from the previous turn's — a thinking model still moving even if its
	// visible action repeats. It selects the lenient tight-loop/spiral thresholds;
	// see maxReasoningRepeats. Deliberately independent of Usage.ReasoningTokens:
	// gemini moves its encrypted thought-signature while reporting zero tokens, and
	// that movement is real thought (a token gate was tried and reverted 2026-06-12
	// after regressing the trace eval 5/5 → 0/5; see loop_tools.go).
	ReasoningAdvanced bool
}

Step is one think->act->observe iteration, captured as data. The trace of Steps is what lets an eval assert BEHAVIOR (did it escalate to `run`? did it avoid the spiral?), not just final-answer correctness — because HP-2/HP-3/HP-7 are behavior problems.

type TerminationPolicy added in v0.2.0

type TerminationPolicy = runspec.TerminationPolicy

TerminationPolicy remains source-compatible while its ownership lives in runspec.

func DefaultTerminationPolicy added in v0.2.0

func DefaultTerminationPolicy() TerminationPolicy

type Tool

type Tool struct {
	Name string
	Desc string                                                // TEXT loop description: states the tool AND its one-line ARG grammar + \n escapes — that framing IS the text protocol.
	Run  func(ctx context.Context, arg string) (string, error) // TEXT loop (and the native bridge fallback): the model fills one string.

	// NativeDesc is the tool-level description the STRUCTURED native loop
	// advertises (via nativeSchemas). It is behavior-only — what the tool does and
	// when to pick it — and deliberately omits the ARG grammar and the \n/\t/\\
	// escapes that Desc carries: in native mode those are FALSE (args are typed
	// JSON fields with real newlines, no escaping) and the per-field Schema
	// descriptions own the format. Leaking the text-protocol framing here is a
	// trap — a model that obeys "write a line break as \n" would write a literal
	// backslash-n into the verbatim structured content. Empty => nativeSchemas
	// falls back to Desc (fine for a custom tool whose Desc has no escape framing).
	NativeDesc string

	// Schema and RunJSON are the STRUCTURED native path. When both are set,
	// RunNative advertises Schema (typed, multi-field args) and dispatches the
	// model's JSON args straight to RunJSON — no single-string parsing in native
	// mode. They are optional and additive: a Tool with only Run still works in
	// both loops (the native loop bridges it to a one-string `arg` schema), so
	// custom/external toolsets are unaffected.
	Schema  json.RawMessage
	RunJSON func(ctx context.Context, args json.RawMessage) (string, error)
}

Tool is a thing the harness can do on the model's behalf. The model NEVER touches the real world directly (P2) — it only names a tool and an argument; our code runs it. Exported so an eval (or any caller) can supply a custom toolset; Config.Tools nil falls back to DefaultTools.

type UsageObserver added in v0.2.0

type UsageObserver interface {
	// StepUsage reports cumulative turn usage after each model call, plus the
	// live context size (this call's prompt+completion). ctxTokens is separate
	// because cumulative.PromptTokens SUMS across iterations.
	StepUsage(cumulative llm.Usage, ctxTokens int)
}

UsageObserver is an OPTIONAL Observer extension, discovered by type-assertion like DeltaObserver/VerifyObserver. It receives typed per-model-call usage for live UI telemetry; the string note path stays intact for observers that print the CLI trace.

type VerificationCause added in v0.2.0

type VerificationCause string

VerificationCause refines EvidenceStatus without creating a second status taxonomy. It records why a check was failed or inconclusive.

const (
	VerificationCauseNone           VerificationCause = ""
	VerificationCauseTestFailure    VerificationCause = "test-failure"
	VerificationCauseTimeout        VerificationCause = "timeout"
	VerificationCauseEnvironment    VerificationCause = "environment-fault"
	VerificationCauseCancellation   VerificationCause = "cancellation"
	VerificationCauseExecutionError VerificationCause = "execution-error"
)

type VerificationEvidence added in v0.2.0

type VerificationEvidence struct {
	Status      EvidenceStatus `json:"status"`
	Command     string         `json:"command"`
	Attempts    int            `json:"attempts"`
	MutationGen int            `json:"mutation_gen"`
	FinalGen    int            `json:"final_gen"`
}

VerificationEvidence identifies what ran and which workspace generation it covered.

type VerificationRecord added in v0.2.0

type VerificationRecord struct {
	Status      EvidenceStatus    `json:"status"`
	Cause       VerificationCause `json:"cause,omitempty"`
	Command     string            `json:"command"`
	Output      string            `json:"output"`
	Fingerprint string            `json:"fingerprint"`
	Tree        string            `json:"tree"`
	Attempts    int               `json:"attempts"`
}

VerificationRecord is the single standing-context record for either a model-issued check or the configured gate. Tree identifies the exact workspace snapshot measured by the command — it is what the staleness label keys on — and Attempts counts gate recurrences so replacement ordering is explicit.

type VerifyObserver

type VerifyObserver interface {
	VerifyResult(cmd string, ok bool)
}

VerifyObserver is an OPTIONAL Observer extension, discovered by type-assertion like DeltaObserver: an Observer that ALSO implements it receives the outcome of every VerifyCmd execution — the finish-gate checks (verifyTermination) and the kill/cap upgrade attempts (upgradeIfVerified) alike. It exists for the TUI's verify chip: a front-end that shows the gate's live green/red must not parse Note prose (a PASSING verification emits no Note at all). ok is the command's ground truth: it started, ran, and exited clean; a command that could not even start reports ok=false — the gate could not confirm success, which is exactly what the chip should say.

type VerifyStartObserver added in v0.2.1

type VerifyStartObserver interface {
	VerifyStart(cmd string)
}

VerifyStartObserver is an OPTIONAL Observer extension, discovered by type-assertion like VerifyObserver: an Observer that ALSO implements it is told immediately BEFORE a VerifyCmd executes. It exists for live front-ends: a closing suite can run for minutes after the model's final token, and a spinner that keeps saying "running" makes gate time indistinguishable from model time. The matching VerifyResult (or the next iteration, for the pre-flight baseline) ends the phase.

type WorkspaceEffect added in v0.2.0

type WorkspaceEffect string

WorkspaceEffect records whether the final workspace tree differs from the run baseline. Unknown means the harness could not establish the fact.

const (
	WorkspaceChanged   WorkspaceEffect = "changed"
	WorkspaceUnchanged WorkspaceEffect = "unchanged"
	WorkspaceUnknown   WorkspaceEffect = "unknown"
)

Directories

Path Synopsis
Package skill implements Agent Skills for the harness: folders of instructions (SKILL.md) plus optional bundled resources, loaded by the agent ON DEMAND instead of stuffed into the system prompt — progressive disclosure (see ../../docs/specs/SKILLS.md).
Package skill implements Agent Skills for the harness: folders of instructions (SKILL.md) plus optional bundled resources, loaded by the agent ON DEMAND instead of stuffed into the system prompt — progressive disclosure (see ../../docs/specs/SKILLS.md).

Jump to

Keyboard shortcuts

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