Evidence#
Evidence is what makes a Flow gate mean something. A gate that passes on an agent's say-so is a checkbox; a gate that passes on inspectable, copied, typed evidence is a record you can trust later. This guide covers what counts as evidence, how gates declare expectations, and how trust artifacts are evaluated.
Evidence kinds#
flow attach-evidence <run-id> --gate <gate> --file <file> --kind <kind> accepts these built-in kinds:
| Kind | Use for |
|---|---|
command |
command output: test runs, lint results, build logs |
file |
any file artifact: a diff, a plan document, a screenshot |
ci |
CI job results |
trust.bundle |
Hachure TrustBundle evidence evaluated against typed gate expectations |
veritas-readiness |
repo/change readiness produced by a Veritas tool |
human-attestation |
a recorded human statement or sign-off |
trace-link |
a pointer to an external trace or observability record |
Unknown kinds are accepted as custom and stored with the originally requested kind, so adapters can introduce their own vocabulary without breaking the manifest.
Every attached file is copied into .kontourai/flow/runs/<run-id>/evidence/ and indexed in evidence/manifest.json (shape: schemas/gate-evidence.schema.json). The run directory stays self-contained: links don't rot, and later edits to the original file don't silently change the record. Runtime commands do not attach to .flow/runs/.
For a paused run whose current gate must be evaluated and optionally continued
as one concurrency-safe operation, use the library's continuePausedGate()
instead of composing attachment, evaluation, and lifecycle calls. It requires
the exact current run head and accepts evidence only for the persisted current
gate. A non-passing result is deliberately dry: Flow returns the evaluated
outcome but leaves its evidence directory, manifest, state, and reports
unchanged. Durable rejected, held, expired, cancelled, or incomplete review
records remain inspectable in their evidence-producing system; Flow does not
claim to have attached them. A passing result commits the copied evidence and
gate transition, then resumes only when the caller explicitly requests it with
a provider-neutral lifecycle authority record. Without that requested resume,
even a passing result is dry. A filesystem interruption after evidence staging
uses Flow's established local saveRun persistence model and may leave derived
artifacts stale; crash-transactional persistence is tracked separately in
Flow #171. See Library.
For command output, flow capture is an optional convenience over preparing a
file yourself:
flow capture dev-1847 --gate verify-gate --kind command -- npm test
It runs only at capture time, writes a canonical receipt described by
schemas/command-evidence.schema.json, and passes that file through the same
attach-evidence copy and hashing path. The receipt includes the exact argument
vector, exit code, stdout and stderr, duration, truncation details, and output
hash. Status comes from the exit code. Gate evaluation stays passive and never
runs commands.
For post-deploy smoke and contract checks, compose captured probe output with a
Hachure contract-claim bundle and a final live-verify gate.
The command receipt is useful retained evidence, but only the derived contract
claim satisfies the typed trust.bundle expectation.
Gate expectations#
Gates declare what they expect before work runs, as typed expects entries. Claim-backed expectations use kind: "trust.bundle" with a bundle_claim selector:
{
"id": "tests-passed",
"kind": "trust.bundle",
"required": true,
"description": "Test results are ready for verification.",
"bundle_claim": {
"claimType": "quality.tests",
"subjectType": "flow-step",
"subjectId": "builder.verify",
"accepted_statuses": ["verified"]
},
"explore_hint": "Run the suite and attach the trust report from CI."
}
| Field | Meaning |
|---|---|
id |
stable expectation id, referenced by evidence and reports |
required |
required expectations must be satisfied for the gate to pass |
description |
human-readable statement of what is expected |
bundle_claim.claimType |
the claim type evidence must carry (e.g. quality.tests) |
bundle_claim.subjectType |
optional Hachure subject type scope (e.g. flow-step) |
bundle_claim.subjectId |
optional subject id scope (e.g. builder.verify) |
bundle_claim.accepted_statuses |
optional list of event statuses that satisfy the gate |
explore_hint |
optional guidance shown when the evidence is missing |
bundle_claim.subjectType and bundle_claim.subjectId are open vocabularies so projects and kits can name their own process subjects — common subject type examples are flow-run, flow-step, work-item, change, pull-request, release, decision, and artifact.
Trust artifacts#
A trust.bundle evidence entry is backed by a copied Hachure TrustBundle JSON file:
flow attach-evidence dev-1847 --gate verify-gate \
--file ./trust-bundle.json --kind trust.bundle
Flow consumes a neutral bundle shape:
{
"schemaVersion": 5,
"source": "ci/main",
"claims": [
{
"id": "claim.quality.tests.verify",
"subjectType": "flow-step",
"subjectId": "builder.verify",
"facet": "quality.developer-evidence",
"claimType": "quality.tests",
"fieldOrBehavior": "testSuite",
"value": "all tests passed",
"createdAt": "2026-06-15T00:00:00.000Z",
"updatedAt": "2026-06-15T00:00:00.000Z"
}
],
"evidence": [
{
"id": "evidence.quality.tests.output",
"claimId": "claim.quality.tests.verify",
"evidenceType": "test_output",
"method": "validation",
"sourceRef": "ci:run-1847",
"excerptOrSummary": "All test suites passed.",
"observedAt": "2026-06-15T00:00:00.000Z",
"collectedBy": "ci/main"
}
],
"policies": [],
"events": [
{
"id": "event.quality.tests.verified",
"claimId": "claim.quality.tests.verify",
"status": "verified",
"actor": "ci/main",
"method": "npm test",
"evidenceIds": ["evidence.quality.tests.output"],
"createdAt": "2026-06-15T00:00:00.000Z",
"verifiedAt": "2026-06-15T00:00:00.000Z"
}
]
}
Flow matches bundle_claim selectors against bundle claims and derives the claim status from bundle events. The legacy surface.claim / claim projection was replaced by trust.bundle / bundle_claim during the trust-bundle migration.
This is a neutral contract: Flow does not import Surface services or Veritas-specific schema fields at runtime. Any tool that can write this JSON shape — CI, Veritas, a review bot, a script — is an evidence producer.
The run-output TrustBundle#
projectRunOutputBundle emits a TrustBundle for the run itself, so a parent
flow can consume a whole run as one referenceable claim.
Flow does not compute the run-level verdict. It emits one member claim per
stage plus an all-required claim group, and a consumer — Surface — folds them.
That delegation is only meaningful if the consumer receives the failures, so the
group's membership is every stage of the definition, never the passing
subset. Flow puts no producer-asserted status on a member claim either; the
ledger of events is what it emits, and the status is derived from it.
Each stage's event is drawn from its recorded gate outcomes:
| Stage | Event | Derived status |
|---|---|---|
| all gates passed on evidence | verified, citing the stage's evidence records |
verified |
| passed on an accepted exception | assumed, carrying a waiver |
assumed |
| a gate blocked or routed back | rejected |
rejected |
| not appraised yet | no event | unknown |
A stage the run has not reached carries no event at all, so it derives unknown
— "nothing to appraise" — rather than Flow asserting anything about it.
Accepted exceptions appear as waivers#
evaluateGate short-circuits on an accepted exception and returns pass with no
evidence. That stage did not pass on evidence, and the bundle says so: its event
is assumed, not verified, and the claim carries a
hachure waivers.md
waiver in metadata:
{
"metadata": {
"waiver": {
"reason": "CI runner unavailable; checked by hand on staging",
"approved_by": "ops@example.test",
"approved_at": "2026-08-02T00:00:00.000Z",
"exceptionId": "ex.1785651688817.1",
"gateId": "verify-gate"
}
}
}
Per that profile a waiver documents an accepted gap; it never upgrades a derived status. A consumer can therefore tell "this stage was verified" from "someone decided not to block on this stage, and here is who and why".
How claim evidence is evaluated#
A trust.bundle expectation is satisfied only when all of these checks pass:
- Type — a bundle claim has
claimTypematchingbundle_claim.claimType. - Subject — the claim matches
bundle_claim.subjectTypeandbundle_claim.subjectIdwhen the expectation configures them. - Status — the latest matching claim event status is in
accepted_statuses. - Freshness —
issued_at/expires_atare honored when present; expired artifacts are stale. - Producer trust — a configured producer pin matches validated
bundle.producerId, or every configured authority scope admits one active, embedded SurfaceauthorityTracewhoseauthorityRef, exact subject, claim/evidence linkage, and actor binding qualify at this evaluation's explicitnow. - Integrity — local integrity metadata (file hashes recorded at attach time) still matches when present.
When a run returns to a gate, Flow also scopes claim evidence to that current gate visit. A reattached bundle cannot reuse a claim from before the latest transition into the gate's step: the matching claim's createdAt, or evidence for that claim's observedAt, must be at or after the transition. Re-entry, attachment, claim, and observation timestamps must be valid RFC3339 date-times; missing, malformed, calendar-invalid, or leap-second values do not satisfy a revisited gate. Lower-case RFC3339 t and z separators and arbitrary fractional precision are accepted and retained verbatim in the producer payload. Flow rejects leap-second notation because its dependencies do not provide a chronology that can compare those instants without collapsing them onto an adjacent second. Prior attachments remain in the manifest for audit.
Unsatisfied artifacts are never hidden as generic missing evidence. Reports carry precise diagnostic reason codes:
| Code | Meaning |
|---|---|
stale |
the artifact expired or fails freshness checks |
rejected |
the claim status is not an accepted status |
untrusted_producer |
an otherwise matching claim has no configured producerId or active scoped embedded authority trace; diagnostics.claim_evaluation[].authority.code names failures such as no_trace, authority_ref_mismatch, subject_mismatch, scope_mismatch, actor_mismatch, not_yet_valid, expired, or revoked |
integrity_mismatch |
the copied artifact no longer matches its recorded integrity metadata |
subject_mismatch |
the claim subject does not match the expectation |
claim_not_current |
the matching claim predates the current gate visit, or has no valid current timestamp |
gate_reentry_pending |
a route-back affected the gate, but the run has not re-entered its step |
gate_reentry_timestamp_invalid |
the transition back into the gate has an invalid timestamp |
attachment_timestamp_invalid |
the evidence attachment timestamp is invalid |
attachment_not_current |
the evidence attachment predates the current gate visit |
Trusted producer mappings and gate overrides live in .flow/config.json — see Project Config. --authority-trace and attachment authorityTrace(s) were removed: opaque manifest metadata has zero trust weight. Put validated rich authority records in the trust.bundle itself; unknown or removed attachment options reject before evidence is copied.
Failed evidence and route metadata#
Evidence can be attached as failed, which is how gates learn that work needs to go back:
flow attach-evidence dev-1847 --gate verify-gate \
--file ./test-output.json --kind command \
--status failed --route-reason implementation_defect
Only route_reason affects routing — Flow uses it with the gate's on_route_back map and persisted transitions to select the target step (Gates & Route-Back has the full rules). Additional metadata is recorded for reports and learning without influencing routing:
flow attach-evidence dev-1847 --gate verify-gate \
--file ./test-output.json --kind command --status failed \
--route-reason implementation_defect \
--classifier-kind manual --classifier-source cli --classifier-confidence 0.75 \
--analytics-loop-key verify:implementation_defect \
--expectation-id tests-passed
For nested metadata, pass --route-metadata ./route-metadata.json with any of route_reason, expectation_ids, classifier, diagnostics, and analytics; explicit CLI flags override overlapping values from the file.
To recover after failed evidence routed work back, attach the replacement with --supersede <evidence-id>: the failed entry stays in the manifest for audit but no longer drives the gate.
The CLI attaches evidence from files; flow capture first creates such a file
for a declared command. Richer adapters — CI jobs, agent harness hooks, release
tooling — can write the same manifest shape directly through the library API.