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:

  1. Type — a bundle claim has claimType matching bundle_claim.claimType.
  2. Subject — the claim matches bundle_claim.subjectType and bundle_claim.subjectId when the expectation configures them.
  3. Status — the latest matching claim event status is in accepted_statuses.
  4. Freshnessissued_at / expires_at are honored when present; expired artifacts are stale.
  5. Producer trust — a configured producer pin matches validated bundle.producerId, or every configured authority scope admits one active, embedded Surface authorityTrace whose authorityRef, exact subject, claim/evidence linkage, and actor binding qualify at this evaluation's explicit now.
  6. 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.