The three verdicts
There is no fourth verdict for errors. A failure inside Selo becomes INCONCLUSIVE with the reason
INTERNAL_ERROR.
The reducer
The rule runs in this order and stops at the first match:1
Any proven blocking FAIL gives FAIL
If any blocking check has status
FAIL and at least one evidence id, the verdict is FAIL. This wins over everything else, including a reason recorded during the run.2
A recorded reason gives INCONCLUSIVE
Otherwise, if the run recorded an inconclusive reason (a timeout, an unresolved payment, a lapsed authorization and so on), the verdict is INCONCLUSIVE with that reason.
3
Missing or unproven results give INCONCLUSIVE
Otherwise, if any check is missing, or any blocking check is
INCONCLUSIVE or has no evidence, the verdict is INCONCLUSIVE with EVIDENCE_INCOMPLETE.4
Everything else is PASS
Otherwise the verdict is PASS. Blocking
WARN results with evidence do not prevent it; they are listed in warnings.packages/core/src/verdict.ts:
packages/core/src/verdict.ts
indexByIdrejects a result set that contains the same check twice.findProvenFailuresreturns the blocking checks with statusFAILand a non-emptyevidencelist.hasIncompleteEvidenceis true when any of the five checks is missing, or a blocking check isINCONCLUSIVEor has an emptyevidencelist.collectWarningsreturns"<check id>: <summary>"for everyWARN, and for any non-blockingFAILorINCONCLUSIVE.
Decision table
Every check Selo runs today is blocking, so in practice every result counts.
How the recorded reason is chosen
While a job runs, a step that cannot reach a definite answer records an inconclusive reason. The run keeps the first reason it records, with one exception:PAYMENT_UNRESOLVED replaces any earlier reason, because a payment with an unknown outcome is the most important thing to report. When the job’s 60-second wall clock has expired by the time the run concludes and no reason was recorded, the reason is TARGET_TIMEOUT.
Inconclusive reasons
Why unknown is never PASS
A release gate is only useful if a PASS means something. Selo treats every gap as a reason not to pass:- A check that did not run is not a pass. A missing result makes the verdict INCONCLUSIVE.
- A pass without evidence is not a pass. Every blocking result must point at evidence that Selo recorded during the run.
- A failure without evidence is not a failure either. Selo blocks on it, but reports INCONCLUSIVE rather than claiming a defect it cannot show.
- Infrastructure trouble is not your fault. A timeout, an unreachable catalog or an unresolved payment yields INCONCLUSIVE, never FAIL, because Selo cannot tell whether your endpoint or the network was at fault.
Internal errors
If something unexpected goes wrong inside Selo during a run, Selo does not drop the job:- A payment that was open is held UNRESOLVED, so its amount still counts against the budget.
- Checks that had started report
INTERNAL_ERROR; checks that had not started reportNOT_RUN. - The verdict is reduced with the reason
INTERNAL_ERROR. A FAIL that was already proven still stands. - The report is written as usual.
From verdict to gate
Theselo CLI does not take the API’s verdict on trust. When selo test receives a report, it decides the gate in this order:
1
Re-derive the verdict
The CLI runs the same reducer on the report’s own checks and
inconclusiveReason. If the result differs from the reported verdict, or the checks cannot be read (for example a check appears twice), it blocks with exit 2.2
Block a FAIL
A FAIL blocks with exit
1, naming every blocking check that failed.3
Block an INCONCLUSIVE
An INCONCLUSIVE blocks with exit
2, naming the reason.4
Require a fully evidenced PASS
A PASS blocks with exit
2 unless the report has a result for all five checks, an inbound settlement transaction id, at least one downstream payment transaction id and a report link.5
Allow
Only then is the gate
ALLOW, with exit 0.selo verify checks a saved attestation offline and exits with the attested verdict (0 for PASS, 1 for FAIL, 2 for INCONCLUSIVE), or 2 when the attestation cannot be verified. See the CLI overview for every case that leads to each exit code, and Gate deploys in GitHub Actions to wire the exit code into a workflow.
Two verdicts from Algorand Mainnet
On 26 October 2026, the demo merchant athttps://demo.useselo.xyz/v1/orders was tested twice at the same URL.
- broken-v1: FAIL
- fixed-v2: PASS
The proven FAIL on
response_contract wins over the inconclusive discovery check, so the verdict is FAIL with inconclusiveReason: null. The gate answered BLOCK, reason FAIL: response_contract, exit 1.Related
The five release checks
Every code each check can report.
Signed attestations
The verdict, signed with Ed25519 and verifiable offline.
CLI overview
Install, environment and the full exit code reference.
Recover an interrupted run
What to do when the gate exits
2 because a payment is unconfirmed.