mergeproof 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. mergeproof-0.2.0/.gitignore +13 -0
  2. mergeproof-0.2.0/CHANGELOG.md +34 -0
  3. mergeproof-0.2.0/LICENSE +21 -0
  4. mergeproof-0.2.0/PKG-INFO +536 -0
  5. mergeproof-0.2.0/README.md +505 -0
  6. mergeproof-0.2.0/action.yml +85 -0
  7. mergeproof-0.2.0/docs/logo-wordmark.svg +20 -0
  8. mergeproof-0.2.0/docs/logo.png +0 -0
  9. mergeproof-0.2.0/docs/logo.svg +31 -0
  10. mergeproof-0.2.0/docs/overview.svg +36 -0
  11. mergeproof-0.2.0/docs/pipeline.svg +32 -0
  12. mergeproof-0.2.0/docs/pr-comment.svg +53 -0
  13. mergeproof-0.2.0/examples/README.md +20 -0
  14. mergeproof-0.2.0/examples/github-workflow.yml +30 -0
  15. mergeproof-0.2.0/examples/mcp-server/README.md +38 -0
  16. mergeproof-0.2.0/examples/mcp-server/mergeproof.yaml +79 -0
  17. mergeproof-0.2.0/examples/mcp-server/scenarios/fix-awaiting-review.json +49 -0
  18. mergeproof-0.2.0/examples/mcp-server/scenarios/fix-verified.json +54 -0
  19. mergeproof-0.2.0/examples/mcp-server/scenarios/fix-without-evidence.json +49 -0
  20. mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/README.md +37 -0
  21. mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/pyproject.toml +24 -0
  22. mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/src/mergeproof_langfuse/__init__.py +65 -0
  23. mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/tests/test_langfuse.py +45 -0
  24. mergeproof-0.2.0/examples/python-library/README.md +16 -0
  25. mergeproof-0.2.0/examples/python-library/mergeproof.yaml +32 -0
  26. mergeproof-0.2.0/examples/python-library/scenarios/complete.json +18 -0
  27. mergeproof-0.2.0/examples/python-library/scenarios/missing-tests.json +11 -0
  28. mergeproof-0.2.0/examples/python-library/scenarios/pending-ci.json +15 -0
  29. mergeproof-0.2.0/examples/web-service/README.md +17 -0
  30. mergeproof-0.2.0/examples/web-service/mergeproof.yaml +47 -0
  31. mergeproof-0.2.0/examples/web-service/scenarios/auth-change-awaiting-security.json +39 -0
  32. mergeproof-0.2.0/examples/web-service/scenarios/migration-missing-rollback.json +28 -0
  33. mergeproof-0.2.0/examples/web-service/scenarios/ui-change-complete.json +30 -0
  34. mergeproof-0.2.0/pyproject.toml +106 -0
  35. mergeproof-0.2.0/src/mergeproof/__init__.py +32 -0
  36. mergeproof-0.2.0/src/mergeproof/__main__.py +4 -0
  37. mergeproof-0.2.0/src/mergeproof/checks/__init__.py +4 -0
  38. mergeproof-0.2.0/src/mergeproof/checks/agent_verdict.py +88 -0
  39. mergeproof-0.2.0/src/mergeproof/checks/base.py +71 -0
  40. mergeproof-0.2.0/src/mergeproof/checks/body.py +46 -0
  41. mergeproof-0.2.0/src/mergeproof/checks/ci_job.py +74 -0
  42. mergeproof-0.2.0/src/mergeproof/checks/evidence_field.py +81 -0
  43. mergeproof-0.2.0/src/mergeproof/checks/evidence_links.py +128 -0
  44. mergeproof-0.2.0/src/mergeproof/checks/files.py +47 -0
  45. mergeproof-0.2.0/src/mergeproof/checks/human_verified.py +65 -0
  46. mergeproof-0.2.0/src/mergeproof/checks/labels.py +44 -0
  47. mergeproof-0.2.0/src/mergeproof/checks/registry.py +66 -0
  48. mergeproof-0.2.0/src/mergeproof/checks/shell.py +61 -0
  49. mergeproof-0.2.0/src/mergeproof/checks/tests_changed.py +72 -0
  50. mergeproof-0.2.0/src/mergeproof/cli.py +384 -0
  51. mergeproof-0.2.0/src/mergeproof/context.py +79 -0
  52. mergeproof-0.2.0/src/mergeproof/engine.py +86 -0
  53. mergeproof-0.2.0/src/mergeproof/evidence.py +90 -0
  54. mergeproof-0.2.0/src/mergeproof/mcp_server.py +105 -0
  55. mergeproof-0.2.0/src/mergeproof/patterns.py +67 -0
  56. mergeproof-0.2.0/src/mergeproof/policy.py +135 -0
  57. mergeproof-0.2.0/src/mergeproof/providers/__init__.py +0 -0
  58. mergeproof-0.2.0/src/mergeproof/providers/git.py +98 -0
  59. mergeproof-0.2.0/src/mergeproof/providers/github.py +238 -0
  60. mergeproof-0.2.0/src/mergeproof/render/__init__.py +20 -0
  61. mergeproof-0.2.0/src/mergeproof/render/agent.py +71 -0
  62. mergeproof-0.2.0/src/mergeproof/render/junit.py +52 -0
  63. mergeproof-0.2.0/src/mergeproof/render/markdown.py +174 -0
  64. mergeproof-0.2.0/src/mergeproof/render/rdjson.py +33 -0
  65. mergeproof-0.2.0/src/mergeproof/render/text.py +38 -0
  66. mergeproof-0.2.0/src/mergeproof/report.py +131 -0
  67. mergeproof-0.2.0/src/mergeproof/telemetry.py +48 -0
  68. mergeproof-0.2.0/src/mergeproof/verifiers/__init__.py +3 -0
  69. mergeproof-0.2.0/src/mergeproof/verifiers/base.py +39 -0
  70. mergeproof-0.2.0/src/mergeproof/verifiers/http.py +32 -0
  71. mergeproof-0.2.0/tests/__init__.py +0 -0
  72. mergeproof-0.2.0/tests/integration/__init__.py +0 -0
  73. mergeproof-0.2.0/tests/integration/conftest.py +90 -0
  74. mergeproof-0.2.0/tests/integration/test_examples.py +56 -0
  75. mergeproof-0.2.0/tests/integration/test_mcp.py +130 -0
  76. mergeproof-0.2.0/tests/integration/test_pipeline.py +58 -0
  77. mergeproof-0.2.0/tests/integration/test_plugins.py +26 -0
  78. mergeproof-0.2.0/tests/unit/__init__.py +0 -0
  79. mergeproof-0.2.0/tests/unit/conftest.py +55 -0
  80. mergeproof-0.2.0/tests/unit/test_checks.py +286 -0
  81. mergeproof-0.2.0/tests/unit/test_cli.py +204 -0
  82. mergeproof-0.2.0/tests/unit/test_engine.py +140 -0
  83. mergeproof-0.2.0/tests/unit/test_evidence.py +50 -0
  84. mergeproof-0.2.0/tests/unit/test_patterns.py +29 -0
  85. mergeproof-0.2.0/tests/unit/test_policy.py +62 -0
  86. mergeproof-0.2.0/tests/unit/test_providers.py +222 -0
  87. mergeproof-0.2.0/tests/unit/test_render.py +174 -0
  88. mergeproof-0.2.0/tests/unit/test_telemetry.py +46 -0
  89. mergeproof-0.2.0/tests/unit/test_verifiers.py +30 -0
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ .itenv/
3
+ __pycache__/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ mergeproof-report.json
13
+ uv.lock
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). From 0.2.0 on, entries are generated
5
+ by release-please from Conventional Commit messages.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-09-13
10
+
11
+ ### Added
12
+ - Plumbing commands `context`, `report`, `comment` and `template`; contexts and reports are JSON and pipe between them.
13
+ - `mergeproof mcp`: an MCP server (stdio) exposing explain, check, evidence_block, validate_policy, list_checks and agent_instructions.
14
+ - `mergeproof[otel]`: the MCP server exports OpenTelemetry spans over OTLP/HTTP when `OTEL_EXPORTER_OTLP_ENDPOINT` is set (Langfuse, Jaeger, Phoenix, any collector).
15
+ - `evidence.links`: vendor-neutral before/after link pairs with pluggable verifiers (`mergeproof.verifiers` entry-point group); `http` verifier built in.
16
+ - `files.changed` check.
17
+ - Reporting through the commit status (`--status`) and a Check Run with file annotations (`--check-run`); the action enables both by default.
18
+ - `-f junit` and `-f rdjson` renderings, written by the action, so test-result reporters and reviewdog can present the gate.
19
+ - Container image `ghcr.io/aryamanz29/mergeproof` with the CLI and MCP server: `:edge` on every push to main, `:X.Y.Z`, `:X.Y`, `:X` and `:latest` on release tags.
20
+ - Example projects with scenario fixtures that the integration suite executes, and a `mergeproof-langfuse` verifier plugin.
21
+ - Fixed exit codes: 0 pass or warn, 1 fail, 2 pending, 3 usage or policy error.
22
+
23
+ ### Changed
24
+ - PR comment redesigned: logo, status badge and links in the header, one plain sentence, one table with a Status column in words, numbered next steps only when something is missing, and no comment at all when no rule applies.
25
+ - `ci.job_passed` considers only the newest check run per name, so runs cancelled by a newer push no longer count as failures.
26
+ - `mergeproof checks` marks checks that only produce results in GitHub mode.
27
+
28
+ ### Removed
29
+ - Braintrust-specific trace check (`evidence.traces`). Use `evidence.links` with a verifier plugin.
30
+
31
+ ## [0.1.0] - 2026-09-12
32
+
33
+ ### Added
34
+ - First cut: policy file, built-in checks, `check` / `explain` / `agent-prompt`, composite GitHub Action.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aryaman Bhushan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,536 @@
1
+ Metadata-Version: 2.5
2
+ Name: mergeproof
3
+ Version: 0.2.0
4
+ Summary: Evidence gates for pull requests: declare what a change must prove, enforce it in CI, explain it to agents.
5
+ Project-URL: Homepage, https://github.com/Aryamanz29/mergeproof
6
+ Project-URL: Issues, https://github.com/Aryamanz29/mergeproof/issues
7
+ Project-URL: Changelog, https://github.com/Aryamanz29/mergeproof/blob/main/CHANGELOG.md
8
+ Author: Aryaman Bhushan
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ci,evidence,github-actions,guardrails,mcp,pull-request
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Topic :: Software Development :: Version Control :: Git
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx<1,>=0.27
23
+ Requires-Dist: pydantic<3,>=2.6
24
+ Requires-Dist: pyyaml>=6.0
25
+ Provides-Extra: mcp
26
+ Requires-Dist: mcp<3,>=2.1.1; extra == 'mcp'
27
+ Provides-Extra: otel
28
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.30; extra == 'otel'
29
+ Requires-Dist: opentelemetry-sdk<2,>=1.30; extra == 'otel'
30
+ Description-Content-Type: text/markdown
31
+
32
+ <p align="center">
33
+ <img src="docs/logo.svg" alt="mergeproof" width="128">
34
+ </p>
35
+
36
+ <h1 align="center">mergeproof</h1>
37
+
38
+ <p align="center"><strong>Proof before merge.</strong> Pull requests earn their merge with evidence, not claims.</p>
39
+
40
+ <p align="center">
41
+
42
+ [![CI](https://github.com/Aryamanz29/mergeproof/actions/workflows/ci.yml/badge.svg)](https://github.com/Aryamanz29/mergeproof/actions/workflows/ci.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/mergeproof)](https://pypi.org/project/mergeproof/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/mergeproof)](https://pypi.org/project/mergeproof/)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
46
+
47
+ </p>
48
+
49
+ `mergeproof` is an evidence gate for pull requests. One YAML file says what a change must
50
+ *prove* before it merges: tests for the modules it touched, a green integration job,
51
+ before/after links from a live environment, a human who actually opened those links. CI
52
+ enforces it. Coding agents read the same file, so the process stops living in review comments.
53
+
54
+ ![Policy file feeding CI, agents and reviewers](docs/overview.svg)
55
+
56
+ ## Why
57
+
58
+ Agents are good at claiming outcomes. "Added tests, verified on staging" costs nothing to
59
+ write. Checklists don't help; a checkbox is self-attestation. What holds up is an artifact
60
+ somebody else can open: a changed test file, a trace id that resolves, a screenshot pair, a
61
+ sign-off tied to the exact commit. `mergeproof` checks those, verifies them against their
62
+ source when it can, and leaves the last word to a person who is not the author.
63
+
64
+ ## Install
65
+
66
+ ```sh
67
+ pip install mergeproof # or: uv tool install mergeproof
68
+ pip install 'mergeproof[mcp]' # adds the MCP server for coding agents
69
+ ```
70
+
71
+ Or run it without installing anything. `uvx` fetches the package into a cache on first use;
72
+ the container image on GHCR bundles Python, git and the `mcp` and `otel` extras:
73
+
74
+ ```sh
75
+ uvx --from 'mergeproof[mcp]' mergeproof explain
76
+ docker run --rm -v "$PWD":/repo ghcr.io/aryamanz29/mergeproof check --local
77
+ docker run -i --rm -v "$PWD":/repo ghcr.io/aryamanz29/mergeproof mcp # MCP over stdio, -i is required
78
+ ```
79
+
80
+ Image tags follow the release tag: `v1.2.3` publishes `:1.2.3`, `:1.2`, `:1` and `:latest`; `:edge` tracks `main`. The image expects the repository mounted at `/repo`. For an MCP client, the same command goes in
81
+ the settings file: `"command": "docker", "args": ["run", "-i", "--rm", "-v", "/path/to/repo:/repo", "ghcr.io/aryamanz29/mergeproof", "mcp"]`.
82
+
83
+ ## Five-minute tour
84
+
85
+ ```sh
86
+ mergeproof init # writes a starter mergeproof.yaml
87
+ mergeproof checks # what checks exist and what they take
88
+ mergeproof explain # what must the current diff prove, what is missing right now
89
+ mergeproof template # the evidence block still missing, ready to paste into the PR
90
+ mergeproof check # the gate: exit 0 pass, 1 fail, 2 pending
91
+ ```
92
+
93
+ `explain` on a branch that changed a tool module and nothing else:
94
+
95
+ ````text
96
+ # What this change must prove (fail)
97
+
98
+ ## tool-change-needs-unit-tests [block]
99
+ Every changed tool module ships with changed unit tests for that module.
100
+ Triggered by `server/tools/search.py`.
101
+
102
+ - ❌ **unit tests for touched tools**: Each changed source file needs a changed test file:
103
+ `server/tools/{name}.py` needs `tests/unit/**/test_{name}*.py`.
104
+ - now: 1 changed source file(s) without test changes
105
+ - server/tools/search.py: expected a changed test matching tests/unit/**/test_search*.py
106
+ - fix: Add a regression test for each listed file: it should fail before the change and pass after.
107
+ - ⏳ **unit tests green**: CI check run matching `^unit` is green on the head commit.
108
+ - now: CI status for `^unit` is only visible in GitHub mode
109
+
110
+ ## fix-needs-live-evidence [block]
111
+ ...
112
+ ## Evidence block to add to the PR description
113
+
114
+ ```evidence
115
+ environment: staging
116
+ image: <registry>/mcp-server:pr-<n>-<sha7>
117
+ traces:
118
+ - what: <what was exercised>
119
+ before: https://langfuse.example.com/project/<project-id>/traces/<trace-id>
120
+ after: https://langfuse.example.com/project/<project-id>/traces/<trace-id>
121
+ ```
122
+ ````
123
+
124
+ ## The policy file
125
+
126
+ ```yaml
127
+ version: 1
128
+ project: my-service
129
+
130
+ rules:
131
+ - id: source-needs-tests
132
+ description: Source changes ship with tests for the touched module.
133
+ when:
134
+ paths: ["src/**/*.py"]
135
+ exclude_paths: ["src/**/__init__.py"]
136
+ require:
137
+ - check: tests.changed
138
+ with:
139
+ map: { "src/{pkg}/{name}.py": "tests/**/test_{name}*.py" }
140
+ - check: ci.job_passed
141
+ with: { name: "^unit", regex: true }
142
+
143
+ - id: fix-needs-live-evidence
144
+ description: Bug fixes show the behaviour before and after on staging.
145
+ when:
146
+ title: "^fix"
147
+ instructions: Reproduce on staging, keep the link, deploy, repeat, keep that link too.
148
+ require:
149
+ - check: evidence.field
150
+ with: { key: environment, equals: staging }
151
+ - check: evidence.links
152
+ with: { min_pairs: 1 }
153
+ - check: review.human_verified
154
+ with: { phrase: "/verified", bind_to_head: true }
155
+ ```
156
+
157
+ A rule has a `when` (which changes it applies to) and a list of `require`ments, each a check
158
+ with parameters. `severity: warn` on a rule or a requirement reports without blocking.
159
+
160
+ `when` matchers: `paths`, `exclude_paths`, `labels`, `title` (regex), `base_branches`,
161
+ `authors`. That last one lets you hold `copilot[bot]` or `claude[bot]` to a stricter rule set
162
+ than people.
163
+
164
+ ## Evidence lives in the PR description
165
+
166
+ A fenced YAML block. Agents can write it, machines can read it, humans can still skim it.
167
+
168
+ ````markdown
169
+ ```evidence
170
+ environment: staging
171
+ image: registry.example.com/mcp-server:pr-77-4444444
172
+ traces:
173
+ - what: search with empty query
174
+ before: https://langfuse.example.com/project/p1/traces/trace-before-1
175
+ after: https://langfuse.example.com/project/p1/traces/trace-after-1
176
+ ```
177
+ ````
178
+
179
+ `mergeproof template` prints exactly the block a change still needs. Multiple blocks merge.
180
+
181
+ ## What the PR sees
182
+
183
+ One comment, updated in place on every push, label change, CI completion and review comment:
184
+
185
+ ![The sticky PR comment](docs/pr-comment.svg)
186
+
187
+ Wire it up with the action (copy [`examples/github-workflow.yml`](examples/github-workflow.yml)):
188
+
189
+ ```yaml
190
+ on:
191
+ pull_request:
192
+ types: [opened, synchronize, reopened, edited, labeled, unlabeled]
193
+ issue_comment: { types: [created, edited] } # /verified and agent verdicts land here
194
+ check_suite: { types: [completed] } # re-check when CI finishes
195
+ jobs:
196
+ gate:
197
+ if: github.event_name != 'issue_comment' || github.event.issue.pull_request
198
+ runs-on: ubuntu-latest
199
+ permissions: { contents: read, pull-requests: write, statuses: write, checks: write }
200
+ steps:
201
+ - uses: actions/checkout@v4
202
+ with: { ref: ${{ github.event.repository.default_branch }} } # policy from the base branch
203
+ - uses: Aryamanz29/mergeproof@v0.2.0
204
+ env:
205
+ MERGEPROOF_PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
206
+ ```
207
+
208
+ The action reports in three places, each switchable: the sticky comment above, a **commit status**
209
+ named `mergeproof` in the merge box with the headline (`3 of 5 requirements satisfied, 2 pending`),
210
+ and a **Check Run** in the Checks tab whose annotations land on the files concerned, for example
211
+ "expected a changed test matching tests/**/test_search*.py" on `server/tools/search.py`. Require
212
+ the `mergeproof` status in branch protection and the gate is enforced.
213
+
214
+ None of this blocks a merge until the repository says so. GitHub merges anything unless a
215
+ ruleset requires specific checks (rulesets need a public repository or a paid plan), so create
216
+ one that requires the `mergeproof` status next to the CI jobs you already trust:
217
+
218
+ ```sh
219
+ gh api -X POST repos/OWNER/REPO/rulesets --input ruleset.json
220
+ ```
221
+
222
+ ```json
223
+ { "name": "main: pull requests with evidence", "target": "branch", "enforcement": "active",
224
+ "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
225
+ "rules": [
226
+ { "type": "pull_request", "parameters": { "required_approving_review_count": 0,
227
+ "dismiss_stale_reviews_on_push": true, "require_code_owner_review": false,
228
+ "require_last_push_approval": false, "required_review_thread_resolution": false } },
229
+ { "type": "required_status_checks", "parameters": { "strict_required_status_checks_policy": false,
230
+ "required_status_checks": [ { "context": "mergeproof" }, { "context": "unit" } ] } } ] }
231
+ ```
232
+
233
+ With `pending-ok: "true"` the workflow job stays green while evidence is outstanding, but the
234
+ `mergeproof` status stays `pending`, and a pending required status blocks the merge button. That
235
+ split is deliberate: the job says the tool ran, the status says whether the evidence is there.
236
+
237
+ ### Rendering with tools you already use
238
+
239
+ The action also writes the report as **JUnit XML** (`mergeproof-junit.xml`, one suite per rule,
240
+ one case per requirement) and in **reviewdog's format** (`mergeproof.rdjson`, one diagnostic per
241
+ file annotation). Any renderer for those formats then does the presentation:
242
+
243
+ ```yaml
244
+ - uses: Aryamanz29/mergeproof@v0.2.0
245
+ id: gate
246
+ - uses: EnricoMi/publish-unit-test-result-action@v2 # rich check run: counts, per-requirement detail, trends
247
+ if: always()
248
+ with: { files: mergeproof-junit.xml, check_name: mergeproof requirements, comment_mode: off }
249
+ - uses: reviewdog/action-setup@v1 # inline review comments on the files concerned
250
+ - run: reviewdog -f=rdjson -reporter=github-pr-review -level=error < mergeproof.rdjson
251
+ env: { REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }} }
252
+ ```
253
+
254
+ `dorny/test-reporter` reads the same JUnit file. Locally, `mergeproof check -f junit` and
255
+ `-f rdjson` print the same documents.
256
+
257
+ By default all three appear under `github-actions[bot]` with GitHub's avatar. To have them show
258
+ as **mergeproof** with the shield, create a GitHub App named mergeproof (avatar `docs/logo.svg`,
259
+ permissions: pull requests write, checks write, commit statuses write), install it on the repo,
260
+ and mint its token in the workflow:
261
+
262
+ ```yaml
263
+ - uses: actions/create-github-app-token@v1
264
+ id: app
265
+ with:
266
+ app-id: ${{ vars.MERGEPROOF_APP_ID }}
267
+ private-key: ${{ secrets.MERGEPROOF_APP_KEY }}
268
+ - uses: Aryamanz29/mergeproof@v0.2.0
269
+ with:
270
+ github-token: ${{ steps.app.outputs.token }}
271
+ ```
272
+
273
+ The comment, the status and the Check Run are then attributed to the app, and a later push from
274
+ the app's token does not trigger workflows recursively, which the default token would not either.
275
+
276
+ ## Built-in checks
277
+
278
+ | check | proves |
279
+ |---|---|
280
+ | `tests.changed` | each changed source file (via `{capture}` globs) or any file has a changed test |
281
+ | `files.changed` | the change touches / avoids certain paths (a down-migration, a changelog entry) |
282
+ | `evidence.field` | a key in the evidence block exists, equals, matches, or has N items |
283
+ | `evidence.links` | before/after link pairs matching a pattern; optional verification through a verifier |
284
+ | `ci.job_passed` | a named check run on the head commit succeeded (pending while it runs) |
285
+ | `review.human_verified` | a non-author, non-bot comment `/verified <sha7>`; a new push invalidates it |
286
+ | `agent.verdict` | an allowed bot posted a head-bound ```verdict block with `verdict: pass` |
287
+ | `pr.labels`, `pr.body` | labels present or absent; required sections, regex, minimum length |
288
+ | `shell` | a command from the policy exits 0 (changed files in `$MERGEPROOF_FILES`) |
289
+
290
+ `mergeproof checks` prints every parameter with its default.
291
+
292
+ ## Verifying links against their source
293
+
294
+ A pasted link should have to be real. `evidence.links` accepts a `pattern` with named groups and
295
+ a `verify` name:
296
+
297
+ ```yaml
298
+ - check: evidence.links
299
+ with:
300
+ key: traces
301
+ pattern: "^(?P<host>https?://[^/]+)/project/(?P<project>[^/]+)/traces/(?P<trace_id>[\\w-]+)"
302
+ verify: langfuse
303
+ ```
304
+
305
+ The core ships `http` (the URL answers 2xx; `verify_options: {auth_header_env: DASH_TOKEN}` for
306
+ private dashboards). Anything vendor-specific is a plugin: a class with `verify(url, match)`,
307
+ registered under the `mergeproof.verifiers` entry-point group.
308
+ [`examples/plugins/mergeproof-langfuse`](examples/plugins/mergeproof-langfuse) is a complete one in
309
+ forty lines, for [Langfuse](https://langfuse.com), an MIT-licensed tracer you can self-host and
310
+ feed from OpenTelemetry. Jaeger and Arize Phoenix work the same way.
311
+
312
+ ## Non-deterministic reviewers
313
+
314
+ LLM review agents are witnesses, not the gate. Have yours post a comment:
315
+
316
+ ````markdown
317
+ ```verdict
318
+ check: trace-review
319
+ verdict: pass
320
+ head: 4444444
321
+ confidence: 0.85
322
+ summary: the after-trace returns the empty page; the before-trace raises.
323
+ ```
324
+ ````
325
+
326
+ and require it:
327
+
328
+ ```yaml
329
+ - check: agent.verdict
330
+ with: { name: trace-review, authors: ["github-actions[bot]"], min_confidence: 0.8 }
331
+ ```
332
+
333
+ `authors` pins the verdict to the bot that runs the agent, `bind_to_head` (default) discards it on
334
+ the next push, and pairing it with `review.human_verified` keeps a person in the loop. If your
335
+ agent already speaks in labels (`reviewed` / `review-failed`), `pr.labels` consumes those.
336
+
337
+ ## For coding agents
338
+
339
+ Agents get the rules two ways. Both are generated from the policy, so what an agent reads is
340
+ what CI enforces.
341
+
342
+ **A Markdown section** for `AGENTS.md` or `CLAUDE.md`:
343
+
344
+ ```sh
345
+ mergeproof agent-prompt >> AGENTS.md
346
+ ```
347
+
348
+ It lists every rule, when it applies, what satisfies each requirement, and the evidence block
349
+ to fill in, followed by three rules for agents: never fabricate evidence, never post the human
350
+ verification phrase, keep the evidence block plain YAML.
351
+
352
+ **An MCP server** over stdio, for agents that can call tools:
353
+
354
+ ```sh
355
+ pip install 'mergeproof[mcp]'
356
+ mergeproof mcp --policy mergeproof.yaml # what the client launches
357
+ ```
358
+
359
+ Register it once per repository. Claude Code reads `.mcp.json` at the repo root; Cursor and
360
+ other clients take the same command and arguments in their own settings file:
361
+
362
+ ```json
363
+ {
364
+ "mcpServers": {
365
+ "mergeproof": { "command": "mergeproof", "args": ["mcp", "--policy", "mergeproof.yaml"] }
366
+ }
367
+ }
368
+ ```
369
+
370
+ The server exposes six tools and one resource:
371
+
372
+ | tool | arguments | returns | an agent calls it to |
373
+ |---|---|---|---|
374
+ | `explain` | `pr_title?`, `pr_body?` | `verdict`, `markdown`, `evidence_template` | learn what the current diff must prove, and what is already covered, before writing more code |
375
+ | `check` | `pr_title?`, `pr_body?` | `verdict`, `exit_code`, the full JSON report | dry-run the gate against the working tree with the PR description it is about to submit |
376
+ | `evidence_block` | `data` (mapping) | the fenced block as text | format the evidence for the PR description instead of hand-writing YAML |
377
+ | `validate_policy` | `text?` | `ok`, `problems`, `rules` | edit `mergeproof.yaml` safely; without `text` it validates the repo's file |
378
+ | `list_checks` | none | id, description and parameter schema for every check, plugins included | write or change rules with the right check ids and parameter names |
379
+ | `agent_instructions` | none | the same Markdown `agent-prompt` prints | refresh the rules mid-session |
380
+
381
+ Resource `mergeproof://policy` is the policy file itself.
382
+
383
+ Everything is read-only against the working tree. The server cannot post comments, add labels,
384
+ or produce the human sign-off; an agent can find out what to prove and check its own work, and
385
+ that is all. A typical session:
386
+
387
+ ```mermaid
388
+ sequenceDiagram
389
+ participant A as Coding agent
390
+ participant M as mergeproof mcp
391
+ participant G as GitHub
392
+ A->>M: explain()
393
+ M-->>A: fail: tests missing for tools/search.py, evidence block missing
394
+ A->>A: write the regression test, reproduce on staging, keep both trace links
395
+ A->>M: evidence_block({environment, image, traces})
396
+ M-->>A: fenced evidence block
397
+ A->>M: check(pr_body)
398
+ M-->>A: pending: only CI status and reviewer sign-off remain
399
+ A->>G: open the PR with that description
400
+ G->>G: CI runs mergeproof check --github --comment
401
+ Note over G: a human opens the traces and posts /verified sha7
402
+ ```
403
+
404
+ The CLI and the server answer the same questions:
405
+
406
+ | CLI | MCP tool |
407
+ |---|---|
408
+ | `mergeproof explain` | `explain` |
409
+ | `mergeproof check --body-file FILE` | `check` |
410
+ | `mergeproof template` | `explain`, field `evidence_template` |
411
+ | `mergeproof validate` | `validate_policy` |
412
+ | `mergeproof checks` | `list_checks` |
413
+ | `mergeproof agent-prompt` | `agent_instructions` |
414
+
415
+ ### Tracing the server itself
416
+
417
+ The MCP SDK wraps every tool call in an OpenTelemetry span. Install the `otel` extra and point the
418
+ standard variables at any OTLP/HTTP receiver and those spans are exported; nothing is sent
419
+ otherwise.
420
+
421
+ ```sh
422
+ pip install 'mergeproof[mcp,otel]'
423
+
424
+ # Langfuse (self-hosted or cloud): basic auth with the project keys
425
+ export OTEL_EXPORTER_OTLP_ENDPOINT=https://langfuse.example.com/api/public/otel
426
+ export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(printf '%s:%s' "$LANGFUSE_PUBLIC_KEY" "$LANGFUSE_SECRET_KEY" | base64)"
427
+
428
+ # Jaeger or an OpenTelemetry Collector on the default OTLP/HTTP port
429
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
430
+ ```
431
+
432
+ Each span is named after the MCP method and tool (`tools/call explain`) and carries the
433
+ `gen_ai.tool.name` attribute, so a dashboard can answer "which agent asked what, and how often did
434
+ `check` come back failing" without any code in this project knowing which backend it talks to.
435
+
436
+ ### Tracing the server itself
437
+
438
+ The MCP SDK wraps every tool call in an OpenTelemetry span. Install the `otel` extra and point the
439
+ standard variables at any OTLP/HTTP receiver and those spans are exported; nothing is sent
440
+ otherwise.
441
+
442
+ ```sh
443
+ pip install 'mergeproof[mcp,otel]'
444
+
445
+ # Langfuse (self-hosted or cloud): basic auth with the project keys
446
+ export OTEL_EXPORTER_OTLP_ENDPOINT=https://langfuse.example.com/api/public/otel
447
+ export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(printf '%s:%s' "$LANGFUSE_PUBLIC_KEY" "$LANGFUSE_SECRET_KEY" | base64)"
448
+
449
+ # Jaeger or an OpenTelemetry Collector on the default OTLP/HTTP port
450
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
451
+ ```
452
+
453
+ Each span is named after the MCP method and tool (`tools/call explain`) and carries the
454
+ `gen_ai.tool.name` attribute, so a dashboard can answer "which agent asked what, and how often did
455
+ `check` come back failing" without any code in this project knowing which backend it talks to.
456
+
457
+ ## Plumbing
458
+
459
+ The porcelain commands are compositions of filters that read and write JSON:
460
+
461
+ ![context, check, report, comment as a pipeline](docs/pipeline.svg)
462
+
463
+ ```sh
464
+ mergeproof context --github > pr.json # snapshot a PR (files, body, labels, comments, check runs)
465
+ mergeproof check --context pr.json -f json > report.json
466
+ mergeproof report report.json -f md # render later, elsewhere
467
+ mergeproof comment report.json # post it
468
+ ```
469
+
470
+ Snapshots make policies testable: the [examples](examples/) ship scenario files that the
471
+ integration suite runs through the CLI, asserting the verdict each one claims.
472
+
473
+ ## Writing a check
474
+
475
+ ```python
476
+ from pydantic import BaseModel
477
+ from mergeproof import Check, Context, Outcome, Status
478
+
479
+
480
+ class ImageSmokeTested(Check):
481
+ id = "image.smoke_tested"
482
+ description = "The image named in the evidence block was exercised against the environment."
483
+
484
+ class Params(BaseModel):
485
+ registry: str
486
+
487
+ def run(self, ctx: Context, params: Params, files: list[str]) -> Outcome:
488
+ tag = ctx.evidence().get("image")
489
+ if not tag or not tag.startswith(params.registry):
490
+ return Outcome(
491
+ status=Status.FAIL,
492
+ summary="no image under the expected registry",
493
+ fix="Build the branch image and record its tag under `image`.",
494
+ )
495
+ # query your deployment API here
496
+ return Outcome(status=Status.PASS, summary=f"{tag} smoke-tested")
497
+
498
+ def explain(self, params: Params) -> str:
499
+ return f"An image under `{params.registry}` recorded as `image` and smoke-tested."
500
+
501
+ def evidence_template(self, params: Params) -> dict:
502
+ return {"image": f"{params.registry}/<name>:pr-<n>-<sha7>"}
503
+ ```
504
+
505
+ ```toml
506
+ [project.entry-points."mergeproof.checks"]
507
+ "image.smoke_tested" = "mypkg.checks:ImageSmokeTested"
508
+ ```
509
+
510
+ ## Prior art
511
+
512
+ [Danger](https://danger.systems/js/) runs rules written in JavaScript and comments on the PR;
513
+ `mergeproof` is declarative, has a notion of evidence, and explains itself to agents.
514
+ [policy-bot](https://github.com/palantir/policy-bot) enforces rich approval policies but only sees
515
+ GitHub-native data. Checklist actions fail on unticked boxes, which is the thing agents will tick.
516
+ Hosted "evidence gate" products evaluate your artifacts on their servers. `mergeproof` borrows
517
+ policy-bot's matchers and Danger's sticky comment and runs entirely in your CI.
518
+
519
+ ## Exit codes
520
+
521
+ | code | meaning |
522
+ |---|---|
523
+ | 0 | pass, or only warnings |
524
+ | 1 | a blocking requirement failed or errored |
525
+ | 2 | a blocking requirement is pending (CI running, reviewer not yet verified) |
526
+ | 3 | usage or policy error |
527
+
528
+ ## Development
529
+
530
+ ```sh
531
+ make setup # uv sync, example plugin, pre-commit hooks
532
+ make lint typecheck test integration
533
+ make check # this repository's own gate against your working tree
534
+ ```
535
+
536
+ MIT. See [CONTRIBUTING.md](CONTRIBUTING.md).