taskreplay

package
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package taskreplay implements a deterministic task replay harness.

The harness re-executes the prompt that was previously sent for a completed task against a different provider/model combination, then compares the replayed output against the original. Two comparison signals are produced:

  • SimilarityScore: Jaccard token overlap (0.0–1.0) between the two outputs. Cheap, deterministic, and language-agnostic.
  • EquivalenceScore (optional): a 1–10 verdict from a judge provider that reads both outputs and rates them for semantic equivalence. Skipped when no judge is supplied.

Each replay is appended as a row in the replay_runs table so the Web UI and CLI can browse history without re-executing.

The replay is "deterministic" in the input dimension — the prompt and task context are reconstructed from saved state, not regenerated by the AI — but the target provider's response is, of course, subject to the usual model non-determinism.

Index

Constants

View Source
const DefaultReproduceTimeout = 45 * time.Minute

DefaultReproduceTimeout bounds one reproduction when the caller names none.

Generous, because it covers a model re-doing the task plus a full test suite on two trees, and a reproduction that timed out has burned the cost without producing the verdict that justifies it.

Variables

AllVerdicts is every verdict, for callers that enumerate them.

View Source
var ErrNoRecordedCommit = errors.New("task has no recorded write-back commit, so there is nothing to reproduce against")

ErrNoRecordedCommit is returned for a task that has no write-back commit.

It is the ordinary outcome for a task that ran on the host: its changes went straight into the working tree, so there is no commit that is *this task's* and nothing to reproduce against. Callers should present it as "this task cannot be reproduced", not as a failure.

View Source
var ErrNoRunner = errors.New("reproduce: no sandbox runner was supplied, and there is no host fallback by design")

ErrNoRunner is returned when Reproduce is called without a SandboxRunner.

There is deliberately no default. A fallback that quietly ran the harness on the host would violate the isolation this whole subsystem exists to provide, and it would do so in the one command whose output is a trustworthiness claim.

View Source
var ErrNoTargetProvider = errors.New("target provider must be supplied")

ErrNoTargetProvider is returned when the target provider is not configured.

View Source
var ErrReproductionNotFound = errors.New("reproduction not found")

ErrReproductionNotFound is returned by GetReproduction for an unknown id.

View Source
var ErrTaskNotReplayable = errors.New("task is not replayable: must be done/failed and have recorded output")

ErrTaskNotReplayable is returned when the requested task cannot be replayed because it never completed or has no recoverable output.

Functions

func JaccardSimilarity

func JaccardSimilarity(a, b string) float64

JaccardSimilarity returns the Jaccard token-overlap coefficient between two strings (0.0 = no overlap, 1.0 = identical token sets). Both inputs are lowercased and split on Unicode whitespace and punctuation. Returns 0.0 when either input has no tokens.

func ResolveBase

func ResolveBase(ctx context.Context, projectDir string, p *Provenance) error

ResolveBase fills in p.BaseSHA when the original run did not record one.

It must run before the reproduction is dispatched, not at comparison time: the sandbox is provisioned by checking out the base, so a base that does not exist yet is a run that cannot start.

Three sources, in descending order of how much a verdict computed against them is worth — see BaseSource. Whichever is used, a warning is attached unless the base was recorded, so a verdict is never read without knowing where its base came from.

Reading the repository here is strictly read-only: two rev-parse calls and a merge-base, none of which write.

func SaveReproduction

func SaveReproduction(workDir string, rep *Reproduction) error

SaveReproduction appends rep to the reproductions table and sets rep.ID.

Persisting is the caller's decision, not Reproduce's: a CLI invocation that only wants to print a verdict should not grow the project's database, and a dry run must be able to reach a verdict without leaving a record that looks like evidence.

func VerdictCounts

func VerdictCounts(workDir string) (map[Verdict]int, error)

VerdictCounts summarizes reproductions by verdict, for a dashboard that wants "how much of this project's history has been proven to reproduce?".

Types

type BaseSource

type BaseSource string

BaseSource says how the parent commit was determined, because the two ways are not equally trustworthy and a verdict computed on a guessed base is worth less than one computed on a recorded base.

const (
	// BaseRecorded: read from the write_back event the original run wrote.
	// This is the commit the executor actually provisioned its workspace at.
	BaseRecorded BaseSource = "recorded"

	// BaseForkPoint: computed as the merge base of the project's HEAD and the
	// returned commit — the point the write-back branch forked from mainline.
	//
	// Trustworthy, and the reason the ordinary case still gets a strong
	// verdict: it is correct for a write-back of any length, because it asks
	// where the branch diverged rather than assuming how far back that was.
	// It only fails to produce an answer when the write-back has since been
	// merged into HEAD, in which case the merge base is the commit itself and
	// there is no range left to measure.
	BaseForkPoint BaseSource = "fork-point"

	// BaseFirstParent: inferred as the first parent of the returned commit.
	//
	// The last resort, and never trusted. It is correct only when the
	// write-back was a single commit: a three-commit write-back has its base
	// three commits back, so reproducing from the first parent starts from a
	// tree that already contains two thirds of the work — which would then
	// reproduce "identically" for the wrong reason. Nothing here can tell the
	// two apart, so Comparison.BaseConfirmed is false for this source and
	// verdictFor refuses to rule. See TestCompareBundleMultiCommitInferredBase.
	BaseFirstParent BaseSource = "first-parent"
)

func (BaseSource) Trusted

func (b BaseSource) Trusted() bool

Trusted reports whether a verdict computed against this base is worth anything. See BaseFirstParent for the case that is not.

type Comparison

type Comparison struct {
	// BaseSHA is the commit both changes are measured from.
	BaseSHA string `json:"base_sha,omitempty"`
	// OriginalCommit and ReplayCommit are the two tips.
	OriginalCommit string `json:"original_commit,omitempty"`
	ReplayCommit   string `json:"replay_commit,omitempty"`
	// OriginalTree and ReplayTree are what actually decides IDENTICAL.
	OriginalTree string `json:"original_tree,omitempty"`
	ReplayTree   string `json:"replay_tree,omitempty"`
	// Identical is OriginalTree == ReplayTree.
	Identical bool `json:"identical"`
	// BaseConfirmed reports that the original commit really is a descendant of
	// BaseSHA. False means the base was inferred and could not be corroborated,
	// which caps how strong a verdict may be.
	BaseConfirmed bool `json:"base_confirmed"`
	// OriginalCommits is how many commits the original write-back carried.
	// More than one on an inferred base means the base is wrong.
	OriginalCommits int `json:"original_commits,omitempty"`
	// DiffStat is the --stat summary of the diff-of-diffs, DiffOfDiffs the
	// patch itself, truncated at maxDiffBytes.
	DiffStat    string `json:"diff_stat,omitempty"`
	DiffOfDiffs string `json:"diff_of_diffs,omitempty"`
	// DiffTruncated reports that DiffOfDiffs hit the cap.
	DiffTruncated bool `json:"diff_truncated,omitempty"`
	// FilesDiffering names the paths the two changes disagree on.
	FilesDiffering []string `json:"files_differing,omitempty"`
}

Comparison is the git-level answer: same tree or not, and what differs.

func CompareBundle

func CompareBundle(ctx context.Context, projectDir, bundlePath, branch string, p *Provenance) (*Comparison, error)

CompareBundle diffs a reproduction's returned commit against the original.

bundlePath is the git bundle the executor wrote back; branch is the ref inside it. Nothing in projectDir is modified — see the contract at the top of this file.

type Options

type Options struct {
	// Target is the provider being compared against (required).
	Target provider.Provider
	// TargetName is the registered name of the target provider (e.g. "anthropic").
	TargetName string
	// TargetModel is the model identifier (empty = provider default).
	TargetModel string
	// MaxTokens for the target completion (0 = provider default).
	MaxTokens int
	// Timeout caps the target completion call (0 = 5 minutes).
	Timeout time.Duration

	// Judge, when non-nil, is asked to rate semantic equivalence on a 1–10 scale.
	// The judge is typically a strong general-purpose model (e.g. claude-opus).
	// When nil, EquivalenceScore is left at 0 and EquivalenceRationale is empty.
	Judge      provider.Provider
	JudgeModel string
}

Options controls a single replay invocation.

type Provenance

type Provenance struct {
	TaskID    int           `json:"task_id"`
	TaskTitle string        `json:"task_title"`
	Status    pm.TaskStatus `json:"status"`

	// The plan side of the input.
	Goal         string `json:"goal,omitempty"`
	Instructions string `json:"instructions,omitempty"`
	Prompt       string `json:"prompt,omitempty"`
	PromptSHA256 string `json:"prompt_sha256,omitempty"`

	// The model side. Effort is the reasoning-effort level (Task 20149).
	Provider string `json:"provider,omitempty"`
	Model    string `json:"model,omitempty"`
	Effort   string `json:"effort,omitempty"`

	// The sandbox side: the stamp the original run left, plus whether the
	// project's .cloop/sandbox.yaml still hashes to what that run used.
	Sandbox        artifact.SandboxRecord `json:"sandbox"`
	SandboxHashNow string                 `json:"sandbox_hash_now,omitempty"`
	SandboxDrift   bool                   `json:"sandbox_drift"`

	// The git side.
	BaseSHA        string     `json:"base_sha,omitempty"`
	BaseSource     BaseSource `json:"base_source,omitempty"`
	OriginalCommit string     `json:"original_commit,omitempty"`
	OriginalBranch string     `json:"original_branch,omitempty"`

	// Warnings name each link of the chain that could not be recovered. They
	// are the caller's evidence for how much a verdict is worth.
	Warnings []string `json:"warnings,omitempty"`
}

Provenance is the reconstructed input state of one past task execution: everything a reproduction needs to set up the same run again.

func ReadProvenance

func ReadProvenance(workDir string, taskID int) (*Provenance, error)

ReadProvenance reconstructs the input state of taskID's original execution.

func (*Provenance) PinnedImage

func (p *Provenance) PinnedImage() bool

PinnedImage reports whether the original run's filesystem can be reproduced byte-for-byte — that is, whether the image was digest-pinned.

A reproduction against a floating tag is still worth running, but a DIVERGENT verdict from one is not evidence of orchestrator nondeterminism: the image moved underneath it. Callers surface this next to the verdict.

func (*Provenance) ProvenanceAge

func (p *Provenance) ProvenanceAge(now time.Time) time.Duration

ProvenanceAge is how long ago the original run started, when the stamp recorded it. Zero when unknown.

func (*Provenance) Reproducible

func (p *Provenance) Reproducible() bool

Reproducible reports whether enough of the chain survived to place a run.

type ReproduceOptions

type ReproduceOptions struct {
	// Runner places the sandbox. Required — see ErrNoRunner.
	Runner SandboxRunner
	// Timeout bounds the whole reproduction. 0 means DefaultReproduceTimeout.
	Timeout time.Duration
	// TestCommand overrides the detected test command. Supply it for a project
	// whose suite is not what testrun.Detect would guess.
	TestCommand []string
	// SkipTests suppresses the behavioural gate entirely. The result is that a
	// non-identical reproduction can only ever be INCONCLUSIVE — which is the
	// honest outcome, and why this is off by default.
	SkipTests bool
	// Emit receives progress lines. May be nil.
	Emit func(string)
}

ReproduceOptions controls one reproduction.

type Reproduction

type Reproduction struct {
	ID        int64     `json:"id,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	TaskID    int       `json:"task_id"`
	TaskTitle string    `json:"task_title,omitempty"`

	Verdict Verdict `json:"verdict"`
	// Reason states, in one sentence, why this verdict and not another. It is
	// the field an operator reads first and the only one that is never empty.
	Reason string `json:"reason"`

	Provenance *Provenance `json:"provenance,omitempty"`
	Comparison *Comparison `json:"comparison,omitempty"`

	// Where the reproduction ran.
	ExecutorID   string `json:"executor_id,omitempty"`
	ExecutorKind string `json:"executor_kind,omitempty"`
	Isolation    string `json:"isolation,omitempty"`
	PinnedImage  string `json:"pinned_image,omitempty"`

	// The behavioural gate: the same command on both trees.
	OriginalTest *TestOutcome `json:"original_test,omitempty"`
	ReplayTest   *TestOutcome `json:"replay_test,omitempty"`

	// AgentExitCode and AgentOutput are the re-execution's own result.
	AgentExitCode int    `json:"agent_exit_code"`
	AgentOutput   string `json:"agent_output,omitempty"`

	Duration time.Duration `json:"duration,omitempty"`
	// Err is set when the harness failed rather than the comparison.
	Err string `json:"error,omitempty"`
}

Reproduction is the full record of one reproduction attempt.

func GetReproduction

func GetReproduction(workDir string, id int64) (*Reproduction, error)

GetReproduction returns one reproduction by id, diff included.

func ListReproductions

func ListReproductions(workDir string, taskID, limit int) ([]*Reproduction, error)

ListReproductions returns reproductions newest first. taskID == 0 means all tasks; limit <= 0 defaults to 50.

The diff-of-diffs is dropped from every row: a list of fifty verdicts each carrying up to a megabyte of patch is the artifact-read amplification Task 20220 bounded elsewhere. GetReproduction returns it for the one row a reviewer opened.

func Reproduce

func Reproduce(ctx context.Context, workDir string, taskID int, opts ReproduceOptions) (*Reproduction, error)

Reproduce re-executes taskID in a fresh isolating sandbox and returns a verdict on whether it produced the same change.

It never mutates the project: not its refs, not its working tree, not its task record. The returned Reproduction is persisted by the caller via SaveReproduction, so a caller that only wants the answer can decline to store it.

type Result

type Result struct {
	TaskID               int
	TaskTitle            string
	OriginalProvider     string
	OriginalModel        string
	TargetProvider       string
	TargetModel          string
	Prompt               string
	OriginalOutput       string
	ReplayedOutput       string
	SimilarityScore      float64 // Jaccard 0.0–1.0
	EquivalenceScore     int     // 0 = not judged, 1–10 = AI verdict
	EquivalenceRationale string
	Duration             time.Duration
	InputTokens          int
	OutputTokens         int
	Err                  string // non-empty on failure (still persisted for visibility)
	CreatedAt            time.Time
}

Result captures the outcome of one replay against a single task.

func ReplayTask

func ReplayTask(ctx context.Context, workDir string, taskID int, opts Options) (*Result, error)

ReplayTask reconstructs the prompt for a completed task, re-executes it against opts.Target, computes a Jaccard similarity score against the original output, optionally calls opts.Judge for a 1–10 equivalence verdict, and persists the comparison to the replay_runs table.

On hard failure (target call errors, judge errors) the function still returns a partially-populated *Result with Err set so callers can persist the attempt.

type Run

type Run struct {
	ID                   int64
	CreatedAt            time.Time
	TaskID               int
	TaskTitle            string
	OriginalProvider     string
	OriginalModel        string
	TargetProvider       string
	TargetModel          string
	Prompt               string
	OriginalOutput       string
	ReplayedOutput       string
	SimilarityScore      float64
	EquivalenceScore     int
	EquivalenceRationale string
	DurationMS           int64
	InputTokens          int
	OutputTokens         int
	Error                string
}

Run is the on-disk shape of a replay_runs row. Mirrors Result but uses scalar fields suitable for SQL bind/scan.

func GetRun

func GetRun(workDir string, id int64) (*Run, error)

GetRun returns a single replay_runs row by id, or sql.ErrNoRows wrapped when not found.

func ListRuns

func ListRuns(workDir string, taskID int, limit int) ([]Run, error)

ListRuns returns the most recent replays for the given workDir, newest first. taskID == 0 means "all tasks". limit <= 0 defaults to 100.

type RunOutcome

type RunOutcome struct {
	// BundlePath is the git bundle holding the sandbox's commit. The runner
	// owns creating it; Reproduce removes it when it is done.
	BundlePath string
	// Branch is the ref inside the bundle.
	Branch string
	// ExitCode and Output are the agent harness's.
	ExitCode int
	Output   string
	// Skipped and SkipReason report a sandbox that ran and changed nothing.
	Skipped    bool
	SkipReason string
	// Where it ran, for the record and for the UI.
	ExecutorID   string
	ExecutorKind string
	Isolation    string
	PinnedImage  string
	// Test is the project's test command run on the reproduced tree, when a
	// command was supplied.
	Test *TestOutcome
}

RunOutcome is what a SandboxRunner brings back.

type RunRequest

type RunRequest struct {
	// ProjectDir is the hub-side project this reproduction belongs to. It
	// supplies the executor binding and the workspace source; the runner must
	// not execute anything in it.
	ProjectDir string
	// Provenance is the reconstructed input state: prompt, model settings,
	// sandbox, and the commit to start from (Provenance.BaseSHA).
	Provenance *Provenance
	// Branch is the write-back branch the sandbox must commit to. It is
	// generated per reproduction and never collides with a real task branch.
	Branch string
	// TestCommand is the project's own test command, to run inside the sandbox
	// after the agent exits. Empty when none was detected.
	TestCommand []string
	// Timeout bounds the whole sandbox run.
	Timeout time.Duration
}

RunRequest is what a SandboxRunner is asked to do: re-execute one task in a fresh isolating sandbox and bring back the commit.

type SandboxRunner

type SandboxRunner interface {
	RunReproduction(ctx context.Context, req RunRequest) (*RunOutcome, error)
	VerifyCommit(ctx context.Context, req VerifyRequest) (*TestOutcome, error)
}

SandboxRunner places a reproduction on an isolating executor.

It is an interface rather than a concrete implementation because the two callers assemble a spec from different places and neither can import the other: the hub (pkg/ui) has the executor registry, the secret broker, the egress filter and the project binding already wired up, while the CLI builds the same spec standalone. Reproduce owns what a reproduction *means* — reconstruct, compare, judge — and delegates only how a sandbox is started.

An implementation MUST guarantee, and is tested on:

  • the executor isolates (container, VM, or remote); never the host;
  • the workspace is provisioned fresh at Provenance.BaseSHA;
  • egress is denied except for broker-leased grants;
  • write-back is bundle mode, so nothing is pushed anywhere.

type SuiteOptions

type SuiteOptions struct {
	Options
	// Tags, when non-empty, restricts the replay set to tasks matching ANY of
	// the given tags (matches pm.TaskMatchesTags semantics).
	Tags []string
	// IncludeFailed re-runs failed tasks too. Default is done-only.
	IncludeFailed bool
	// MaxTasks limits how many tasks are replayed (0 = unlimited).
	MaxTasks int
}

SuiteOptions controls a batch replay (cloop task replay-suite).

type SuiteSummary

type SuiteSummary struct {
	TotalTasks     int
	Replayed       int
	Skipped        int
	Failed         int
	AverageJaccard float64
	AverageEquiv   float64
	HighEquivCount int // equivalence_score >= 8
	LowEquivCount  int // equivalence_score in [1,4]
	Results        []*Result
}

SuiteSummary aggregates a SuiteRun's results.

func RunSuite

func RunSuite(ctx context.Context, workDir string, opts SuiteOptions) (*SuiteSummary, error)

RunSuite replays every replayable task in the project that matches the suite filter. Each task replay is persisted via the same path as ReplayTask. The summary is computed in memory and returned to the caller.

Errors from individual tasks are recorded in result.Err and do not abort the suite; only catastrophic failures (state load, etc.) bubble up.

type TestOutcome

type TestOutcome struct {
	// Command is what was run, for the record.
	Command []string `json:"command,omitempty"`
	// Framework is the detected harness name ("go test", "pytest", …).
	Framework string `json:"framework,omitempty"`
	// Ran reports that the command actually executed. False means the sandbox
	// could not run it at all, which is not the same as a failing suite.
	Ran bool `json:"ran"`
	// Passed reports a zero exit status.
	Passed bool `json:"passed"`
	// ExitCode is the command's status, meaningful only when Ran.
	ExitCode int `json:"exit_code"`
	// Output is the tail of the command's output, for a reviewer.
	Output string `json:"output,omitempty"`
	// Duration is how long it took.
	Duration time.Duration `json:"duration,omitempty"`
}

TestOutcome is one run of the project's own test command.

func (*TestOutcome) Agrees

func (t *TestOutcome) Agrees(other *TestOutcome) bool

Agrees reports whether two outcomes describe the same behaviour.

Both must have run and both must have passed. "Both failed" is deliberately not agreement: two suites can fail for unrelated reasons, and a reproduction harness that called that EQUIVALENT would certify a broken change as faithfully reproduced.

type Verdict

type Verdict string

Verdict is the outcome of a reproduction.

const (
	// VerdictIdentical: the reproduction produced the same tree hash.
	VerdictIdentical Verdict = "identical"
	// VerdictEquivalent: different bytes, same behaviour under the project's
	// own test command on both trees.
	VerdictEquivalent Verdict = "equivalent"
	// VerdictDivergent: the reproduction produced a different change.
	VerdictDivergent Verdict = "divergent"
	// VerdictInconclusive: no verdict could be reached. See the file comment.
	VerdictInconclusive Verdict = "inconclusive"
)

func (Verdict) Reproducible

func (v Verdict) Reproducible() bool

Reproducible reports whether the verdict is one that supports the claim "this commit reproduces".

func (Verdict) Valid

func (v Verdict) Valid() bool

Valid reports whether v is a known verdict.

type VerifyRequest

type VerifyRequest struct {
	ProjectDir  string
	Provenance  *Provenance
	CommitSHA   string
	TestCommand []string
	Timeout     time.Duration
}

VerifyRequest asks a runner to run only the test command, at a given commit, with no agent involved. It is how the original tree's behaviour is measured.

Jump to

Keyboard shortcuts

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