spotlight_line
The spotlight_line resource, and the line resource of the API: a team's
workflow, made of its stations, the reactions that move tasks between them, and
the repositories its runs work in.
| Kind | Endpoint | Terraform | Address |
|---|---|---|---|
line | /api/resources/teams/{teamId}/lines | spotlight_line | <team key>/<key> |
The request body is the line definition. A read returns teamId, key and
definition, the definition with every default filled in, beside the
common fields. See
Built-in lines for the lines Spotlight opens
for the first team of a workspace created in the web app.
In Terraform, each element of repositories, stations and reactions is a
repository, station or reaction block, and reviews, replayCheck and
source are reviews, replay_check and source blocks. Within a station block, judge is a judge block and each element of
alerts is an alert block.
Example
resource "spotlight_line" "core_delivery" {
team_id = spotlight_team.core.id
key = "delivery"
name = "Delivery"
role = "delivery"
runners = ["vpc-internal"]
repository {
name = "acme/api"
image = "ghcr.io/acme/api-runner:main"
setup = "npm ci"
}
station {
key = "backlog"
name = "Backlog"
kind = "backlog"
passes_to = ["implementation"]
}
station {
key = "implementation"
name = "Implementation"
harness = "claude-code"
profile = spotlight_harness_profile.core_careful.key
prompt = file("${path.module}/prompts/core/delivery/implementation.md")
check = "npm test"
diffs = true
decisions = true
memories = true
tools = ["${spotlight_integration.core_acme.source}.comment", "pull-request"]
skills = [spotlight_skill.core_grafana.key]
runners = ["gpu"]
passes_to = ["code-review"]
judge {
rubric = file("${path.module}/judges/core/delivery/implementation.md")
sample = 0.2
trust = 0.6
}
alert {
measure = "sent_back_rate"
rises_by = 0.2
over = 20
}
}
station {
key = "code-review"
name = "Code review"
harness = "claude-code"
passes_to = ["implementation"]
ends = true
}
station {
key = "completed"
name = "Completed"
kind = "completed"
}
reaction {
happening = "github.checks.failed"
from = "implementation"
to = "implementation"
}
replay_check {
most_regressions = 0
ceiling = 10
currency = "USD"
min_gain = 0.05
}
source {
repository = "acme/platform"
path = "teams/core/delivery.tf"
}
}The same line as a request body:
{
"key": "delivery",
"name": "Delivery",
"role": "delivery",
"runners": ["vpc-internal"],
"repositories": [
{ "name": "acme/api", "image": "ghcr.io/acme/api-runner:main", "setup": "npm ci" }
],
"stations": [
{ "key": "backlog", "name": "Backlog", "kind": "backlog", "passesTo": ["implementation"] },
{
"key": "implementation",
"name": "Implementation",
"harness": "claude-code",
"profile": "careful",
"prompt": "Implement the task.",
"check": "npm test",
"diffs": true,
"decisions": true,
"memories": true,
"tools": ["acme.comment", "pull-request"],
"skills": ["grafana"],
"runners": ["gpu"],
"passesTo": ["code-review"],
"judge": { "rubric": "Did the run meet the task?", "sample": 0.2, "trust": 0.6 },
"alerts": [{ "measure": "sent_back_rate", "risesBy": 0.2, "over": 20 }]
},
{
"key": "code-review",
"name": "Code review",
"harness": "claude-code",
"passesTo": ["implementation"],
"ends": true
},
{ "key": "completed", "name": "Completed", "kind": "completed" }
],
"reactions": [
{ "happening": "github.checks.failed", "from": "implementation", "to": "implementation" }
],
"replayCheck": { "mostRegressions": 0, "ceiling": 10, "currency": "USD" },
"source": { "repository": "acme/platform", "path": "teams/core/delivery.tf" }
}A reviews block, on a line whose role is review:
reviews {
start = "opened"
mention = "@spotlight"
label = "spotlight"
skip {
spotlight = true
drafts = true
authors = ["dependabot[bot]", "renovate[bot]"]
}
}Line fields
| Field | Type | Default | Description |
|---|---|---|---|
key | string | required | Names the line within its team. Lowercase words joined by dashes, at most 64 characters. Fixed: an update that changes it is refused; Terraform replaces the resource. |
name | string | required | What the line is called. At most 100 characters. |
role | string | none | The role the line plays for its team, as in delivery or review. Lowercase words joined by dashes, at most 64 characters. A role names one line per team. |
repositories | array of objects | [] | The repositories the line works on. See repositories. |
stations | array of objects | required | The stations, in order. At least one. See stations. |
reactions | array of objects | [] | Moves a task when something happens. See reactions. |
reviews | object | none | The review policy: which pull requests the line reviews. Only on a line whose role is review. See review policy fields. |
runners | array of strings | [] | Runner labels the line's work is placed on. Empty: any runner of the workspace qualifies. See placement. |
replayCheck | object | none | How a planned change of a station on this line must do to pass spotlight replay check. Without it, no planned change of the line is checked. See replay check fields. |
source | object | none | Where the line's Terraform lives, which Spotlight opens a pull request against when a person picks a search candidate. See source fields. |
Station fields
| Field | Type | Default | Description |
|---|---|---|---|
key | string | required | Names the station within the line, and is what a run hands a task on to. Lowercase words joined by dashes, at most 64 characters. Not review or done. |
name | string | required | What the station is called on the board. At most 100 characters. |
kind | string | work | backlog, work or completed. |
color | string | #a3a8b0 | A six-digit hex colour, as in #6d78d5. Stored in lower case. |
description | string | none | What the station is for. At most 500 characters. |
harness | string | none | The harness that works tasks here: claude-code, cursor or codex. At most 60 characters. A station without one runs nothing, and no run may hand a task on to it. |
model | string | none | The model the harness uses. At most 60 characters. Without it, the harness uses its own default. |
effort | string | none | low, medium, high, xhigh or max. Without it, the harness uses its own default. |
profile | string | none | The key of a team harness profile. A run at a station naming a profile the team does not hold is refused. |
prompt | string | none | Markdown handed to the agent that works the station. At most 20,000 characters. |
check | string | none | A command each layer of a change stack drafted at this station is checked with, as in npm test. One line of at most 500 characters. See checks. |
runners | array of strings | none | Runner labels this station's work is placed on. Without it, the line's runners apply. See placement. |
diffs | boolean | false | Whether a change stack is drafted from each run's diff. See Change stacks and decisions. |
decisions | boolean | false | Whether decisions are drafted from each run. See decisions. |
memories | boolean | false | Whether proposed team memories are drafted from each completed run, for the team to approve in the inbox. They come from what the agent said, thought and planned, never tool output, and from what people said on the task since the last draft. See memory. |
tools | array of strings | [] | The tools given to the agent. See tool names. Each tool once. |
skills | array of strings | [] | The skills given to the agent: a team skill key, or a catalog skill as catalog.<name>@<x.y.z>. Each skill once. A key starting spotlight- is refused. |
passesTo | array of strings | [] | The keys of the stations a run here may hand a task on to. |
ends | boolean | false | Whether a run here may finish the task by handing over with done. From any other station, done goes to a person instead. |
judge | object | none | Scores the station's runs against a rubric, a list of claims, or both. Only on a work station. See judge fields. A read leaves it out when none is set. |
alerts | array of objects | [] | Thresholds on a measure of the station's runs, kept in the order written. Only on a work station. See alert fields. |
Spotlight checks catalog tools and skills against what the workspace's plan
includes when the line is written. It does not check at write time that a
harness exists or that a profile, a team skill or an integration tool is
there.
Tool names
| Form | Example | Tool |
|---|---|---|
| a built-in name | pull-request, review-comment, review | Spotlight's own. |
<integration>.<tool> | acme.comment | A tool an integration of the team offers. |
catalog.<name>@<x.y.z> | catalog.web-search@1.1.0 | A catalog tool at an exact version. |
See Built-in tools and skills for the built-in and catalog tools, and Agent commands for how an agent calls a tool.
Judge fields
| Field | Type | Default | Description |
|---|---|---|---|
rubric | string | none | What a good run looks like, in Markdown. Not empty, at most 20,000 characters. In Terraform, usually read with file(). Required when claims is not set. |
claims | array of strings | none | Checkable claims the judge answers yes or no about each run, in order: 1 to 20, each not empty and at most 500 characters, with surrounding spaces trimmed. Required when rubric is not set. |
passShare | number | 1 | The share of claims a replayed run must meet to pass, from 0 to 1. 1 means every claim. Only with claims. Terraform: pass_share. |
sample | number | required | The share of runs judged, from 0 to 1. |
trust | number | required | The agreement with people the judge needs before its scores show, from 0 to 1: Cohen's kappa on a yes-or-no rubric and on claims, Spearman's ρ on a 1 to 5 rubric. See Agreement. |
A judge has sample, trust, and rubric, claims or both. See
Judging for which runs
are judged and when scores show.
A judge with claims scores claim by claim: its score is the share of claims
met. A rubric beside them is given to the judge with the claims, and needs no
scores: line. passShare decides only whether a replayed run passes; see
Results. A read returns claims and
passShare only when the judge has claims, and rubric only when it has one.
judge {
claims = [
"The change adds a test that fails without it.",
"The change touches only what the task names.",
]
pass_share = 1
sample = 0.2
trust = 0.6
}Without claims, the rubric names its scale on its first line that is not blank, or on a line
of a front matter block that opens the rubric between two --- lines:
| Line | Scale |
|---|---|
scores: yes-no | Yes or no. |
scores: 1-5 | A whole number from 1 to 5. |
| none | Yes or no. |
scores is matched without regard to case, with any spaces around the colon.
A scores: line further down is not read.
An export writes each rubric to
judges/<team>/<line>/<station>.md and reads it with file(). It writes
claims in the block, one claim to a line, with pass_share.
The judge is part of the station's config: a run records one SHA-256 of its
rubric, claims and pass share together, its rubric version, with sample and
trust. A change to any of them changes the
config fingerprint, and a
change to the rubric, a claim or the pass share starts a new rubric version.
| Message | Cause |
|---|---|
Judge rubric must not be empty. | rubric is empty or only whitespace. |
Judge rubric must be at most 20000 characters. | rubric is longer. |
Judge sample must be a share from 0 to 1. | sample is outside 0 to 1. |
Judge trust must be a share from 0 to 1. | trust is outside 0 to 1. |
Judge rubric names its scale as "scores: yes-no" or "scores: 1-5" on its first line. | The judge has no claims, and the scores: line names another scale, or the rubric opens a front matter block with --- and never closes it. |
Judge rubric or claims must be given. | Neither rubric nor claims is set. |
Judge claims must list 1 to 20 claims. | claims is empty or has more than 20. |
Judge claims each must not be empty. | A claim is empty or only whitespace. |
Judge claims each must be at most 500 characters. | A claim is longer. |
Judge passShare needs claims to count. | passShare is set without claims. |
Judge passShare must be a share from 0 to 1. | passShare is outside 0 to 1. |
Each is returned with 422 at the path stations[<n>].judge.<field>.
Alert fields
| Field | Type | Default | Description |
|---|---|---|---|
measure | string | required | sent_back_rate, accepted_rate, checks_failed_rate, failed_rate, merged_rate or cost_per_run. |
risesBy | number | none | The measure over the last over runs is this much higher than over the over runs before. |
fallsBy | number | none | The measure over the last over runs is this much lower than over the over runs before. |
above | number | none | The measure across the last over runs is higher than this. |
below | number | none | The measure across the last over runs is lower than this. |
over | number | required | How many runs are compared, and how many further settled runs an alert that went off must stay within its threshold to recover. A whole number, at least 1. |
In Terraform, risesBy and fallsBy are rises_by and falls_by. An alert
sets exactly one of risesBy, fallsBy, above and below; a read returns
the others as null.
| Measure | Unit | risesBy, fallsBy | above, below |
|---|---|---|---|
The five _rate measures | A share from 0 to 1, so 0.2 is 20 percentage points. | Above 0, at most 1. | 0 to 1. |
cost_per_run | An amount in the workspace's currency. | Above 0. | 0 or more. |
A station may have any number of alerts, but not two on the same measure with
the same kind of threshold: of two alerts that both set risesBy on
sent_back_rate, the later one is refused.
Alerts are not part of the station's config, so changing them does not change
the config fingerprint. Alerts
are kept with the line, exported, listed on the Station page, and checked as
the station's runs end and settle. See
Station alerts.
| Message | Cause |
|---|---|
Alert measure must be one of sent_back_rate, accepted_rate, checks_failed_rate, failed_rate, merged_rate, cost_per_run. | measure is another value. |
Alert must set exactly one of risesBy, above, fallsBy, below. | None, or more than one, is set. Returned at the path of the alert. |
Alert "rises by" value must be a share above 0 and at most 1. | risesBy on a rate is out of bounds. |
Alert "falls by" value must be a share above 0 and at most 1. | fallsBy on a rate is out of bounds. |
Alert "is above" value must be a share from 0 to 1. | above on a rate is out of bounds. |
Alert "is below" value must be a share from 0 to 1. | below on a rate is out of bounds. |
Alert "rises by" value must be an amount above 0. | risesBy on cost_per_run is 0 or less. |
Alert "falls by" value must be an amount above 0. | fallsBy on cost_per_run is 0 or less. |
Alert "is above" value must be an amount of 0 or more. | above on cost_per_run is below 0. |
Alert "is below" value must be an amount of 0 or more. | below on cost_per_run is below 0. |
Alert must compare a whole number of runs, at least 1. | over is not a whole number of at least 1. |
Alert for "<measure> <rises by, is above, falls by or is below>" already exists on this station. | An earlier alert at the station has the same measure and sets the same one of risesBy, above, fallsBy and below. Returned at the path of the later alert. |
Each is returned with 422 at the path stations[<n>].alerts[<n>], followed
by .<field> where the message names one. Paths and messages use the API's
field names, as in risesBy, also for a line written in Terraform.
Repository fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | owner/name. Each repository once per line. |
image | string | none | The runner image every station of the line runs in, as in ghcr.io/acme/api-runner:main. Without it, the host's default image. Lower case except the tag, at most 255 characters, with an optional tag or @sha256: digest. The image is built FROM ghcr.io/usespotlightai/spotlight-runner. |
setup | string | none | A command run in the checkout, as the agent, before each run's agent starts, and before a change stack check wherever a layer changes a lockfile. One line of at most 500 characters. |
The repositories of one line name at most one distinct image. A read returns
repositories sorted by name.
Reaction fields
| Field | Type | Default | Description |
|---|---|---|---|
happening | string | required | What happens, source first, as in github.checks.failed. Matches ^[a-z0-9]+(\.[a-z0-9][a-z0-9_-]*)+$. |
from | string | none | The key of the station the task must stand at for the reaction to apply. Without it, the reaction applies at any station. |
to | string | required | The key of the station the task moves to. While a run holds the task, the move waits until the run ends, and is dropped if the run passes the task on. A move to the completed station instead stops the run and asks a person, or, if the task awaits an answer to the run's question, adds a note to it. See Moves. |
See GitHub events for the happenings GitHub raises. An integration raises its own.
Review policy fields
The review policy (reviews) decides which pull requests a review line takes.
See Review tasks for how a line
is chosen and the task it opens.
| Field | Type | Default | Description |
|---|---|---|---|
start | string | mentioned | mentioned: take a pull request when the mention handle appears in a comment or review on it. opened: also take every pull request as it opens. |
mention | string | @spotlight | The handle that asks for a review. Matches ^@[A-Za-z0-9][A-Za-z0-9_-]*(\[bot\])?$. Matched as a whole word, without regard to case. |
label | string | spotlight | The pull request label that asks for a review, whatever start is. 1 to 50 characters. Matched without regard to case. |
skip.spotlight | boolean | true | Skip pull requests Spotlight opened: a head branch under spotlight/, or a pull request a task opened. |
skip.drafts | boolean | true | Skip draft pull requests when they open. A mention or a label on a draft is still taken. |
skip.authors | array of strings | [] | GitHub logins whose pull requests are skipped, as in dependabot[bot]. Compared, stored and sorted in lower case, each once. |
Replay check fields
The replay check (replayCheck, in Terraform replay_check) sets the threshold
a planned change of one of the line's work stations is held to by
spotlight replay check.
A check reads it from the line as it is applied, never from the plan. A line
read through the team's GET /api/teams/{teamId}/lines carries it as
replayCheck, and a replay set's page shows it in the ⓘ of its "Checks" panel.
See Replay checks API.
| Field | Type | Default | Description |
|---|---|---|---|
mostRegressions | number | 0 | How many tasks a planned change may regress on and still pass. A whole number, 0 or more. In Terraform, most_regressions. |
ceiling | number | 10 | The most each replay a check starts may spend, above 0 and at most 1,000: the planned change's replay, and the baseline replay when the guarding set has none for the live config. See Ceiling. |
currency | string | USD | The three-letter currency ceiling is counted in. Stored in upper case. |
minGain | number | 0.05 | The smallest gain in success rate that counts as more than noise, as a share above 0 and at most 1. A gain over the baseline or the live config reads better only when its whole 95% interval is above 0 and it is at least this, and worse in the same way for a loss. It is read by the replays, searches and checks of the line's work stations; a line without replay_check reads them against 0.05. It does not decide whether a check passes. See Gains. In Terraform, min_gain. |
| Message | Path |
|---|---|
Replay check mostRegressions must be a whole number from 0. | replayCheck.mostRegressions |
Replay check ceiling must be above 0 and at most 1,000. | replayCheck.ceiling |
Replay check minGain must be a share above 0 and at most 1. | replayCheck.minGain |
Replay check currency must be a three-letter currency code, as in USD. | replayCheck.currency |
Source fields
The source (source) names the file that holds this spotlight_line
resource. A picked search
candidate is opened as a pull request against it.
| Field | Type | Default | Description |
|---|---|---|---|
repository | string | required | The GitHub repository, owner/name. Spotlight's GitHub App must be installed on it. |
path | string | required | The .tf file, from the repository's root, with forward slashes and no leading slash, as in teams/core/delivery.tf. 1 to 1,024 characters, with no empty, . or .. part. |
For Spotlight to edit the station, the file must hold exactly one
resource "spotlight_line" with the line's key written as a plain string,
and in it exactly one station block with the station's key written the same
way. An attribute a search may change that is read from a file must be read
as file("${path.module}/<path>"); a plain file("<path>") cannot be
followed.
See The pull request.
| Message | Path |
|---|---|
Config source repository must be a GitHub repository as owner/name, not <repository>. | source.repository |
Config source path must be 1 to 1,024 characters. | source.path |
Config source path must be a path from the repository root with forward slashes, as in teams/core/delivery.tf. | source.path |
Config source path must not contain empty, . or .. parts. | source.path |
Config source path must end in .tf. | source.path |
Placement
A run at a station goes to a runner that carries at least one of the station's
runners labels. A station's runners, when set, are the only labels that
apply. A station without runners uses the line's. With neither, any runner of
the workspace qualifies.
A label is one a runner pool or runner trust
gives the runners it admits. A pool's key is not a label. Each entry,
trimmed of surrounding whitespace, is shaped as a label is, lowercase words joined by dashes of at most 64
characters, and named once; any other entry is refused.
| Message | Cause |
|---|---|
Runners must each name a pool or trust label in lowercase words joined by dashes, at most 64 characters, such as vpc-internal. | An entry is not shaped as a label. |
Runners must name <label> only once. | An entry is given twice. |
Each is returned with 422 at the path runners, or
stations[<n>].runners for a station.
Where the line's repositories name an image, the runner must also run that
image. Where they name none, the runner must run its host's default image.
See Host settings for the
images a host serves.
Layout rules
A line's stations are refused unless they follow every rule below.
- A line must have at least one station.
- Each station key must be used once.
- A line must have exactly one
backlogstation, and it must be first. - A line must have exactly one
completedstation, and it must be last. - The
completedstation must not pass to any station. - A station must not pass to itself.
- A station must pass only to
workstations. - A reaction's
toandfrommust be stations on the line. - A line must not have two reactions with the same
happeningandfrom. - If any station has both
checkanddiffs: true, every repository must havesetup. - A line must name
reviewsonly if itsroleisreview. - Only a
workstation may have ajudgeoralerts. On a backlog the refusal readsLine <line> station <station> is a backlog and has no runs to judge., and on the completed stationLine <line> station <station> is the completed station and has no runs to judge.; for alerts both endand has no runs to alert on.<line>and<station>are keys. The refusal is returned atstations[<n>].judgeorstations[<n>].alerts.
Reserved station keys
| Key | A run that hands over to it |
|---|---|
review | hands its task over to a person |
done | finishes its task |
Update and delete
| Operation | Behaviour |
|---|---|
| update | Replaces the whole definition. A station whose key is kept keeps its identity and the tasks at it. Changing key is refused with 422. Dropping a station where any task stands is refused with 409 in-use, naming the stations and how many tasks stand at each. |
| delete | Refused with 409 in-use while any task stands on the line. |
A second line claiming a role another line of the team holds is refused with
422 on role.