1 and 2 is the point. A build that fails because your provider shipped a breaking change needs a different response from a build that fails because a token expired. A CI gate that collapses the two teaches people to ignore both.
2 also covers results pikopod cannot stand behind. pikopod fix exits 2 when its impact scan finds nothing, because a literal scan cannot distinguish “this field is unused” from “this codebase spells it differently”. See fix.
Readers versus gates
pikopod incidents is a reader, not a gate. It exits 0 whether or not it found anything, and 2 only when it cannot read the event log. Nothing about “I found incidents” is an exit-code signal. Script against --format json instead, where total_matching and truncated are always present so a shortened list can never be mistaken for a clean one.
Reproduce has no exit 1
pikopod scenario reproduce exits 0 when it writes a pack and 2 when it refuses: when the incident’s recording has aged out of retention, or when the pack ran and the failure did not recur, which means pikopod cannot stand behind it as a reproduction. It never fabricates a request to avoid refusing.
The contrast with its sibling is deliberate. pikopod scenario from-drift exits 1 on a failed replay because its pack pins the old contract, so failing confirms the drift. reproduce is inverted: its pack recreates a failure, so failing means the recreation did not work. One is a finding; the other is an unverifiable result.
Where UNVERIFIABLE lands
UNVERIFIABLE is a real outcome, distinct from a pass, but it is not a fourth exit code. It maps onto the existing ones by what it means:
conformancecounts checks whose evidence the sanitizer removed and reports them as neither pass nor violation, leaving the exit code alone.fixandscenario reproduceexit2, because a result pikopod cannot stand behind is a tool outcome, never a finding.- Over MCP, every tool returns the verdict explicitly. See MCP tools.
Schema versions
schema/drift-event.schema.json is the other stable surface. Its schema_version field is the protocol number, pinned in the schema and in the producer at the same time, and a test fails the build if they diverge.
The rule for what moves the version:
- New
kindvalues do not bump it. Treat a kind you do not recognise as informational. - New fields do not bump it. Ignore the ones you do not know.
- Structural changes do: a field removed, a type changed, or an existing field’s meaning changed.
schema_version == "1" for equality, switch to a minimum check. Incidents carry the concrete status in after ("503") and, like every event, a path template in endpoint, never a concrete path. See Drift event schema.