Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
paths:
- 'src-rust/**'
- 'scripts/test-headless-schemas.py'
- '.github/workflows/rust-ci.yml'
workflow_dispatch:

Expand All @@ -18,6 +19,22 @@ env:
CARGO_TERM_COLOR: always

jobs:
headless-schemas:
name: Headless JSON Schema constraints
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Validate runtime fixtures and cross-field constraints
run: |
python -m pip install jsonschema==4.25.1
python scripts/test-headless-schemas.py

rust:
name: Format, lint, and test
runs-on: ubuntu-latest
Expand Down
52 changes: 29 additions & 23 deletions docs/headless-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ The runtime also accepts version `2` briefs during the coven-github migration
and echoes `"2"` in their result envelopes. Version `3` remains the normative
schema; all other major versions are rejected.

This document is the single source of truth for the interface between
`coven-github` (the GitHub ingress adapter) and `coven-code` (the execution
runtime) when the runtime is invoked in headless mode.

It is normative. Where the prose in [`COVEN-GITHUB.md`](../COVEN-GITHUB.md) or any
other doc disagrees with this file, **this file wins**. Both repositories MUST
implement exactly what is specified here, and changes require a contract version
bump (see [Versioning](#versioning)).

The contract is enforced on the `coven-github` side by golden fixtures in
[`docs/contracts/`](contracts/) and a conformance test
(`crates/github/tests/contract.rs`) that round-trips those fixtures through the
Rust types. `coven-code` MUST validate its emitted `result.json` against
[`docs/contracts/result.schema.json`](contracts/result.schema.json) and its
accepted `session-brief.json` against
[`docs/contracts/session-brief.schema.json`](contracts/session-brief.schema.json).
This document specifies the runtime's version 3 interface. The adapter's
[upstream contract](https://github.com/OpenCoven/coven-github/blob/main/docs/headless-contract.md)
and fixtures currently describe version 2. The local fixtures include v3
review-memory provenance and are not verbatim upstream copies.

Where [`COVEN-GITHUB.md`](../COVEN-GITHUB.md) disagrees with this runtime
specification, this file wins. Coordinate breaking changes with the adapter
and bump the contract version (see [Versioning](#6-versioning)).

The runtime fixtures live in
[`src-rust/crates/cli/tests/headless_contract/`](../src-rust/crates/cli/tests/headless_contract/).
Rust tests round-trip those fixtures through runtime types. Run
`uv run scripts/test-headless-schemas.py` to validate the v3 fixtures and
cross-field constraints with a Draft 2020-12 JSON Schema validator.

Accepting a v2 brief and echoing its version does not prove strict adapter-v2
schema conformance: the runtime also emits `review.memory`, which the current
adapter-v2 schema does not accept. That migration requires coordinated work
in both repositories. This document does not claim adapter memory-grant or
memory-write governance support.

The key words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119.

Expand Down Expand Up @@ -197,7 +201,7 @@ the file MAY be absent.
| `summary` | string | One-line familiar-voice summary. Used in the Check Run and PR title. |
| `pr_body` | string | Full PR body, **authored by the familiar** in its own voice — not a template. |
| `review` | object | Structured review evidence and findings. Required even when `mode` is `none`. See [3.2](#32-review). |
| `exit_reason` | string enum \| null | `null` on success; otherwise the terminal cause. See [3.4](#34-exit_reason). |
| `exit_reason` | string enum \| null | `null` or absent on success; required for `failure` and `needs_input`; optional for `partial`. See [3.4](#34-exit_reason). |

> **Drift note (supersedes `COVEN-GITHUB.md`):** the prose result envelope listed
> an `events` array. Progress/event streaming is **not** part of the v3 result
Expand Down Expand Up @@ -241,7 +245,9 @@ and `needs_input` do not open a PR by themselves.

### 3.4 `exit_reason`

`null` on success. Otherwise one of:
For `success`, omit this field or set it to `null`. For `failure` and
`needs_input`, provide one of the causes below. A `partial` result may omit
the field, set it to `null`, or provide a cause when one is known.

| Value | Meaning |
|---|---|
Expand Down Expand Up @@ -319,11 +325,11 @@ must accept the new required nullable provenance fields and `provenance` arrays.

| Artifact | Purpose |
|---|---|
| [`docs/contracts/session-brief.schema.json`](contracts/session-brief.schema.json) | JSON Schema for the input brief. |
| [`docs/contracts/result.schema.json`](contracts/result.schema.json) | JSON Schema for the output envelope. |
| [`docs/contracts/session-brief.example.json`](contracts/session-brief.example.json) | Golden input fixture. |
| [`docs/contracts/result.example.json`](contracts/result.example.json) | Golden output fixture. |
| `crates/github/tests/contract.rs` | Round-trips the golden fixtures through the Rust types — fails the build if the adapter drifts from this contract. |
| [`session-brief.schema.json`](../src-rust/crates/cli/tests/headless_contract/session-brief.schema.json) | JSON Schema for the input brief. |
| [`result.schema.json`](../src-rust/crates/cli/tests/headless_contract/result.schema.json) | JSON Schema for the output envelope. |
| [`session-brief.example.json`](../src-rust/crates/cli/tests/headless_contract/session-brief.example.json) | Golden input fixture. |
| [`result.example.json`](../src-rust/crates/cli/tests/headless_contract/result.example.json) | Golden output fixture. |
| `scripts/test-headless-schemas.py` | Validates runtime fixtures and cross-field schema constraints. |

A `coven-code` change is contract-conformant when its emitted `result.json`
validates against `result.schema.json` and it accepts any brief that validates
Expand Down
71 changes: 71 additions & 0 deletions scripts/test-headless-schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# /// script
# requires-python = ">=3.10"
# dependencies = ["jsonschema==4.25.1"]
# ///
"""Validate the runtime v3 fixtures and their cross-field constraints.

Run from any directory: uv run scripts/test-headless-schemas.py
"""
import copy
import json
from pathlib import Path
import unittest

from jsonschema import Draft202012Validator

FIXTURES = Path(__file__).resolve().parents[1] / "src-rust/crates/cli/tests/headless_contract"


class HeadlessSchemas(unittest.TestCase):
def setUp(self):
self.brief = json.loads((FIXTURES / "session-brief.example.json").read_text())
self.result = json.loads((FIXTURES / "result.example.json").read_text())
self.validators = {}
for name in ("session-brief", "result"):
schema = json.loads((FIXTURES / f"{name}.schema.json").read_text())
Draft202012Validator.check_schema(schema)
self.validators[name] = Draft202012Validator(schema)

def test_golden_fixtures(self):
self.validators["session-brief"].validate(self.brief)
self.validators["result"].validate(self.result)

def test_brief_requires_version(self):
del self.brief["contract_version"]
self.assertFalse(self.validators["session-brief"].is_valid(self.brief))

def test_result_status_and_exit_reason(self):
for status in ("success", "failure", "needs_input", "partial"):
for reason in (None, "test_failure", "ambiguous_spec", "git_conflict", "infra_error"):
with self.subTest(status=status, reason=reason):
value = copy.deepcopy(self.result)
value.update(status=status, exit_reason=reason)
expected = (
reason is None if status == "success"
else status == "partial" or reason is not None
)
self.assertEqual(self.validators["result"].is_valid(value), expected)
for status in ("failure", "needs_input"):
with self.subTest(status=status, reason="absent"):
value = copy.deepcopy(self.result)
value["status"] = status
value.pop("exit_reason", None)
self.assertFalse(self.validators["result"].is_valid(value))

def test_review_mode_and_evidence(self):
for mode in ("none", "pull_request", "review_comment"):
for evidence in ("not_applicable", "complete", "partial", "missing"):
for files in ([], ["src/example.rs"]):
with self.subTest(mode=mode, evidence=evidence, files=files):
value = copy.deepcopy(self.result)
value["review"].update(mode=mode, evidence_status=evidence, reviewed_files=files)
expected = (
evidence == "not_applicable" if mode == "none"
else evidence != "not_applicable"
and (bool(files) or evidence == "missing")
)
self.assertEqual(self.validators["result"].is_valid(value), expected)


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion src-rust/crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ async fn main() -> anyhow::Result<()> {
.with_writer(std::io::stderr)
.init();

// --context loads a coven-github session brief (headless contract v1). It
// --context loads a coven-github session brief (headless contract v3). It
// implies full headless + skip-permissions and overrides model/cwd from the
// brief. A brief with an unsupported major version is rejected here.
let github_context = SessionBrief::load(cli.context.as_ref())?;
Expand Down
36 changes: 20 additions & 16 deletions src-rust/crates/cli/tests/headless_contract/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
# Vendored headless-contract fixtures
# Runtime headless-contract fixtures

These four files are **verbatim copies** of the locked headless execution
contract artifacts owned by the `coven-github` repository:
These fixtures describe Coven Code's version 3 headless interface:

- `session-brief.schema.json`
- `result.schema.json`
- `session-brief.example.json`
- `result.example.json`

Canonical source (single source of truth):
`OpenCoven/coven-github` → `docs/headless-contract.md` + `docs/contracts/`.
Run the JSON Schema regression tests from the repository root:

**Contract version: `1`.**
```sh
uv run scripts/test-headless-schemas.py
```

`coven-code` is the *consumer* of the session brief and the *producer* of the
result envelope. The conformance tests in `crates/cli/src/headless.rs` round-trip
these golden fixtures through the runtime types and assert that:
The script pins its validator dependency and tests both golden fixtures,
required version declarations, review evidence, and result exit reasons.
Rust tests in `crates/cli/src/headless.rs` separately exercise runtime parsing,
serialization, and selected schema properties.

- every brief that validates against `session-brief.schema.json` is accepted,
- every `result.json` the runtime emits validates against `result.schema.json`.
The [upstream adapter contract](https://github.com/OpenCoven/coven-github/blob/main/docs/headless-contract.md)
and its `docs/contracts/` fixtures currently use version 2. These runtime
fixtures use version 3 and include `review.memory` provenance. They are not
verbatim copies. The runtime still accepts v2 briefs and echoes that version,
but its extra review provenance needs reconciliation with the strict adapter-v2
schema before claiming cross-repository conformance.

If a test here fails, the runtime drifted from the contract **or** the contract
changed. Do not "bless" the fixtures — re-copy them from `coven-github` only as
part of a deliberate, version-bumped contract change (see the contract doc's
Versioning section). A breaking change MUST bump `contract_version` on both
sides.
See the [runtime contract](../../../../../docs/headless-contract.md) for the
normative v3 behavior. Coordinate breaking changes and version bumps with
both repositories; do not overwrite these fixtures with an older upstream
schema.
42 changes: 41 additions & 1 deletion src-rust/crates/cli/tests/headless_contract/result.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,57 @@
"required": ["mode"]
},
"then": {
"properties": {
"evidence_status": { "enum": ["complete", "partial", "missing"] }
},
"anyOf": [
{ "properties": { "reviewed_files": { "minItems": 1 } }, "required": ["reviewed_files"] },
{ "properties": { "evidence_status": { "const": "missing" } }, "required": ["evidence_status"] }
]
}
},
{
"if": {
"properties": { "mode": { "const": "none" } },
"required": ["mode"]
},
"then": {
"properties": { "evidence_status": { "const": "not_applicable" } }
}
}
]
},
"exit_reason": {
"type": ["string", "null"],
"enum": ["test_failure", "ambiguous_spec", "git_conflict", "infra_error", null]
}
}
},
"allOf": [
{
"if": {
"properties": { "status": { "const": "success" } },
"required": ["status"]
},
"then": {
"properties": {
"exit_reason": { "const": null }
}
}
},
{
"if": {
"properties": { "status": { "enum": ["failure", "needs_input"] } },
"required": ["status"]
},
"then": {
"properties": {
"exit_reason": {
"type": "string",
"enum": ["test_failure", "ambiguous_spec", "git_conflict", "infra_error"]
}
},
"required": ["exit_reason"]
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "Tokenless read-context payload the adapter writes for coven-code --headless --context. Contract version 3.",
"type": "object",
"additionalProperties": false,
"required": ["trigger", "repo", "task", "familiar", "workspace"],
"required": ["contract_version", "trigger", "repo", "task", "familiar", "workspace"],
"properties": {
"contract_version": {
"type": "string",
Expand Down
Loading