JSON output schema

opensip fit --json, opensip sim --json, opensip graph --json, opensip yagni --json, installed adapter runs such as opensip gitleaks --json, opensip graph lookup --json, opensip audit --json, opensip suite run <name> --json, and opensip config validate|schema|migrate --json all emit one CommandOutcome wrapper on stdout (ADR-0024, ADR-0065). Run commands carry a SignalEnvelope under .envelope; list/report/config/suite commands carry their result under .data; failures carry structured errors. The top-level exitCode always equals the process exit code (ADR-0132). This is the contract surface for CI integrations.

{
  "kind": "fit.run",          // '<tool>.run' (envelope) | '<result.type>' (data) | 'bootstrap.error'
  "status": "ok",             // 'ok' | 'error' | 'partial'
  "exitCode": 0,
  "envelope": { /* the SignalEnvelope, unchanged — see below */ },
  "diagnostics": { /* RunDiagnostics — lifecycle events, JSON-emittable */ }
}

CommandOutcome<T> lives in packages/contracts/src/command-outcome.ts. The host ASSEMBLES it from each handler's unchanged domain return and serializes it through one renderer; no tool chooses its own error JSON or success carrier. A list/report command sets .data (a CommandResult) instead of .envelope; a failure — including a pre-handler bootstrap failure such as no project found — sets status:"error" + .errors[] ({ message, suggestion?, code? }) with neither payload. The current outer wrapper contract is COMMAND_OUTCOME_CONTRACT_VERSION = 1.

The inner SignalEnvelope is documented below. It lives in packages/contracts/src/signal-envelope.ts (the envelope) and packages/core/src/types/signal.ts (the Signal). Per ADR-0011, Signal is the single output currency of every tool: a fit check, a graph rule, and a sim scenario are all units that produce signals, and every run yields one envelope.

Stability: the schemaVersion: 2 field on the envelope is the output-contract version (SIGNAL_ENVELOPE_SCHEMA_VERSION = 2, independent of any package version). Adding optional fields is a minor change; removing or changing types is a major change. The compatibility matrix in .config/compatibility-matrix.json checks these constants against public fixtures in CI.

Suite Run Results

opensip audit --json and opensip suite run <name> --json emit a CommandOutcome whose .data is a

SuiteRunResult (ADR-0093,

ADR-0100,

ADR-0131,

ADR-0110,

ADR-0143, and

ADR-0155).

Suite steps dispatch through the same command-action pipeline as normal mounted

commands, and the suite exit code remains the numeric worst step exit code. The

aggregate, per-step verdict, and per-step errorCode fields are additive; older

fields keep their names and types.

{
  "kind": "suite-run",
  "status": "ok",
  "exitCode": 1,
  "data": {
    "type": "suite-run",
    "suite": "security",
    "runId": "RUN_4f8b7c",
    "suiteRunId": "suite_3c4e8a1b9d21",
    "exitCode": 1,
    "durationMs": 1842,
    "scope": {
      "mode": "changed",
      "source": "default",
      "changedFiles": 14
    },
    "aggregate": {
      "steps": 3,
      "passed": 1,
      "failed": 1,
      "faulted": 1,
      "errors": 2,
      "warnings": 4
    },
    "steps": [
      {
        "tool": "fitness",
        "stableId": "00000000-0000-4000-8000-000000000111",
        "command": "fit",
        "exitCode": 0,
        "durationMs": 612,
        "verdict": {
          "passed": true,
          "errors": 0,
          "warnings": 4,
          "findings": 4
        }
      },
      {
        "tool": "graph",
        "stableId": "00000000-0000-4000-8000-000000000222",
        "command": "graph",
        "exitCode": 1,
        "durationMs": 740,
        "verdict": {
          "passed": false,
          "errors": 2,
          "warnings": 0,
          "findings": 2
        }
      },
      {
        "tool": "sim",
        "stableId": "00000000-0000-4000-8000-000000000333",
        "command": "sim",
        "exitCode": 1,
        "durationMs": 490,
        "error": "scenario faulted",
        "errorCode": "scenario-fault"
      }
    ],
    "reviewBrief": {
      "version": 1,
      "suite": "security",
      "suiteRunId": "suite_3c4e8a1b9d21",
      "verdict": "fail",
      "changedFiles": null,
      "topRisks": [
        {
          "source": "graph",
          "ruleId": "graph:cycle",
          "message": "Cycle crosses package boundary",
          "severity": "high",
          "file": "src/a.ts",
          "line": 42,
          "column": 0,
          "isNew": false,
          "signalRef": {
            "tool": "graph",
            "suiteRunId": "suite_3c4e8a1b9d21",
            "stepIndex": 1,
            "runId": "GRAPH_abc",
            "fingerprint": "graph:cycle|src/a.ts|42|0",
            "signalIndex": 0
          },
          "entities": [
            {
              "kind": "symbol",
              "id": "src/a.ts#loadUser",
              "confidence": "high",
              "label": "src/a.ts#loadUser",
              "file": "src/a.ts",
              "line": 42,
              "source": "metadata.qualifiedName"
            }
          ],
          "correlationKeys": [
            {
              "kind": "symbol",
              "value": "src/a.ts#loadUser",
              "confidence": "high"
            }
          ]
        }
      ],
      "newFindings": [],
      "correlatedRisks": [
        {
          "id": "corr-symbol-src-a-ts-loaduser",
          "title": "Related findings for symbol src/a.ts#loadUser",
          "severity": "high",
          "isNew": false,
          "primary": {
            "source": "graph",
            "ruleId": "graph:cycle",
            "file": "src/a.ts",
            "line": 42,
            "column": 0,
            "signalRef": {
              "tool": "graph",
              "suiteRunId": "suite_3c4e8a1b9d21",
              "stepIndex": 1,
              "runId": "GRAPH_abc",
              "fingerprint": "graph:cycle|src/a.ts|42|0",
              "signalIndex": 0
            }
          },
          "members": [
            {
              "source": "graph",
              "ruleId": "graph:cycle",
              "file": "src/a.ts",
              "line": 42,
              "column": 0,
              "signalRef": {
                "tool": "graph",
                "suiteRunId": "suite_3c4e8a1b9d21",
                "stepIndex": 1,
                "runId": "GRAPH_abc",
                "fingerprint": "graph:cycle|src/a.ts|42|0",
                "signalIndex": 0
              }
            },
            {
              "source": "fit",
              "ruleId": "typescript:no-unsafe-async",
              "file": "src/a.ts",
              "line": 42,
              "column": 0,
              "signalRef": {
                "tool": "fit",
                "suiteRunId": "suite_3c4e8a1b9d21",
                "stepIndex": 0,
                "runId": "FIT_def",
                "fingerprint": "typescript:no-unsafe-async|src/a.ts|42|0",
                "signalIndex": 0
              }
            }
          ],
          "entities": [
            {
              "kind": "symbol",
              "id": "src/a.ts#loadUser",
              "confidence": "high",
              "label": "src/a.ts#loadUser",
              "file": "src/a.ts",
              "line": 42,
              "source": "metadata.qualifiedName"
            }
          ],
          "reasons": [
            {
              "kind": "same-symbol",
              "key": {
                "kind": "symbol",
                "value": "src/a.ts#loadUser",
                "confidence": "high"
              },
              "confidence": "high",
              "message": "Risks share symbol correlation key 'src/a.ts#loadUser'."
            }
          ]
        }
      ],
      "baselineDelta": {
        "available": false,
        "added": 0,
        "removed": 0,
        "unchanged": 0
      },
      "degraded": [
        {
          "source": "sim",
          "code": "missing-envelope",
          "stepIndex": 2,
          "reason": "Suite step 'sim' did not emit a SignalEnvelope."
        }
      ],
      "recommendedActions": [
        {
          "priority": "high",
          "source": "suite",
          "message": "Review and fix the error-severity top risks before merging."
        }
      ]
    }
  }
}

SuiteRunResult

| Field | Type | Required | Description |

|---|---|---|---|

| type | "suite-run" | yes | Discriminant for suite run command results. |

| suite | string | yes | Suite name from suites.<name>. |

| runId | string | no | Authoritative persisted parent StoredRun.id. Absent when Run-ledger persistence was unavailable; do not substitute suiteRunId or a latest-row lookup. |

| suiteRunId | string | yes | Host-generated id shared by the step sessions produced in the run. |

| exitCode | number | yes | Worst step exit code. |

| durationMs | number | yes | Host-measured suite duration. |

| scope | SuiteRunScope | no | Host-resolved suite scope. Present on current suite runs; absent on older stored payloads means "not recorded". |

| aggregate | object | no | Additive roll-up over step summaries. Present on current CLI output; optional for compatibility. |

| steps | SuiteStepSummary[] | yes | One summary per configured step, in execution order. |

| reviewBrief | ReviewBrief | no | Host-owned v1 review aggregate. Present on current suite runs; optional for compatibility. |

| contextManifest | TaskContextManifest | no | Present only for the built-in agent-context evidence suite. It is mutually exclusive with finding-oriented reviewBrief. |

SuiteRunScope

| Field | Type | Required | Description |

|---|---|---|---|

| mode | "changed" \| "full" | yes | Whether the suite ran changed-scope or whole-repo scope. |

| source | "default" \| "explicit" \| "fallback" | yes | Why the mode applies: built-in default, user flag/selector, or suite-level fallback. |

| ref | string | no | Git ref base from --since. |

| changedFiles | number | no | Host-resolved changed-file count for display and review brief context. The file list is not persisted here. |

| notice | string | no | Human-readable fallback notice, for example when the built-in audit default runs outside a git work tree. |

SuiteAggregate

| Field | Type | Description |

|---|---|---|

| steps | number | Total configured steps that ran. |

| passed | number | Steps with a passing emitted verdict and successful step exit. |

| failed | number | Non-faulted steps with a failing emitted verdict or non-zero step exit. |

| faulted | number | Steps that threw or faulted before completing normally. |

| errors | number | Sum of steps[].verdict.errors across envelope-emitting steps. |

| warnings | number | Sum of steps[].verdict.warnings across envelope-emitting steps. |

SuiteStepSummary

| Field | Type | Required | Description |

|---|---|---|---|

| tool | string | yes | Tool display name. |

| stableId | string | yes | Stable tool UUID used by suite config. |

| command | string | yes | Command run for this step. |

| exitCode | number | yes | Captured step exit code. |

| durationMs | number | yes | Host-measured step duration. |

| error | string | no | Error or reported-failure message when the step did not produce a normal verdict. Truncated to 1000 characters. |

| errorCode | string | no | Machine-readable ToolError or reportFailure code for the step failure, when available. |

| verdict | object | no | Counts-only projection of the step's last emitted SignalEnvelope. Absent means the step emitted no envelope. |

| verification | ImpactTrust | no | Authoritative scoped-verification projection copied from the step envelope. Inspect before claiming changed/impacted coverage is complete. |

| kind | "verdict" \| "evidence" | no | Additive step discriminator. Absent on older rows means verdict-style legacy behavior. |

| readiness | "ready" \| "degraded" \| "unavailable" | no | Evidence-step readiness. Never a finding verdict. |

steps[].verdict contains only passed, errors, warnings, and findings

(SignalEnvelope.signals.length). It intentionally excludes signal messages,

file paths, symbols, match snippets, and raw scanner output.

TaskContextManifest

opensip suite run agent-context --files <path> --json adds a numeric-versioned

manifest to the parent suite result and persisted Run:

{
  "schemaVersion": 1,
  "suite": "agent-context",
  "runId": "RUN_...",
  "createdAt": "2026-07-12T12:00:00.000Z",
  "projectIdentity": "sha256:...",
  "readiness": "ready",
  "sourceStart": { "configIdentity": "sha256:...", "status": "captured", "reasonCodes": [] },
  "sourceEnd": { "configIdentity": "sha256:...", "status": "captured", "reasonCodes": [] },
  "fileScope": { "mode": "explicit", "fileCount": 1, "filesIdentity": "sha256:..." },
  "graphIdentity": "g1:...",
  "inventoryIdentity": "i1:...",
  "planes": [
    {
      "kind": "inventory",
      "required": true,
      "status": "rebuilt",
      "producer": { "toolId": "...", "command": "graph-context-inventory", "version": "0.6.0" },
      "pointer": { "owner": "graph", "kind": "inventory", "id": "i1:...", "schemaVersion": 1 },
      "step": { "runId": "RUN_...", "stepId": "STEP_...", "logicalStepKey": "0:...", "ordinal": 0, "attempt": 1 },
      "freshness": { "status": "current", "reasonCodes": [] },
      "coverage": { "status": "complete", "reasonCodes": [], "observed": 42, "total": 42 },
      "caps": { "status": "not-hit", "reasonCodes": [] },
      "reasonCodes": [],
      "followUpReads": ["get_file_context"]
    }
  ],
  "reasonCodes": [],
  "nextActions": ["get_context_status", "impact_files", "select_tests"]
}

The manifest is capped at 16 planes and 64 KiB. fileScope persists only a

mode, count, and SHA-256 set identity—never raw task paths. projectIdentity

is a SHA-256 identity of the canonical project root, never the path. Absent means an

ordinary or pre-feature Run. Plane pointers name exact immutable graph-owned

evidence; an evicted pointer is reported as missing and a retained-but-replaced

inventory is reported as stale, without exposing or substituting the newer

identity. The parent Run can still be replayed in either case. Context evidence

does not appear in reviewBrief, fingerprints, baselines, SARIF, or generic

Tool sessions. run_steps.evidence is explicitly discriminated as

signal-envelope or evidence-snapshots and stores only bounded projections,

never snapshot payloads or source text.

For get_context_status, do not trust fileScope.status: matched alone. Require

response status: available, manifest.readiness: ready, and every required

plane to be current, complete, uncapped, and backed by an exact pointer whose

replay status is available.

ReviewBrief

The review brief is built by the CLI host after all suite steps complete. It is

a bounded projection over captured SignalEnvelopes, not a replacement for raw

per-tool output.

| Field | Type | Required | Description |

|---|---|---|---|

| version | 1 | yes | Review-brief contract version. |

| suite | string | yes | Suite name. |

| suiteRunId | string | yes | Host-generated suite id, repeated in every signalRef. |

| verdict | "pass" \| "warn" \| "fail" | yes | fail for error-severity risks, warn for warning-only risks or degraded evidence, pass when clean. |

| changedFiles | number \| null | yes | Changed-file count when trustworthy; null when unavailable. |

| topRisks | ReviewBriefRisk[] | yes | Deterministically ranked current risks, capped by the host. |

| newFindings | ReviewBriefRisk[] | yes | Risks explicitly marked new by baseline evidence. Empty when baseline state is unavailable. Can diverge from topRisks when older high-severity risks fill the top cap — the Change Impact report lists both sections so net-new findings are not lost. |

| correlatedRisks | ReviewBriefCorrelationGroup[] | no | Bounded, explainable groups of related risks. Additive and absent when no group has at least two risks. |

| baselineDelta | object | yes | { available, added, removed, unchanged }; available:false means the suite did not capture compare evidence. |

| degraded | object[] | yes | Evidence-quality notes such as missing envelopes, step faults, missing fingerprints, partial impact verification, or failing verdicts without signals. |

| recommendedActions | object[] | yes | Short host-generated next steps for agents and CI annotations. |

Each topRisks[] item carries source, ruleId, message, severity, file,

optional line/column, isNew, optional repair, optional blastRadius,

optional entities, optional correlationKeys, and signalRef. signalRef

preserves provenance back to the original evidence: tool, suiteRunId,

stepIndex, optional runId, optional fingerprint, and signalIndex.

entities[] and correlationKeys[] are deterministic projections from the

source Signal, not separate evidence. They are capped and derived only from

trusted scalar signal fields plus allowlisted metadata such as qualifiedName,

bodyHash, sccId, package, packages, and relatedPackageCycle.

ReviewBriefCorrelationGroup

Correlation groups are additive. They do not deduplicate or suppress entries in

topRisks[], newFindings[], or raw stored envelopes. Always follow

members[].signalRef back to source evidence before editing code.

| Field | Type | Required | Description |

|---|---|---|---|

| id | string | yes | Stable group id derived from the correlation key. |

| title | string | yes | Short display title for the shared entity or key. |

| severity | "critical" \| "high" \| "medium" \| "low" | yes | Highest-ranked member severity. |

| isNew | boolean | yes | True when any member is new according to baseline metadata. |

| primary | ReviewBriefRiskRef | yes | Highest-ranked member used as the lead finding. |

| members | ReviewBriefRiskRef[] | yes | Bounded member list with source, rule, file, location, and signalRef. |

| entities | ReviewBriefEntityRef[] | yes | Bounded union of projected entities across members. |

| reasons | ReviewBriefCorrelationReason[] | yes | Explainable reasons such as same-symbol or same-graph-node. |

| blastRadius | object | no | Strongest member blast-radius projection when present. |

ReviewBriefEntityRef

| Field | Type | Required | Description |

|---|---|---|---|

| kind | "fingerprint" \| "file" \| "file-range" \| "symbol" \| "graph-node" \| "package" | yes | Entity namespace. |

| id | string | yes | Deterministic entity id. |

| confidence | "low" \| "medium" \| "high" | yes | Strength of the projection. |

| label | string | no | Display label. |

| file | string | no | File associated with the entity. |

| line | number | no | Line associated with the entity. |

| source | string | no | Signal field or metadata key that produced the entity. |

ReviewBriefCorrelationReason

| Field | Type | Required | Description |

|---|---|---|---|

| kind | string | yes | Reason enum, currently same-fingerprint, same-graph-node, same-symbol, same-rule-location, same-file-range, same-package, or same-file. |

| key | ReviewBriefCorrelationKey | yes | Correlation key shared by the group members. |

| confidence | "low" \| "medium" \| "high" | yes | Strength of the reason. |

| message | string | yes | Human-readable reason summary. |

There is no suite-level review-brief SARIF output in v1. Use each source tool's

existing SARIF path when SARIF is required.

The SignalEnvelope

{
  "schemaVersion": 2,
  "tool": "fit",
  "recipe": "default",
  "runId": "run_9bb6ef4d07c0",
  "createdAt": "2026-05-15T10:30:00.000Z",
  "verdict": {
    "score": 87,
    "passed": false,
    "summary": {
      "total": 80,
      "passed": 78,
      "failed": 2,
      "errors": 5,
      "warnings": 12
    }
  },
  "units": [ /* UnitResult[] */ ],
  "signals": [ /* Signal[] */ ],
  "baselineIdentity": {
    "fingerprintStrategyId": "opensip.default.rule-file-line-col",
    "fingerprintStrategyVersion": 1
  },
  "declaredInputs": {
    "cliVersion": "0.1.16",
    "nodeVersion": "24.16.0",
    "packageManager": "pnpm@11.5.1",
    "platform": "darwin/arm64",
    "tool": "fit",
    "engineVersion": "0.1.16",
    "baselineIdentity": {
      "fingerprintStrategyId": "opensip.default.rule-file-line-col",
      "fingerprintStrategyVersion": 1
    }
  }
}

Top-level fields

| Field | Type | Required | Description |

|---|---|---|---|

| schemaVersion | 2 | yes | Output-contract version. Bumped on breaking changes; independent of package version. |

| tool | "fit" \| "sim" \| "graph" | yes | The tool that produced this envelope. |

| recipe | string | no | Recipe name if --recipe was used (or the default recipe's name). |

| runId | string | yes | Stable identifier for this run (also used as the cloud-egress / --report-to idempotency root). |

| createdAt | string (ISO 8601) | yes | When the run was assembled. |

| verdict | RunVerdict | yes | Run-level pass/fail header. See below. |

| units | UnitResult[] | yes | Per-unit ran/errored/timing facts. May be []. |

| signals | Signal[] | yes | The flat list of findings the run produced. May be []. |

| baselineIdentity | { fingerprintStrategyId: string; fingerprintStrategyVersion: number } | yes | Fingerprint strategy that stamped signal fingerprints; persisted on --gate-save and compared on --gate-compare (ADR-0075). |

| declaredInputs | DeclaredInputs | no | Host-stamped verdict provenance: CLI/Node/package-manager/platform/tool/engine/baseline identity. Optional for additive compatibility; absence means an older/no-manifest producer (ADR-0097). |

| resolutionMode | "exact" \| "fast" | no | graph-only edge-fidelity marker. Absent for fit / sim. |

DeclaredInputs

declaredInputs is added by the host before JSON outcome rendering, delivery,

SARIF reporting, and dashboard/report composition. It is an allowlist, not an

environment dump.

{
  "cliVersion": "0.1.16",
  "nodeVersion": "24.16.0",
  "packageManager": "pnpm@11.5.1",
  "platform": "darwin/arm64",
  "tool": "fit",
  "engineVersion": "0.1.16",
  "baselineIdentity": {
    "fingerprintStrategyId": "opensip.default.rule-file-line-col",
    "fingerprintStrategyVersion": 1
  }
}

| Field | Type | Required | Description |

|---|---|---|---|

| cliVersion | string | yes | Installed opensip-cli package version that emitted the envelope. |

| nodeVersion | string | yes | process.versions.node from the host process. |

| packageManager | string | no | Nearest package.json#packageManager, falling back to the first package token in npm_config_user_agent. |

| platform | string | yes | Host platform/architecture, e.g. darwin/arm64. |

| tool | string | yes | Tool id for the run envelope. |

| engineVersion | string | no | Tool/engine package version when available from the active tool manifest. |

| baselineIdentity | { fingerprintStrategyId: string; fingerprintStrategyVersion: number } | no | Baseline fingerprint identity copied from the envelope. |

The manifest intentionally excludes absolute paths, full environment variables,

credentials, and config payloads.

RunVerdict

{
  "score": 87,
  "passed": false,
  "summary": {
    "total": 80,
    "passed": 78,
    "failed": 2,
    "errors": 5,
    "warnings": 12
  }
}

| Field | Type | Required | Description |

|---|---|---|---|

| score | number (0..100) | yes | Pass percentage. Deterministic given the same set of units/signals. |

| passed | boolean | yes | trueno critical/high signals (the "error rung"). This is the CI gate: --json \| jq -e '.envelope.verdict.passed'. |

| summary.total | number | yes | Total units that ran. |

| summary.passed | number | yes | Units that passed (emitted no critical/high signals). |

| summary.failed | number | yes | Units that failed. |

| summary.errors | number | yes | Total critical + high signals across the run. |

| summary.warnings | number | yes | Total medium + low signals across the run. |

UnitResult

A unit is the neutral umbrella over a fit check, a graph rule, and a sim scenario. units[] carries only what a flat Signal[] cannot express — that a unit ran, whether it errored, and timing.

{
  "slug": "no-console-log",
  "passed": false,
  "violationCount": 2,
  "durationMs": 87,
  "filesValidated": 450,
  "itemType": "files",
  "ignoredCount": 1
}

| Field | Type | Required | Description |

|---|---|---|---|

| slug | string | yes | The unit's identifier (check slug / graph rule slug / scenario id). |

| passed | boolean | yes | true ⇔ the unit emitted no critical/high signals. |

| violationCount | number | no | Number of signals the unit produced. |

| durationMs | number | yes | Time the unit took to execute. |

| error | string | no | Error message if the unit errored (e.g. an agent provider unreachable for a sim scenario). A unit can have run-and-errored with zero signals. |

| filesValidated | number | no | fitness-only. Files the check scanned this run (the "Validated" column). A check that scanned 450 files and emitted 0 signals still reports filesValidated: 450. Graph rules / sim scenarios don't scan files and omit it. |

| itemType | string | no | fitness-only. Names the scanned noun ("files" / "packages" / …) for the column label that pairs with filesValidated. |

| ignoredCount | number | no | fitness-only. Findings suppressed by an inline @fitness-ignore directive this run (the "Ignores" column). Omitted by tools without a suppression mechanism. |

Signal

Each entry in signals[] is a Signal (packages/core/src/types/signal.ts).

It carries 4-level severity, a category, a provider, a fingerprint, and a

fix hint with confidence.

{
  "id": "sig_a3f9c204e1b2",
  "source": "no-console-log",
  "provider": "opensip-cli",
  "severity": "high",
  "category": "quality",
  "ruleId": "fit:no-console-log",
  "message": "console.log is forbidden in production",
  "suggestion": "Replace with structured logger.info()",
  "filePath": "services/api/src/routes/health.ts",
  "line": 42,
  "column": 17,
  "code": { "file": "services/api/src/routes/health.ts", "line": 42, "column": 17 },
  "fixAction": "replace-with-logger",
  "fixConfidence": 0.8,
  "metadata": {},
  "createdAt": "2026-05-15T10:30:00.000Z"
}

| Field | Type | Required | Description |

|---|---|---|---|

| id | string | yes | Per-signal identifier (sig_<12 hex>). |

| source | string | yes | The producing unit's slug — the join key back to units[].slug. For graph this is the OpenSIP-convention rule id (graph.<family>.<rule>). |

| provider | string | yes | The producer's namespace. "opensip-cli" for built-in checks/rules; command-mode wrappers carry the wrapped tool's name. |

| severity | "critical" \| "high" \| "medium" \| "low" | yes | 4-level severity. critical/high are the "error rung" (drive verdict.passed); medium/low are the "warning rung". |

| category | string | yes | Canonical labels: security \| quality \| architecture \| testing \| resilience \| documentation \| warning \| performance \| error. Open at the plugin layer (a plugin may declare its own). |

| ruleId | string | yes | Rule identifier. fit:<slug> for fit checks, graph.<family>.<rule> for graph rules, <provider>:<rule> for command-mode wrappers. |

| message | string | yes | Human-readable description. |

| suggestion | string | no | Optional fix suggestion. |

| filePath | string | yes | Project-relative file path. Empty string ("") for cross-cutting signals with no location. |

| line | number | no | 1-based line number. Absent for signals without a location. |

| column | number | no | 1-based column number. |

| code | { file?, line?, column? } | no | Structured location echo (mirrors filePath/line/column). |

| fixAction | string | no | Machine label for the suggested fix. |

| fixConfidence | number (0..1) | no | Confidence in the suggested fix. |

| metadata | object | yes | Open key/value bag for rule-specific detail. May be {}. |

| strength | number | no | Optional signal-strength weight. |

| fingerprint | string | no | Stable de-dup fingerprint when the producer computes one. |

| createdAt | string (ISO 8601) | yes | When the signal was created. |

| repair | SignalRepair | no | Structured repair guidance for agents (ADR-0086). Omitted when no guidance exists. SARIF exports only a bounded repair projection in result.properties.repair. |

SignalRepair (optional)

{
  "repairKind": "split-function",  // e.g. add-test | split-function | manual | unknown
  "autofixable": false,
  "suggestedCommand": "opensip fit --check large-function",
  "docsRef": "../60-guides/use-opensip-with-ai-agents.md",
  "confidence": 0.7,
  "patchHint": { "kind": "text", "summary": "Extract helper from lines 40-120" },
  "actions": [
    {
      "id": "replace-ts-ignore",
      "kind": "text-replacement",
      "title": "Replace @ts-ignore with @ts-expect-error",
      "autofixable": true,
      "confidence": 0.95,
      "patchHint": {
        "kind": "text",
        "summary": "Replace @ts-ignore with @ts-expect-error",
        "target": "src/example.ts"
      },
      "verification": { "commands": ["pnpm typecheck"] },
      "target": {
        "filePath": "src/example.ts",
        "line": 12,
        "expectedText": "@ts-ignore",
        "replacementText": "@ts-expect-error"
      }
    }
  ]
}

actions[] is optional and additive. Each action has a stable id, open

kind, human title, autofixable, optional confidence, optional

patchHint, optional verification.commands[] / verification.notes[], and a

scalar target metadata bag. The CLI host understands only documented first-

party action ids; unknown ids are preserved in JSON but refused by

opensip repair preview|apply.

opensip repair apply --verify returns a command-result payload under

CommandOutcome.data:

{
  "type": "repair-apply-verify",
  "status": "applied",
  "session": { "id": "sess_1", "tool": "fit" },
  "signal": { "id": "sig_1", "ruleId": "typescript-directive-hygiene" },
  "action": { "id": "replace-ts-ignore", "kind": "text-replacement", "title": "Replace @ts-ignore", "autofixable": true },
  "changes": [],
  "force": false,
  "verification": {
    "status": "verified",
    "coverage": "full",
    "scope": {
      "tool": "fit",
      "ruleId": "typescript-directive-hygiene",
      "files": ["src/example.ts"],
      "checkRan": true,
      "changedImpacted": true,
      "fallback": "targeted"
    },
    "commands": [
      {
        "tool": "fit",
        "args": ["fit", "--check", "typescript-directive-hygiene", "--changed", "--include-impacted", "--json"],
        "cwd": "/repo",
        "check": "typescript-directive-hygiene"
      }
    ],
    "remainingFindings": [],
    "trust": {
      "coverage": "full",
      "fallback": "targeted",
      "fullyVerified": true,
      "uncertainties": []
    }
  }
}

Verification statuses are verified, partial, unverified, and skipped.

Only verified means the deterministic verification command ran and proved the

selected finding absent. partial and unverified are intentionally not

success claims.

The line and column are 1-based to match SARIF and most editor conventions. A signal without a location omits line / column and carries an empty filePath.


Per-tool notes

All envelope-producing tools emit the same envelope; the differences are confined to a few fields:

error set when a scenario errored).

"semgrep", "ruff", "osv-scanner", "trivy", or another installed Tool); the envelope is the

same contract after the adapter normalizes native scanner output to Signals.

Per-kind sim detail (load p99, chaos recovery time) is not in the envelope. It lives in the session's session_tool_payload row persisted to the project-local SQLite store (<project>/opensip-cli/.runtime/datastore.sqlite) via SessionRepo. The dashboard reads the session record for the deeper view.


Error result — status: "error"

When a run fails before producing an envelope (config invalid, plugin failed to load, baseline missing), the --json output is still a CommandOutcomestatus: "error" with neither .envelope nor .data, only a structured errors[]:

{
  "kind": "command.error",
  "status": "error",
  "exitCode": 2,
  "errors": [
    {
      "message": "Gate baseline not found in the project SQLite store. Run `opensip fit --gate-save` first to create one.",
      "suggestion": "Run opensip fit --gate-save.",
      "code": "CONFIGURATION_ERROR"
    }
  ]
}

Each ErrorDetail carries a message, an optional actionable suggestion, and an optional machine code. The exitCode is 2 (configuration/runtime error), 130 for cooperative cancellation (EXIT_CODES.CANCELLED), or whatever the throwing code specified — and it matches the top-level exitCode field as well as the process exit code.

Failure axes (host-normalized)

Command failures are normalized once into a versioned failure envelope

(ADR-0181).

Public --json keeps the stable ErrorDetail surface above. Machine / worker /

operator sinks may also carry definition axes from the envelope:

| Field | Public JSON? | Notes |

|---|---|---|

| code | yes (when known) | Registered or legacy-adapted code |

| message | yes | Bounded, control-scrubbed |

| suggestion | yes (optional) | Operator action / recovery hint |

| source / kind / retry / exitClass | machine sinks | Orthogonal axes; not required on every public errors[] row yet |

| metadata (allowlisted keys only) | selective | Secrets and non-allowlisted keys never public |

| operatorDetail / stacks / raw cause | no | Operator/log only; never worker public wire |

SignalSeverity on findings is not a failure-envelope field and does not

decide ToolRunOutcome. Full catalog of registered codes:

error code index. Contributor model:

error and resiliency model.


Compatibility commitments


Reading the output in CI

A few CI patterns:

The envelope is nested under .envelope of the CommandOutcome wrapper — every path below reflects that.

# Fail on any error-rung (critical/high) signal:
opensip fit --json | jq -e '.envelope.verdict.passed'

# Print only failed units:
opensip fit --json | jq '.envelope.units | map(select(.passed == false))'

# Count error-rung signals by file:
opensip fit --json | jq '.envelope.signals[] | select(.severity == "critical" or .severity == "high") | .filePath' | sort | uniq -c

# All signals for one unit (join on source → slug):
opensip fit --json | jq '.envelope.signals[] | select(.source == "no-console-log")'

# Score gate:
opensip fit --json | jq -e '.envelope.verdict.score >= 90'

For SARIF, use --sarif, --report-to, or the tool-specific export path. The

SARIF shape is the SARIF 2.1.0 spec's, not opensip-cli's, but OpenSIP preserves

identity through result.partialFingerprints.opensipFingerprint and bounded

OpenSIP property bags. See 10-concepts/05-architecture-gate.md.

Native Evidence Authority

OpenSIP Cloud signal sync uses the native SignalBatch wire contract from

packages/core/src/types/signal-batch.ts.

The batch has schemaVersion: 1; spec 20 adds an optional evidence header

without bumping that version.

{
  "schemaVersion": 1,
  "tool": "fit",
  "runId": "run_9bb6ef4d07c0",
  "repo": { "commit": "abc123" },
  "counts": { "total": 1, "bySeverity": { "high": 1 } },
  "evidence": {
    "contractVersion": 1,
    "tier": "cli-attested", // cloud-derived | cli-attested | external-untrusted
    "attestation": { "status": "verified" },
    "baselineIdentity": {
      "fingerprintStrategyId": "fitness.sha256-file-rule-message",
      "fingerprintStrategyVersion": 1
    },
    "declaredInputs": {
      "cliVersion": "0.4.2",
      "nodeVersion": "24.16.0",
      "platform": "darwin/arm64",
      "tool": "fit",
      "engineVersion": "0.4.2"
    },
    "divergenceContractVersion": 1
  },
  "signals": [ /* Signal[] */ ]
}

Absence of evidence means an older/no-header producer and must be treated by

consumers as external-untrusted. The CLI emits cli-attested only when local

provenance and declared inputs are complete enough to support that claim;

otherwise the header is present but downgraded to external-untrusted.

Agent-filtered live/replay output

When fit/graph/sim run with --json and any --filter token (or

sessions show with filters), the payload includes filter metadata

(ADR-0085):

{
  "filtersApplied": ["errors-only", "top:20"],
  "originalSignalCount": 142,
  "returnedSignalCount": 20,
  "envelope": { /* filtered SignalEnvelope */ }
}

With --raw, the outer CommandOutcome wrapper is omitted and only the

filtered result object is emitted.

graph impact --json

{
  "kind": "graph-impact",
  "status": "ok",
  "exitCode": 0,
  "data": {
    "type": "graph-impact",
    "catalog": {
      "builtAt": "2026-07-12T18:00:00.000Z",
      "language": "typescript",
      "filesFingerprint": "<64 lowercase hex characters>",
      "resolutionMode": "exact",
      "cacheKeyDigest": "<64 lowercase hex characters>"
    },
    "basis": { "type": "changed", "ref": "main" },
    "changedFiles": ["src/foo.ts"],
    "changedFunctions": [ /* ImpactFunction[] */ ],
    "impactedFunctions": [ /* ImpactFunction[] */ ],
    "impactedPackages": [ /* ImpactPackage[] */ ],
    "impactedFiles": ["src/foo.ts", "src/caller.ts"],
    "trust": {
      "coverage": "full",
      "fallback": "targeted",
      "fullyVerified": true,
      "uncertainties": []
    },
    "recommendedCommands": ["opensip fit --changed --include-impacted --json"],
    "truncated": false
  }
}

trust.fullyVerified is the field agents should inspect before claiming a

targeted verification was complete. Non-empty uncertainties[] explain why the

result is partial or unknown.

catalog is optional only for results produced before catalog identity was

added or when a loaded legacy identity cannot be represented safely. The cache

key and file fingerprint fields are SHA-256 digests; their raw forms are never

copied because they can contain absolute paths. The delivered

SignalEnvelope.verification equals the result's full trust value.

Stored graph-impact report projection

Every current graph-impact command also returns a generic session contribution,

including wrapped and raw JSON modes. Graph owns an optional bounded impact

field inside its opaque GraphSessionPayload.__version: 1:

{
  "__version": 1,
  "impactStatus": "available",
  "impact": {
    "catalog": { /* the bounded identity above */ },
    "basis": { "type": "changed", "source": "git", "warningCodes": [] },
    "changedFiles": ["src/foo.ts"],
    "changedFunctions": [ /* bounded ImpactFunction[] */ ],
    "impactedFunctions": [ /* bounded ImpactFunction[] */ ],
    "impactedFiles": ["src/caller.ts", "src/foo.ts"],
    "impactedPackages": [ /* bounded ImpactPackage[] */ ],
    "trust": { /* bounded display copy; envelope verification is authoritative */ },
    "recommendedCommands": ["opensip fit --changed --include-impacted --json"],
    "truncated": false,
    "omitted": {
      "changedFiles": 0,
      "changedFunctions": 0,
      "impactedFunctions": 0,
      "impactedFiles": 0,
      "impactedPackages": 0,
      "recommendedCommands": 0
    },
    "detailTruncated": false,
    "metadataOmitted": false
  }
}

Caps are 200 changed files, 200 changed functions, 500 impacted functions, 500

impacted files, 100 impacted packages, 20 recommended commands, and 1 MiB for

the complete UTF-8 projection. Exact omitted counts distinguish persisted

truncation from zero. impactStatus: "omitted-overflow" means analysis

completed but the scalar projection could not fit; impact is absent in that

case. Both status and projection absent means a legacy or non-impact session.

These storage states never change the analysis verdict or process exit code.

See ADR-0156.

graph lookup --json

{
  "kind": "graph-lookup",
  "status": "ok",
  "exitCode": 0,
  "data": {
    "type": "graph-lookup",
    "name": "saveBaseline",
    "resolutionMode": "exact",
    "matches": [ /* GraphLookupMatch[] */ ]
  }
}

config validate|schema|migrate --json

// validate success
{
  "kind": "config-validate",
  "status": "ok",
  "exitCode": 0,
  "data": {
    "type": "config-validate",
    "valid": true,
    "configPath": "/path/to/opensip-cli.config.yml",
    "namespaces": ["cli", "fitness", "graph", "targets"]
  }
}

// schema success
{
  "kind": "config-schema",
  "status": "ok",
  "exitCode": 0,
  "data": {
    "type": "config-schema",
    "schema": { /* JSON Schema document */ },
    "namespaces": ["cli", "fitness", "graph"]
  }
}

// migrate dry-run/check result
{
  "kind": "config-migrate",
  "status": "ok",
  "exitCode": 2,
  "data": {
    "type": "config-migrate",
    "configPath": "/path/to/opensip-cli.config.yml",
    "changed": true,
    "dryRun": true,
    "check": true,
    "wrote": false,
    "fromVersion": 1,
    "targetVersion": 1,
    "operations": [
      {
        "kind": "add-schema-version",
        "toVersion": 1,
        "message": "Added schemaVersion: 1."
      }
    ]
  }
}

What's next