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
- Variables
- func JaccardSimilarity(a, b string) float64
- func ResolveBase(ctx context.Context, projectDir string, p *Provenance) error
- func SaveReproduction(workDir string, rep *Reproduction) error
- func VerdictCounts(workDir string) (map[Verdict]int, error)
- type BaseSource
- type Comparison
- type Options
- type Provenance
- type ReproduceOptions
- type Reproduction
- type Result
- type Run
- type RunOutcome
- type RunRequest
- type SandboxRunner
- type SuiteOptions
- type SuiteSummary
- type TestOutcome
- type Verdict
- type VerifyRequest
Constants ¶
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 ¶
var AllVerdicts = []Verdict{VerdictIdentical, VerdictEquivalent, VerdictDivergent, VerdictInconclusive}
AllVerdicts is every verdict, for callers that enumerate them.
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.
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.
var ErrNoTargetProvider = errors.New("target provider must be supplied")
ErrNoTargetProvider is returned when the target provider is not configured.
var ErrReproductionNotFound = errors.New("reproduction not found")
ErrReproductionNotFound is returned by GetReproduction for an unknown id.
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 ¶
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.
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
// Env is added to the environment of a provider that runs a program, for
// both calls — what pins a hub's replay to the Claude account of the user
// who asked for it (Task 20442). See provider.Options.Env.
Env []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 ¶
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.
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 ¶
Reproducible reports whether the verdict is one that supports the claim "this commit reproduces".
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.