Postil

Envelope schema

The envelope is the contract between the CLI and everything else: the hosted worker, the Action, your own tooling. It is versioned and frozen; the control plane stores it verbatim.

Why the envelope exists alongside SARIF

The envelope is the single JSON object postil review emits: findings and the gate verdict, counts, confidence distribution, token usage, and provenance (base/head/since SHAs, model used) all travel together as one versioned unit. Consumers (the hosted worker, the dashboard, postil plan) read one object and get the whole picture of a review, not just its findings.

Postil also emits SARIF 2.1.0 (--sarif <path>) for interop with code-scanning viewers that expect it: GitHub code scanning, GitLab SAST, and other SARIF-aware tooling. SARIF is a results format: it has no structured gate concept (Postil tucks the verdict into a SARIF properties bag, but that is a nonstandard extension no consumer can rely on), no confidence buckets, no token usage. It answers "what did the reviewer find," not "did this PR pass." That gap is why the envelope exists as its own format rather than Postil standardizing on SARIF alone.

The schema below is version 1, frozen. Changes that do not break existing consumers (new optional fields) ship in place under version: 1; consumers should already tolerate unknown fields. Any breaking change ships as a new version: 2 alongside version 1, never in place of it. See stability, below.

Schema (version 1)

{
  "version": 1,
  "summary": "1-3 sentence merge-relevant summary, empty string when silent",
  "silent": true,
  "findings": [
    { "path": "src/x.ts", "line": 42, "endLine": 45,
      "severity": "info|warn|error",
      "kind": "risk|humanEscalation|guardrail|uncertainty|contentPolicy",
      "confidence": 0.85, "title": "short", "body": "markdown" }
  ],
  "resolved": [ /* same shape as findings; no longer apply as of this head */ ],
  "counts": { "info": 0, "warn": 0, "error": 0, "suppressed": 0, "ungrounded": 0 },
  "confidenceBuckets": [0, 0, 0, 0, 0],
  "gate": { "failOn": "error", "failing": false },
  "modelUsed": "deepseek/deepseek-v4-pro",
  "usage": { "promptTokens": 0, "completionTokens": 0 },
  "durationMs": 0,
  "baseSha": "...", "headSha": "...", "sinceSha": null
}

Field notes

FieldMeaning
silentTrue when the review produced no shippable finding. The summary is the empty string and no comment is posted anywhere.
findings[].kindrisk (a concrete defect or hazard), humanEscalation (a consequential decision that needs an accountable human), guardrail (the change violates a rule stated in .postil/guardrails.md; the finding quotes the rule it breaks), uncertainty (the model flags its own doubt), contentPolicy (default-on review of prose in the diff; see content policy).
findings[].confidence0 to 1. Findings below minConfidence are suppressed and counted in counts.suppressed.
resolvedOnly populated on an incremental review (--since-sha with a --baseline envelope from the previous review of the same PR head lineage): findings from that baseline which no longer apply at the new head. This is a diff against the prior envelope, not conversation memory. Postil does not carry chat history or retain state between PRs. Powers "N resolved, M open" on incremental re-review.
confidenceBucketsFive counts over [0-0.2, 0.2-0.4, 0.4-0.6, 0.6-0.8, 0.8-1.0]. Aggregated across reviews, this is the dashboard's confidence distribution.
gateThe configured fail-on severity and whether this review fails the gate. Mirrors the exit code: failing: true means exit 1.
counts.ungroundedFindings the model reported that did not cite a changed line and were dropped. A nonzero value is a model-quality signal; a run where every finding was ungrounded fails closed. Optional within v1 (absent means 0).
durationMsWall-clock duration of the review engine run in milliseconds. Optional within v1 (absent means 0 from older CLIs).
sinceShaThe previously reviewed head when this was an incremental review; null on a full review.

Grounding guarantee

Every finding must cite a (path, line) present in the reviewed diff. Ungrounded model output is dropped; an entirely invalid response becomes a synthetic error finding at .postil/model-output:1 after one JSON-repair retry, and the gate fails. There is no code path in which malformed model output produces a passing review.

Stability

Consumers should accept unknown additional fields and reject unknown version values. Any breaking change ships as version: 2 alongside, never in place.