Replay checks API

A replay check replays a planned change of a work station on the replay set that guards the station's changes, compares it with the live config, and gives a verdict under the line's replay_check. spotlight replay check calls these endpoints from CI. They sit under /api/teams/{teamId}, answer JSON, and answer errors as application/problem+json, as on the resource API.

Endpoints

Method and pathReturns
POST /replay-sets/{setId}/guards-changes204: the set guards its station's changes.
DELETE /replay-sets/{setId}/guards-changes204: the set no longer guards them. Checks already started keep running.
POST /replay-checks201 and a check, or 200 and the check this change already has. See Start a check. Requires Idempotency-Key.
GET /replay-checks/{checkId}A check and its verdict.
GET /replay-sets/{setId}/replay-checksThe checks replayed over a set, newest first, a page at a time: each check's id, stationId, stationKey, verdict, undecidedBecause, regressions (how many tasks it regressed on), attempt, reference, createdAt and replayUrl. Takes limit (default 50, at most 200) and before, a check's id; a check the set does not hold answers an empty page.

A replay set's reads in the Replays API carry guardsChanges, true for the set that guards its station's changes. A station has at most one.

Start a check

{
  "lineId": "<line id>",
  "stationKey": "build",
  "station": {
    "key": "build",
    "name": "Build",
    "kind": "work",
    "harness": "claude-code",
    "prompt": "Build it, then drain the queue.",
    "passesTo": ["ship"]
  },
  "line": {
    "repositories": [{ "name": "acme/api", "setup": "npm ci" }]
  },
  "reference": { "commit": "4f7c2a9e1b3d", "pullRequest": 42 },
  "repeats": 1
}
FieldTypeDefaultDescription
lineIdstringrequiredThe line the station is on.
stationKeystringrequiredThe station the change plans, by its key.
stationobjectrequiredThe station block as the plan would apply it, in the shape of one entry of a line's stations.
lineobjectnoneThe line's repositories as the plan would apply them, each with its image and setup, as { "repositories": [...] }. At most 64,000 characters. Without it, the live line's.
reference.commitstringnoneThe commit that plans the change, a hex sha of 7 to 64 characters.
reference.pullRequestnumbernoneThe pull request that plans the change.
repeatsnumber1How many times the check replays each run of the set, 1 to 5. The line's ceiling covers every copy.

The check:

  1. Reads replay_check from the line as it is applied, never from the plan.
  2. Replaces the planned block's check, judge, diffs and runners with the live station's, and records in keptLive each one the plan changed.
  3. Checks the block as the line would be with the station replaced, as a replay candidate is checked.
  4. Replays it, under the planned repositories, images and setup commands, on every run of the guarding set, repeats times, on the team's runners. When the set has no baseline of the current live config with at least as many copies, it also starts a baseline replay of the live config with the same repeats. See Ceiling.

A change that changes nothing about how the station's runs go is refused with 409 and the type urn:spotlight:problem:nothing-to-replay.

A change already checked is answered with that check, 200 and reused: true, while the earlier check is running, passed or failed, and only if it replayed the same runs of the set against the same live config, the same threshold, the same scoring and the same repeats. Otherwise a new check starts, with the next attempt. A request sent again with the same Idempotency-Key answers 201 with the check it started and the header Idempotent-Replayed: true.

Ceiling

The line's replay_check.ceiling caps each replay a check starts, each on its own:

  • the replay of the planned change, which every check starts;
  • the baseline replay of the live config, only when the guarding set has no baseline for the current live config with at least as many copies of each run. A baseline already there costs the check nothing.

So one check spends at most twice the ceiling, plus what runs already running spend after a replay reaches it. A run whose cost is not reported, or is in another currency, counts as the ceiling divided by the number of runs in the set times repeats: threshold.unpricedRun. When a replay reaches its ceiling, no more of its runs start, and runs already running finish, so it can end a little over. A check whose replay reached its ceiling is undecided, over-ceiling, unless it has already failed.

The check replays a candidate Spotlight names Planned change, PR #<n>, or after the commit's first 7 characters, or the date. It is not a replay candidate resource. A proposed replay candidate may not be named starting Planned change.

Check

FieldTypeDescription
id, teamId, lineId, stationId, stationKeystring
setId, setNamestring, string or nullThe guarding set.
setVersionnumberThe version of the set the check replayed.
candidateId, replayIdstringThe planned candidate and its replay.
baselineReplayIdstring or nullThe replay of the live config it is compared with. Null while it is being started.
attemptnumberCounts checks of the same change against the same live config, from 1.
referenceobject{ commit, pullRequest }.
liveFingerprint, candidateFingerprintstringThe config fingerprints of the live and planned station.
keptLivearray of stringsWhat the plan changed of check, judge, diffs and runners, which the check kept as the live station has them.
thresholdobject{ mostRegressions, minGain, ceiling, unpricedRun }, from the live line when the check started. minGain is its replay_check.min_gain; ceiling caps each replay of the check; unpricedRun is what an unpriced run counts against it, or null once the replay is gone.
repeatsnumberHow many times the check replays each run of the set.
gainobject or nullThe candidate's gain in success over the baseline, { gain, low, high, verdict, minGain, tasks }, as a replay's gain. It does not decide verdict. Null with no baseline or no replay, and while no task was scored on both sides.
verdictstringSee Verdicts.
undecidedBecausestring or nullSee Verdicts.
tasksnumberHow many tasks the set replays.
comparednumberHow many were scored on both sides.
candidate, baselineobjectEach { state, entries, scored, succeeded, successRate, interval, tasksScored, spent }. state is the replay's state, or null once it is gone. entries, scored and succeeded count copies; successRate and interval are as on a replay, and tasksScored counts tasks with a scored copy.
regressionsarrayEach task it regressed on: { taskId, originalRunId, title, candidate, baseline, scoreDelta, reasons }. candidate and baseline list the result of each side's copies, in order; reasons are the reasons of the change's copies as { code, detail }, each once.
replayUrlstring or nullThe check's replay in the web app.
reusedbooleantrue when starting a check answered one this change already had. Always false on a read.
createdAtstringAn RFC 3339 time.

Verdicts

VerdictShown in the web appWhen
failedToo many regressionsMore tasks regressed than mostRegressions allows. Decided as soon as it happens, while replays still run. A task counts only once each of its copies, on both sides, has settled.
runningCheckingThe change's replay or its baseline is queued or running, or the baseline is still being started.
undecidedUndecided: <reason>See below.
passedPassedNone of the above.

A task regressed as in a replay.

undecidedBecauseShown in the web app after Undecided:
replay-gonereplay is gone
stoppedreplay was stopped
over-ceilingreplay reached its ceiling
no-baselineno live-config replay to compare with
baseline-unfinishedlive config's replay didn't finish
too-few-scoredunder half the tasks scored on both sides

Who may read and change

Reading needs a person on the team or an owner or admin of its workspace, or a workspace token of the team's workspace with the read scope. Anyone else is answered 404. Marking a set and starting a check also need plan. See Team outcomes.

Starting a check is recorded as a configuration change of the kind replay-check. Marking or unmarking a set is recorded as an updated change of the kind replay-set.

Errors

StatusCause
401No session or token, or an unknown token.
403A change by a caller who may read the team but not plan.
404The team, line, station, set or check is not one the caller may see, as in Replay check <checkId> not found.
409Marking a set while another at the station guards its changes (Another replay set already guards config changes of station <stationId>. Only one replay set of a station can guard config changes.), or a set whose station has left its line. A line with no replay_check (Line <lineKey> has no replay_check block. Please add one with most_regressions, ceiling and currency.). A station no set guards (No replay set guards config changes of station <stationKey>. Please turn on "Guards config changes" for one of its replay sets.). A change with nothing to replay, nothing-to-replay: There is nothing to replay. The plan changes nothing that affects how the station's runs go., or There is nothing to replay. A check keeps these changed fields at their live values: <fields>.
422The planned block is not one the line takes, each problem at its path under station. A missing lineId (Line ID must name the line of the station.) or stationKey (Station key must be the key of the planned station.), or a reference or line not in its form. repeats not a number (Repeats must be a number.) or out of range (Replay check repeats must be from 1 to 5.). The 409 causes are checked first, so a check of a line with no replay_check, of a station no set guards, or with nothing to replay answers 409 whatever its repeats.

On this page