Keelmarker

Guide

Last updated: October 29, 2026

Keelmarker keeps a list of things you promise about your software (requirements), links tests to them, and answers one question: is every promise backed by a passing test, or by a named person who accepted the risk? The answer is the gate: pass or blocked, with the reasons. It is the same answer for people, scripts and AI agents.

Get started

  1. Sign in at /app/. Access is by invitation while Keelmarker is in beta; the person who invited you sends a link.
  2. Create a product (a short code and a name) and one component for each part of it you want gated on its own.
  3. Add requirements, one sentence each, saying what must be true (“Orders are saved with the right total”). A starter pack (Packs tab) adds common ones for you to review.
  4. Link a test to each requirement, by the test’s name. The names are the link: renaming a test breaks the link.
  5. Send results from your test runs (next sections). The gate recomputes and tells you what is missing.

Requirements nobody assigned to a component still gate, as “Unassigned”, so a pass can never skip them.

Reading the gate

Each requirement is in exactly one state:

StateMeaning
VerifiedAt least one linked test ran and passed.
AttestedNo passing test, but a named person signed off the risk. Counted separately from verified.
FailingLinked tests ran and all failed. A sign-off cannot override a failing test.
Not runTests are linked but none has run.
No testNothing is linked yet.
Awaiting sign-offFlagged human-only: blocked until a person signs it off, even when its tests pass.

A component is blocked when any requirement in it has no executed test, or when its tests’ pass rate is below the threshold (default 100%). The product passes only when no component is blocked. A product with no requirements is blocked: nothing has been checked.

Partly proven. A requirement with one passing and one failing test reads verified, but the gate is still blocked by the failing test. The summary and the Markdown output list such requirements as “partly proven” with the failing test names. It is only an explanation: the verdict is computed exactly as above.

Commits. Pass a commit to pin the verdict to shipped code: a test then counts only through runs reported for that commit. Use the short SHA; a full SHA shows a false “blocked”.

Stale. A stored verdict says when requirements, links, sign-offs or results changed after it was computed.

To see what blocks and how to fix it in a few lines:

keelmarker gate status --product-id PRODUCT_ID           # latest stored answer, no recompute
keelmarker gate check  --product-id PRODUCT_ID --commit-sha abc1234 --format markdown

Sending test results

Send the JUnit XML your test runner already writes. Nothing about your code is uploaded.

keelmarker results ingest --product-id PRODUCT_ID --junit junit.xml --commit-sha abc1234

Tests are matched by name. Names that match no test in Keelmarker are listed in the reply so you can add or link them.

Using it from an AI agent

Create an agent token in Settings (it can read and write, never sign off), then connect any MCP client that supports streamable HTTP:

claude mcp add --transport http keelmarker https://YOUR-HOST/api/v1/mcp --header "Authorization: Bearer YOUR_TOKEN"

Each call runs as an ordinary API request with the token’s own permissions. A typical task:

  1. at_task_start with a title and objective, then at_promise for each thing the agent will make true.
  2. Make the change, add or link tests, report results with at_report_results.
  3. at_check_gate returns a compact answer: verdict, counts, what blocks, how to fix it. Pass full=true for every requirement row (large on big products). at_gate_summary reads the latest stored answer without recomputing.
  4. at_task_close succeeds only when every promise is settled and the gate passes.

An agent can add and strengthen requirements, report findings and results, and read everything. It cannot sign off a risk, dismiss a finding, promote a rule or retire a test: those need a person.

Decisions only a person makes

A person is someone whose token or role carries the attest permission (a workspace owner or admin). These actions can weaken the gate, so agents are refused:

Findings and signal rate

A review or audit finding becomes a requirement with no test yet, so the gate stays blocked until a real test proves the fix (keelmarker finding ... or at_report_finding). Findings keep their source, severity and location. Their outcome is worked out from the requirement:

Dismissing (Findings tab, or keelmarker findings dismiss) always asks you to choose what happens to the requirement:

For a risk you are knowingly accepting, a recorded sign-off usually fits better than a dismissal, because it stays visible in the gate as attested. Dismissing suggests tests that could be retired but never retires anything.

Signal rate (keelmarker findings stats, or at_finding_stats) shows per source and severity how many resolved findings were fixed or accepted rather than dismissed, and the median time to resolve. With fewer than 5 resolved findings it says “low sample”.

Extra gate rules and shadow mode

A product can add built-in rules on top of the standard gate: pass_rate_threshold, no_unassigned_requirements, max_attested. An agent or script can propose one, but every new rule starts in shadow mode: the gate records what the rule would have decided and never lets it change the verdict. Read the track record in the Rules tab (gates watched, how many it would have decided differently). When you trust it, a person promotes it and from then on it can block. A person can revoke it at any time. Rules only ever add blocks; they never turn a blocked gate into a pass.

Using the gate in CI

The command exits 1 when the gate is blocked, so a CI job is your status check. Nothing is posted to your repository by Keelmarker, and no repository token is stored here.

- run: echo "SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
- run: pytest --junitxml=junit.xml || true
- run: keelmarker results ingest --product-id "$PRODUCT_ID" --junit junit.xml --commit-sha "$SHA"
- run: keelmarker gate check --product-id "$PRODUCT_ID" --commit-sha "$SHA" --format markdown | tee -a "$GITHUB_STEP_SUMMARY"

Set KEELMARKER_BASE_URL and KEELMARKER_API_TOKEN as variables and secrets. Make the job a required check in branch protection to block merging on a blocked gate. --format json gives the same answer for scripts.

Evidence from other tools

Browser runners, scanners, load tests and other reviewers can send evidence about your product. Evidence is shown beside the requirements it bears on, but it never counts as proof: only tests and people’s sign-offs move the gate.

keelmarker evidence template > record.json      # a record to fill in
keelmarker evidence ingest record.json --product-id PRODUCT_ID --spec-id SPEC_ID --note "what this shows"

A good record has an evidence_id (yours; sending it again is harmless), provider.id, operation, subject.name and predicate_type, and for a run of the app a result with status (passed, failed or inconclusive) and observed_at, plus commit_sha, confidence and limitations. A thin record is still accepted; the reply lists warnings saying what to add.

Tidying old tests

Tests that no requirement uses and that never ran can be listed (keelmarker test retire-unlinked --product-id PRODUCT_ID is a dry run) and retired by a person from an explicit list of ids. Each is checked again when you retire it. There is no “retire everything”.

Command and tool reference

I want to…CommandAgent tool
See what blocks the gatekeelmarker gate statusat_gate_summary
Recompute the gatekeelmarker gate checkat_check_gate
Send test resultskeelmarker results ingestat_report_results
Record a findingkeelmarker findingat_report_finding
List findings, see signal ratekeelmarker findings list, findings statsat_list_findings, at_finding_stats
Dismiss or reopen a findingkeelmarker findings dismiss, findings reopen(a person only)
Accept a riskkeelmarker risk accept(a person only)
List gate rulesRules tabat_list_gate_rules
Send evidencekeelmarker evidence ingestat_ingest_evidence
Start and close a taskkeelmarker task start, task closeat_task_start, at_task_close
Record a deploymentkeelmarker deployat_record_deployment
List tests nothing useskeelmarker test retire-unlinked(a person retires)

Questions or something unclear? Use the contact form.