Released · free and open source

Dunstan holds a coding agent’s report to the record.

An agent finishes and writes a handback: files changed, tests run, references closed, head commit, merged at. Dunstan reads a block the agent declares in that handback and checks each claim against the repository’s own record, with deterministic checks. It writes a record anyone can re-run and get the same verdict.

Three rules

What it checks

Each claim is pass, fail or unverifiable, with a reason code. The record is an in-toto statement whose digests let anyone recompute every verdict offline.

Quick start: the GitHub Action

Three steps. No checkout, no secrets, and nothing leaves GitHub.

1. Add the workflow

Save this as .github/workflows/dunstan.yml. The Action is pinned to the full commit of v0.1.6.

.github/workflows/dunstan.yml
name: Dunstan

on:
  pull_request:
    types: [opened, edited, synchronize, reopened, ready_for_review]

permissions:
  contents: read
  pull-requests: read
  issues: read
  deployments: read
  checks: write
  actions: read

jobs:
  handback:
    runs-on: ubuntu-latest
    steps:
      - uses: BargLabs/dunstan@e0ec7ed05272e09fee21f0f29d70395f846547ac # v0.1.6
        with:
          report-source: pr-body

2. Have the agent declare its claims

The agent puts one block in its pull request description. dunstan template writes most of it from local git, and dunstan suggest lists any claim in the prose that the block does not declare.

in the pull request description
```dunstan-handback
{
  "dunstan": "0.1",
  "headCommit": "c0ffee12345678abcdef0123456789abcdef0123",
  "filesChanged": ["src/cache.ts", "src/cache.test.ts"],
  "references": [{ "issue": "#41", "relation": "closes" }]
}
```

3. Require the check

In branch protection or a ruleset, require the status check dunstan. Only pass concludes success. fail and unverifiable conclude failure, unless you set unverifiable-conclusion: neutral. Dunstan never comments, labels, reviews or merges. The full reference is in docs/action.md.

Quick start: the CLI

build it, then check a pull request
git clone --branch v0.1.6 https://github.com/BargLabs/dunstan.git
cd dunstan && pnpm install && pnpm build

# offline: recompute an example record's verdict and digests
node dist/dunstan.mjs verify spec/examples/records/fail.json

# online: check a pull request against a report that holds a block
GITHUB_TOKEN=<read-only token> node dist/dunstan.mjs check \
  --repo <owner/name> --pr <n> --report-file report.md --out record.json

Here is the output of that last command, verbatim, on a public pull request, run on 8 October 2026 with the public v0.1.6 build and a block taken from the demo:

node dist/dunstan.mjs check --repo cdcseacave/openMVS --pr 1246 --report-file report.md --out record.json
dunstan 0.1.6  cdcseacave/openMVS#1246  head 66e9ec39994c81e416b2970fb3d158760389f567
report: file report.md (sha256 28e5cf87fc33372e57c78dd0fd7de22f9b7c128fbaabdf537c92a8635dc39c78)
block: found (sha256 f911a1b6cf1b7cb17586c59570b1ac13b10c7cac1a0f6c7c9260705f05c5941d)

VERDICT  CLAIM                  DECLARED                                  OBSERVED                                  REASON
pass     head:/headCommit       66e9ec39994c81e416b2970fb3d158760389f567  66e9ec39994c81e416b2970fb3d158760389f567
pass     scope:/filesChanged/0  apps/Tests/Tests.cpp                      apps/Tests/Tests.cpp
pass     scope:/filesChanged/1  libs/IO/ImageTIFF.cpp                     libs/IO/ImageTIFF.cpp
pass     scope:/filesChanged/2  libs/IO/ImageTIFF.h                       libs/IO/ImageTIFF.h
pass     time:/mergedAt         2026-09-11T23:57:04Z                      2026-09-11T23:57:04Z

verdict: pass
record: record.json

Exit codes: 0 for pass, 1 for fail, 2 for unverifiable. dunstan rerun record.json re-reads the sources and reports any evidence that has changed. dunstan mcp serves the same checker as an MCP tool, so an agent can check its block before it says it is done. See the README at v0.1.6 and docs/agents.md.

What a result looks like

Every record carries a claims table like these, and the Action writes the same table as the summary of its dunstan check run. The first two are real public pull requests from the demo; the third is the specification’s own constructed example.

pass: every claim holds

cdcseacave/openMVS#1246, written by an agent (Google Jules). Record: demo/2026-10/4.

VerdictClaimDeclaredObservedReason
passhead:/headCommit66e9ec39…66e9ec39…
passscope:/filesChanged/0apps/Tests/Tests.cppapps/Tests/Tests.cpp
passscope:/filesChanged/1libs/IO/ImageTIFF.cpplibs/IO/ImageTIFF.cpp
passscope:/filesChanged/2libs/IO/ImageTIFF.hlibs/IO/ImageTIFF.h
passtime:/mergedAt2026-09-11T23:57:04Z2026-09-11T23:57:04Z

unverifiable: the record cannot answer one claim

QuantEcon/QuantEcon.py#798, written by an agent (GitHub Copilot coding agent). Eleven claims hold. The report says 605 tests passed but names no machine-readable test record, so Dunstan has nothing to check that count against. It says so instead of passing it. Record: demo/2026-10/5.

VerdictClaimDeclaredObservedReason
passhead:/headCommit2fcb68c3…2fcb68c3…
passscope:/filesChanged/0–67 test filesthe same 7 files
passreference:/references/0cites #787exists
passreference:/references/1cites #790exists
unverifiablecount:/tests/0/count605—no_comparable_record_field
passtime:/mergedAt2026-09-10T01:54:40Z2026-09-10T01:54:40Z

fail: the record contradicts the report

A constructed example from the specification (example-org/example-repo, not a real pull request): the agent left a changed file undeclared and claimed 40 tests where the CI record shows 38. Its closes #21 is unverifiable because GitHub has not yet linked the issue to the open pull request. A fail decides the verdict even beside an unverifiable row. Record: spec/examples/records/fail.json.

VerdictClaimDeclaredObservedReason
passhead:/headCommitc0ffee12…c0ffee12…
passscope:/filesChanged/0src/cache.tssrc/cache.ts
passscope:/filesChanged/1src/cache.test.tssrc/cache.test.ts
failscope:undeclared:package.json—package.jsonundeclared_file
unverifiablereference:/references/0closes #21—closing_link_unsettled
failcount:/tests/0/count4038count_mismatch

The specification

The claim format, the block an agent declares and the record Dunstan writes, is an open specification: claim-format.md (Apache-2.0). The checker is AGPL-3.0. JSON Schemas are served at their identifiers:

Record predicate types: /dunstan/record/v0.1 and /dunstan/record/v0.2-draft.

Evidence for the claims we make

A five-pull-request demo, chosen by a rule committed before any search, with its records and an erratum, is in the repository’s demo/2026-10/. Measured figures for the advisory prose reader, with their method and the corpus each was measured on, are in docs/advisory.md. On constructed reports, extractor 0.1.3 flags 280 of 380 planted false claims (73.7%), with 280 of its 315 flags on the planted claim; all of its misses are one sentence shape it does not read. Two studies on public agent-written pull requests, each preregistered before any pull request was selected and published with every label, found that 87% to 97% of what the reader proposes is a real claim, and that about a third of its disagreement notes on 300 pull requests marked a claim the record contradicts. They also found that fixing how it reads file claims does not generalise: see study 1 and study 2. That is why the gate checks only what an agent declares. Why a reader that finds claims in prose is not enough: Reading, not checking on cejel.dev.

Hosted

A hosted GitHub App that runs the same checker is built but not yet offered. If you want early access, ask us.