Mediva CI
Gate pull requests and issues on GitHub with one command — friendly, signal-not-verdict checks that never reject the author.
DOCKING CONTROL, Babylon 5: "This is Command. Your manifest is missing the cargo declaration on bay four. You are not refused — hold position, amend the manifest, and you are cleared to dock."
Mediva CI is docking control for your repository. Every pull request and every issue is an incoming craft requesting clearance; the check reads the manifest against your contract, and if something's missing it posts a calm, plain-language note saying exactly which line to amend. It never impounds the ship — the PR is never closed, the issue is never deleted. It just won't wave a half-filled manifest through.
One command to wire it up
npx mediva initinit scaffolds a complete, working setup into the current repo:
.github/pull_request_template.md # the PR contract (template + schema in one file)
.github/ISSUE_TEMPLATE/issue.md # the issue contract
.github/workflows/mdv.yml # the GitHub Action that runs the check
README.mdv.md # a starter README contract
README.md # a starter README (only if you don't have one)Review the templates, adjust the rules to fit your project, commit, and the check is live on your next PR or issue. The template is the contract — the same <!-- mdv: … --> rules a contributor reads are the ones mediva enforces. One form, every courier.
What the check does
The scaffolded mdv.yml fires on pull_request (opened, edited, reopened, synchronize) and issues (opened, edited, reopened). On each run it validates the title and body against the matching template and reports back two ways:
- Inline annotations for engineers reading the diff — each violation becomes a GitHub
::error::/::warning::annotation tagged with the human label (Too short), not the machine code. - A plain-language Step Summary panel for whoever opened the PR or issue — they may not be an engineer and may never have heard of mediva. It names the section, reassures them nothing was rejected, and says exactly where to edit.
## This pull request needs a few small edits
An automated check makes sure the pull request template is filled in before it
merges. Your pull request was **not** rejected or deleted — just edit the pull
request description to address these:
- The "Description" section is too short (12 words). Add more detail — aim for at least 20 words.
Edit the pull request text (not the code), and this check re-runs automatically.Exit codes and merge gating
0— no errors. The check is green; the bay doors open.1— one or more errors. The check fails red. Mark it a required status check in branch protection to block merge.>1— mediva itself failed to run (usage/config error). The step surfaces the error and fails hard, so a broken setup can never pass silently.
Warnings never fail the run on their own — only error-severity diagnostics flip the exit code.
Two details that make it tamper-proof
- Base-branch contract. The workflow checks out the PR's base sha and validates against the template there, so a PR cannot edit the template to weaken the very contract it has to pass.
- Bootstrap skip. On the first PR that ever adopts mediva, the base commit has no template yet. A missing base schema means "no contract to enforce yet", not a failure — the step emits a
::notice::and exits0. Enforcement begins the moment the template lands on the base branch. (Deleting an established template still leaves it visible on the base, so an existing contract stays enforced — only genuine first adoption is skipped.)
Friendly bot comments (the dogfood setup)
The mdv.yml from init is the lightweight path — annotations and a Step Summary, no comment. init also drops a richer mediva-ci.yml: a tiny caller for mediva's reusable workflows (mediva-pr.yml and mediva-issue.yml), which render the diagnostics into a single self-updating comment. The renderer ships in the package as mdv comment, so there is no script to copy.
When something's missing, the bot posts one table and keeps editing it in place as the author pushes fixes — never a pile of duplicate comments:
👋 Thanks for the pull request! **Mediva CI** checks descriptions against this repo's contract.
**2 items need attention before this is ready to act on:**
| What | Status | What's needed |
| --- | :---: | --- |
| Title | ❌ | Title must match: feat(scope): <message> |
| Description | ⚠️ | 20–180 words required; yours has 12 |
These aren't a judgement on you or your tools — they make the change reviewable in a single pass.
Edit the PR description (or push a commit) and I'll re-check automatically.Once the author fixes everything, the same comment clears itself:
✅ **Mediva CI:** thanks — this pull request now matches the repo contract. Clearing my earlier note.That is the signal, not verdict stance: the comment guides, it never scolds, and it never claims the author "used AI". It only reports which contract fields are unfilled.
The renderer is also hardened against comment injection — a hostile PR body can't break out of the table or ping people. Newlines collapse to spaces, | is escaped, @mentions and #refs are neutered with a zero-width space, and </> are HTML-escaped. Bodies always travel env var → file → CLI arg (never shell interpolation), and the comment is posted with --body-file, never --body.
The issue workflow adds a daily stale sweep: issues left invalid past a grace window (the grace-days input, default 7) get a heads-up footer and are eventually auto-closed — reopen anytime once addressed. The sweep re-validates each issue with the same mdv binary, so it can never close one the live check would now pass; it runs inline in the reusable workflow (bash + gh + mdv), so consumers need no extra script.
Agent-grade guards
PRs and issues are increasingly opened by coding agents. Mediva CI ships rules that catch the tells of unreviewed machine output — on the title and on every required field:
| Guard | Rule | Catches |
|---|---|---|
| LLM residue | noLLMResidue | Boilerplate like "I've implemented…", "As requested…" pasted into a title or body. Promoted to a hard error with error=llm-residue. |
| Truncation | noTruncation | A title cut off with … or [truncated]. |
| Fence wrapper | noFenceWrapper | A title still wrapped in ``` or ~~~ code fences. |
| Placeholder | title.noPlaceholder / noPlaceholder | TODO, FILL ME IN, your answer here, bare .... |
| Falsifiable evidence | evidenceList each.falsifiable error=vague-evidence | "Tested thoroughly" with no command or observed result. Each item must be checkable. |
Beyond catching residue, the templates ask the author to disclose and attest rather than auto-rejecting agent work:
- Authoring disclosure — a required
choice(Human-authored / Agent-assisted / Agent-authored). Selecting "Agent-authored" is never rejected; it triggers a Human Review Attestation field instead — a human confirming they read the diff. - Scope — required when the change is tiered "Standard", forcing an explicit statement of what the PR does and does not touch.
- Dependency provenance — required when a "supply-chain surface" box is checked, so each new dependency is named and confirmed a genuine package (not a typosquat).
Document-scope severity overrides
Every rule's severity is tunable from the directive itself, so you decide what blocks versus what merely nudges:
<!-- mdv: block required minWords=20 warn=too-few-words --> <!-- soft: prints a warning, never fails -->
<!-- mdv: evidenceList required each.falsifiable error=vague-evidence --> <!-- hard: blocks merge -->warn=<code> downgrades a diagnostic to a non-blocking warning, error=<code> promotes one to a hard failure, and off=<code> silences it. See Overrides for the full model.
Adopting the commenting setup
The PR and issue workflows are reusable workflows — you don't copy their logic, you call them. mdv init writes the caller (.github/workflows/mediva-ci.yml) and the two contract templates for you. By hand it's three steps:
-
Add a caller workflow that references the reusable workflows by tag and grants the write scope each needs:
# .github/workflows/mediva-ci.yml on: pull_request: types: [opened, edited, reopened, synchronize] issues: types: [opened, edited, reopened] schedule: - cron: "17 9 * * *" # daily stale sweep workflow_dispatch: jobs: pr: if: github.event_name == 'pull_request' permissions: { contents: read, pull-requests: write } uses: jamespacileo/mediva/.github/workflows/mediva-pr.yml@v0.2.0 with: { mediva-version: "0.2.0" } issue: if: github.event_name != 'pull_request' permissions: { contents: read, issues: write } uses: jamespacileo/mediva/.github/workflows/mediva-issue.yml@v0.2.0 with: { mediva-version: "0.2.0" } -
Copy and customise
.github/pull_request_template.mdand.github/ISSUE_TEMPLATE/issue.md— these are your contract. -
That's it. The reusable workflow runs
npx mediva@<mediva-version>for both the check and the comment, so you don't addmedivaas a dependency, build anything, or copy a renderer script.
Pin the @<tag> to a mediva release and bump it to take fixes. Inputs you can override: mediva-version, pr-template / issue-template (schema paths), comment-marker, and for the issue sweep invalid-label and grace-days. The lighter mdv.yml (also from init) only annotates inline, needs no write permissions, and is the right pick if you don't want the bot comment.
Permissions and secrets
No API keys. Mediva CI runs entirely on the auto-provided GITHUB_TOKEN. It needs only:
permissions:
contents: read
pull-requests: write # to post/update the PR comment
issues: write # to post/update the issue comment + sweepThe lightweight mdv.yml (annotations + Step Summary only) needs just contents: read.
Where to go next
- Gate PR bodies — a complete single-form PR contract, line by line.
- Gate issue forms — validate titles with
--titleand require reproduction steps. - Monorepo CI — one zero-config scan across every package README.
- CLI — the
checkflags (--json,--sarif,--title,--context) the workflows drive. - Overrides —
warn=/error=/off=severity tuning.